Adding --check to run a serials network test against GitHub or GHES. (#900)

* add --check.
This commit is contained in:
Tingluo Huang
2021-01-14 13:26:07 -05:00
committed by GitHub
parent 3b34e203dc
commit 7ee333b5cd
22 changed files with 1404 additions and 16 deletions

44
docs/checks/actions.md Normal file
View File

@@ -0,0 +1,44 @@
# Actions Connection Check
## What is this check for?
Make sure the runner has access to actions service for GitHub.com or GitHub Enterprise Server
- For GitHub.com
- The runner needs to access https://api.github.com for downloading actions.
- The runner needs to access https://vstoken.actions.githubusercontent.com/_apis/.../ for requesting an access token.
- The runner needs to access https://pipelines.actions.githubusercontent.com/_apis/.../ for receiving workflow jobs.
- For GitHub Enterprise Server
- The runner needs to access https://myGHES.com/api/v3 for downloading actions.
- The runner needs to access https://myGHES.com/_services/vstoken/_apis/.../ for requesting an access token.
- The runner needs to access https://myGHES.com/_services/pipelines/_apis/.../ for receiving workflow jobs.
## What is checked?
- DNS lookup for api.github.com or myGHES.com using dotnet
- Ping api.github.com or myGHES.com using dotnet
- Make HTTP GET to https://api.github.com or https://myGHES.com/api/v3 using dotnet, check response headers contains `X-GitHub-Request-Id`
---
- DNS lookup for vstoken.actions.githubusercontent.com using dotnet
- Ping vstoken.actions.githubusercontent.com using dotnet
- Make HTTP GET to https://vstoken.actions.githubusercontent.com/_apis/health or https://myGHES.com/_services/vstoken/_apis/health using dotnet, check response headers contains `x-vss-e2eid`
---
- DNS lookup for pipelines.actions.githubusercontent.com using dotnet
- Ping pipelines.actions.githubusercontent.com using dotnet
- Make HTTP GET to https://pipelines.actions.githubusercontent.com/_apis/health or https://myGHES.com/_services/pipelines/_apis/health using dotnet, check response headers contains `x-vss-e2eid`
## How to fix the issue?
### 1. Check the common network issue
> Please check the [network doc](./network.md)
### 2. SSL certificate related issue
If you are seeing `System.Net.Http.HttpRequestException: The SSL connection could not be established, see inner exception.` in the log, it means the runner can't connect to Actions service due to SSL handshake failure.
> Please check the [SSL cert doc](./sslcert.md)
## Still not working?
Contact GitHub customer service or log an issue at https://github.com/actions/runner if you think it's a runner issue.

34
docs/checks/git.md Normal file
View File

@@ -0,0 +1,34 @@
# Git Connection Check
## What is this check for?
Make sure `git` can access GitHub.com or your GitHub Enterprise Server.
## What is checked?
The test is done by executing
```bash
# For GitHub.com
git ls-remote --exit-code https://github.com/actions/checkout HEAD
# For GitHub Enterprise Server
git ls-remote --exit-code https://ghes.me/actions/checkout HEAD
```
The test also set environment variable `GIT_TRACE=1` and `GIT_CURL_VERBOSE=1` before running `git ls-remote`, this will make `git` to produce debug log for better debug any potential issues.
## How to fix the issue?
### 1. Check the common network issue
> Please check the [network doc](./network.md)
### 2. SSL certificate related issue
If you are seeing `SSL Certificate problem:` in the log, it means the `git` can't connect to the GitHub server due to SSL handshake failure.
> Please check the [SSL cert doc](./sslcert.md)
## Still not working?
Contact GitHub customer service or log an issue at https://github.com/actions/runner if you think it's a runner issue.

26
docs/checks/internet.md Normal file
View File

@@ -0,0 +1,26 @@
# Internet Connection Check
## What is this check for?
Make sure the runner has access to https://api.github.com
The runner needs to access https://api.github.com to download any actions from the marketplace.
Even the runner is configured to GitHub Enterprise Server, the runner can still download actions from GitHub.com with [GitHub Connect](https://docs.github.com/en/enterprise-server@2.22/admin/github-actions/enabling-automatic-access-to-githubcom-actions-using-github-connect)
## What is checked?
- DNS lookup for api.github.com using dotnet
- Ping api.github.com using dotnet
- Make HTTP GET to https://api.github.com using dotnet, check response headers contains `X-GitHub-Request-Id`
## How to fix the issue?
### 1. Check the common network issue
> Please check the [network doc](./network.md)
## Still not working?
Contact GitHub customer service or log an issue at https://github.com/actions/runner if you think it's a runner issue.

29
docs/checks/network.md Normal file
View File

@@ -0,0 +1,29 @@
## Common Network Related Issues
### Common things that can cause the runner to not working properly
- Bug in the runner or the dotnet framework that causes actions runner can't make Http request in a certain network environment.
- Proxy/Firewall block certain HTTP method, like it block all POST and PUT calls which the runner will use to upload logs.
- Proxy/Firewall only allows requests with certain user-agent to pass through and the actions runner user-agent is not in the allow list.
- Proxy try to decrypt and exam HTTPS traffic for security purpose but cause the actions-runner to fail to finish SSL handshake due to the lack of trusting proxy's CA.
- Firewall rules that block action runner from accessing certain hosts, ex: `*.github.com`, `*.actions.githubusercontent.com`, etc.
### Identify and solve these problems
The key is to figure out where is the problem, the network environment, or the actions runner?
Use a 3rd party tool to make the same requests as the runner did would be a good start point.
- Use `nslookup` to check DNS
- Use `ping` to check Ping
- Use `curl -v` to check the network stack, good for verifying default certificate/proxy settings.
- Use `Invoke-WebRequest` from `pwsh` (`PowerShell Core`) to check the dotnet network stack, good for verifying bugs in the dotnet framework.
If the 3rd party tool is also experiencing the same error as the runner does, then you might want to contact your network administrator for help.
Otherwise, contact GitHub customer support or log an issue at https://github.com/actions/runner

30
docs/checks/nodejs.md Normal file
View File

@@ -0,0 +1,30 @@
# Node.js Connection Check
## What is this check for?
Make sure the built-in node.js has access to GitHub.com or GitHub Enterprise Server.
The runner carries it's own copy of node.js executable under `<runner_root>/externals/node12/`.
All javascript base Actions will get executed by the built-in `node` at `<runner_root>/externals/node12/`.
> Not the `node` from `$PATH`
## What is checked?
- Make HTTPS GET to https://api.github.com or https://myGHES.com/api/v3 using node.js, make sure it gets 200 response code.
## How to fix the issue?
### 1. Check the common network issue
> Please check the [network doc](./network.md)
### 2. SSL certificate related issue
If you are seeing `Https request failed due to SSL cert issue` in the log, it means the `node.js` can't connect to the GitHub server due to SSL handshake failure.
> Please check the [SSL cert doc](./sslcert.md)
## Still not working?
Contact GitHub customer service or log an issue at https://github.com/actions/runner if you think it's a runner issue.

89
docs/checks/sslcert.md Normal file
View File

@@ -0,0 +1,89 @@
## SSL Certificate Related Issues
You might run into an SSL certificate error when your GitHub Enterprise Server is using a self-signed SSL server certificate or a web proxy within your network is decrypting HTTPS traffic for a security audit.
As long as your certificate is generated properly, most of the issues should be fixed after your trust the certificate properly on the runner machine.
> Different OS might have extra requirements on SSL certificate,
> Ex: macOS requires `ExtendedKeyUsage` https://support.apple.com/en-us/HT210176
### Don't skip SSL cert validation
> !!! DO NOT SKIP SSL CERT VALIDATION !!!
> !!! IT IS A BAD SECURITY PRACTICE !!!
### Download SSL certificate chain
Depends on how your SSL server certificate gets configured, you might need to download the whole certificate chain from a machine that has trusted the SSL certificate's CA.
- Approach 1: Download certificate chain using a browser (Chrome, Firefox, IT), you can google for more example, [here is what I found](https://medium.com/@menakajain/export-download-ssl-certificate-from-server-site-url-bcfc41ea46a2)
- Approach 2: Download certificate chain using OpenSSL, you can google for more example, [here is what I found](https://superuser.com/a/176721)
- Approach 3: Ask your network administrator or the owner of the CA certificate to send you a copy of it
### Trust CA certificate for the Runner
The actions runner is a dotnet core application which will follow how dotnet load SSL CA certificates on each OS.
You can get full details documentation at [here](https://docs.microsoft.com/en-us/dotnet/standard/security/cross-platform-cryptography#x509store)
In short:
- Windows: Load from Windows certificate store.
- Linux: Load from OpenSSL CA cert bundle.
- macOS: Load from macOS KeyChain.
To let the runner trusts your CA certificate, you will need to:
1. Save your SSL certificate chain which includes the root CA and all intermediate CAs into a `.pem` file.
2. Use `OpenSSL` to convert `.pem` file to a proper format for different OS, here is some [doc with sample commands](https://www.sslshopper.com/ssl-converter.html)
3. Trust CA on different OS:
- Windows: https://docs.microsoft.com/en-us/skype-sdk/sdn/articles/installing-the-trusted-root-certificate
- macOS: ![trust ca cert](./../res/macOStrustCA.gif)
- Linux: Refer to the distribution documentation
1. RedHat: https://www.redhat.com/sysadmin/ca-certificates-cli
2. Ubuntu: http://manpages.ubuntu.com/manpages/focal/man8/update-ca-certificates.8.html
3. Google search: "trust ca certificate on [linux distribution]"
4. If all approaches failed, set environment variable `SSL_CERT_FILE` to the CA bundle `.pem` file we get.
> To verity cert gets installed properly on Linux, you can try use `curl -v https://sitewithsslissue.com` and `pwsh -Command \"Invoke-WebRequest -Uri https://sitewithsslissue.com\"`
### Trust CA certificate for Git CLI
Git uses various CA bundle file depends on your operation system.
- Git packaged the CA bundle file within the Git installation on Windows
- Git use OpenSSL certificate CA bundle file on Linux and macOS
You can check where Git check CA file by running:
```bash
export GIT_CURL_VERBOSE=1
git ls-remote https://github.com/actions/runner HEAD
```
You should see something like:
```
* Couldn't find host github.com in the .netrc file; using defaults
* Trying 140.82.114.4...
* TCP_NODELAY set
* Connected to github.com (140.82.114.4) port 443 (#0)
* ALPN, offering h2
* ALPN, offering http/1.1
* successfully set certificate verify locations:
* CAfile: /etc/ssl/cert.pem
CApath: none
* SSL connection using TLSv1.2 / ECDHE-RSA-AES128-GCM-SHA256
```
This tells me `/etc/ssl/cert.pem` is where it read trusted CA certificates.
To let Git trusts your CA certificate, you will need to:
1. Save your SSL certificate chain which includes the root CA and all intermediate CAs into a `.pem` file.
2. Set `http.sslCAInfo` Git config or `GIT_SSL_CAINFO` environment variable to the full path of the `.pem` file [Git Doc](https://git-scm.com/docs/git-config#Documentation/git-config.txt-httpsslCAInfo)
> I would recommend using `http.sslCAInfo` since it can be scope to certain hosts that need the extra trusted CA.
> Ex: `git config --global http.https://myghes.com/.sslCAInfo /extra/ca/cert.pem`
> This will make Git use the `/extra/ca/cert.pem` only when communicates with `https://myghes.com` and keep using the default CA bundle with others.
### Trust CA certificate for Node.js
Node.js has compiled a snapshot of the Mozilla CA store that is fixed at each version of Node.js' release time.
To let Node.js trusts your CA certificate, you will need to:
1. Save your SSL certificate chain which includes the root CA and all intermediate CAs into a `.pem` file.
2. Set environment variable `NODE_EXTRA_CA_CERTS` which point to the file. ex: `export NODE_EXTRA_CA_CERTS=/full/path/to/cacert.pem` or `set NODE_EXTRA_CA_CERTS=C:\full\path\to\cacert.pem`

BIN
docs/res/macOStrustCA.gif Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 MiB

View File

@@ -0,0 +1,115 @@
const https = require('https')
const fs = require('fs')
const http = require('http')
const hostname = process.env['HOSTNAME'] || ''
const port = process.env['PORT'] || ''
const path = process.env['PATH'] || ''
const pat = process.env['PAT'] || ''
const proxyHost = process.env['PROXYHOST'] || ''
const proxyPort = process.env['PROXYPORT'] || ''
const proxyUsername = process.env['PROXYUSERNAME'] || ''
const proxyPassword = process.env['PROXYPASSWORD'] || ''
process.env['NODE_TLS_REJECT_UNAUTHORIZED'] = '0'
if (proxyHost === '') {
const options = {
hostname: hostname,
port: port,
path: path,
method: 'GET',
headers: {
'User-Agent': 'GitHubActionsRunnerCheck/1.0',
'Authorization': `token ${pat}`
},
}
const req = https.request(options, res => {
console.log(`statusCode: ${res.statusCode}`)
console.log(`headers: ${JSON.stringify(res.headers)}`)
let cert = socket.getPeerCertificate(true)
let certPEM = ''
let fingerprints = {}
while (cert != null && fingerprints[cert.fingerprint] != '1') {
fingerprints[cert.fingerprint] = '1'
certPEM = certPEM + '-----BEGIN CERTIFICATE-----\n'
let certEncoded = cert.raw.toString('base64')
for (let i = 0; i < certEncoded.length; i++) {
certPEM = certPEM + certEncoded[i]
if (i != certEncoded.length - 1 && (i + 1) % 64 == 0) {
certPEM = certPEM + '\n'
}
}
certPEM = certPEM + '\n-----END CERTIFICATE-----\n'
cert = cert.issuerCertificate
}
console.log(certPEM)
fs.writeFileSync('./download_ca_cert.pem', certPEM)
res.on('data', d => {
process.stdout.write(d)
})
})
req.on('error', error => {
console.error(error)
})
req.end()
}
else {
const auth = 'Basic ' + Buffer.from(proxyUsername + ':' + proxyPassword).toString('base64')
const options = {
host: proxyHost,
port: proxyPort,
method: 'CONNECT',
path: `${hostname}:${port}`,
}
if (proxyUsername != '' || proxyPassword != '') {
options.headers = {
'Proxy-Authorization': auth,
}
}
http.request(options).on('connect', (res, socket) => {
if (res.statusCode != 200) {
throw new Error(`Proxy returns code: ${res.statusCode}`)
}
https.get({
host: hostname,
port: port,
socket: socket,
agent: false,
path: '/',
headers: {
'User-Agent': 'GitHubActionsRunnerCheck/1.0',
'Authorization': `token ${pat}`
}
}, (res) => {
let cert = res.socket.getPeerCertificate(true)
let certPEM = ''
let fingerprints = {}
while (cert != null && fingerprints[cert.fingerprint] != '1') {
fingerprints[cert.fingerprint] = '1'
certPEM = certPEM + '-----BEGIN CERTIFICATE-----\n'
let certEncoded = cert.raw.toString('base64')
for (let i = 0; i < certEncoded.length; i++) {
certPEM = certPEM + certEncoded[i]
if (i != certEncoded.length - 1 && (i + 1) % 64 == 0) {
certPEM = certPEM + '\n'
}
}
certPEM = certPEM + '\n-----END CERTIFICATE-----\n'
cert = cert.issuerCertificate
}
console.log(certPEM)
fs.writeFileSync('./download_ca_cert.pem', certPEM)
console.log(`statusCode: ${res.statusCode}`)
console.log(`headers: ${JSON.stringify(res.headers)}`)
res.on('data', d => {
process.stdout.write(d)
})
})
}).on('error', (err) => {
console.error('error', err)
}).end()
}

View File

@@ -0,0 +1,75 @@
const https = require('https')
const http = require('http')
const hostname = process.env['HOSTNAME'] || ''
const port = process.env['PORT'] || ''
const path = process.env['PATH'] || ''
const pat = process.env['PAT'] || ''
const proxyHost = process.env['PROXYHOST'] || ''
const proxyPort = process.env['PROXYPORT'] || ''
const proxyUsername = process.env['PROXYUSERNAME'] || ''
const proxyPassword = process.env['PROXYPASSWORD'] || ''
if (proxyHost === '') {
const options = {
hostname: hostname,
port: port,
path: path,
method: 'GET',
headers: {
'User-Agent': 'GitHubActionsRunnerCheck/1.0',
'Authorization': `token ${pat}`,
}
}
const req = https.request(options, res => {
console.log(`statusCode: ${res.statusCode}`)
console.log(`headers: ${JSON.stringify(res.headers)}`)
res.on('data', d => {
process.stdout.write(d)
})
})
req.on('error', error => {
console.error(error)
})
req.end()
}
else {
const proxyAuth = 'Basic ' + Buffer.from(proxyUsername + ':' + proxyPassword).toString('base64')
const options = {
hostname: proxyHost,
port: proxyPort,
method: 'CONNECT',
path: `${hostname}:${port}`
}
if (proxyUsername != '' || proxyPassword != '') {
options.headers = {
'Proxy-Authorization': proxyAuth,
}
}
http.request(options).on('connect', (res, socket) => {
if (res.statusCode != 200) {
throw new Error(`Proxy returns code: ${res.statusCode}`)
}
https.get({
host: hostname,
port: port,
socket: socket,
agent: false,
path: path,
headers: {
'User-Agent': 'GitHubActionsRunnerCheck/1.0',
'Authorization': `token ${pat}`,
}
}, (res) => {
console.log(`statusCode: ${res.statusCode}`)
console.log(`headers: ${JSON.stringify(res.headers)}`)
res.on('data', d => {
process.stdout.write(d)
})
})
}).on('error', (err) => {
console.error('error', err)
}).end()
}

View File

@@ -121,6 +121,7 @@ namespace GitHub.Runner.Common
//validFlags array as well present in the CommandSettings.cs
public static class Flags
{
public static readonly string Check = "check";
public static readonly string Commit = "commit";
public static readonly string Help = "help";
public static readonly string Replace = "replace";

View File

@@ -60,6 +60,12 @@ namespace GitHub.Runner.Common
Add<T>(extensions, "GitHub.Runner.Worker.AddPathFileCommand, Runner.Worker");
Add<T>(extensions, "GitHub.Runner.Worker.SetEnvFileCommand, Runner.Worker");
break;
case "GitHub.Runner.Listener.Check.ICheckExtension":
Add<T>(extensions, "GitHub.Runner.Listener.Check.InternetCheck, Runner.Listener");
Add<T>(extensions, "GitHub.Runner.Listener.Check.ActionsCheck, Runner.Listener");
Add<T>(extensions, "GitHub.Runner.Listener.Check.GitCheck, Runner.Listener");
Add<T>(extensions, "GitHub.Runner.Listener.Check.NodeJsCheck, Runner.Listener");
break;
default:
// This should never happen.
throw new NotSupportedException($"Unexpected extension type: '{typeof(T).FullName}'");

View File

@@ -0,0 +1,90 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Threading.Tasks;
using GitHub.Runner.Common;
using GitHub.Runner.Sdk;
namespace GitHub.Runner.Listener.Check
{
public sealed class ActionsCheck : RunnerService, ICheckExtension
{
private string _logFile = null;
public int Order => 2;
public string CheckName => "GitHub Actions Connection";
public string CheckDescription => "Make sure the actions runner have access to the GitHub Actions Service.";
public string CheckLog => _logFile;
public string HelpLink => "https://github.com/actions/runner/blob/main/docs/checks/actions.md";
public Type ExtensionType => typeof(ICheckExtension);
public override void Initialize(IHostContext hostContext)
{
base.Initialize(hostContext);
_logFile = Path.Combine(HostContext.GetDirectory(WellKnownDirectory.Diag), StringUtil.Format("{0}_{1:yyyyMMdd-HHmmss}-utc.log", nameof(ActionsCheck), DateTime.UtcNow));
}
// runner access to actions service
public async Task<bool> RunCheck(string url, string pat)
{
await File.AppendAllLinesAsync(_logFile, HostContext.WarnLog());
await File.AppendAllLinesAsync(_logFile, HostContext.CheckProxy());
var checkTasks = new List<Task<CheckResult>>();
string githubApiUrl = null;
string actionsTokenServiceUrl = null;
string actionsPipelinesServiceUrl = null;
var urlBuilder = new UriBuilder(url);
if (UrlUtil.IsHostedServer(urlBuilder))
{
urlBuilder.Host = $"api.{urlBuilder.Host}";
urlBuilder.Path = "";
githubApiUrl = urlBuilder.Uri.AbsoluteUri;
actionsTokenServiceUrl = "https://vstoken.actions.githubusercontent.com/_apis/health";
actionsPipelinesServiceUrl = "https://pipelines.actions.githubusercontent.com/_apis/health";
}
else
{
urlBuilder.Path = "api/v3";
githubApiUrl = urlBuilder.Uri.AbsoluteUri;
urlBuilder.Path = "_services/vstoken/_apis/health";
actionsTokenServiceUrl = urlBuilder.Uri.AbsoluteUri;
urlBuilder.Path = "_services/pipelines/_apis/health";
actionsPipelinesServiceUrl = urlBuilder.Uri.AbsoluteUri;
}
// check github api
checkTasks.Add(CheckUtil.CheckDns(githubApiUrl));
checkTasks.Add(CheckUtil.CheckPing(githubApiUrl));
checkTasks.Add(HostContext.CheckHttpsRequests(githubApiUrl, pat, expectedHeader: "X-GitHub-Request-Id"));
// check actions token service
checkTasks.Add(CheckUtil.CheckDns(actionsTokenServiceUrl));
checkTasks.Add(CheckUtil.CheckPing(actionsTokenServiceUrl));
checkTasks.Add(HostContext.CheckHttpsRequests(actionsTokenServiceUrl, pat, expectedHeader: "x-vss-e2eid"));
// check actions pipelines service
checkTasks.Add(CheckUtil.CheckDns(actionsPipelinesServiceUrl));
checkTasks.Add(CheckUtil.CheckPing(actionsPipelinesServiceUrl));
checkTasks.Add(HostContext.CheckHttpsRequests(actionsPipelinesServiceUrl, pat, expectedHeader: "x-vss-e2eid"));
var result = true;
while (checkTasks.Count > 0)
{
var finishedCheckTask = await Task.WhenAny<CheckResult>(checkTasks);
var finishedCheck = await finishedCheckTask;
result = result && finishedCheck.Pass;
await File.AppendAllLinesAsync(_logFile, finishedCheck.Logs);
checkTasks.Remove(finishedCheckTask);
}
await Task.WhenAll(checkTasks);
return result;
}
}
}

View File

@@ -0,0 +1,351 @@
using System;
using System.Collections.Generic;
using System.Diagnostics.Tracing;
using System.IO;
using System.Linq;
using System.Net;
using System.Net.Http;
using System.Net.Http.Headers;
using System.Net.NetworkInformation;
using System.Threading;
using System.Threading.Tasks;
using GitHub.Runner.Common;
using GitHub.Runner.Sdk;
using GitHub.Services.Common;
namespace GitHub.Runner.Listener.Check
{
public static class CheckUtil
{
public static List<string> WarnLog(this IHostContext hostContext)
{
var logs = new List<string>();
logs.Add($"{DateTime.UtcNow.ToString("O")} ***************************************************************************************************************");
logs.Add($"{DateTime.UtcNow.ToString("O")} **** ****");
logs.Add($"{DateTime.UtcNow.ToString("O")} **** !!! WARNING !!! ");
logs.Add($"{DateTime.UtcNow.ToString("O")} **** DO NOT share the log in public place! The log may contains secrets in plain text. ");
logs.Add($"{DateTime.UtcNow.ToString("O")} **** !!! WARNING !!! ");
logs.Add($"{DateTime.UtcNow.ToString("O")} **** ****");
logs.Add($"{DateTime.UtcNow.ToString("O")} ***************************************************************************************************************");
return logs;
}
public static List<string> CheckProxy(this IHostContext hostContext)
{
var logs = new List<string>();
if (!string.IsNullOrEmpty(hostContext.WebProxy.HttpProxyAddress) ||
!string.IsNullOrEmpty(hostContext.WebProxy.HttpsProxyAddress))
{
logs.Add($"{DateTime.UtcNow.ToString("O")} ***************************************************************************************************************");
logs.Add($"{DateTime.UtcNow.ToString("O")} **** ****");
logs.Add($"{DateTime.UtcNow.ToString("O")} **** Runner is behind web proxy {hostContext.WebProxy.HttpsProxyAddress ?? hostContext.WebProxy.HttpProxyAddress} ");
logs.Add($"{DateTime.UtcNow.ToString("O")} **** ****");
logs.Add($"{DateTime.UtcNow.ToString("O")} ***************************************************************************************************************");
}
return logs;
}
public static async Task<CheckResult> CheckDns(string targetUrl)
{
var result = new CheckResult();
var url = new Uri(targetUrl);
try
{
result.Logs.Add($"{DateTime.UtcNow.ToString("O")} ***************************************************************************************************************");
result.Logs.Add($"{DateTime.UtcNow.ToString("O")} **** ****");
result.Logs.Add($"{DateTime.UtcNow.ToString("O")} **** Try DNS lookup for {url.Host} ");
result.Logs.Add($"{DateTime.UtcNow.ToString("O")} **** ****");
result.Logs.Add($"{DateTime.UtcNow.ToString("O")} ***************************************************************************************************************");
IPHostEntry host = await Dns.GetHostEntryAsync(url.Host);
foreach (var address in host.AddressList)
{
result.Logs.Add($"{DateTime.UtcNow.ToString("O")} Resolved DNS for {url.Host} to '{address}'");
}
result.Pass = true;
}
catch (Exception ex)
{
result.Pass = false;
result.Logs.Add($"{DateTime.UtcNow.ToString("O")} ***************************************************************************************************************");
result.Logs.Add($"{DateTime.UtcNow.ToString("O")} **** ****");
result.Logs.Add($"{DateTime.UtcNow.ToString("O")} **** Resolved DNS for {url.Host} failed with error: {ex}");
result.Logs.Add($"{DateTime.UtcNow.ToString("O")} **** ****");
result.Logs.Add($"{DateTime.UtcNow.ToString("O")} ***************************************************************************************************************");
}
return result;
}
public static async Task<CheckResult> CheckPing(string targetUrl)
{
var result = new CheckResult();
var url = new Uri(targetUrl);
try
{
result.Logs.Add($"{DateTime.UtcNow.ToString("O")} ***************************************************************************************************************");
result.Logs.Add($"{DateTime.UtcNow.ToString("O")} **** ****");
result.Logs.Add($"{DateTime.UtcNow.ToString("O")} **** Try ping {url.Host} ");
result.Logs.Add($"{DateTime.UtcNow.ToString("O")} **** ****");
result.Logs.Add($"{DateTime.UtcNow.ToString("O")} ***************************************************************************************************************");
using (var ping = new Ping())
{
var reply = await ping.SendPingAsync(url.Host);
if (reply.Status == IPStatus.Success)
{
result.Pass = true;
result.Logs.Add($"{DateTime.UtcNow.ToString("O")} Ping {url.Host} ({reply.Address}) succeed within to '{reply.RoundtripTime} ms'");
}
else
{
result.Pass = false;
result.Logs.Add($"{DateTime.UtcNow.ToString("O")} Ping {url.Host} ({reply.Address}) failed with '{reply.Status}'");
}
}
}
catch (Exception ex)
{
result.Pass = false;
result.Logs.Add($"{DateTime.UtcNow.ToString("O")} ***************************************************************************************************************");
result.Logs.Add($"{DateTime.UtcNow.ToString("O")} **** ****");
result.Logs.Add($"{DateTime.UtcNow.ToString("O")} **** Ping api.github.com failed with error: {ex}");
result.Logs.Add($"{DateTime.UtcNow.ToString("O")} **** ****");
result.Logs.Add($"{DateTime.UtcNow.ToString("O")} ***************************************************************************************************************");
}
return result;
}
public static async Task<CheckResult> CheckHttpsRequests(this IHostContext hostContext, string url, string pat, string expectedHeader)
{
var result = new CheckResult();
try
{
result.Logs.Add($"{DateTime.UtcNow.ToString("O")} ***************************************************************************************************************");
result.Logs.Add($"{DateTime.UtcNow.ToString("O")} **** ****");
result.Logs.Add($"{DateTime.UtcNow.ToString("O")} **** Send HTTPS Request to {url} ");
result.Logs.Add($"{DateTime.UtcNow.ToString("O")} **** ****");
result.Logs.Add($"{DateTime.UtcNow.ToString("O")} ***************************************************************************************************************");
using (var _ = new HttpEventSourceListener(result.Logs))
using (var httpClientHandler = hostContext.CreateHttpClientHandler())
using (var httpClient = new HttpClient(httpClientHandler))
{
httpClient.DefaultRequestHeaders.UserAgent.AddRange(hostContext.UserAgents);
if (!string.IsNullOrEmpty(pat))
{
httpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("token", pat);
}
var response = await httpClient.GetAsync(url);
result.Logs.Add($"{DateTime.UtcNow.ToString("O")} Http status code: {response.StatusCode}");
result.Logs.Add($"{DateTime.UtcNow.ToString("O")} Http response headers: {response.Headers}");
var responseContent = await response.Content.ReadAsStringAsync();
result.Logs.Add($"{DateTime.UtcNow.ToString("O")} Http response body: {responseContent}");
if (response.IsSuccessStatusCode)
{
if (response.Headers.Contains(expectedHeader))
{
result.Pass = true;
result.Logs.Add($"{DateTime.UtcNow.ToString("O")} ***************************************************************************************************************");
result.Logs.Add($"{DateTime.UtcNow.ToString("O")} Http request 'GET' to {url} succeed");
result.Logs.Add($"{DateTime.UtcNow.ToString("O")} ***************************************************************************************************************");
result.Logs.Add($"{DateTime.UtcNow.ToString("O")} ");
result.Logs.Add($"{DateTime.UtcNow.ToString("O")} ");
}
else
{
result.Pass = false;
result.Logs.Add($"{DateTime.UtcNow.ToString("O")} ***************************************************************************************************************");
result.Logs.Add($"{DateTime.UtcNow.ToString("O")} Http request 'GET' to {url} succeed but doesn't have expected HTTP Header.");
result.Logs.Add($"{DateTime.UtcNow.ToString("O")} ***************************************************************************************************************");
result.Logs.Add($"{DateTime.UtcNow.ToString("O")} ");
result.Logs.Add($"{DateTime.UtcNow.ToString("O")} ");
}
}
else
{
result.Pass = false;
result.Logs.Add($"{DateTime.UtcNow.ToString("O")} ***************************************************************************************************************");
result.Logs.Add($"{DateTime.UtcNow.ToString("O")} Http request 'GET' to {url} failed with {response.StatusCode}");
result.Logs.Add($"{DateTime.UtcNow.ToString("O")} ***************************************************************************************************************");
result.Logs.Add($"{DateTime.UtcNow.ToString("O")} ");
result.Logs.Add($"{DateTime.UtcNow.ToString("O")} ");
}
}
}
catch (Exception ex)
{
result.Pass = false;
result.Logs.Add($"{DateTime.UtcNow.ToString("O")} ***************************************************************************************************************");
result.Logs.Add($"{DateTime.UtcNow.ToString("O")} **** ****");
result.Logs.Add($"{DateTime.UtcNow.ToString("O")} **** Https request 'GET' to {url} failed with error: {ex}");
result.Logs.Add($"{DateTime.UtcNow.ToString("O")} **** ****");
result.Logs.Add($"{DateTime.UtcNow.ToString("O")} ***************************************************************************************************************");
}
return result;
}
public static async Task<CheckResult> DownloadExtraCA(this IHostContext hostContext, string url, string pat)
{
var result = new CheckResult();
try
{
result.Logs.Add($"{DateTime.UtcNow.ToString("O")} ***************************************************************************************************************");
result.Logs.Add($"{DateTime.UtcNow.ToString("O")} **** ****");
result.Logs.Add($"{DateTime.UtcNow.ToString("O")} **** Download SSL Certificate from {url} ");
result.Logs.Add($"{DateTime.UtcNow.ToString("O")} **** ****");
result.Logs.Add($"{DateTime.UtcNow.ToString("O")} ***************************************************************************************************************");
var uri = new Uri(url);
var env = new Dictionary<string, string>()
{
{ "HOSTNAME", uri.Host },
{ "PORT", uri.IsDefaultPort ? (uri.Scheme.ToLowerInvariant() == "https" ? "443" : "80") : uri.Port.ToString() },
{ "PATH", uri.AbsolutePath },
{ "PAT", pat }
};
var proxy = hostContext.WebProxy.GetProxy(uri);
if (proxy != null)
{
env["PROXYHOST"] = proxy.Host;
env["PROXYPORT"] = proxy.IsDefaultPort ? (proxy.Scheme.ToLowerInvariant() == "https" ? "443" : "80") : proxy.Port.ToString();
if (hostContext.WebProxy.HttpProxyUsername != null ||
hostContext.WebProxy.HttpsProxyUsername != null)
{
env["PROXYUSERNAME"] = hostContext.WebProxy.HttpProxyUsername ?? hostContext.WebProxy.HttpsProxyUsername;
env["PROXYPASSWORD"] = hostContext.WebProxy.HttpProxyPassword ?? hostContext.WebProxy.HttpsProxyPassword;
}
else
{
env["PROXYUSERNAME"] = "";
env["PROXYPASSWORD"] = "";
}
}
else
{
env["PROXYHOST"] = "";
env["PROXYPORT"] = "";
env["PROXYUSERNAME"] = "";
env["PROXYPASSWORD"] = "";
}
using (var processInvoker = hostContext.CreateService<IProcessInvoker>())
{
processInvoker.OutputDataReceived += new EventHandler<ProcessDataReceivedEventArgs>((sender, args) =>
{
if (!string.IsNullOrEmpty(args.Data))
{
result.Logs.Add($"{DateTime.UtcNow.ToString("O")} [STDOUT] {args.Data}");
}
});
processInvoker.ErrorDataReceived += new EventHandler<ProcessDataReceivedEventArgs>((sender, args) =>
{
if (!string.IsNullOrEmpty(args.Data))
{
result.Logs.Add($"{DateTime.UtcNow.ToString("O")} [STDERR] {args.Data}");
}
});
var downloadCertScript = Path.Combine(hostContext.GetDirectory(WellKnownDirectory.Bin), "checkScripts", "downloadCert");
var node12 = Path.Combine(hostContext.GetDirectory(WellKnownDirectory.Externals), "node12", "bin", $"node{IOUtil.ExeExtension}");
result.Logs.Add($"{DateTime.UtcNow.ToString("O")} Run '{node12} \"{downloadCertScript}\"' ");
result.Logs.Add($"{DateTime.UtcNow.ToString("O")} {StringUtil.ConvertToJson(env)}");
await processInvoker.ExecuteAsync(
hostContext.GetDirectory(WellKnownDirectory.Root),
node12,
$"\"{downloadCertScript}\"",
env,
true,
CancellationToken.None);
}
result.Pass = true;
}
catch (Exception ex)
{
result.Pass = false;
result.Logs.Add($"{DateTime.UtcNow.ToString("O")} ***************************************************************************************************************");
result.Logs.Add($"{DateTime.UtcNow.ToString("O")} **** ****");
result.Logs.Add($"{DateTime.UtcNow.ToString("O")} **** Download SSL Certificate from '{url}' failed with error: {ex}");
result.Logs.Add($"{DateTime.UtcNow.ToString("O")} **** ****");
result.Logs.Add($"{DateTime.UtcNow.ToString("O")} ***************************************************************************************************************");
}
return result;
}
}
// EventSource listener for dotnet debug trace for HTTP and SSL
public sealed class HttpEventSourceListener : EventListener
{
private readonly List<string> _logs;
private readonly object _lock = new object();
private readonly Dictionary<string, HashSet<string>> _ignoredEvent = new Dictionary<string, HashSet<string>>
{
{
"Private.InternalDiagnostics.System.Net.Http",
new HashSet<string>
{
"Info",
"Associate"
}
},
{
"Private.InternalDiagnostics.System.Net.Security",
new HashSet<string>
{
"Info",
"SslStreamCtor",
"SecureChannelCtor",
"NoDelegateNoClientCert",
"CertsAfterFiltering",
"UsingCachedCredential",
"SspiSelectedCipherSuite"
}
}
};
public HttpEventSourceListener(List<string> logs)
{
_logs = logs;
if (Environment.GetEnvironmentVariable("ACTIONS_RUNNER_TRACE_ALL_HTTP_EVENT") == "1")
{
_ignoredEvent.Clear();
}
}
protected override void OnEventSourceCreated(EventSource eventSource)
{
base.OnEventSourceCreated(eventSource);
if (eventSource.Name == "Private.InternalDiagnostics.System.Net.Http" ||
eventSource.Name == "Private.InternalDiagnostics.System.Net.Security")
{
EnableEvents(eventSource, EventLevel.Verbose, EventKeywords.All);
}
}
protected override void OnEventWritten(EventWrittenEventArgs eventData)
{
base.OnEventWritten(eventData);
lock (_lock)
{
if (_ignoredEvent.TryGetValue(eventData.EventSource.Name, out var ignored) &&
ignored.Contains(eventData.EventName))
{
return;
}
_logs.Add($"{DateTime.UtcNow.ToString("O")} [START {eventData.EventSource.Name} - {eventData.EventName}]");
_logs.AddRange(eventData.Payload.Select(x => string.Join(Environment.NewLine, x.ToString().Split(Environment.NewLine).Select(y => $"{DateTime.UtcNow.ToString("O")} {y}"))));
_logs.Add($"{DateTime.UtcNow.ToString("O")} [END {eventData.EventSource.Name} - {eventData.EventName}]");
}
}
}
}

View File

@@ -0,0 +1,171 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Net;
using System.Threading;
using System.Threading.Tasks;
using GitHub.Runner.Common;
using GitHub.Runner.Sdk;
namespace GitHub.Runner.Listener.Check
{
public sealed class GitCheck : RunnerService, ICheckExtension
{
private string _logFile = null;
private string _gitPath = null;
public int Order => 3;
public string CheckName => "Git Certificate/Proxy Validation";
public string CheckDescription => "Make sure the git cli can access to GitHub.com or the GitHub Enterprise Server.";
public string CheckLog => _logFile;
public string HelpLink => "https://github.com/actions/runner/blob/main/docs/checks/git.md";
public Type ExtensionType => typeof(ICheckExtension);
public override void Initialize(IHostContext hostContext)
{
base.Initialize(hostContext);
_logFile = Path.Combine(HostContext.GetDirectory(WellKnownDirectory.Diag), StringUtil.Format("{0}_{1:yyyyMMdd-HHmmss}-utc.log", nameof(GitCheck), DateTime.UtcNow));
_gitPath = WhichUtil.Which("git");
}
// git access to ghes/gh
public async Task<bool> RunCheck(string url, string pat)
{
await File.AppendAllLinesAsync(_logFile, HostContext.WarnLog());
await File.AppendAllLinesAsync(_logFile, HostContext.CheckProxy());
if (string.IsNullOrEmpty(_gitPath))
{
await File.AppendAllLinesAsync(_logFile, new[] { $"{DateTime.UtcNow.ToString("O")} Can't verify git with GitHub.com or GitHub Enterprise Server since git is not installed." });
return false;
}
var checkGit = await CheckGit(url, pat);
var result = checkGit.Pass;
await File.AppendAllLinesAsync(_logFile, checkGit.Logs);
// try fix SSL error by providing extra CA certificate.
if (checkGit.SslError)
{
await File.AppendAllLinesAsync(_logFile, new[] { $"{DateTime.UtcNow.ToString("O")} Try fix SSL error by providing extra CA certificate." });
var downloadCert = await HostContext.DownloadExtraCA(url, pat);
await File.AppendAllLinesAsync(_logFile, downloadCert.Logs);
if (downloadCert.Pass)
{
var recheckGit = await CheckGit(url, pat, extraCA: true);
await File.AppendAllLinesAsync(_logFile, recheckGit.Logs);
if (recheckGit.Pass)
{
await File.AppendAllLinesAsync(_logFile, new[] { $"{DateTime.UtcNow.ToString("O")} Fixed SSL error by providing extra CA certs." });
}
}
}
return result;
}
private async Task<CheckResult> CheckGit(string url, string pat, bool extraCA = false)
{
var result = new CheckResult();
try
{
result.Logs.Add($"{DateTime.UtcNow.ToString("O")} ***************************************************************************************************************");
result.Logs.Add($"{DateTime.UtcNow.ToString("O")} **** ****");
result.Logs.Add($"{DateTime.UtcNow.ToString("O")} **** Validate server cert and proxy configuration with Git ");
result.Logs.Add($"{DateTime.UtcNow.ToString("O")} **** ****");
result.Logs.Add($"{DateTime.UtcNow.ToString("O")} ***************************************************************************************************************");
var repoUrlBuilder = new UriBuilder(url);
repoUrlBuilder.Path = "actions/checkout";
repoUrlBuilder.UserName = "gh";
repoUrlBuilder.Password = pat;
var gitProxy = "";
var proxy = HostContext.WebProxy.GetProxy(repoUrlBuilder.Uri);
if (proxy != null)
{
result.Logs.Add($"{DateTime.UtcNow.ToString("O")} Runner is behind http proxy '{proxy.AbsoluteUri}'");
if (HostContext.WebProxy.HttpProxyUsername != null ||
HostContext.WebProxy.HttpsProxyUsername != null)
{
var proxyUrlWithCred = UrlUtil.GetCredentialEmbeddedUrl(
proxy,
HostContext.WebProxy.HttpProxyUsername ?? HostContext.WebProxy.HttpsProxyUsername,
HostContext.WebProxy.HttpProxyPassword ?? HostContext.WebProxy.HttpsProxyPassword);
gitProxy = $"-c http.proxy={proxyUrlWithCred}";
}
else
{
gitProxy = $"-c http.proxy={proxy.AbsoluteUri}";
}
}
using (var processInvoker = HostContext.CreateService<IProcessInvoker>())
{
processInvoker.OutputDataReceived += new EventHandler<ProcessDataReceivedEventArgs>((sender, args) =>
{
if (!string.IsNullOrEmpty(args.Data))
{
result.Logs.Add($"{DateTime.UtcNow.ToString("O")} {args.Data}");
}
});
processInvoker.ErrorDataReceived += new EventHandler<ProcessDataReceivedEventArgs>((sender, args) =>
{
if (!string.IsNullOrEmpty(args.Data))
{
result.Logs.Add($"{DateTime.UtcNow.ToString("O")} {args.Data}");
}
});
var gitArgs = $"{gitProxy} ls-remote --exit-code {repoUrlBuilder.Uri.AbsoluteUri} HEAD";
result.Logs.Add($"{DateTime.UtcNow.ToString("O")} Run 'git {gitArgs}' ");
var env = new Dictionary<string, string>
{
{ "GIT_TRACE", "1" },
{ "GIT_CURL_VERBOSE", "1" }
};
if (extraCA)
{
env["GIT_SSL_CAINFO"] = Path.Combine(HostContext.GetDirectory(WellKnownDirectory.Root), "download_ca_cert.pem");
}
await processInvoker.ExecuteAsync(
HostContext.GetDirectory(WellKnownDirectory.Root),
_gitPath,
gitArgs,
env,
true,
CancellationToken.None);
}
result.Pass = true;
}
catch (Exception ex)
{
result.Pass = false;
result.Logs.Add($"{DateTime.UtcNow.ToString("O")} ***************************************************************************************************************");
result.Logs.Add($"{DateTime.UtcNow.ToString("O")} **** ****");
result.Logs.Add($"{DateTime.UtcNow.ToString("O")} **** git ls-remote failed with error: {ex}");
if (result.Logs.Any(x => x.Contains("SSL Certificate problem", StringComparison.OrdinalIgnoreCase)))
{
result.Logs.Add($"{DateTime.UtcNow.ToString("O")} **** git ls-remote failed due to SSL cert issue.");
result.SslError = true;
}
result.Logs.Add($"{DateTime.UtcNow.ToString("O")} **** ****");
result.Logs.Add($"{DateTime.UtcNow.ToString("O")} ***************************************************************************************************************");
}
return result;
}
}
}

View File

@@ -0,0 +1,30 @@
using System.Collections.Generic;
using System.Threading.Tasks;
using GitHub.Runner.Common;
namespace GitHub.Runner.Listener.Check
{
public interface ICheckExtension : IExtension
{
int Order { get; }
string CheckName { get; }
string CheckDescription { get; }
string CheckLog { get; }
string HelpLink { get; }
Task<bool> RunCheck(string url, string pat);
}
public class CheckResult
{
public CheckResult()
{
Logs = new List<string>();
}
public bool Pass { get; set; }
public bool SslError { get; set; }
public List<string> Logs { get; set; }
}
}

View File

@@ -0,0 +1,59 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Threading.Tasks;
using GitHub.Runner.Common;
using GitHub.Runner.Sdk;
namespace GitHub.Runner.Listener.Check
{
public sealed class InternetCheck : RunnerService, ICheckExtension
{
private string _logFile = null;
public int Order => 1;
public string CheckName => "Internet Connection";
public string CheckDescription => "Make sure the actions runner have access to public internet.";
public string CheckLog => _logFile;
public string HelpLink => "https://github.com/actions/runner/blob/main/docs/checks/internet.md";
public Type ExtensionType => typeof(ICheckExtension);
public override void Initialize(IHostContext hostContext)
{
base.Initialize(hostContext);
_logFile = Path.Combine(HostContext.GetDirectory(WellKnownDirectory.Diag), StringUtil.Format("{0}_{1:yyyyMMdd-HHmmss}-utc.log", nameof(InternetCheck), DateTime.UtcNow));
}
// check runner access to api.github.com
public async Task<bool> RunCheck(string url, string pat)
{
await File.AppendAllLinesAsync(_logFile, HostContext.WarnLog());
await File.AppendAllLinesAsync(_logFile, HostContext.CheckProxy());
var checkTasks = new List<Task<CheckResult>>();
checkTasks.Add(CheckUtil.CheckDns("https://api.github.com"));
checkTasks.Add(CheckUtil.CheckPing("https://api.github.com"));
// We don't need to pass a PAT since it might be a token for GHES.
checkTasks.Add(HostContext.CheckHttpsRequests("https://api.github.com", pat: null, expectedHeader: "X-GitHub-Request-Id"));
var result = true;
while (checkTasks.Count > 0)
{
var finishedCheckTask = await Task.WhenAny<CheckResult>(checkTasks);
var finishedCheck = await finishedCheckTask;
result = result && finishedCheck.Pass;
await File.AppendAllLinesAsync(_logFile, finishedCheck.Logs);
checkTasks.Remove(finishedCheckTask);
}
await Task.WhenAll(checkTasks);
return result;
}
}
}

View File

@@ -0,0 +1,181 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Net;
using System.Threading;
using System.Threading.Tasks;
using GitHub.Runner.Common;
using GitHub.Runner.Sdk;
namespace GitHub.Runner.Listener.Check
{
public sealed class NodeJsCheck : RunnerService, ICheckExtension
{
private string _logFile = null;
public int Order => 4;
public string CheckName => "Node.js Certificate/Proxy Validation";
public string CheckDescription => "Make sure the node.js have access to GitHub.com or the GitHub Enterprise Server.";
public string CheckLog => _logFile;
public string HelpLink => "https://github.com/actions/runner/blob/main/docs/checks/nodejs.md";
public Type ExtensionType => typeof(ICheckExtension);
public override void Initialize(IHostContext hostContext)
{
base.Initialize(hostContext);
_logFile = Path.Combine(HostContext.GetDirectory(WellKnownDirectory.Diag), StringUtil.Format("{0}_{1:yyyyMMdd-HHmmss}-utc.log", nameof(NodeJsCheck), DateTime.UtcNow));
}
// node access to ghes/gh
public async Task<bool> RunCheck(string url, string pat)
{
await File.AppendAllLinesAsync(_logFile, HostContext.WarnLog());
await File.AppendAllLinesAsync(_logFile, HostContext.CheckProxy());
// Request to github.com or ghes server
var urlBuilder = new UriBuilder(url);
if (UrlUtil.IsHostedServer(urlBuilder))
{
urlBuilder.Host = $"api.{urlBuilder.Host}";
urlBuilder.Path = "";
}
else
{
urlBuilder.Path = "api/v3";
}
var checkNode = await CheckNodeJs(urlBuilder.Uri.AbsoluteUri, pat);
var result = checkNode.Pass;
await File.AppendAllLinesAsync(_logFile, checkNode.Logs);
// try fix SSL error by providing extra CA certificate.
if (checkNode.SslError)
{
var downloadCert = await HostContext.DownloadExtraCA(urlBuilder.Uri.AbsoluteUri, pat);
await File.AppendAllLinesAsync(_logFile, downloadCert.Logs);
if (downloadCert.Pass)
{
var recheckNode = await CheckNodeJs(urlBuilder.Uri.AbsoluteUri, pat, extraCA: true);
await File.AppendAllLinesAsync(_logFile, recheckNode.Logs);
if (recheckNode.Pass)
{
await File.AppendAllLinesAsync(_logFile, new[] { $"{DateTime.UtcNow.ToString("O")} Fixed SSL error by providing extra CA certs." });
}
}
}
return result;
}
private async Task<CheckResult> CheckNodeJs(string url, string pat, bool extraCA = false)
{
var result = new CheckResult();
try
{
result.Logs.Add($"{DateTime.UtcNow.ToString("O")} ***************************************************************************************************************");
result.Logs.Add($"{DateTime.UtcNow.ToString("O")} **** ****");
result.Logs.Add($"{DateTime.UtcNow.ToString("O")} **** Make Http request to {url} using node.js ");
result.Logs.Add($"{DateTime.UtcNow.ToString("O")} **** ****");
result.Logs.Add($"{DateTime.UtcNow.ToString("O")} ***************************************************************************************************************");
// Request to github.com or ghes server
Uri requestUrl = new Uri(url);
var env = new Dictionary<string, string>()
{
{ "HOSTNAME", requestUrl.Host },
{ "PORT", requestUrl.IsDefaultPort ? (requestUrl.Scheme.ToLowerInvariant() == "https" ? "443" : "80") : requestUrl.Port.ToString() },
{ "PATH", requestUrl.AbsolutePath },
{ "PAT", pat }
};
var proxy = HostContext.WebProxy.GetProxy(requestUrl);
if (proxy != null)
{
env["PROXYHOST"] = proxy.Host;
env["PROXYPORT"] = proxy.IsDefaultPort ? (proxy.Scheme.ToLowerInvariant() == "https" ? "443" : "80") : proxy.Port.ToString();
if (HostContext.WebProxy.HttpProxyUsername != null ||
HostContext.WebProxy.HttpsProxyUsername != null)
{
env["PROXYUSERNAME"] = HostContext.WebProxy.HttpProxyUsername ?? HostContext.WebProxy.HttpsProxyUsername;
env["PROXYPASSWORD"] = HostContext.WebProxy.HttpProxyPassword ?? HostContext.WebProxy.HttpsProxyPassword;
}
else
{
env["PROXYUSERNAME"] = "";
env["PROXYPASSWORD"] = "";
}
}
else
{
env["PROXYHOST"] = "";
env["PROXYPORT"] = "";
env["PROXYUSERNAME"] = "";
env["PROXYPASSWORD"] = "";
}
if (extraCA)
{
env["NODE_EXTRA_CA_CERTS"] = Path.Combine(HostContext.GetDirectory(WellKnownDirectory.Root), "download_ca_cert.pem");
}
using (var processInvoker = HostContext.CreateService<IProcessInvoker>())
{
processInvoker.OutputDataReceived += new EventHandler<ProcessDataReceivedEventArgs>((sender, args) =>
{
if (!string.IsNullOrEmpty(args.Data))
{
result.Logs.Add($"{DateTime.UtcNow.ToString("O")} [STDOUT] {args.Data}");
}
});
processInvoker.ErrorDataReceived += new EventHandler<ProcessDataReceivedEventArgs>((sender, args) =>
{
if (!string.IsNullOrEmpty(args.Data))
{
result.Logs.Add($"{DateTime.UtcNow.ToString("O")} [STDERR] {args.Data}");
}
});
var makeWebRequestScript = Path.Combine(HostContext.GetDirectory(WellKnownDirectory.Bin), "checkScripts", "makeWebRequest.js");
var node12 = Path.Combine(HostContext.GetDirectory(WellKnownDirectory.Externals), "node12", "bin", $"node{IOUtil.ExeExtension}");
result.Logs.Add($"{DateTime.UtcNow.ToString("O")} Run '{node12} \"{makeWebRequestScript}\"' ");
result.Logs.Add($"{DateTime.UtcNow.ToString("O")} {StringUtil.ConvertToJson(env)}");
await processInvoker.ExecuteAsync(
HostContext.GetDirectory(WellKnownDirectory.Root),
node12,
$"\"{makeWebRequestScript}\"",
env,
true,
CancellationToken.None);
}
result.Pass = true;
}
catch (Exception ex)
{
result.Pass = false;
result.Logs.Add($"{DateTime.UtcNow.ToString("O")} ***************************************************************************************************************");
result.Logs.Add($"{DateTime.UtcNow.ToString("O")} **** ****");
result.Logs.Add($"{DateTime.UtcNow.ToString("O")} **** Make https request to {url} using node.js failed with error: {ex}");
if (result.Logs.Any(x => x.Contains("UNABLE_TO_VERIFY_LEAF_SIGNATURE") ||
x.Contains("UNABLE_TO_GET_ISSUER_CERT_LOCALLY") ||
x.Contains("SELF_SIGNED_CERT_IN_CHAIN")))
{
result.Logs.Add($"{DateTime.UtcNow.ToString("O")} **** Https request failed due to SSL cert issue.");
result.SslError = true;
}
result.Logs.Add($"{DateTime.UtcNow.ToString("O")} **** ****");
result.Logs.Add($"{DateTime.UtcNow.ToString("O")} ***************************************************************************************************************");
}
return result;
}
}
}

View File

@@ -27,6 +27,7 @@ namespace GitHub.Runner.Listener
private readonly string[] validFlags =
{
Constants.Runner.CommandLine.Flags.Check,
Constants.Runner.CommandLine.Flags.Commit,
Constants.Runner.CommandLine.Flags.Help,
Constants.Runner.CommandLine.Flags.Replace,
@@ -60,6 +61,7 @@ namespace GitHub.Runner.Listener
public bool Warmup => TestCommand(Constants.Runner.CommandLine.Commands.Warmup);
// Flags.
public bool Check => TestFlag(Constants.Runner.CommandLine.Flags.Check);
public bool Commit => TestFlag(Constants.Runner.CommandLine.Flags.Commit);
public bool Help => TestFlag(Constants.Runner.CommandLine.Flags.Help);
public bool Unattended => TestFlag(Constants.Runner.CommandLine.Flags.Unattended);
@@ -188,10 +190,21 @@ namespace GitHub.Runner.Listener
validator: Validators.NonEmptyValidator);
}
public string GetGitHubPersonalAccessToken()
public string GetGitHubPersonalAccessToken(bool required = false)
{
if (required)
{
return GetArgOrPrompt(
name: Constants.Runner.CommandLine.Args.PAT,
description: "What is your GitHub personal access token?",
defaultValue: string.Empty,
validator: Validators.NonEmptyValidator);
}
else
{
return GetArg(name: Constants.Runner.CommandLine.Args.PAT);
}
}
public string GetRunnerRegisterToken()
{

View File

@@ -117,7 +117,7 @@ namespace GitHub.Runner.Listener.Configuration
try
{
// Determine the service deployment type based on connection data. (Hosted/OnPremises)
runnerSettings.IsHostedServer = runnerSettings.GitHubUrl == null || IsHostedServer(new UriBuilder(runnerSettings.GitHubUrl));
runnerSettings.IsHostedServer = runnerSettings.GitHubUrl == null || UrlUtil.IsHostedServer(new UriBuilder(runnerSettings.GitHubUrl));
// Warn if the Actions server url and GHES server url has different Host
if (!runnerSettings.IsHostedServer)
@@ -508,13 +508,6 @@ namespace GitHub.Runner.Listener.Configuration
return agent;
}
private bool IsHostedServer(UriBuilder gitHubUrl)
{
return string.Equals(gitHubUrl.Host, "github.com", StringComparison.OrdinalIgnoreCase) ||
string.Equals(gitHubUrl.Host, "www.github.com", StringComparison.OrdinalIgnoreCase) ||
string.Equals(gitHubUrl.Host, "github.localhost", StringComparison.OrdinalIgnoreCase);
}
private async Task<string> GetRunnerTokenAsync(CommandSettings command, string githubUrl, string tokenType)
{
var githubPAT = command.GetGitHubPersonalAccessToken();
@@ -551,7 +544,7 @@ namespace GitHub.Runner.Listener.Configuration
if (path.Length == 1)
{
// org runner
if (IsHostedServer(gitHubUrlBuilder))
if (UrlUtil.IsHostedServer(gitHubUrlBuilder))
{
githubApiUrl = $"{gitHubUrlBuilder.Scheme}://api.{gitHubUrlBuilder.Host}/orgs/{path[0]}/actions/runners/{tokenType}-token";
}
@@ -569,7 +562,7 @@ namespace GitHub.Runner.Listener.Configuration
repoScope = "";
}
if (IsHostedServer(gitHubUrlBuilder))
if (UrlUtil.IsHostedServer(gitHubUrlBuilder))
{
githubApiUrl = $"{gitHubUrlBuilder.Scheme}://api.{gitHubUrlBuilder.Host}/{repoScope}{path[0]}/{path[1]}/actions/runners/{tokenType}-token";
}
@@ -615,7 +608,7 @@ namespace GitHub.Runner.Listener.Configuration
{
var githubApiUrl = "";
var gitHubUrlBuilder = new UriBuilder(githubUrl);
if (IsHostedServer(gitHubUrlBuilder))
if (UrlUtil.IsHostedServer(gitHubUrlBuilder))
{
githubApiUrl = $"{gitHubUrlBuilder.Scheme}://api.{gitHubUrlBuilder.Host}/actions/runner-registration";
}

View File

@@ -1,6 +1,5 @@
using GitHub.DistributedTask.WebApi;
using GitHub.Runner.Listener.Configuration;
using GitHub.Runner.Common.Util;
using System;
using System.Threading;
using System.Threading.Tasks;
@@ -11,6 +10,8 @@ using System.Reflection;
using System.Runtime.CompilerServices;
using GitHub.Runner.Common;
using GitHub.Runner.Sdk;
using System.Linq;
using GitHub.Runner.Listener.Check;
namespace GitHub.Runner.Listener
{
@@ -72,6 +73,46 @@ namespace GitHub.Runner.Listener
return Constants.Runner.ReturnCode.Success;
}
if (command.Check)
{
var url = command.GetUrl();
var pat = command.GetGitHubPersonalAccessToken(required: true);
var checkExtensions = HostContext.GetService<IExtensionManager>().GetExtensions<ICheckExtension>();
var sortedChecks = checkExtensions.OrderBy(x => x.Order);
foreach (var check in sortedChecks)
{
_term.WriteLine($"**********************************************************************************************************************");
_term.WriteLine($"** Check: {check.CheckName}");
_term.WriteLine($"** Description: {check.CheckDescription}");
_term.WriteLine($"**********************************************************************************************************************");
var result = await check.RunCheck(url, pat);
if (!result)
{
_term.WriteLine($"** **");
_term.WriteLine($"** F A I L **");
_term.WriteLine($"** **");
_term.WriteLine($"**********************************************************************************************************************");
_term.WriteLine($"** Log: {check.CheckLog}");
_term.WriteLine($"** Help Doc: {check.HelpLink}");
_term.WriteLine($"**********************************************************************************************************************");
}
else
{
_term.WriteLine($"** **");
_term.WriteLine($"** P A S S **");
_term.WriteLine($"** **");
_term.WriteLine($"**********************************************************************************************************************");
_term.WriteLine($"** Log: {check.CheckLog}");
_term.WriteLine($"**********************************************************************************************************************");
}
_term.WriteLine();
_term.WriteLine();
}
return Constants.Runner.ReturnCode.Success;
}
// Configure runner prompt for args if not supplied
// Unattended configure mode will not prompt for args if not supplied and error on any missing or invalid value.
if (command.Configure)

View File

@@ -4,6 +4,13 @@ namespace GitHub.Runner.Sdk
{
public static class UrlUtil
{
public static bool IsHostedServer(UriBuilder gitHubUrl)
{
return string.Equals(gitHubUrl.Host, "github.com", StringComparison.OrdinalIgnoreCase) ||
string.Equals(gitHubUrl.Host, "www.github.com", StringComparison.OrdinalIgnoreCase) ||
string.Equals(gitHubUrl.Host, "github.localhost", StringComparison.OrdinalIgnoreCase);
}
public static Uri GetCredentialEmbeddedUrl(Uri baseUrl, string username, string password)
{
ArgUtil.NotNull(baseUrl, nameof(baseUrl));

View File

@@ -1,4 +1,5 @@
using GitHub.Runner.Listener;
using GitHub.Runner.Listener.Check;
using GitHub.Runner.Listener.Configuration;
using GitHub.Runner.Worker;
using GitHub.Runner.Worker.Handlers;
@@ -21,7 +22,8 @@ namespace GitHub.Runner.Common.Tests
// Otherwise, the interface needs to whitelisted.
var whitelist = new[]
{
typeof(ICredentialProvider)
typeof(ICredentialProvider),
typeof(ICheckExtension),
};
Validate(
assembly: typeof(IMessageListener).GetTypeInfo().Assembly,
@@ -85,7 +87,8 @@ namespace GitHub.Runner.Common.Tests
continue;
}
if (interfaceTypeInfo.FullName.Contains("IConverter")){
if (interfaceTypeInfo.FullName.Contains("IConverter"))
{
continue;
}