mirror of
https://github.com/actions/runner.git
synced 2025-12-10 20:36:49 +00:00
Compare commits
16 Commits
users/juli
...
v2.164.0
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
40302373ba | ||
|
|
9a08f7418f | ||
|
|
80b6038cdc | ||
|
|
70a09bc5ac | ||
|
|
c6cf1eb3f1 | ||
|
|
50d979f1b2 | ||
|
|
91b7e7a07a | ||
|
|
d0a4a41a63 | ||
|
|
c3c66bb14a | ||
|
|
86df779fe9 | ||
|
|
1918906505 | ||
|
|
9448135fcd | ||
|
|
f3aedd86fd | ||
|
|
d778f13dee | ||
|
|
9bbbca9e5d | ||
|
|
2cac011558 |
137
.github/workflows/release.yml
vendored
137
.github/workflows/release.yml
vendored
@@ -3,10 +3,47 @@ name: Runner CD
|
||||
on:
|
||||
push:
|
||||
paths:
|
||||
- src/runnerversion_block # Change this to src/runnerversion when we are ready.
|
||||
- releaseVersion
|
||||
|
||||
jobs:
|
||||
check:
|
||||
if: startsWith(github.ref, 'refs/heads/releases/') || github.ref == 'refs/heads/master'
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
|
||||
# Make sure ./releaseVersion match ./src/runnerversion
|
||||
# Query GitHub release ensure version is not used
|
||||
- name: Check version
|
||||
uses: actions/github-script@0.3.0
|
||||
with:
|
||||
github-token: ${{secrets.GITHUB_TOKEN}}
|
||||
script: |
|
||||
const core = require('@actions/core')
|
||||
const fs = require('fs');
|
||||
const runnerVersion = fs.readFileSync('${{ github.workspace }}/src/runnerversion', 'utf8').replace(/\n$/g, '')
|
||||
const releaseVersion = fs.readFileSync('${{ github.workspace }}/releaseVersion', 'utf8').replace(/\n$/g, '')
|
||||
if (runnerVersion != releaseVersion) {
|
||||
console.log('Request Release Version: ' + releaseVersion + '\nCurrent Runner Version: ' + runnerVersion)
|
||||
core.setFailed('Version mismatch! Make sure ./releaseVersion match ./src/runnerVersion')
|
||||
return
|
||||
}
|
||||
try {
|
||||
const release = await github.repos.getReleaseByTag({
|
||||
owner: '${{ github.event.repository.owner.name }}',
|
||||
repo: '${{ github.event.repository.name }}',
|
||||
tag: 'v' + runnerVersion
|
||||
})
|
||||
core.setFailed('Release with same tag already created: ' + release.data.html_url)
|
||||
} catch (e) {
|
||||
// We are good to create the release if release with same tag doesn't exists
|
||||
if (e.status != 404) {
|
||||
throw e
|
||||
}
|
||||
}
|
||||
|
||||
build:
|
||||
needs: check
|
||||
strategy:
|
||||
matrix:
|
||||
runtime: [ linux-x64, linux-arm64, linux-arm, win-x64, osx-x64 ]
|
||||
@@ -52,7 +89,7 @@ jobs:
|
||||
- name: Package Release
|
||||
if: github.event_name != 'pull_request'
|
||||
run: |
|
||||
${{ matrix.devScript }} package Release
|
||||
${{ matrix.devScript }} package Release ${{ matrix.runtime }}
|
||||
working-directory: src
|
||||
|
||||
# Upload runner package tar.gz/zip as artifact.
|
||||
@@ -66,14 +103,17 @@ jobs:
|
||||
|
||||
release:
|
||||
needs: build
|
||||
runs-on: linux-latest
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
|
||||
- uses: actions/checkout@v2
|
||||
|
||||
# Download runner package tar.gz/zip produced by 'build' job
|
||||
- name: Download Artifact
|
||||
uses: actions/download-artifact@v1
|
||||
with:
|
||||
name: runner-packages
|
||||
path: ./
|
||||
|
||||
# Create ReleaseNote file
|
||||
- name: Create ReleaseNote
|
||||
@@ -82,103 +122,74 @@ jobs:
|
||||
with:
|
||||
github-token: ${{secrets.GITHUB_TOKEN}}
|
||||
script: |
|
||||
const core = require('@actions/core')
|
||||
const fs = require('fs');
|
||||
// Get runner version from ./src/runnerVersion file
|
||||
const versionContent = await github.repos.getContents({
|
||||
owner: '${{ github.event.repository.owner.name }}',
|
||||
repo: '${{ github.event.repository.name }}',
|
||||
path: 'src/runnerversion'
|
||||
ref: ${{ github.sha }}
|
||||
})
|
||||
const runnerVersion = Buffer.from(versionContent.data.content, 'base64').toString()
|
||||
console.log("Runner Version ' + runnerVersion)
|
||||
const runnerVersion = fs.readFileSync('${{ github.workspace }}/src/runnerversion', 'utf8').replace(/\n$/g, '')
|
||||
const releaseNote = fs.readFileSync('${{ github.workspace }}/releaseNote.md', 'utf8').replace(/<RUNNER_VERSION>/g, runnerVersion)
|
||||
console.log(releaseNote)
|
||||
core.setOutput('version', runnerVersion);
|
||||
|
||||
// Query GitHub release ensure version is bumped
|
||||
const latestRelease = await github.repos.getLatestRelease({
|
||||
owner: '${{ github.event.repository.owner.name }}',
|
||||
repo: '${{ github.event.repository.name }}'
|
||||
})
|
||||
console.log(latestRelease.name)
|
||||
const latestReleaseVersion = latestRelease.name.substring(1)
|
||||
const vLatest = latestReleaseVersion.split('.')
|
||||
const vNew = runnerVersion.split('.')
|
||||
let versionBumped = true
|
||||
for (let i = 0; i < 3; ++i) {
|
||||
var v1 = parseInt(vLatest[i], 10);
|
||||
var v2 = parseInt(vNew[i], 10);
|
||||
if (v2 > v1) {
|
||||
console.log(runnerVersion + " > " + latestReleaseVersion + "(Latest)")
|
||||
break
|
||||
}
|
||||
|
||||
if (v1 > v2) {
|
||||
versionBumped = false
|
||||
core.setFailed(runnerVersion + " < " + latestReleaseVersion + "(Latest)")
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
// Generate release note
|
||||
if (versionBumped) {
|
||||
const releaseNoteContent = await github.repos.getContents({
|
||||
owner: '${{ github.event.repository.owner.name }}',
|
||||
repo: '${{ github.event.repository.name }}',
|
||||
path: 'releaseNote.md'
|
||||
ref: ${{ github.sha }}
|
||||
})
|
||||
const releaseNote = Buffer.from(releaseNoteContent.data.content, 'base64').toString().replace("<RUNNER_VERSION>", runnerVersion)
|
||||
console.log(releaseNote)
|
||||
core.setOutput('note', releaseNote);
|
||||
}
|
||||
|
||||
core.setOutput('note', releaseNote);
|
||||
|
||||
# Create GitHub release
|
||||
- uses: actions/create-release@v1
|
||||
- uses: actions/create-release@master
|
||||
id: createRelease
|
||||
name: Create ${{ steps.releaseNote.outputs.version }} Runner Release
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
with:
|
||||
tag_name: "v${{ steps.releaseNote.outputs.version }}"
|
||||
release_name: "v${{ steps.releaseNote.outputs.version }}"
|
||||
body: ${{ steps.releaseNote.outputs.note }}
|
||||
body: |
|
||||
${{ steps.releaseNote.outputs.note }}
|
||||
prerelease: true
|
||||
|
||||
# Upload release assets
|
||||
- name: Upload Release Asset (win-x64)
|
||||
uses: actions/upload-release-asset@v1.0.1
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
with:
|
||||
upload_url: ${{ steps.createRelease.outputs.upload_url }}
|
||||
asset_path: ./actions-runner-win-x64-${{ steps.releaseNote.outputs.version }}.zip
|
||||
asset_path: ${{ github.workspace }}/actions-runner-win-x64-${{ steps.releaseNote.outputs.version }}.zip
|
||||
asset_name: actions-runner-win-x64-${{ steps.releaseNote.outputs.version }}.zip
|
||||
asset_content_type: application/octet-stream
|
||||
|
||||
- name: Upload Release Asset (linux-x64)
|
||||
uses: actions/upload-release-asset@v1.0.1
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
with:
|
||||
upload_url: ${{ steps.createRelease.outputs.upload_url }}
|
||||
asset_path: ./actions-runner-linux-x64-${{ steps.releaseNote.outputs.version }}.zip
|
||||
asset_name: actions-runner-linux-x64-${{ steps.releaseNote.outputs.version }}.zip
|
||||
asset_path: ${{ github.workspace }}/actions-runner-linux-x64-${{ steps.releaseNote.outputs.version }}.tar.gz
|
||||
asset_name: actions-runner-linux-x64-${{ steps.releaseNote.outputs.version }}.tar.gz
|
||||
asset_content_type: application/octet-stream
|
||||
|
||||
- name: Upload Release Asset (mac-x64)
|
||||
- name: Upload Release Asset (osx-x64)
|
||||
uses: actions/upload-release-asset@v1.0.1
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
with:
|
||||
upload_url: ${{ steps.createRelease.outputs.upload_url }}
|
||||
asset_path: ./actions-runner-mac-x64-${{ steps.releaseNote.outputs.version }}.zip
|
||||
asset_name: actions-runner-mac-x64-${{ steps.releaseNote.outputs.version }}.zip
|
||||
asset_path: ${{ github.workspace }}/actions-runner-osx-x64-${{ steps.releaseNote.outputs.version }}.tar.gz
|
||||
asset_name: actions-runner-osx-x64-${{ steps.releaseNote.outputs.version }}.tar.gz
|
||||
asset_content_type: application/octet-stream
|
||||
|
||||
- name: Upload Release Asset (linux-arm)
|
||||
uses: actions/upload-release-asset@v1.0.1
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
with:
|
||||
upload_url: ${{ steps.createRelease.outputs.upload_url }}
|
||||
asset_path: ./actions-runner-linux-arm-${{ steps.releaseNote.outputs.version }}.zip
|
||||
asset_name: actions-runner-linux-arm-${{ steps.releaseNote.outputs.version }}.zip
|
||||
asset_path: ${{ github.workspace }}/actions-runner-linux-arm-${{ steps.releaseNote.outputs.version }}.tar.gz
|
||||
asset_name: actions-runner-linux-arm-${{ steps.releaseNote.outputs.version }}.tar.gz
|
||||
asset_content_type: application/octet-stream
|
||||
|
||||
- name: Upload Release Asset (linux-arm64)
|
||||
uses: actions/upload-release-asset@v1.0.1
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
with:
|
||||
upload_url: ${{ steps.createRelease.outputs.upload_url }}
|
||||
asset_path: ./actions-runner-linux-arm64-${{ steps.releaseNote.outputs.version }}.zip
|
||||
asset_name: actions-runner-linux-arm64-${{ steps.releaseNote.outputs.version }}.zip
|
||||
asset_path: ${{ github.workspace }}/actions-runner-linux-arm64-${{ steps.releaseNote.outputs.version }}.tar.gz
|
||||
asset_name: actions-runner-linux-arm64-${{ steps.releaseNote.outputs.version }}.tar.gz
|
||||
asset_content_type: application/octet-stream
|
||||
32
assets.json
32
assets.json
@@ -1,32 +0,0 @@
|
||||
[
|
||||
{
|
||||
"name": "actions-runner-win-x64-<RUNNER_VERSION>.zip",
|
||||
"platform": "win-x64",
|
||||
"version": "<RUNNER_VERSION>",
|
||||
"downloadUrl": "https://githubassets.azureedge.net/runners/<RUNNER_VERSION>/actions-runner-win-x64-<RUNNER_VERSION>.zip"
|
||||
},
|
||||
{
|
||||
"name": "actions-runner-osx-x64-<RUNNER_VERSION>.tar.gz",
|
||||
"platform": "osx-x64",
|
||||
"version": "<RUNNER_VERSION>",
|
||||
"downloadUrl": "https://githubassets.azureedge.net/runners/<RUNNER_VERSION>/actions-runner-osx-x64-<RUNNER_VERSION>.tar.gz"
|
||||
},
|
||||
{
|
||||
"name": "actions-runner-linux-x64-<RUNNER_VERSION>.tar.gz",
|
||||
"platform": "linux-x64",
|
||||
"version": "<RUNNER_VERSION>",
|
||||
"downloadUrl": "https://githubassets.azureedge.net/runners/<RUNNER_VERSION>/actions-runner-linux-x64-<RUNNER_VERSION>.tar.gz"
|
||||
},
|
||||
{
|
||||
"name": "actions-runner-linux-arm64-<RUNNER_VERSION>.tar.gz",
|
||||
"platform": "linux-arm64",
|
||||
"version": "<RUNNER_VERSION>",
|
||||
"downloadUrl": "https://githubassets.azureedge.net/runners/<RUNNER_VERSION>/actions-runner-linux-arm64-<RUNNER_VERSION>.tar.gz"
|
||||
},
|
||||
{
|
||||
"name": "actions-runner-linux-arm-<RUNNER_VERSION>.tar.gz",
|
||||
"platform": "linux-arm",
|
||||
"version": "<RUNNER_VERSION>",
|
||||
"downloadUrl": "https://githubassets.azureedge.net/runners/<RUNNER_VERSION>/actions-runner-linux-arm-<RUNNER_VERSION>.tar.gz"
|
||||
}
|
||||
]
|
||||
@@ -1,237 +0,0 @@
|
||||
stages:
|
||||
- stage: Build
|
||||
jobs:
|
||||
################################################################################
|
||||
- job: build_windows_agent_x64
|
||||
################################################################################
|
||||
displayName: Windows Agent (x64)
|
||||
pool:
|
||||
vmImage: vs2017-win2016
|
||||
steps:
|
||||
|
||||
# Steps template for windows platform
|
||||
- template: windows.template.yml
|
||||
parameters:
|
||||
targetRuntime: win-x64
|
||||
|
||||
# Package dotnet core windows dependency (VC++ Redistributable)
|
||||
- powershell: |
|
||||
Write-Host "Downloading 'VC++ Redistributable' package."
|
||||
$outDir = Join-Path -Path $env:TMP -ChildPath ([Guid]::NewGuid())
|
||||
New-Item -Path $outDir -ItemType directory
|
||||
$outFile = Join-Path -Path $outDir -ChildPath "ucrt.zip"
|
||||
Invoke-WebRequest -Uri https://vstsagenttools.blob.core.windows.net/tools/ucrt/ucrt_x64.zip -OutFile $outFile
|
||||
Write-Host "Unzipping 'VC++ Redistributable' package to agent layout."
|
||||
$unzipDir = Join-Path -Path $outDir -ChildPath "unzip"
|
||||
Add-Type -AssemblyName System.IO.Compression.FileSystem
|
||||
[System.IO.Compression.ZipFile]::ExtractToDirectory($outFile, $unzipDir)
|
||||
$agentLayoutBin = Join-Path -Path $(Build.SourcesDirectory) -ChildPath "_layout\bin"
|
||||
Copy-Item -Path $unzipDir -Destination $agentLayoutBin -Force
|
||||
displayName: Package UCRT
|
||||
|
||||
# Create agent package zip
|
||||
- script: dev.cmd package Release win-x64
|
||||
workingDirectory: src
|
||||
displayName: Package Release
|
||||
|
||||
# Upload agent package zip as build artifact
|
||||
- task: PublishBuildArtifacts@1
|
||||
displayName: Publish Artifact (Windows x64)
|
||||
inputs:
|
||||
pathToPublish: _package
|
||||
artifactName: runners
|
||||
artifactType: container
|
||||
|
||||
################################################################################
|
||||
- job: build_linux_agent_x64
|
||||
################################################################################
|
||||
displayName: Linux Agent (x64)
|
||||
pool:
|
||||
vmImage: ubuntu-16.04
|
||||
steps:
|
||||
|
||||
# Steps template for non-windows platform
|
||||
- template: nonwindows.template.yml
|
||||
parameters:
|
||||
targetRuntime: linux-x64
|
||||
|
||||
# Create agent package zip
|
||||
- script: ./dev.sh package Release linux-x64
|
||||
workingDirectory: src
|
||||
displayName: Package Release
|
||||
|
||||
# Upload agent package zip as build artifact
|
||||
- task: PublishBuildArtifacts@1
|
||||
displayName: Publish Artifact (Linux x64)
|
||||
inputs:
|
||||
pathToPublish: _package
|
||||
artifactName: runners
|
||||
artifactType: container
|
||||
|
||||
################################################################################
|
||||
- job: build_linux_agent_arm64
|
||||
################################################################################
|
||||
displayName: Linux Agent (arm64)
|
||||
pool:
|
||||
vmImage: ubuntu-16.04
|
||||
steps:
|
||||
|
||||
# Steps template for non-windows platform
|
||||
- template: nonwindows.template.yml
|
||||
parameters:
|
||||
targetRuntime: linux-arm64
|
||||
|
||||
# Create agent package zip
|
||||
- script: ./dev.sh package Release linux-arm64
|
||||
workingDirectory: src
|
||||
displayName: Package Release
|
||||
|
||||
# Upload agent package zip as build artifact
|
||||
- task: PublishBuildArtifacts@1
|
||||
displayName: Publish Artifact (Linux ARM64)
|
||||
inputs:
|
||||
pathToPublish: _package
|
||||
artifactName: runners
|
||||
artifactType: container
|
||||
|
||||
################################################################################
|
||||
- job: build_linux_agent_arm
|
||||
################################################################################
|
||||
displayName: Linux Agent (arm)
|
||||
pool:
|
||||
vmImage: ubuntu-16.04
|
||||
steps:
|
||||
|
||||
# Steps template for non-windows platform
|
||||
- template: nonwindows.template.yml
|
||||
parameters:
|
||||
targetRuntime: linux-arm
|
||||
|
||||
# Create agent package zip
|
||||
- script: ./dev.sh package Release linux-arm
|
||||
workingDirectory: src
|
||||
displayName: Package Release
|
||||
|
||||
# Upload agent package zip as build artifact
|
||||
- task: PublishBuildArtifacts@1
|
||||
displayName: Publish Artifact (Linux ARM)
|
||||
inputs:
|
||||
pathToPublish: _package
|
||||
artifactName: runners
|
||||
artifactType: container
|
||||
|
||||
################################################################################
|
||||
- job: build_osx_agent_x64
|
||||
################################################################################
|
||||
displayName: macOS Agent (x64)
|
||||
pool:
|
||||
vmImage: macOS-10.13
|
||||
steps:
|
||||
|
||||
# Steps template for non-windows platform
|
||||
- template: nonwindows.template.yml
|
||||
parameters:
|
||||
targetRuntime: osx-x64
|
||||
|
||||
# Create agent package zip
|
||||
- script: ./dev.sh package Release osx-x64
|
||||
workingDirectory: src
|
||||
displayName: Package Release
|
||||
|
||||
# Upload agent package zip as build artifact
|
||||
- task: PublishBuildArtifacts@1
|
||||
displayName: Publish Artifact (OSX x64)
|
||||
inputs:
|
||||
pathToPublish: _package
|
||||
artifactName: runners
|
||||
artifactType: container
|
||||
|
||||
- stage: Release
|
||||
dependsOn: Build
|
||||
jobs:
|
||||
################################################################################
|
||||
- job: publish_agent_packages
|
||||
################################################################################
|
||||
displayName: Publish Agents (Windows/Linux/OSX)
|
||||
pool:
|
||||
name: ProductionRMAgents
|
||||
steps:
|
||||
|
||||
# Download all agent packages from all previous phases
|
||||
- task: DownloadBuildArtifacts@0
|
||||
displayName: Download Agent Packages
|
||||
inputs:
|
||||
artifactName: runners
|
||||
|
||||
# Upload agent packages to Azure blob storage and refresh Azure CDN
|
||||
- powershell: |
|
||||
Write-Host "Preloading Azure modules." # This is for better performance, to avoid module-autoloading.
|
||||
Import-Module AzureRM, AzureRM.profile, AzureRM.Storage, Azure.Storage, AzureRM.Cdn -ErrorAction Ignore -PassThru
|
||||
Enable-AzureRmAlias -Scope CurrentUser
|
||||
$uploadFiles = New-Object System.Collections.ArrayList
|
||||
$certificateThumbprint = (Get-ItemProperty -Path "$(ServicePrincipalReg)").ServicePrincipalCertThumbprint
|
||||
$clientId = (Get-ItemProperty -Path "$(ServicePrincipalReg)").ServicePrincipalClientId
|
||||
Write-Host "##vso[task.setsecret]$certificateThumbprint"
|
||||
Write-Host "##vso[task.setsecret]$clientId"
|
||||
Login-AzureRmAccount -ServicePrincipal -CertificateThumbprint $certificateThumbprint -ApplicationId $clientId -TenantId $(GitHubTenantId)
|
||||
Select-AzureRmSubscription -SubscriptionId $(GitHubSubscriptionId)
|
||||
$storage = Get-AzureRmStorageAccount -ResourceGroupName githubassets -AccountName githubassets
|
||||
Get-ChildItem -LiteralPath "$(System.ArtifactsDirectory)/runners" | ForEach-Object {
|
||||
$versionDir = $_.Name.Trim('.zip').Trim('.tar.gz')
|
||||
$versionDir = $versionDir.SubString($versionDir.LastIndexOf('-') + 1)
|
||||
Write-Host "##vso[task.setvariable variable=ReleaseAgentVersion;]$versionDir"
|
||||
Write-Host "Uploading $_ to BlobStorage githubassets/runners/$versionDir"
|
||||
Set-AzureStorageBlobContent -Context $storage.Context -Container runners -File "$(System.ArtifactsDirectory)/runners/$_" -Blob "$versionDir/$_" -Force
|
||||
$uploadFiles.Add("/runners/$versionDir/$_")
|
||||
}
|
||||
Write-Host "Get CDN info"
|
||||
Get-AzureRmCdnEndpoint -ProfileName githubassets -ResourceGroupName githubassets
|
||||
Write-Host "Purge Azure CDN Cache"
|
||||
Unpublish-AzureRmCdnEndpointContent -EndpointName githubassets -ProfileName githubassets -ResourceGroupName githubassets -PurgeContent $uploadFiles
|
||||
Write-Host "Pull assets through Azure CDN"
|
||||
$uploadFiles | ForEach-Object {
|
||||
$downloadUrl = "https://githubassets.azureedge.net" + $_
|
||||
Write-Host $downloadUrl
|
||||
Invoke-WebRequest -Uri $downloadUrl -OutFile $_.SubString($_.LastIndexOf('/') + 1)
|
||||
}
|
||||
displayName: Upload to Azure Blob
|
||||
|
||||
# Create agent release on Github
|
||||
- powershell: |
|
||||
Write-Host "Creating github release."
|
||||
$releaseNotes = [System.IO.File]::ReadAllText("$(Build.SourcesDirectory)\releaseNote.md").Replace("<RUNNER_VERSION>","$(ReleaseAgentVersion)")
|
||||
$releaseData = @{
|
||||
tag_name = "v$(ReleaseAgentVersion)";
|
||||
target_commitish = "$(Build.SourceVersion)";
|
||||
name = "v$(ReleaseAgentVersion)";
|
||||
body = $releaseNotes;
|
||||
draft = $false;
|
||||
prerelease = $true;
|
||||
}
|
||||
$releaseParams = @{
|
||||
Uri = "https://api.github.com/repos/actions/runner/releases";
|
||||
Method = 'POST';
|
||||
Headers = @{
|
||||
Authorization = 'Basic ' + [Convert]::ToBase64String([Text.Encoding]::ASCII.GetBytes("github:$(GithubToken)"));
|
||||
}
|
||||
ContentType = 'application/json';
|
||||
Body = (ConvertTo-Json $releaseData -Compress)
|
||||
}
|
||||
[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12
|
||||
$releaseCreated = Invoke-RestMethod @releaseParams
|
||||
Write-Host $releaseCreated
|
||||
$releaseId = $releaseCreated.id
|
||||
Get-ChildItem -LiteralPath "$(System.ArtifactsDirectory)/runners" | ForEach-Object {
|
||||
Write-Host "Uploading $_ as GitHub release assets"
|
||||
$assetsParams = @{
|
||||
Uri = "https://uploads.github.com/repos/actions/runner/releases/$releaseId/assets?name=$($_.Name)"
|
||||
Method = 'POST';
|
||||
Headers = @{
|
||||
Authorization = 'Basic ' + [Convert]::ToBase64String([Text.Encoding]::ASCII.GetBytes("github:$(GithubToken)"));
|
||||
}
|
||||
ContentType = 'application/octet-stream';
|
||||
Body = [System.IO.File]::ReadAllBytes($_.FullName)
|
||||
}
|
||||
Invoke-RestMethod @assetsParams
|
||||
}
|
||||
displayName: Create agent release on Github
|
||||
@@ -1,95 +0,0 @@
|
||||
jobs:
|
||||
|
||||
################################################################################
|
||||
- job: build_windows_x64_agent
|
||||
################################################################################
|
||||
displayName: Windows Agent (x64)
|
||||
pool:
|
||||
vmImage: vs2017-win2016
|
||||
steps:
|
||||
|
||||
# Steps template for windows platform
|
||||
- template: windows.template.yml
|
||||
|
||||
# Package dotnet core windows dependency (VC++ Redistributable)
|
||||
- powershell: |
|
||||
Write-Host "Downloading 'VC++ Redistributable' package."
|
||||
$outDir = Join-Path -Path $env:TMP -ChildPath ([Guid]::NewGuid())
|
||||
New-Item -Path $outDir -ItemType directory
|
||||
$outFile = Join-Path -Path $outDir -ChildPath "ucrt.zip"
|
||||
Invoke-WebRequest -Uri https://vstsagenttools.blob.core.windows.net/tools/ucrt/ucrt_x64.zip -OutFile $outFile
|
||||
Write-Host "Unzipping 'VC++ Redistributable' package to agent layout."
|
||||
$unzipDir = Join-Path -Path $outDir -ChildPath "unzip"
|
||||
Add-Type -AssemblyName System.IO.Compression.FileSystem
|
||||
[System.IO.Compression.ZipFile]::ExtractToDirectory($outFile, $unzipDir)
|
||||
$agentLayoutBin = Join-Path -Path $(Build.SourcesDirectory) -ChildPath "_layout\bin"
|
||||
Copy-Item -Path $unzipDir -Destination $agentLayoutBin -Force
|
||||
displayName: Package UCRT
|
||||
condition: and(succeeded(), ne(variables['build.reason'], 'PullRequest'))
|
||||
|
||||
# Create agent package zip
|
||||
- script: dev.cmd package Release
|
||||
workingDirectory: src
|
||||
displayName: Package Release
|
||||
condition: and(succeeded(), ne(variables['build.reason'], 'PullRequest'))
|
||||
|
||||
# Upload agent package zip as build artifact
|
||||
- task: PublishBuildArtifacts@1
|
||||
displayName: Publish Artifact (Windows x64)
|
||||
condition: and(succeeded(), ne(variables['build.reason'], 'PullRequest'))
|
||||
inputs:
|
||||
pathToPublish: _package
|
||||
artifactName: agent
|
||||
artifactType: container
|
||||
|
||||
################################################################################
|
||||
- job: build_linux_x64_agent
|
||||
################################################################################
|
||||
displayName: Linux Agent (x64)
|
||||
pool:
|
||||
vmImage: ubuntu-16.04
|
||||
steps:
|
||||
|
||||
# Steps template for non-windows platform
|
||||
- template: nonwindows.template.yml
|
||||
|
||||
# Create agent package zip
|
||||
- script: ./dev.sh package Release
|
||||
workingDirectory: src
|
||||
displayName: Package Release
|
||||
condition: and(succeeded(), ne(variables['build.reason'], 'PullRequest'))
|
||||
|
||||
# Upload agent package zip as build artifact
|
||||
- task: PublishBuildArtifacts@1
|
||||
displayName: Publish Artifact (Linux x64)
|
||||
condition: and(succeeded(), ne(variables['build.reason'], 'PullRequest'))
|
||||
inputs:
|
||||
pathToPublish: _package
|
||||
artifactName: agent
|
||||
artifactType: container
|
||||
|
||||
################################################################################
|
||||
- job: build_osx_agent
|
||||
################################################################################
|
||||
displayName: macOS Agent (x64)
|
||||
pool:
|
||||
vmImage: macOS-10.14
|
||||
steps:
|
||||
|
||||
# Steps template for non-windows platform
|
||||
- template: nonwindows.template.yml
|
||||
|
||||
# Create agent package zip
|
||||
- script: ./dev.sh package Release
|
||||
workingDirectory: src
|
||||
displayName: Package Release
|
||||
condition: and(succeeded(), ne(variables['build.reason'], 'PullRequest'))
|
||||
|
||||
# Upload agent package zip as build artifact
|
||||
- task: PublishBuildArtifacts@1
|
||||
displayName: Publish Artifact (OSX)
|
||||
condition: and(succeeded(), ne(variables['build.reason'], 'PullRequest'))
|
||||
inputs:
|
||||
pathToPublish: _package
|
||||
artifactName: agent
|
||||
artifactType: container
|
||||
@@ -14,16 +14,16 @@ Navigate to the `src` directory and run the following command:
|
||||
|
||||
**Commands:**
|
||||
|
||||
* `layout` (`l`): Run first time to create a full agent layout in `{root}/_layout`
|
||||
* `build` (`b`): Build everything and update agent layout folder
|
||||
* `test` (`t`): Build agent binaries and run unit tests
|
||||
* `layout` (`l`): Run first time to create a full runner layout in `{root}/_layout`
|
||||
* `build` (`b`): Build everything and update runner layout folder
|
||||
* `test` (`t`): Build runner binaries and run unit tests
|
||||
|
||||
Sample developer flow:
|
||||
|
||||
```bash
|
||||
git clone https://github.com/actions/runner
|
||||
cd ./src
|
||||
./dev.(sh/cmd) layout # the agent that build from source is in {root}/_layout
|
||||
./dev.(sh/cmd) layout # the runner that build from source is in {root}/_layout
|
||||
<make code changes>
|
||||
./dev.(sh/cmd) build # {root}/_layout will get updated
|
||||
./dev.(sh/cmd) test # run all unit tests before git commit/push
|
||||
|
||||
@@ -1,33 +0,0 @@
|
||||
parameters:
|
||||
targetRuntime: ''
|
||||
|
||||
steps:
|
||||
|
||||
# Build agent layout
|
||||
- script: ./dev.sh layout Release ${{ parameters.targetRuntime }}
|
||||
workingDirectory: src
|
||||
displayName: Build & Layout Release ${{ parameters.targetRuntime }}
|
||||
|
||||
# Run test
|
||||
- script: ./dev.sh test
|
||||
workingDirectory: src
|
||||
displayName: Test
|
||||
condition: and(ne('${{ parameters.targetRuntime }}', 'linux-arm64'), ne('${{ parameters.targetRuntime }}', 'linux-arm'))
|
||||
|
||||
# # Publish test results
|
||||
# - task: PublishTestResults@2
|
||||
# displayName: Publish Test Results **/*.trx
|
||||
# condition: always()
|
||||
# inputs:
|
||||
# testRunner: VSTest
|
||||
# testResultsFiles: '**/*.trx'
|
||||
# testRunTitle: 'Agent Tests'
|
||||
|
||||
# # Upload test log
|
||||
# - task: PublishBuildArtifacts@1
|
||||
# displayName: Publish Test logs
|
||||
# condition: always()
|
||||
# inputs:
|
||||
# pathToPublish: src/Test/TestLogs
|
||||
# artifactName: $(System.JobId)
|
||||
# artifactType: container
|
||||
@@ -1,13 +1,14 @@
|
||||
## Features
|
||||
- Added the "severity" keyword to allow action authors to set the default severity for problem matchers (#203)
|
||||
- Remove runner flow: Change from PAT to "deletion token" in prompt (#225)
|
||||
- Expose github.run_id and github.run_number to action runtime env. (#224)
|
||||
|
||||
## Bugs
|
||||
- Fixed generated self-hosted runner names to never go over 80 characters (helps Windows customers) (#193)
|
||||
- Fixed `PrepareActions_DownloadActionFromGraph` test by pointing to an active Actions repository (#205)
|
||||
- Clean up error messages for container scenarios (#221)
|
||||
- Pick shell from prependpath (#231)
|
||||
|
||||
## Misc
|
||||
- Updated the publish and download artifact actions to use the v2 endpoint (#188)
|
||||
- Updated the service name on self-hosted runner name to include repository or organization information (#193)
|
||||
- Runner code cleanup (#218 #227, #228, #229, #230)
|
||||
- Consume dotnet core 3.1 in runner. (#213)
|
||||
|
||||
## Windows x64
|
||||
We recommend configuring the runner under "<DRIVE>:\actions-runner". This will help avoid issues related to service identity folder permissions and long file path restrictions on Windows
|
||||
@@ -15,7 +16,7 @@ We recommend configuring the runner under "<DRIVE>:\actions-runner". This will h
|
||||
// Create a folder under the drive root
|
||||
mkdir \actions-runner ; cd \actions-runner
|
||||
// Download the latest runner package
|
||||
Invoke-WebRequest -Uri https://githubassets.azureedge.net/runners/<RUNNER_VERSION>/actions-runner-win-x64-<RUNNER_VERSION>.zip -OutFile actions-runner-win-x64-<RUNNER_VERSION>.zip
|
||||
Invoke-WebRequest -Uri https://github.com/actions/runner/releases/download/v<RUNNER_VERSION>/actions-runner-win-x64-<RUNNER_VERSION>.zip -OutFile actions-runner-win-x64-<RUNNER_VERSION>.zip
|
||||
// Extract the installer
|
||||
Add-Type -AssemblyName System.IO.Compression.FileSystem ;
|
||||
[System.IO.Compression.ZipFile]::ExtractToDirectory("$HOME\Downloads\actions-runner-win-x64-<RUNNER_VERSION>.zip", "$PWD")
|
||||
@@ -27,7 +28,7 @@ Add-Type -AssemblyName System.IO.Compression.FileSystem ;
|
||||
// Create a folder
|
||||
mkdir actions-runner && cd actions-runner
|
||||
// Download the latest runner package
|
||||
curl -O https://githubassets.azureedge.net/runners/<RUNNER_VERSION>/actions-runner-osx-x64-<RUNNER_VERSION>.tar.gz
|
||||
curl -O https://github.com/actions/runner/releases/download/v<RUNNER_VERSION>/actions-runner-osx-x64-<RUNNER_VERSION>.tar.gz
|
||||
// Extract the installer
|
||||
tar xzf ./actions-runner-osx-x64-<RUNNER_VERSION>.tar.gz
|
||||
```
|
||||
@@ -38,7 +39,7 @@ tar xzf ./actions-runner-osx-x64-<RUNNER_VERSION>.tar.gz
|
||||
// Create a folder
|
||||
mkdir actions-runner && cd actions-runner
|
||||
// Download the latest runner package
|
||||
curl -O https://githubassets.azureedge.net/runners/<RUNNER_VERSION>/actions-runner-linux-x64-<RUNNER_VERSION>.tar.gz
|
||||
curl -O https://github.com/actions/runner/releases/download/v<RUNNER_VERSION>/actions-runner-linux-x64-<RUNNER_VERSION>.tar.gz
|
||||
// Extract the installer
|
||||
tar xzf ./actions-runner-linux-x64-<RUNNER_VERSION>.tar.gz
|
||||
```
|
||||
@@ -49,7 +50,7 @@ tar xzf ./actions-runner-linux-x64-<RUNNER_VERSION>.tar.gz
|
||||
// Create a folder
|
||||
mkdir actions-runner && cd actions-runner
|
||||
// Download the latest runner package
|
||||
curl -O https://githubassets.azureedge.net/runners/<RUNNER_VERSION>/actions-runner-linux-arm64-<RUNNER_VERSION>.tar.gz
|
||||
curl -O https://github.com/actions/runner/releases/download/v<RUNNER_VERSION>/actions-runner-linux-arm64-<RUNNER_VERSION>.tar.gz
|
||||
// Extract the installer
|
||||
tar xzf ./actions-runner-linux-arm64-<RUNNER_VERSION>.tar.gz
|
||||
```
|
||||
@@ -60,7 +61,7 @@ tar xzf ./actions-runner-linux-arm64-<RUNNER_VERSION>.tar.gz
|
||||
// Create a folder
|
||||
mkdir actions-runner && cd actions-runner
|
||||
// Download the latest runner package
|
||||
curl -O https://githubassets.azureedge.net/runners/<RUNNER_VERSION>/actions-runner-linux-arm-<RUNNER_VERSION>.tar.gz
|
||||
curl -O https://github.com/actions/runner/releases/download/v<RUNNER_VERSION>/actions-runner-linux-arm-<RUNNER_VERSION>.tar.gz
|
||||
// Extract the installer
|
||||
tar xzf ./actions-runner-linux-arm-<RUNNER_VERSION>.tar.gz
|
||||
```
|
||||
|
||||
1
releaseVersion
Normal file
1
releaseVersion
Normal file
@@ -0,0 +1 @@
|
||||
2.164.0
|
||||
@@ -3,7 +3,7 @@
|
||||
user_id=`id -u`
|
||||
|
||||
# we want to snapshot the environment of the config user
|
||||
if [ $user_id -eq 0 -a -z "$AGENT_ALLOW_RUNASROOT" ]; then
|
||||
if [ $user_id -eq 0 -a -z "$RUNNER_ALLOW_RUNASROOT" ]; then
|
||||
echo "Must not run with sudo"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
# Validate not sudo
|
||||
user_id=`id -u`
|
||||
if [ $user_id -eq 0 -a -z "$AGENT_ALLOW_RUNASROOT" ]; then
|
||||
if [ $user_id -eq 0 -a -z "$RUNNER_ALLOW_RUNASROOT" ]; then
|
||||
echo "Must not run interactively with sudo"
|
||||
exit 1
|
||||
fi
|
||||
@@ -26,8 +26,8 @@ if [[ "$1" == "localRun" ]]; then
|
||||
else
|
||||
"$DIR"/bin/Runner.Listener run $*
|
||||
|
||||
# Return code 4 means the run once agent received an update message.
|
||||
# Sleep 5 seconds to wait for the update process finish and run the agent again.
|
||||
# Return code 4 means the run once runner received an update message.
|
||||
# Sleep 5 seconds to wait for the update process finish and run the runner again.
|
||||
returnCode=$?
|
||||
if [[ $returnCode == 4 ]]; then
|
||||
if [ ! -x "$(command -v sleep)" ]; then
|
||||
|
||||
@@ -1,33 +0,0 @@
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace GitHub.Runner.Common
|
||||
{
|
||||
//Stephen Toub: http://blogs.msdn.com/b/pfxteam/archive/2012/02/11/10266920.aspx
|
||||
|
||||
public class AsyncManualResetEvent
|
||||
{
|
||||
private volatile TaskCompletionSource<bool> m_tcs = new TaskCompletionSource<bool>();
|
||||
|
||||
public Task WaitAsync() { return m_tcs.Task; }
|
||||
|
||||
public void Set()
|
||||
{
|
||||
var tcs = m_tcs;
|
||||
Task.Factory.StartNew(s => ((TaskCompletionSource<bool>)s).TrySetResult(true),
|
||||
tcs, CancellationToken.None, TaskCreationOptions.PreferFairness, TaskScheduler.Default);
|
||||
tcs.Task.Wait();
|
||||
}
|
||||
|
||||
public void Reset()
|
||||
{
|
||||
while (true)
|
||||
{
|
||||
var tcs = m_tcs;
|
||||
if (!tcs.Task.IsCompleted ||
|
||||
Interlocked.CompareExchange(ref m_tcs, new TaskCompletionSource<bool>(), tcs) == tcs)
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -71,15 +71,6 @@ namespace GitHub.Runner.Common
|
||||
}
|
||||
}
|
||||
|
||||
[DataContract]
|
||||
public sealed class RunnerRuntimeOptions
|
||||
{
|
||||
#if OS_WINDOWS
|
||||
[DataMember(EmitDefaultValue = false)]
|
||||
public bool GitUseSecureChannel { get; set; }
|
||||
#endif
|
||||
}
|
||||
|
||||
[ServiceLocator(Default = typeof(ConfigurationStore))]
|
||||
public interface IConfigurationStore : IRunnerService
|
||||
{
|
||||
@@ -92,9 +83,6 @@ namespace GitHub.Runner.Common
|
||||
void SaveSettings(RunnerSettings settings);
|
||||
void DeleteCredential();
|
||||
void DeleteSettings();
|
||||
RunnerRuntimeOptions GetRunnerRuntimeOptions();
|
||||
void SaveRunnerRuntimeOptions(RunnerRuntimeOptions options);
|
||||
void DeleteRunnerRuntimeOptions();
|
||||
}
|
||||
|
||||
public sealed class ConfigurationStore : RunnerService, IConfigurationStore
|
||||
@@ -103,11 +91,9 @@ namespace GitHub.Runner.Common
|
||||
private string _configFilePath;
|
||||
private string _credFilePath;
|
||||
private string _serviceConfigFilePath;
|
||||
private string _runtimeOptionsFilePath;
|
||||
|
||||
private CredentialData _creds;
|
||||
private RunnerSettings _settings;
|
||||
private RunnerRuntimeOptions _runtimeOptions;
|
||||
|
||||
public override void Initialize(IHostContext hostContext)
|
||||
{
|
||||
@@ -130,9 +116,6 @@ namespace GitHub.Runner.Common
|
||||
|
||||
_serviceConfigFilePath = hostContext.GetConfigFile(WellKnownConfigFile.Service);
|
||||
Trace.Info("ServiceConfigFilePath: {0}", _serviceConfigFilePath);
|
||||
|
||||
_runtimeOptionsFilePath = hostContext.GetConfigFile(WellKnownConfigFile.Options);
|
||||
Trace.Info("RuntimeOptionsFilePath: {0}", _runtimeOptionsFilePath);
|
||||
}
|
||||
|
||||
public string RootFolder { get; private set; }
|
||||
@@ -229,35 +212,5 @@ namespace GitHub.Runner.Common
|
||||
{
|
||||
IOUtil.Delete(_configFilePath, default(CancellationToken));
|
||||
}
|
||||
|
||||
public RunnerRuntimeOptions GetRunnerRuntimeOptions()
|
||||
{
|
||||
if (_runtimeOptions == null && File.Exists(_runtimeOptionsFilePath))
|
||||
{
|
||||
_runtimeOptions = IOUtil.LoadObject<RunnerRuntimeOptions>(_runtimeOptionsFilePath);
|
||||
}
|
||||
|
||||
return _runtimeOptions;
|
||||
}
|
||||
|
||||
public void SaveRunnerRuntimeOptions(RunnerRuntimeOptions options)
|
||||
{
|
||||
Trace.Info("Saving runtime options.");
|
||||
if (File.Exists(_runtimeOptionsFilePath))
|
||||
{
|
||||
// Delete existing runtime options file first, since the file is hidden and not able to overwrite.
|
||||
Trace.Info("Delete exist runtime options file.");
|
||||
IOUtil.DeleteFile(_runtimeOptionsFilePath);
|
||||
}
|
||||
|
||||
IOUtil.SaveObject(options, _runtimeOptionsFilePath);
|
||||
Trace.Info("Options Saved.");
|
||||
File.SetAttributes(_runtimeOptionsFilePath, File.GetAttributes(_runtimeOptionsFilePath) | FileAttributes.Hidden);
|
||||
}
|
||||
|
||||
public void DeleteRunnerRuntimeOptions()
|
||||
{
|
||||
IOUtil.Delete(_runtimeOptionsFilePath, default(CancellationToken));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -88,10 +88,6 @@ namespace GitHub.Runner.Common
|
||||
public static readonly string MonitorSocketAddress = "monitorsocketaddress";
|
||||
public static readonly string Name = "name";
|
||||
public static readonly string Pool = "pool";
|
||||
public static readonly string SslCACert = "sslcacert";
|
||||
public static readonly string SslClientCert = "sslclientcert";
|
||||
public static readonly string SslClientCertKey = "sslclientcertkey";
|
||||
public static readonly string SslClientCertArchive = "sslclientcertarchive";
|
||||
public static readonly string StartupType = "startuptype";
|
||||
public static readonly string Url = "url";
|
||||
public static readonly string UserName = "username";
|
||||
@@ -99,14 +95,10 @@ namespace GitHub.Runner.Common
|
||||
public static readonly string Work = "work";
|
||||
|
||||
// Secret args. Must be added to the "Secrets" getter as well.
|
||||
public static readonly string Password = "password";
|
||||
public static readonly string SslClientCertPassword = "sslclientcertpassword";
|
||||
public static readonly string Token = "token";
|
||||
public static readonly string WindowsLogonPassword = "windowslogonpassword";
|
||||
public static string[] Secrets => new[]
|
||||
{
|
||||
Password,
|
||||
SslClientCertPassword,
|
||||
Token,
|
||||
WindowsLogonPassword,
|
||||
};
|
||||
@@ -125,13 +117,10 @@ namespace GitHub.Runner.Common
|
||||
public static class Flags
|
||||
{
|
||||
public static readonly string Commit = "commit";
|
||||
public static readonly string GitUseSChannel = "gituseschannel";
|
||||
public static readonly string Help = "help";
|
||||
public static readonly string Replace = "replace";
|
||||
public static readonly string LaunchBrowser = "launchbrowser";
|
||||
public static readonly string Once = "once";
|
||||
public static readonly string RunAsService = "runasservice";
|
||||
public static readonly string SslSkipCertValidation = "sslskipcertvalidation";
|
||||
public static readonly string Unattended = "unattended";
|
||||
public static readonly string Version = "version";
|
||||
}
|
||||
@@ -200,6 +189,11 @@ namespace GitHub.Runner.Common
|
||||
public static readonly string StepDebug = "ACTIONS_STEP_DEBUG";
|
||||
}
|
||||
|
||||
public static class Agent
|
||||
{
|
||||
public static readonly string ToolsDirectory = "agent.ToolsDirectory";
|
||||
}
|
||||
|
||||
public static class System
|
||||
{
|
||||
//
|
||||
|
||||
@@ -58,8 +58,8 @@ namespace GitHub.Runner.Common
|
||||
private CancellationTokenSource _runnerShutdownTokenSource = new CancellationTokenSource();
|
||||
private object _perfLock = new object();
|
||||
private Tracing _trace;
|
||||
private Tracing _vssTrace;
|
||||
private Tracing _httpTrace;
|
||||
private Tracing _actionsHttpTrace;
|
||||
private Tracing _netcoreHttpTrace;
|
||||
private ITraceManager _traceManager;
|
||||
private AssemblyLoadContext _loadContext;
|
||||
private IDisposable _httpTraceSubscription;
|
||||
@@ -117,8 +117,7 @@ namespace GitHub.Runner.Common
|
||||
}
|
||||
|
||||
_trace = GetTrace(nameof(HostContext));
|
||||
_vssTrace = GetTrace("GitHubActionsRunner"); // VisualStudioService
|
||||
|
||||
_actionsHttpTrace = GetTrace("GitHubActionsService");
|
||||
// Enable Http trace
|
||||
bool enableHttpTrace;
|
||||
if (bool.TryParse(Environment.GetEnvironmentVariable("GITHUB_ACTIONS_RUNNER_HTTPTRACE"), out enableHttpTrace) && enableHttpTrace)
|
||||
@@ -130,7 +129,7 @@ namespace GitHub.Runner.Common
|
||||
_trace.Warning("** **");
|
||||
_trace.Warning("*****************************************************************************************");
|
||||
|
||||
_httpTrace = GetTrace("HttpTrace");
|
||||
_netcoreHttpTrace = GetTrace("HttpTrace");
|
||||
_diagListenerSubscription = DiagnosticListener.AllListeners.Subscribe(this);
|
||||
}
|
||||
|
||||
@@ -230,8 +229,9 @@ namespace GitHub.Runner.Common
|
||||
break;
|
||||
|
||||
case WellKnownDirectory.Tools:
|
||||
path = Environment.GetEnvironmentVariable("RUNNER_TOOL_CACHE");
|
||||
|
||||
// TODO: Coallesce to just check RUNNER_TOOL_CACHE when images stabilize
|
||||
path = Environment.GetEnvironmentVariable("RUNNER_TOOL_CACHE") ?? Environment.GetEnvironmentVariable("RUNNER_TOOLSDIRECTORY") ?? Environment.GetEnvironmentVariable("AGENT_TOOLSDIRECTORY") ?? Environment.GetEnvironmentVariable(Constants.Variables.Agent.ToolsDirectory);
|
||||
|
||||
if (string.IsNullOrEmpty(path))
|
||||
{
|
||||
path = Path.Combine(
|
||||
@@ -478,12 +478,12 @@ namespace GitHub.Runner.Common
|
||||
|
||||
void IObserver<DiagnosticListener>.OnCompleted()
|
||||
{
|
||||
_httpTrace.Info("DiagListeners finished transmitting data.");
|
||||
_netcoreHttpTrace.Info("DiagListeners finished transmitting data.");
|
||||
}
|
||||
|
||||
void IObserver<DiagnosticListener>.OnError(Exception error)
|
||||
{
|
||||
_httpTrace.Error(error);
|
||||
_netcoreHttpTrace.Error(error);
|
||||
}
|
||||
|
||||
void IObserver<DiagnosticListener>.OnNext(DiagnosticListener listener)
|
||||
@@ -496,22 +496,22 @@ namespace GitHub.Runner.Common
|
||||
|
||||
void IObserver<KeyValuePair<string, object>>.OnCompleted()
|
||||
{
|
||||
_httpTrace.Info("HttpHandlerDiagnosticListener finished transmitting data.");
|
||||
_netcoreHttpTrace.Info("HttpHandlerDiagnosticListener finished transmitting data.");
|
||||
}
|
||||
|
||||
void IObserver<KeyValuePair<string, object>>.OnError(Exception error)
|
||||
{
|
||||
_httpTrace.Error(error);
|
||||
_netcoreHttpTrace.Error(error);
|
||||
}
|
||||
|
||||
void IObserver<KeyValuePair<string, object>>.OnNext(KeyValuePair<string, object> value)
|
||||
{
|
||||
_httpTrace.Info($"Trace {value.Key} event:{Environment.NewLine}{value.Value.ToString()}");
|
||||
_netcoreHttpTrace.Info($"Trace {value.Key} event:{Environment.NewLine}{value.Value.ToString()}");
|
||||
}
|
||||
|
||||
protected override void OnEventSourceCreated(EventSource source)
|
||||
{
|
||||
if (source.Name.Equals("Microsoft-VSS-Http"))
|
||||
if (source.Name.Equals("GitHub-Actions-Http"))
|
||||
{
|
||||
EnableEvents(source, EventLevel.Verbose);
|
||||
}
|
||||
@@ -551,24 +551,24 @@ namespace GitHub.Runner.Common
|
||||
{
|
||||
case EventLevel.Critical:
|
||||
case EventLevel.Error:
|
||||
_vssTrace.Error(message);
|
||||
_actionsHttpTrace.Error(message);
|
||||
break;
|
||||
case EventLevel.Warning:
|
||||
_vssTrace.Warning(message);
|
||||
_actionsHttpTrace.Warning(message);
|
||||
break;
|
||||
case EventLevel.Informational:
|
||||
_vssTrace.Info(message);
|
||||
_actionsHttpTrace.Info(message);
|
||||
break;
|
||||
default:
|
||||
_vssTrace.Verbose(message);
|
||||
_actionsHttpTrace.Verbose(message);
|
||||
break;
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_vssTrace.Error(ex);
|
||||
_vssTrace.Info(eventData.Message);
|
||||
_vssTrace.Info(string.Join(", ", eventData.Payload?.ToArray() ?? new string[0]));
|
||||
_actionsHttpTrace.Error(ex);
|
||||
_actionsHttpTrace.Info(eventData.Message);
|
||||
_actionsHttpTrace.Info(string.Join(", ", eventData.Payload?.ToArray() ?? new string[0]));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>netcoreapp3.0</TargetFramework>
|
||||
<TargetFramework>netcoreapp3.1</TargetFramework>
|
||||
<OutputType>Library</OutputType>
|
||||
<RuntimeIdentifiers>win-x64;win-x86;linux-x64;linux-arm64;linux-arm;osx-x64</RuntimeIdentifiers>
|
||||
<TargetLatestRuntimePatch>true</TargetLatestRuntimePatch>
|
||||
|
||||
@@ -1,231 +0,0 @@
|
||||
using System;
|
||||
using GitHub.Runner.Common.Util;
|
||||
using System.IO;
|
||||
using System.Runtime.Serialization;
|
||||
using GitHub.Services.Common;
|
||||
using System.Security.Cryptography.X509Certificates;
|
||||
using System.Net;
|
||||
using System.Net.Security;
|
||||
using System.Net.Http;
|
||||
using GitHub.Services.WebApi;
|
||||
using GitHub.Runner.Sdk;
|
||||
|
||||
namespace GitHub.Runner.Common
|
||||
{
|
||||
[ServiceLocator(Default = typeof(RunnerCertificateManager))]
|
||||
public interface IRunnerCertificateManager : IRunnerService
|
||||
{
|
||||
bool SkipServerCertificateValidation { get; }
|
||||
string CACertificateFile { get; }
|
||||
string ClientCertificateFile { get; }
|
||||
string ClientCertificatePrivateKeyFile { get; }
|
||||
string ClientCertificateArchiveFile { get; }
|
||||
string ClientCertificatePassword { get; }
|
||||
IVssClientCertificateManager VssClientCertificateManager { get; }
|
||||
}
|
||||
|
||||
public class RunnerCertificateManager : RunnerService, IRunnerCertificateManager
|
||||
{
|
||||
private RunnerClientCertificateManager _runnerClientCertificateManager = new RunnerClientCertificateManager();
|
||||
|
||||
public bool SkipServerCertificateValidation { private set; get; }
|
||||
public string CACertificateFile { private set; get; }
|
||||
public string ClientCertificateFile { private set; get; }
|
||||
public string ClientCertificatePrivateKeyFile { private set; get; }
|
||||
public string ClientCertificateArchiveFile { private set; get; }
|
||||
public string ClientCertificatePassword { private set; get; }
|
||||
public IVssClientCertificateManager VssClientCertificateManager => _runnerClientCertificateManager;
|
||||
|
||||
public override void Initialize(IHostContext hostContext)
|
||||
{
|
||||
base.Initialize(hostContext);
|
||||
LoadCertificateSettings();
|
||||
}
|
||||
|
||||
// This should only be called from config
|
||||
public void SetupCertificate(bool skipCertValidation, string caCert, string clientCert, string clientCertPrivateKey, string clientCertArchive, string clientCertPassword)
|
||||
{
|
||||
Trace.Info("Setup runner certificate setting base on configuration inputs.");
|
||||
|
||||
if (skipCertValidation)
|
||||
{
|
||||
Trace.Info("Ignore SSL server certificate validation error");
|
||||
SkipServerCertificateValidation = true;
|
||||
VssClientHttpRequestSettings.Default.ServerCertificateValidationCallback = HttpClientHandler.DangerousAcceptAnyServerCertificateValidator;
|
||||
}
|
||||
|
||||
if (!string.IsNullOrEmpty(caCert))
|
||||
{
|
||||
ArgUtil.File(caCert, nameof(caCert));
|
||||
Trace.Info($"Self-Signed CA '{caCert}'");
|
||||
}
|
||||
|
||||
if (!string.IsNullOrEmpty(clientCert))
|
||||
{
|
||||
ArgUtil.File(clientCert, nameof(clientCert));
|
||||
ArgUtil.File(clientCertPrivateKey, nameof(clientCertPrivateKey));
|
||||
ArgUtil.File(clientCertArchive, nameof(clientCertArchive));
|
||||
|
||||
Trace.Info($"Client cert '{clientCert}'");
|
||||
Trace.Info($"Client cert private key '{clientCertPrivateKey}'");
|
||||
Trace.Info($"Client cert archive '{clientCertArchive}'");
|
||||
}
|
||||
|
||||
CACertificateFile = caCert;
|
||||
ClientCertificateFile = clientCert;
|
||||
ClientCertificatePrivateKeyFile = clientCertPrivateKey;
|
||||
ClientCertificateArchiveFile = clientCertArchive;
|
||||
ClientCertificatePassword = clientCertPassword;
|
||||
|
||||
_runnerClientCertificateManager.AddClientCertificate(ClientCertificateArchiveFile, ClientCertificatePassword);
|
||||
}
|
||||
|
||||
// This should only be called from config
|
||||
public void SaveCertificateSetting()
|
||||
{
|
||||
string certSettingFile = HostContext.GetConfigFile(WellKnownConfigFile.Certificates);
|
||||
IOUtil.DeleteFile(certSettingFile);
|
||||
|
||||
var setting = new RunnerCertificateSetting();
|
||||
if (SkipServerCertificateValidation)
|
||||
{
|
||||
Trace.Info($"Store Skip ServerCertificateValidation setting to '{certSettingFile}'");
|
||||
setting.SkipServerCertValidation = true;
|
||||
}
|
||||
|
||||
if (!string.IsNullOrEmpty(CACertificateFile))
|
||||
{
|
||||
Trace.Info($"Store CA cert setting to '{certSettingFile}'");
|
||||
setting.CACert = CACertificateFile;
|
||||
}
|
||||
|
||||
if (!string.IsNullOrEmpty(ClientCertificateFile) &&
|
||||
!string.IsNullOrEmpty(ClientCertificatePrivateKeyFile) &&
|
||||
!string.IsNullOrEmpty(ClientCertificateArchiveFile))
|
||||
{
|
||||
Trace.Info($"Store client cert settings to '{certSettingFile}'");
|
||||
|
||||
setting.ClientCert = ClientCertificateFile;
|
||||
setting.ClientCertPrivatekey = ClientCertificatePrivateKeyFile;
|
||||
setting.ClientCertArchive = ClientCertificateArchiveFile;
|
||||
|
||||
if (!string.IsNullOrEmpty(ClientCertificatePassword))
|
||||
{
|
||||
string lookupKey = Guid.NewGuid().ToString("D").ToUpperInvariant();
|
||||
Trace.Info($"Store client cert private key password with lookup key {lookupKey}");
|
||||
|
||||
var credStore = HostContext.GetService<IRunnerCredentialStore>();
|
||||
credStore.Write($"GITHUB_ACTIONS_RUNNER_CLIENT_CERT_PASSWORD_{lookupKey}", "GitHub", ClientCertificatePassword);
|
||||
|
||||
setting.ClientCertPasswordLookupKey = lookupKey;
|
||||
}
|
||||
}
|
||||
|
||||
if (SkipServerCertificateValidation ||
|
||||
!string.IsNullOrEmpty(CACertificateFile) ||
|
||||
!string.IsNullOrEmpty(ClientCertificateFile))
|
||||
{
|
||||
IOUtil.SaveObject(setting, certSettingFile);
|
||||
File.SetAttributes(certSettingFile, File.GetAttributes(certSettingFile) | FileAttributes.Hidden);
|
||||
}
|
||||
}
|
||||
|
||||
// This should only be called from unconfig
|
||||
public void DeleteCertificateSetting()
|
||||
{
|
||||
string certSettingFile = HostContext.GetConfigFile(WellKnownConfigFile.Certificates);
|
||||
if (File.Exists(certSettingFile))
|
||||
{
|
||||
Trace.Info($"Load runner certificate setting from '{certSettingFile}'");
|
||||
var certSetting = IOUtil.LoadObject<RunnerCertificateSetting>(certSettingFile);
|
||||
|
||||
if (certSetting != null && !string.IsNullOrEmpty(certSetting.ClientCertPasswordLookupKey))
|
||||
{
|
||||
Trace.Info("Delete client cert private key password from credential store.");
|
||||
var credStore = HostContext.GetService<IRunnerCredentialStore>();
|
||||
credStore.Delete($"GITHUB_ACTIONS_RUNNER_CLIENT_CERT_PASSWORD_{certSetting.ClientCertPasswordLookupKey}");
|
||||
}
|
||||
|
||||
Trace.Info($"Delete cert setting file: {certSettingFile}");
|
||||
IOUtil.DeleteFile(certSettingFile);
|
||||
}
|
||||
}
|
||||
|
||||
public void LoadCertificateSettings()
|
||||
{
|
||||
string certSettingFile = HostContext.GetConfigFile(WellKnownConfigFile.Certificates);
|
||||
if (File.Exists(certSettingFile))
|
||||
{
|
||||
Trace.Info($"Load runner certificate setting from '{certSettingFile}'");
|
||||
var certSetting = IOUtil.LoadObject<RunnerCertificateSetting>(certSettingFile);
|
||||
ArgUtil.NotNull(certSetting, nameof(RunnerCertificateSetting));
|
||||
|
||||
if (certSetting.SkipServerCertValidation)
|
||||
{
|
||||
Trace.Info("Ignore SSL server certificate validation error");
|
||||
SkipServerCertificateValidation = true;
|
||||
VssClientHttpRequestSettings.Default.ServerCertificateValidationCallback = HttpClientHandler.DangerousAcceptAnyServerCertificateValidator;
|
||||
}
|
||||
|
||||
if (!string.IsNullOrEmpty(certSetting.CACert))
|
||||
{
|
||||
// make sure all settings file exist
|
||||
ArgUtil.File(certSetting.CACert, nameof(certSetting.CACert));
|
||||
Trace.Info($"CA '{certSetting.CACert}'");
|
||||
CACertificateFile = certSetting.CACert;
|
||||
}
|
||||
|
||||
if (!string.IsNullOrEmpty(certSetting.ClientCert))
|
||||
{
|
||||
// make sure all settings file exist
|
||||
ArgUtil.File(certSetting.ClientCert, nameof(certSetting.ClientCert));
|
||||
ArgUtil.File(certSetting.ClientCertPrivatekey, nameof(certSetting.ClientCertPrivatekey));
|
||||
ArgUtil.File(certSetting.ClientCertArchive, nameof(certSetting.ClientCertArchive));
|
||||
|
||||
Trace.Info($"Client cert '{certSetting.ClientCert}'");
|
||||
Trace.Info($"Client cert private key '{certSetting.ClientCertPrivatekey}'");
|
||||
Trace.Info($"Client cert archive '{certSetting.ClientCertArchive}'");
|
||||
|
||||
ClientCertificateFile = certSetting.ClientCert;
|
||||
ClientCertificatePrivateKeyFile = certSetting.ClientCertPrivatekey;
|
||||
ClientCertificateArchiveFile = certSetting.ClientCertArchive;
|
||||
|
||||
if (!string.IsNullOrEmpty(certSetting.ClientCertPasswordLookupKey))
|
||||
{
|
||||
var cerdStore = HostContext.GetService<IRunnerCredentialStore>();
|
||||
ClientCertificatePassword = cerdStore.Read($"GITHUB_ACTIONS_RUNNER_CLIENT_CERT_PASSWORD_{certSetting.ClientCertPasswordLookupKey}").Password;
|
||||
HostContext.SecretMasker.AddValue(ClientCertificatePassword);
|
||||
}
|
||||
|
||||
_runnerClientCertificateManager.AddClientCertificate(ClientCertificateArchiveFile, ClientCertificatePassword);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
Trace.Info("No certificate setting found.");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
[DataContract]
|
||||
internal class RunnerCertificateSetting
|
||||
{
|
||||
[DataMember]
|
||||
public bool SkipServerCertValidation { get; set; }
|
||||
|
||||
[DataMember]
|
||||
public string CACert { get; set; }
|
||||
|
||||
[DataMember]
|
||||
public string ClientCert { get; set; }
|
||||
|
||||
[DataMember]
|
||||
public string ClientCertPrivatekey { get; set; }
|
||||
|
||||
[DataMember]
|
||||
public string ClientCertArchive { get; set; }
|
||||
|
||||
[DataMember]
|
||||
public string ClientCertPasswordLookupKey { get; set; }
|
||||
}
|
||||
}
|
||||
@@ -1,948 +0,0 @@
|
||||
using System;
|
||||
using System.Collections.Concurrent;
|
||||
using System.Collections.Generic;
|
||||
using System.ComponentModel;
|
||||
using System.Linq;
|
||||
using System.Net;
|
||||
using System.Runtime.InteropServices;
|
||||
using System.Text;
|
||||
using System.Text.RegularExpressions;
|
||||
using System.Threading;
|
||||
using GitHub.Runner.Common.Util;
|
||||
using Newtonsoft.Json;
|
||||
using System.IO;
|
||||
using System.Runtime.Serialization;
|
||||
using System.Security.Cryptography;
|
||||
using GitHub.Runner.Sdk;
|
||||
|
||||
namespace GitHub.Runner.Common
|
||||
{
|
||||
// The purpose of this class is to store user's credential during runner configuration and retrive the credential back at runtime.
|
||||
#if OS_WINDOWS
|
||||
[ServiceLocator(Default = typeof(WindowsRunnerCredentialStore))]
|
||||
#elif OS_OSX
|
||||
[ServiceLocator(Default = typeof(MacOSRunnerCredentialStore))]
|
||||
#else
|
||||
[ServiceLocator(Default = typeof(LinuxRunnerCredentialStore))]
|
||||
#endif
|
||||
public interface IRunnerCredentialStore : IRunnerService
|
||||
{
|
||||
NetworkCredential Write(string target, string username, string password);
|
||||
|
||||
// throw exception when target not found from cred store
|
||||
NetworkCredential Read(string target);
|
||||
|
||||
// throw exception when target not found from cred store
|
||||
void Delete(string target);
|
||||
}
|
||||
|
||||
#if OS_WINDOWS
|
||||
// Windows credential store is per user.
|
||||
// This is a limitation for user configure the runner run as windows service, when user's current login account is different with the service run as account.
|
||||
// Ex: I login the box as domain\admin, configure the runner as windows service and run as domian\buildserver
|
||||
// domain\buildserver won't read the stored credential from domain\admin's windows credential store.
|
||||
// To workaround this limitation.
|
||||
// Anytime we try to save a credential:
|
||||
// 1. store it into current user's windows credential store
|
||||
// 2. use DP-API do a machine level encrypt and store the encrypted content on disk.
|
||||
// At the first time we try to read the credential:
|
||||
// 1. read from current user's windows credential store, delete the DP-API encrypted backup content on disk if the windows credential store read succeed.
|
||||
// 2. if credential not found in current user's windows credential store, read from the DP-API encrypted backup content on disk,
|
||||
// write the credential back the current user's windows credential store and delete the backup on disk.
|
||||
public sealed class WindowsRunnerCredentialStore : RunnerService, IRunnerCredentialStore
|
||||
{
|
||||
private string _credStoreFile;
|
||||
private Dictionary<string, string> _credStore;
|
||||
|
||||
public override void Initialize(IHostContext hostContext)
|
||||
{
|
||||
base.Initialize(hostContext);
|
||||
|
||||
_credStoreFile = hostContext.GetConfigFile(WellKnownConfigFile.CredentialStore);
|
||||
if (File.Exists(_credStoreFile))
|
||||
{
|
||||
_credStore = IOUtil.LoadObject<Dictionary<string, string>>(_credStoreFile);
|
||||
}
|
||||
else
|
||||
{
|
||||
_credStore = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
|
||||
}
|
||||
}
|
||||
|
||||
public NetworkCredential Write(string target, string username, string password)
|
||||
{
|
||||
Trace.Entering();
|
||||
ArgUtil.NotNullOrEmpty(target, nameof(target));
|
||||
ArgUtil.NotNullOrEmpty(username, nameof(username));
|
||||
ArgUtil.NotNullOrEmpty(password, nameof(password));
|
||||
|
||||
// save to .credential_store file first, then Windows credential store
|
||||
string usernameBase64 = Convert.ToBase64String(Encoding.UTF8.GetBytes(username));
|
||||
string passwordBase64 = Convert.ToBase64String(Encoding.UTF8.GetBytes(password));
|
||||
|
||||
// Base64Username:Base64Password -> DP-API machine level encrypt -> Base64Encoding
|
||||
string encryptedUsernamePassword = Convert.ToBase64String(ProtectedData.Protect(Encoding.UTF8.GetBytes($"{usernameBase64}:{passwordBase64}"), null, DataProtectionScope.LocalMachine));
|
||||
Trace.Info($"Credentials for '{target}' written to credential store file.");
|
||||
_credStore[target] = encryptedUsernamePassword;
|
||||
|
||||
// save to .credential_store file
|
||||
SyncCredentialStoreFile();
|
||||
|
||||
// save to Windows Credential Store
|
||||
return WriteInternal(target, username, password);
|
||||
}
|
||||
|
||||
public NetworkCredential Read(string target)
|
||||
{
|
||||
Trace.Entering();
|
||||
ArgUtil.NotNullOrEmpty(target, nameof(target));
|
||||
IntPtr credPtr = IntPtr.Zero;
|
||||
try
|
||||
{
|
||||
if (CredRead(target, CredentialType.Generic, 0, out credPtr))
|
||||
{
|
||||
Credential credStruct = (Credential)Marshal.PtrToStructure(credPtr, typeof(Credential));
|
||||
int passwordLength = (int)credStruct.CredentialBlobSize;
|
||||
string password = passwordLength > 0 ? Marshal.PtrToStringUni(credStruct.CredentialBlob, passwordLength / sizeof(char)) : String.Empty;
|
||||
string username = Marshal.PtrToStringUni(credStruct.UserName);
|
||||
Trace.Info($"Credentials for '{target}' read from windows credential store.");
|
||||
|
||||
// delete from .credential_store file since we are able to read it from windows credential store
|
||||
if (_credStore.Remove(target))
|
||||
{
|
||||
Trace.Info($"Delete credentials for '{target}' from credential store file.");
|
||||
SyncCredentialStoreFile();
|
||||
}
|
||||
|
||||
return new NetworkCredential(username, password);
|
||||
}
|
||||
else
|
||||
{
|
||||
// Can't read from Windows Credential Store, fail back to .credential_store file
|
||||
if (_credStore.ContainsKey(target) && !string.IsNullOrEmpty(_credStore[target]))
|
||||
{
|
||||
Trace.Info($"Credentials for '{target}' read from credential store file.");
|
||||
|
||||
// Base64Decode -> DP-API machine level decrypt -> Base64Username:Base64Password -> Base64Decode
|
||||
string decryptedUsernamePassword = Encoding.UTF8.GetString(ProtectedData.Unprotect(Convert.FromBase64String(_credStore[target]), null, DataProtectionScope.LocalMachine));
|
||||
|
||||
string[] credential = decryptedUsernamePassword.Split(':');
|
||||
if (credential.Length == 2 && !string.IsNullOrEmpty(credential[0]) && !string.IsNullOrEmpty(credential[1]))
|
||||
{
|
||||
string username = Encoding.UTF8.GetString(Convert.FromBase64String(credential[0]));
|
||||
string password = Encoding.UTF8.GetString(Convert.FromBase64String(credential[1]));
|
||||
|
||||
// store back to windows credential store for current user
|
||||
NetworkCredential creds = WriteInternal(target, username, password);
|
||||
|
||||
// delete from .credential_store file since we are able to write the credential to windows credential store for current user.
|
||||
if (_credStore.Remove(target))
|
||||
{
|
||||
Trace.Info($"Delete credentials for '{target}' from credential store file.");
|
||||
SyncCredentialStoreFile();
|
||||
}
|
||||
|
||||
return creds;
|
||||
}
|
||||
else
|
||||
{
|
||||
throw new ArgumentOutOfRangeException(nameof(decryptedUsernamePassword));
|
||||
}
|
||||
}
|
||||
|
||||
throw new Win32Exception(Marshal.GetLastWin32Error(), $"CredRead throw an error for '{target}'");
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
if (credPtr != IntPtr.Zero)
|
||||
{
|
||||
CredFree(credPtr);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void Delete(string target)
|
||||
{
|
||||
Trace.Entering();
|
||||
ArgUtil.NotNullOrEmpty(target, nameof(target));
|
||||
|
||||
// remove from .credential_store file
|
||||
if (_credStore.Remove(target))
|
||||
{
|
||||
Trace.Info($"Delete credentials for '{target}' from credential store file.");
|
||||
SyncCredentialStoreFile();
|
||||
}
|
||||
|
||||
// remove from windows credential store
|
||||
if (!CredDelete(target, CredentialType.Generic, 0))
|
||||
{
|
||||
throw new Win32Exception(Marshal.GetLastWin32Error(), $"Failed to delete credentials for {target}");
|
||||
}
|
||||
else
|
||||
{
|
||||
Trace.Info($"Credentials for '{target}' deleted from windows credential store.");
|
||||
}
|
||||
}
|
||||
|
||||
private NetworkCredential WriteInternal(string target, string username, string password)
|
||||
{
|
||||
// save to Windows Credential Store
|
||||
Credential credential = new Credential()
|
||||
{
|
||||
Type = CredentialType.Generic,
|
||||
Persist = (UInt32)CredentialPersist.LocalMachine,
|
||||
TargetName = Marshal.StringToCoTaskMemUni(target),
|
||||
UserName = Marshal.StringToCoTaskMemUni(username),
|
||||
CredentialBlob = Marshal.StringToCoTaskMemUni(password),
|
||||
CredentialBlobSize = (UInt32)Encoding.Unicode.GetByteCount(password),
|
||||
AttributeCount = 0,
|
||||
Comment = IntPtr.Zero,
|
||||
Attributes = IntPtr.Zero,
|
||||
TargetAlias = IntPtr.Zero
|
||||
};
|
||||
|
||||
try
|
||||
{
|
||||
if (CredWrite(ref credential, 0))
|
||||
{
|
||||
Trace.Info($"Credentials for '{target}' written to windows credential store.");
|
||||
return new NetworkCredential(username, password);
|
||||
}
|
||||
else
|
||||
{
|
||||
int error = Marshal.GetLastWin32Error();
|
||||
throw new Win32Exception(error, "Failed to write credentials");
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
if (credential.CredentialBlob != IntPtr.Zero)
|
||||
{
|
||||
Marshal.FreeCoTaskMem(credential.CredentialBlob);
|
||||
}
|
||||
if (credential.TargetName != IntPtr.Zero)
|
||||
{
|
||||
Marshal.FreeCoTaskMem(credential.TargetName);
|
||||
}
|
||||
if (credential.UserName != IntPtr.Zero)
|
||||
{
|
||||
Marshal.FreeCoTaskMem(credential.UserName);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void SyncCredentialStoreFile()
|
||||
{
|
||||
Trace.Info("Sync in-memory credential store with credential store file.");
|
||||
|
||||
// delete the cred store file first anyway, since it's a readonly file.
|
||||
IOUtil.DeleteFile(_credStoreFile);
|
||||
|
||||
// delete cred store file when all creds gone
|
||||
if (_credStore.Count == 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
else
|
||||
{
|
||||
IOUtil.SaveObject(_credStore, _credStoreFile);
|
||||
File.SetAttributes(_credStoreFile, File.GetAttributes(_credStoreFile) | FileAttributes.Hidden);
|
||||
}
|
||||
}
|
||||
|
||||
[DllImport("Advapi32.dll", EntryPoint = "CredDeleteW", CharSet = CharSet.Unicode, SetLastError = true)]
|
||||
internal static extern bool CredDelete(string target, CredentialType type, int reservedFlag);
|
||||
|
||||
[DllImport("Advapi32.dll", EntryPoint = "CredReadW", CharSet = CharSet.Unicode, SetLastError = true)]
|
||||
internal static extern bool CredRead(string target, CredentialType type, int reservedFlag, out IntPtr CredentialPtr);
|
||||
|
||||
[DllImport("Advapi32.dll", EntryPoint = "CredWriteW", CharSet = CharSet.Unicode, SetLastError = true)]
|
||||
internal static extern bool CredWrite([In] ref Credential userCredential, [In] UInt32 flags);
|
||||
|
||||
[DllImport("Advapi32.dll", EntryPoint = "CredFree", SetLastError = true)]
|
||||
internal static extern bool CredFree([In] IntPtr cred);
|
||||
|
||||
internal enum CredentialPersist : UInt32
|
||||
{
|
||||
Session = 0x01,
|
||||
LocalMachine = 0x02
|
||||
}
|
||||
|
||||
internal enum CredentialType : uint
|
||||
{
|
||||
Generic = 0x01,
|
||||
DomainPassword = 0x02,
|
||||
DomainCertificate = 0x03
|
||||
}
|
||||
|
||||
[StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)]
|
||||
internal struct Credential
|
||||
{
|
||||
public UInt32 Flags;
|
||||
public CredentialType Type;
|
||||
public IntPtr TargetName;
|
||||
public IntPtr Comment;
|
||||
public System.Runtime.InteropServices.ComTypes.FILETIME LastWritten;
|
||||
public UInt32 CredentialBlobSize;
|
||||
public IntPtr CredentialBlob;
|
||||
public UInt32 Persist;
|
||||
public UInt32 AttributeCount;
|
||||
public IntPtr Attributes;
|
||||
public IntPtr TargetAlias;
|
||||
public IntPtr UserName;
|
||||
}
|
||||
}
|
||||
#elif OS_OSX
|
||||
public sealed class MacOSRunnerCredentialStore : RunnerService, IRunnerCredentialStore
|
||||
{
|
||||
private const string _osxRunnerCredStoreKeyChainName = "_GITHUB_ACTIONS_RUNNER_CREDSTORE_INTERNAL_";
|
||||
|
||||
// Keychain requires a password, but this is not intended to add security
|
||||
private const string _osxRunnerCredStoreKeyChainPassword = "C46F23C36AF94B72B1EAEE32C68670A0";
|
||||
|
||||
private string _securityUtil;
|
||||
|
||||
private string _runnerCredStoreKeyChain;
|
||||
|
||||
public override void Initialize(IHostContext hostContext)
|
||||
{
|
||||
base.Initialize(hostContext);
|
||||
|
||||
_securityUtil = WhichUtil.Which("security", true, Trace);
|
||||
|
||||
_runnerCredStoreKeyChain = hostContext.GetConfigFile(WellKnownConfigFile.CredentialStore);
|
||||
|
||||
// Create osx key chain if it doesn't exists.
|
||||
if (!File.Exists(_runnerCredStoreKeyChain))
|
||||
{
|
||||
List<string> securityOut = new List<string>();
|
||||
List<string> securityError = new List<string>();
|
||||
object outputLock = new object();
|
||||
using (var p = HostContext.CreateService<IProcessInvoker>())
|
||||
{
|
||||
p.OutputDataReceived += delegate (object sender, ProcessDataReceivedEventArgs stdout)
|
||||
{
|
||||
if (!string.IsNullOrEmpty(stdout.Data))
|
||||
{
|
||||
lock (outputLock)
|
||||
{
|
||||
securityOut.Add(stdout.Data);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
p.ErrorDataReceived += delegate (object sender, ProcessDataReceivedEventArgs stderr)
|
||||
{
|
||||
if (!string.IsNullOrEmpty(stderr.Data))
|
||||
{
|
||||
lock (outputLock)
|
||||
{
|
||||
securityError.Add(stderr.Data);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// make sure the 'security' has access to the key so we won't get prompt at runtime.
|
||||
int exitCode = p.ExecuteAsync(workingDirectory: HostContext.GetDirectory(WellKnownDirectory.Root),
|
||||
fileName: _securityUtil,
|
||||
arguments: $"create-keychain -p {_osxRunnerCredStoreKeyChainPassword} \"{_runnerCredStoreKeyChain}\"",
|
||||
environment: null,
|
||||
cancellationToken: CancellationToken.None).GetAwaiter().GetResult();
|
||||
if (exitCode == 0)
|
||||
{
|
||||
Trace.Info($"Successfully create-keychain for {_runnerCredStoreKeyChain}");
|
||||
}
|
||||
else
|
||||
{
|
||||
if (securityOut.Count > 0)
|
||||
{
|
||||
Trace.Error(string.Join(Environment.NewLine, securityOut));
|
||||
}
|
||||
if (securityError.Count > 0)
|
||||
{
|
||||
Trace.Error(string.Join(Environment.NewLine, securityError));
|
||||
}
|
||||
|
||||
throw new InvalidOperationException($"'security create-keychain' failed with exit code {exitCode}.");
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
// Try unlock and lock the keychain, make sure it's still in good stage
|
||||
UnlockKeyChain();
|
||||
LockKeyChain();
|
||||
}
|
||||
}
|
||||
|
||||
public NetworkCredential Write(string target, string username, string password)
|
||||
{
|
||||
Trace.Entering();
|
||||
ArgUtil.NotNullOrEmpty(target, nameof(target));
|
||||
ArgUtil.NotNullOrEmpty(username, nameof(username));
|
||||
ArgUtil.NotNullOrEmpty(password, nameof(password));
|
||||
|
||||
try
|
||||
{
|
||||
UnlockKeyChain();
|
||||
|
||||
// base64encode username + ':' + base64encode password
|
||||
// OSX keychain requires you provide -s target and -a username to retrieve password
|
||||
// So, we will trade both username and password as 'secret' store into keychain
|
||||
string usernameBase64 = Convert.ToBase64String(Encoding.UTF8.GetBytes(username));
|
||||
string passwordBase64 = Convert.ToBase64String(Encoding.UTF8.GetBytes(password));
|
||||
string secretForKeyChain = $"{usernameBase64}:{passwordBase64}";
|
||||
|
||||
List<string> securityOut = new List<string>();
|
||||
List<string> securityError = new List<string>();
|
||||
object outputLock = new object();
|
||||
using (var p = HostContext.CreateService<IProcessInvoker>())
|
||||
{
|
||||
p.OutputDataReceived += delegate (object sender, ProcessDataReceivedEventArgs stdout)
|
||||
{
|
||||
if (!string.IsNullOrEmpty(stdout.Data))
|
||||
{
|
||||
lock (outputLock)
|
||||
{
|
||||
securityOut.Add(stdout.Data);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
p.ErrorDataReceived += delegate (object sender, ProcessDataReceivedEventArgs stderr)
|
||||
{
|
||||
if (!string.IsNullOrEmpty(stderr.Data))
|
||||
{
|
||||
lock (outputLock)
|
||||
{
|
||||
securityError.Add(stderr.Data);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// make sure the 'security' has access to the key so we won't get prompt at runtime.
|
||||
int exitCode = p.ExecuteAsync(workingDirectory: HostContext.GetDirectory(WellKnownDirectory.Root),
|
||||
fileName: _securityUtil,
|
||||
arguments: $"add-generic-password -s {target} -a GITHUBACTIONSRUNNER -w {secretForKeyChain} -T \"{_securityUtil}\" \"{_runnerCredStoreKeyChain}\"",
|
||||
environment: null,
|
||||
cancellationToken: CancellationToken.None).GetAwaiter().GetResult();
|
||||
if (exitCode == 0)
|
||||
{
|
||||
Trace.Info($"Successfully add-generic-password for {target} (GITHUBACTIONSRUNNER)");
|
||||
}
|
||||
else
|
||||
{
|
||||
if (securityOut.Count > 0)
|
||||
{
|
||||
Trace.Error(string.Join(Environment.NewLine, securityOut));
|
||||
}
|
||||
if (securityError.Count > 0)
|
||||
{
|
||||
Trace.Error(string.Join(Environment.NewLine, securityError));
|
||||
}
|
||||
|
||||
throw new InvalidOperationException($"'security add-generic-password' failed with exit code {exitCode}.");
|
||||
}
|
||||
}
|
||||
|
||||
return new NetworkCredential(username, password);
|
||||
}
|
||||
finally
|
||||
{
|
||||
LockKeyChain();
|
||||
}
|
||||
}
|
||||
|
||||
public NetworkCredential Read(string target)
|
||||
{
|
||||
Trace.Entering();
|
||||
ArgUtil.NotNullOrEmpty(target, nameof(target));
|
||||
|
||||
try
|
||||
{
|
||||
UnlockKeyChain();
|
||||
|
||||
string username;
|
||||
string password;
|
||||
|
||||
List<string> securityOut = new List<string>();
|
||||
List<string> securityError = new List<string>();
|
||||
object outputLock = new object();
|
||||
using (var p = HostContext.CreateService<IProcessInvoker>())
|
||||
{
|
||||
p.OutputDataReceived += delegate (object sender, ProcessDataReceivedEventArgs stdout)
|
||||
{
|
||||
if (!string.IsNullOrEmpty(stdout.Data))
|
||||
{
|
||||
lock (outputLock)
|
||||
{
|
||||
securityOut.Add(stdout.Data);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
p.ErrorDataReceived += delegate (object sender, ProcessDataReceivedEventArgs stderr)
|
||||
{
|
||||
if (!string.IsNullOrEmpty(stderr.Data))
|
||||
{
|
||||
lock (outputLock)
|
||||
{
|
||||
securityError.Add(stderr.Data);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
int exitCode = p.ExecuteAsync(workingDirectory: HostContext.GetDirectory(WellKnownDirectory.Root),
|
||||
fileName: _securityUtil,
|
||||
arguments: $"find-generic-password -s {target} -a GITHUBACTIONSRUNNER -w -g \"{_runnerCredStoreKeyChain}\"",
|
||||
environment: null,
|
||||
cancellationToken: CancellationToken.None).GetAwaiter().GetResult();
|
||||
if (exitCode == 0)
|
||||
{
|
||||
string keyChainSecret = securityOut.First();
|
||||
string[] secrets = keyChainSecret.Split(':');
|
||||
if (secrets.Length == 2 && !string.IsNullOrEmpty(secrets[0]) && !string.IsNullOrEmpty(secrets[1]))
|
||||
{
|
||||
Trace.Info($"Successfully find-generic-password for {target} (GITHUBACTIONSRUNNER)");
|
||||
username = Encoding.UTF8.GetString(Convert.FromBase64String(secrets[0]));
|
||||
password = Encoding.UTF8.GetString(Convert.FromBase64String(secrets[1]));
|
||||
return new NetworkCredential(username, password);
|
||||
}
|
||||
else
|
||||
{
|
||||
throw new ArgumentOutOfRangeException(nameof(keyChainSecret));
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
if (securityOut.Count > 0)
|
||||
{
|
||||
Trace.Error(string.Join(Environment.NewLine, securityOut));
|
||||
}
|
||||
if (securityError.Count > 0)
|
||||
{
|
||||
Trace.Error(string.Join(Environment.NewLine, securityError));
|
||||
}
|
||||
|
||||
throw new InvalidOperationException($"'security find-generic-password' failed with exit code {exitCode}.");
|
||||
}
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
LockKeyChain();
|
||||
}
|
||||
}
|
||||
|
||||
public void Delete(string target)
|
||||
{
|
||||
Trace.Entering();
|
||||
ArgUtil.NotNullOrEmpty(target, nameof(target));
|
||||
|
||||
try
|
||||
{
|
||||
UnlockKeyChain();
|
||||
|
||||
List<string> securityOut = new List<string>();
|
||||
List<string> securityError = new List<string>();
|
||||
object outputLock = new object();
|
||||
|
||||
using (var p = HostContext.CreateService<IProcessInvoker>())
|
||||
{
|
||||
p.OutputDataReceived += delegate (object sender, ProcessDataReceivedEventArgs stdout)
|
||||
{
|
||||
if (!string.IsNullOrEmpty(stdout.Data))
|
||||
{
|
||||
lock (outputLock)
|
||||
{
|
||||
securityOut.Add(stdout.Data);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
p.ErrorDataReceived += delegate (object sender, ProcessDataReceivedEventArgs stderr)
|
||||
{
|
||||
if (!string.IsNullOrEmpty(stderr.Data))
|
||||
{
|
||||
lock (outputLock)
|
||||
{
|
||||
securityError.Add(stderr.Data);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
int exitCode = p.ExecuteAsync(workingDirectory: HostContext.GetDirectory(WellKnownDirectory.Root),
|
||||
fileName: _securityUtil,
|
||||
arguments: $"delete-generic-password -s {target} -a GITHUBACTIONSRUNNER \"{_runnerCredStoreKeyChain}\"",
|
||||
environment: null,
|
||||
cancellationToken: CancellationToken.None).GetAwaiter().GetResult();
|
||||
if (exitCode == 0)
|
||||
{
|
||||
Trace.Info($"Successfully delete-generic-password for {target} (GITHUBACTIONSRUNNER)");
|
||||
}
|
||||
else
|
||||
{
|
||||
if (securityOut.Count > 0)
|
||||
{
|
||||
Trace.Error(string.Join(Environment.NewLine, securityOut));
|
||||
}
|
||||
if (securityError.Count > 0)
|
||||
{
|
||||
Trace.Error(string.Join(Environment.NewLine, securityError));
|
||||
}
|
||||
|
||||
throw new InvalidOperationException($"'security delete-generic-password' failed with exit code {exitCode}.");
|
||||
}
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
LockKeyChain();
|
||||
}
|
||||
}
|
||||
|
||||
private void UnlockKeyChain()
|
||||
{
|
||||
Trace.Entering();
|
||||
ArgUtil.NotNullOrEmpty(_securityUtil, nameof(_securityUtil));
|
||||
ArgUtil.NotNullOrEmpty(_runnerCredStoreKeyChain, nameof(_runnerCredStoreKeyChain));
|
||||
|
||||
List<string> securityOut = new List<string>();
|
||||
List<string> securityError = new List<string>();
|
||||
object outputLock = new object();
|
||||
using (var p = HostContext.CreateService<IProcessInvoker>())
|
||||
{
|
||||
p.OutputDataReceived += delegate (object sender, ProcessDataReceivedEventArgs stdout)
|
||||
{
|
||||
if (!string.IsNullOrEmpty(stdout.Data))
|
||||
{
|
||||
lock (outputLock)
|
||||
{
|
||||
securityOut.Add(stdout.Data);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
p.ErrorDataReceived += delegate (object sender, ProcessDataReceivedEventArgs stderr)
|
||||
{
|
||||
if (!string.IsNullOrEmpty(stderr.Data))
|
||||
{
|
||||
lock (outputLock)
|
||||
{
|
||||
securityError.Add(stderr.Data);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// make sure the 'security' has access to the key so we won't get prompt at runtime.
|
||||
int exitCode = p.ExecuteAsync(workingDirectory: HostContext.GetDirectory(WellKnownDirectory.Root),
|
||||
fileName: _securityUtil,
|
||||
arguments: $"unlock-keychain -p {_osxRunnerCredStoreKeyChainPassword} \"{_runnerCredStoreKeyChain}\"",
|
||||
environment: null,
|
||||
cancellationToken: CancellationToken.None).GetAwaiter().GetResult();
|
||||
if (exitCode == 0)
|
||||
{
|
||||
Trace.Info($"Successfully unlock-keychain for {_runnerCredStoreKeyChain}");
|
||||
}
|
||||
else
|
||||
{
|
||||
if (securityOut.Count > 0)
|
||||
{
|
||||
Trace.Error(string.Join(Environment.NewLine, securityOut));
|
||||
}
|
||||
if (securityError.Count > 0)
|
||||
{
|
||||
Trace.Error(string.Join(Environment.NewLine, securityError));
|
||||
}
|
||||
|
||||
throw new InvalidOperationException($"'security unlock-keychain' failed with exit code {exitCode}.");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void LockKeyChain()
|
||||
{
|
||||
Trace.Entering();
|
||||
ArgUtil.NotNullOrEmpty(_securityUtil, nameof(_securityUtil));
|
||||
ArgUtil.NotNullOrEmpty(_runnerCredStoreKeyChain, nameof(_runnerCredStoreKeyChain));
|
||||
|
||||
List<string> securityOut = new List<string>();
|
||||
List<string> securityError = new List<string>();
|
||||
object outputLock = new object();
|
||||
using (var p = HostContext.CreateService<IProcessInvoker>())
|
||||
{
|
||||
p.OutputDataReceived += delegate (object sender, ProcessDataReceivedEventArgs stdout)
|
||||
{
|
||||
if (!string.IsNullOrEmpty(stdout.Data))
|
||||
{
|
||||
lock (outputLock)
|
||||
{
|
||||
securityOut.Add(stdout.Data);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
p.ErrorDataReceived += delegate (object sender, ProcessDataReceivedEventArgs stderr)
|
||||
{
|
||||
if (!string.IsNullOrEmpty(stderr.Data))
|
||||
{
|
||||
lock (outputLock)
|
||||
{
|
||||
securityError.Add(stderr.Data);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// make sure the 'security' has access to the key so we won't get prompt at runtime.
|
||||
int exitCode = p.ExecuteAsync(workingDirectory: HostContext.GetDirectory(WellKnownDirectory.Root),
|
||||
fileName: _securityUtil,
|
||||
arguments: $"lock-keychain \"{_runnerCredStoreKeyChain}\"",
|
||||
environment: null,
|
||||
cancellationToken: CancellationToken.None).GetAwaiter().GetResult();
|
||||
if (exitCode == 0)
|
||||
{
|
||||
Trace.Info($"Successfully lock-keychain for {_runnerCredStoreKeyChain}");
|
||||
}
|
||||
else
|
||||
{
|
||||
if (securityOut.Count > 0)
|
||||
{
|
||||
Trace.Error(string.Join(Environment.NewLine, securityOut));
|
||||
}
|
||||
if (securityError.Count > 0)
|
||||
{
|
||||
Trace.Error(string.Join(Environment.NewLine, securityError));
|
||||
}
|
||||
|
||||
throw new InvalidOperationException($"'security lock-keychain' failed with exit code {exitCode}.");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
#else
|
||||
public sealed class LinuxRunnerCredentialStore : RunnerService, IRunnerCredentialStore
|
||||
{
|
||||
// 'ghrunner' 128 bits iv
|
||||
private readonly byte[] iv = new byte[] { 0x67, 0x68, 0x72, 0x75, 0x6e, 0x6e, 0x65, 0x72, 0x67, 0x68, 0x72, 0x75, 0x6e, 0x6e, 0x65, 0x72 };
|
||||
|
||||
// 256 bits key
|
||||
private byte[] _symmetricKey;
|
||||
private string _credStoreFile;
|
||||
private Dictionary<string, Credential> _credStore;
|
||||
|
||||
public override void Initialize(IHostContext hostContext)
|
||||
{
|
||||
base.Initialize(hostContext);
|
||||
|
||||
_credStoreFile = hostContext.GetConfigFile(WellKnownConfigFile.CredentialStore);
|
||||
if (File.Exists(_credStoreFile))
|
||||
{
|
||||
_credStore = IOUtil.LoadObject<Dictionary<string, Credential>>(_credStoreFile);
|
||||
}
|
||||
else
|
||||
{
|
||||
_credStore = new Dictionary<string, Credential>(StringComparer.OrdinalIgnoreCase);
|
||||
}
|
||||
|
||||
string machineId;
|
||||
if (File.Exists("/etc/machine-id"))
|
||||
{
|
||||
// try use machine-id as encryption key
|
||||
// this helps avoid accidental information disclosure, but isn't intended for true security
|
||||
machineId = File.ReadAllLines("/etc/machine-id").FirstOrDefault();
|
||||
Trace.Info($"machine-id length {machineId?.Length ?? 0}.");
|
||||
|
||||
// machine-id doesn't exist or machine-id is not 256 bits
|
||||
if (string.IsNullOrEmpty(machineId) || machineId.Length != 32)
|
||||
{
|
||||
Trace.Warning("Can not get valid machine id from '/etc/machine-id'.");
|
||||
machineId = "43e7fe5da07740cf914b90f1dac51c2a";
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
// /etc/machine-id not exist
|
||||
Trace.Warning("/etc/machine-id doesn't exist.");
|
||||
machineId = "43e7fe5da07740cf914b90f1dac51c2a";
|
||||
}
|
||||
|
||||
List<byte> keyBuilder = new List<byte>();
|
||||
foreach (var c in machineId)
|
||||
{
|
||||
keyBuilder.Add(Convert.ToByte(c));
|
||||
}
|
||||
|
||||
_symmetricKey = keyBuilder.ToArray();
|
||||
}
|
||||
|
||||
public NetworkCredential Write(string target, string username, string password)
|
||||
{
|
||||
Trace.Entering();
|
||||
ArgUtil.NotNullOrEmpty(target, nameof(target));
|
||||
ArgUtil.NotNullOrEmpty(username, nameof(username));
|
||||
ArgUtil.NotNullOrEmpty(password, nameof(password));
|
||||
|
||||
Trace.Info($"Store credential for '{target}' to cred store.");
|
||||
Credential cred = new Credential(username, Encrypt(password));
|
||||
_credStore[target] = cred;
|
||||
SyncCredentialStoreFile();
|
||||
return new NetworkCredential(username, password);
|
||||
}
|
||||
|
||||
public NetworkCredential Read(string target)
|
||||
{
|
||||
Trace.Entering();
|
||||
ArgUtil.NotNullOrEmpty(target, nameof(target));
|
||||
Trace.Info($"Read credential for '{target}' from cred store.");
|
||||
if (_credStore.ContainsKey(target))
|
||||
{
|
||||
Credential cred = _credStore[target];
|
||||
if (!string.IsNullOrEmpty(cred.UserName) && !string.IsNullOrEmpty(cred.Password))
|
||||
{
|
||||
Trace.Info($"Return credential for '{target}' from cred store.");
|
||||
return new NetworkCredential(cred.UserName, Decrypt(cred.Password));
|
||||
}
|
||||
}
|
||||
|
||||
throw new KeyNotFoundException(target);
|
||||
}
|
||||
|
||||
public void Delete(string target)
|
||||
{
|
||||
Trace.Entering();
|
||||
ArgUtil.NotNullOrEmpty(target, nameof(target));
|
||||
|
||||
if (_credStore.ContainsKey(target))
|
||||
{
|
||||
Trace.Info($"Delete credential for '{target}' from cred store.");
|
||||
_credStore.Remove(target);
|
||||
SyncCredentialStoreFile();
|
||||
}
|
||||
else
|
||||
{
|
||||
throw new KeyNotFoundException(target);
|
||||
}
|
||||
}
|
||||
|
||||
private void SyncCredentialStoreFile()
|
||||
{
|
||||
Trace.Entering();
|
||||
Trace.Info("Sync in-memory credential store with credential store file.");
|
||||
|
||||
// delete cred store file when all creds gone
|
||||
if (_credStore.Count == 0)
|
||||
{
|
||||
IOUtil.DeleteFile(_credStoreFile);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!File.Exists(_credStoreFile))
|
||||
{
|
||||
CreateCredentialStoreFile();
|
||||
}
|
||||
|
||||
IOUtil.SaveObject(_credStore, _credStoreFile);
|
||||
}
|
||||
|
||||
private string Encrypt(string secret)
|
||||
{
|
||||
using (Aes aes = Aes.Create())
|
||||
{
|
||||
aes.Key = _symmetricKey;
|
||||
aes.IV = iv;
|
||||
|
||||
// Create a decrytor to perform the stream transform.
|
||||
ICryptoTransform encryptor = aes.CreateEncryptor();
|
||||
|
||||
// Create the streams used for encryption.
|
||||
using (MemoryStream msEncrypt = new MemoryStream())
|
||||
{
|
||||
using (CryptoStream csEncrypt = new CryptoStream(msEncrypt, encryptor, CryptoStreamMode.Write))
|
||||
{
|
||||
using (StreamWriter swEncrypt = new StreamWriter(csEncrypt))
|
||||
{
|
||||
swEncrypt.Write(secret);
|
||||
}
|
||||
|
||||
return Convert.ToBase64String(msEncrypt.ToArray());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private string Decrypt(string encryptedText)
|
||||
{
|
||||
using (Aes aes = Aes.Create())
|
||||
{
|
||||
aes.Key = _symmetricKey;
|
||||
aes.IV = iv;
|
||||
|
||||
// Create a decrytor to perform the stream transform.
|
||||
ICryptoTransform decryptor = aes.CreateDecryptor();
|
||||
|
||||
// Create the streams used for decryption.
|
||||
using (MemoryStream msDecrypt = new MemoryStream(Convert.FromBase64String(encryptedText)))
|
||||
{
|
||||
using (CryptoStream csDecrypt = new CryptoStream(msDecrypt, decryptor, CryptoStreamMode.Read))
|
||||
{
|
||||
using (StreamReader srDecrypt = new StreamReader(csDecrypt))
|
||||
{
|
||||
// Read the decrypted bytes from the decrypting stream and place them in a string.
|
||||
return srDecrypt.ReadToEnd();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void CreateCredentialStoreFile()
|
||||
{
|
||||
File.WriteAllText(_credStoreFile, "");
|
||||
File.SetAttributes(_credStoreFile, File.GetAttributes(_credStoreFile) | FileAttributes.Hidden);
|
||||
|
||||
// Try to lock down the .credentials_store file to the owner/group
|
||||
var chmodPath = WhichUtil.Which("chmod", trace: Trace);
|
||||
if (!String.IsNullOrEmpty(chmodPath))
|
||||
{
|
||||
var arguments = $"600 {new FileInfo(_credStoreFile).FullName}";
|
||||
using (var invoker = HostContext.CreateService<IProcessInvoker>())
|
||||
{
|
||||
var exitCode = invoker.ExecuteAsync(HostContext.GetDirectory(WellKnownDirectory.Root), chmodPath, arguments, null, default(CancellationToken)).GetAwaiter().GetResult();
|
||||
if (exitCode == 0)
|
||||
{
|
||||
Trace.Info("Successfully set permissions for credentials store file {0}", _credStoreFile);
|
||||
}
|
||||
else
|
||||
{
|
||||
Trace.Warning("Unable to successfully set permissions for credentials store file {0}. Received exit code {1} from {2}", _credStoreFile, exitCode, chmodPath);
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
Trace.Warning("Unable to locate chmod to set permissions for credentials store file {0}.", _credStoreFile);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
[DataContract]
|
||||
internal class Credential
|
||||
{
|
||||
public Credential()
|
||||
{ }
|
||||
|
||||
public Credential(string userName, string password)
|
||||
{
|
||||
UserName = userName;
|
||||
Password = password;
|
||||
}
|
||||
|
||||
[DataMember(IsRequired = true)]
|
||||
public string UserName { get; set; }
|
||||
|
||||
[DataMember(IsRequired = true)]
|
||||
public string Password { get; set; }
|
||||
}
|
||||
#endif
|
||||
}
|
||||
@@ -28,14 +28,10 @@ namespace GitHub.Runner.Listener
|
||||
private readonly string[] validFlags =
|
||||
{
|
||||
Constants.Runner.CommandLine.Flags.Commit,
|
||||
#if OS_WINDOWS
|
||||
Constants.Runner.CommandLine.Flags.GitUseSChannel,
|
||||
#endif
|
||||
Constants.Runner.CommandLine.Flags.Help,
|
||||
Constants.Runner.CommandLine.Flags.Replace,
|
||||
Constants.Runner.CommandLine.Flags.RunAsService,
|
||||
Constants.Runner.CommandLine.Flags.Once,
|
||||
Constants.Runner.CommandLine.Flags.SslSkipCertValidation,
|
||||
Constants.Runner.CommandLine.Flags.Unattended,
|
||||
Constants.Runner.CommandLine.Flags.Version
|
||||
};
|
||||
@@ -45,13 +41,7 @@ namespace GitHub.Runner.Listener
|
||||
Constants.Runner.CommandLine.Args.Auth,
|
||||
Constants.Runner.CommandLine.Args.MonitorSocketAddress,
|
||||
Constants.Runner.CommandLine.Args.Name,
|
||||
Constants.Runner.CommandLine.Args.Password,
|
||||
Constants.Runner.CommandLine.Args.Pool,
|
||||
Constants.Runner.CommandLine.Args.SslCACert,
|
||||
Constants.Runner.CommandLine.Args.SslClientCert,
|
||||
Constants.Runner.CommandLine.Args.SslClientCertKey,
|
||||
Constants.Runner.CommandLine.Args.SslClientCertArchive,
|
||||
Constants.Runner.CommandLine.Args.SslClientCertPassword,
|
||||
Constants.Runner.CommandLine.Args.StartupType,
|
||||
Constants.Runner.CommandLine.Args.Token,
|
||||
Constants.Runner.CommandLine.Args.Url,
|
||||
@@ -73,9 +63,6 @@ namespace GitHub.Runner.Listener
|
||||
public bool Unattended => TestFlag(Constants.Runner.CommandLine.Flags.Unattended);
|
||||
public bool Version => TestFlag(Constants.Runner.CommandLine.Flags.Version);
|
||||
|
||||
#if OS_WINDOWS
|
||||
public bool GitUseSChannel => TestFlag(Constants.Runner.CommandLine.Flags.GitUseSChannel);
|
||||
#endif
|
||||
public bool RunOnce => TestFlag(Constants.Runner.CommandLine.Flags.Once);
|
||||
|
||||
// Constructor.
|
||||
@@ -160,13 +147,6 @@ namespace GitHub.Runner.Listener
|
||||
defaultValue: false);
|
||||
}
|
||||
|
||||
public bool GetAutoLaunchBrowser()
|
||||
{
|
||||
return TestFlagOrPrompt(
|
||||
name: Constants.Runner.CommandLine.Flags.LaunchBrowser,
|
||||
description: "Would you like to launch your browser for AAD Device Code Flow? (Y/N)",
|
||||
defaultValue: true);
|
||||
}
|
||||
//
|
||||
// Args.
|
||||
//
|
||||
@@ -179,24 +159,6 @@ namespace GitHub.Runner.Listener
|
||||
validator: Validators.AuthSchemeValidator);
|
||||
}
|
||||
|
||||
public string GetPassword()
|
||||
{
|
||||
return GetArgOrPrompt(
|
||||
name: Constants.Runner.CommandLine.Args.Password,
|
||||
description: "What is your GitHub password?",
|
||||
defaultValue: string.Empty,
|
||||
validator: Validators.NonEmptyValidator);
|
||||
}
|
||||
|
||||
public string GetPool()
|
||||
{
|
||||
return GetArgOrPrompt(
|
||||
name: Constants.Runner.CommandLine.Args.Pool,
|
||||
description: "Enter the name of your runner pool:",
|
||||
defaultValue: "default",
|
||||
validator: Validators.NonEmptyValidator);
|
||||
}
|
||||
|
||||
public string GetRunnerName()
|
||||
{
|
||||
return GetArgOrPrompt(
|
||||
@@ -210,7 +172,7 @@ namespace GitHub.Runner.Listener
|
||||
{
|
||||
return GetArgOrPrompt(
|
||||
name: Constants.Runner.CommandLine.Args.Token,
|
||||
description: "Enter your personal access token:",
|
||||
description: "What is your pool admin oauth access token?",
|
||||
defaultValue: string.Empty,
|
||||
validator: Validators.NonEmptyValidator);
|
||||
}
|
||||
@@ -219,7 +181,16 @@ namespace GitHub.Runner.Listener
|
||||
{
|
||||
return GetArgOrPrompt(
|
||||
name: Constants.Runner.CommandLine.Args.Token,
|
||||
description: "Enter runner register token:",
|
||||
description: "What is your runner register token?",
|
||||
defaultValue: string.Empty,
|
||||
validator: Validators.NonEmptyValidator);
|
||||
}
|
||||
|
||||
public string GetRunnerDeletionToken()
|
||||
{
|
||||
return GetArgOrPrompt(
|
||||
name: Constants.Runner.CommandLine.Args.Token,
|
||||
description: "Enter runner deletion token:",
|
||||
defaultValue: string.Empty,
|
||||
validator: Validators.NonEmptyValidator);
|
||||
}
|
||||
@@ -240,15 +211,6 @@ namespace GitHub.Runner.Listener
|
||||
validator: Validators.ServerUrlValidator);
|
||||
}
|
||||
|
||||
public string GetUserName()
|
||||
{
|
||||
return GetArgOrPrompt(
|
||||
name: Constants.Runner.CommandLine.Args.UserName,
|
||||
description: "What is your GitHub username?",
|
||||
defaultValue: string.Empty,
|
||||
validator: Validators.NonEmptyValidator);
|
||||
}
|
||||
|
||||
public string GetWindowsLogonAccount(string defaultValue, string descriptionMsg)
|
||||
{
|
||||
return GetArgOrPrompt(
|
||||
@@ -287,36 +249,6 @@ namespace GitHub.Runner.Listener
|
||||
return GetArg(Constants.Runner.CommandLine.Args.StartupType);
|
||||
}
|
||||
|
||||
public bool GetSkipCertificateValidation()
|
||||
{
|
||||
return TestFlag(Constants.Runner.CommandLine.Flags.SslSkipCertValidation);
|
||||
}
|
||||
|
||||
public string GetCACertificate()
|
||||
{
|
||||
return GetArg(Constants.Runner.CommandLine.Args.SslCACert);
|
||||
}
|
||||
|
||||
public string GetClientCertificate()
|
||||
{
|
||||
return GetArg(Constants.Runner.CommandLine.Args.SslClientCert);
|
||||
}
|
||||
|
||||
public string GetClientCertificatePrivateKey()
|
||||
{
|
||||
return GetArg(Constants.Runner.CommandLine.Args.SslClientCertKey);
|
||||
}
|
||||
|
||||
public string GetClientCertificateArchrive()
|
||||
{
|
||||
return GetArg(Constants.Runner.CommandLine.Args.SslClientCertArchive);
|
||||
}
|
||||
|
||||
public string GetClientCertificatePassword()
|
||||
{
|
||||
return GetArg(Constants.Runner.CommandLine.Args.SslClientCertPassword);
|
||||
}
|
||||
|
||||
//
|
||||
// Private helpers.
|
||||
//
|
||||
|
||||
@@ -85,54 +85,6 @@ namespace GitHub.Runner.Listener.Configuration
|
||||
throw new InvalidOperationException("Cannot configure the runner because it is already configured. To reconfigure the runner, run 'config.cmd remove' or './config.sh remove' first.");
|
||||
}
|
||||
|
||||
// Populate cert setting from commandline args
|
||||
var runnerCertManager = HostContext.GetService<IRunnerCertificateManager>();
|
||||
bool saveCertSetting = false;
|
||||
bool skipCertValidation = command.GetSkipCertificateValidation();
|
||||
string caCert = command.GetCACertificate();
|
||||
string clientCert = command.GetClientCertificate();
|
||||
string clientCertKey = command.GetClientCertificatePrivateKey();
|
||||
string clientCertArchive = command.GetClientCertificateArchrive();
|
||||
string clientCertPassword = command.GetClientCertificatePassword();
|
||||
|
||||
// We require all Certificate files are under agent root.
|
||||
// So we can set ACL correctly when configure as service
|
||||
if (!string.IsNullOrEmpty(caCert))
|
||||
{
|
||||
caCert = Path.Combine(HostContext.GetDirectory(WellKnownDirectory.Root), caCert);
|
||||
ArgUtil.File(caCert, nameof(caCert));
|
||||
}
|
||||
|
||||
if (!string.IsNullOrEmpty(clientCert) &&
|
||||
!string.IsNullOrEmpty(clientCertKey) &&
|
||||
!string.IsNullOrEmpty(clientCertArchive))
|
||||
{
|
||||
// Ensure all client cert pieces are there.
|
||||
clientCert = Path.Combine(HostContext.GetDirectory(WellKnownDirectory.Root), clientCert);
|
||||
clientCertKey = Path.Combine(HostContext.GetDirectory(WellKnownDirectory.Root), clientCertKey);
|
||||
clientCertArchive = Path.Combine(HostContext.GetDirectory(WellKnownDirectory.Root), clientCertArchive);
|
||||
|
||||
ArgUtil.File(clientCert, nameof(clientCert));
|
||||
ArgUtil.File(clientCertKey, nameof(clientCertKey));
|
||||
ArgUtil.File(clientCertArchive, nameof(clientCertArchive));
|
||||
}
|
||||
else if (!string.IsNullOrEmpty(clientCert) ||
|
||||
!string.IsNullOrEmpty(clientCertKey) ||
|
||||
!string.IsNullOrEmpty(clientCertArchive))
|
||||
{
|
||||
// Print out which args are missing.
|
||||
ArgUtil.NotNullOrEmpty(Constants.Runner.CommandLine.Args.SslClientCert, Constants.Runner.CommandLine.Args.SslClientCert);
|
||||
ArgUtil.NotNullOrEmpty(Constants.Runner.CommandLine.Args.SslClientCertKey, Constants.Runner.CommandLine.Args.SslClientCertKey);
|
||||
ArgUtil.NotNullOrEmpty(Constants.Runner.CommandLine.Args.SslClientCertArchive, Constants.Runner.CommandLine.Args.SslClientCertArchive);
|
||||
}
|
||||
|
||||
if (skipCertValidation || !string.IsNullOrEmpty(caCert) || !string.IsNullOrEmpty(clientCert))
|
||||
{
|
||||
Trace.Info("Reset runner cert setting base on commandline args.");
|
||||
(runnerCertManager as RunnerCertificateManager).SetupCertificate(skipCertValidation, caCert, clientCert, clientCertKey, clientCertArchive, clientCertPassword);
|
||||
saveCertSetting = true;
|
||||
}
|
||||
|
||||
RunnerSettings runnerSettings = new RunnerSettings();
|
||||
|
||||
bool isHostedServer = false;
|
||||
@@ -352,31 +304,10 @@ namespace GitHub.Runner.Listener.Configuration
|
||||
|
||||
_store.SaveSettings(runnerSettings);
|
||||
|
||||
if (saveCertSetting)
|
||||
{
|
||||
Trace.Info("Save agent cert setting to disk.");
|
||||
(runnerCertManager as RunnerCertificateManager).SaveCertificateSetting();
|
||||
}
|
||||
|
||||
_term.WriteLine();
|
||||
_term.WriteSuccessMessage("Settings Saved.");
|
||||
_term.WriteLine();
|
||||
|
||||
bool saveRuntimeOptions = false;
|
||||
var runtimeOptions = new RunnerRuntimeOptions();
|
||||
#if OS_WINDOWS
|
||||
if (command.GitUseSChannel)
|
||||
{
|
||||
saveRuntimeOptions = true;
|
||||
runtimeOptions.GitUseSecureChannel = true;
|
||||
}
|
||||
#endif
|
||||
if (saveRuntimeOptions)
|
||||
{
|
||||
Trace.Info("Save agent runtime options to disk.");
|
||||
_store.SaveRunnerRuntimeOptions(runtimeOptions);
|
||||
}
|
||||
|
||||
#if OS_WINDOWS
|
||||
// config windows service
|
||||
bool runAsService = command.GetRunAsService();
|
||||
@@ -441,7 +372,7 @@ namespace GitHub.Runner.Listener.Configuration
|
||||
}
|
||||
else
|
||||
{
|
||||
var githubToken = command.GetToken();
|
||||
var githubToken = command.GetRunnerDeletionToken();
|
||||
GitHubAuthResult authResult = await GetTenantCredential(settings.GitHubUrl, githubToken);
|
||||
creds = authResult.ToVssCredentials();
|
||||
Trace.Info("cred retrieved via GitHub auth");
|
||||
@@ -489,13 +420,6 @@ namespace GitHub.Runner.Listener.Configuration
|
||||
currentAction = "Removing .runner";
|
||||
if (isConfigured)
|
||||
{
|
||||
|
||||
// delete agent cert setting
|
||||
(HostContext.GetService<IRunnerCertificateManager>() as RunnerCertificateManager).DeleteCertificateSetting();
|
||||
|
||||
// delete agent runtime option
|
||||
_store.DeleteRunnerRuntimeOptions();
|
||||
|
||||
_store.DeleteSettings();
|
||||
_term.WriteSuccessMessage("Removed .runner");
|
||||
}
|
||||
|
||||
@@ -78,7 +78,7 @@ namespace GitHub.Runner.Listener.Configuration
|
||||
|
||||
if (string.Equals(TokenSchema, "OAuthAccessToken", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return new VssCredentials(null, new VssOAuthAccessTokenCredential(Token), CredentialPromptType.DoNotPrompt);
|
||||
return new VssCredentials(new VssOAuthAccessTokenCredential(Token), CredentialPromptType.DoNotPrompt);
|
||||
}
|
||||
else
|
||||
{
|
||||
|
||||
@@ -48,7 +48,7 @@ namespace GitHub.Runner.Listener.Configuration
|
||||
ArgUtil.NotNullOrEmpty(token, nameof(token));
|
||||
|
||||
trace.Info("token retrieved: {0} chars", token.Length);
|
||||
VssCredentials creds = new VssCredentials(null, new VssOAuthAccessTokenCredential(token), CredentialPromptType.DoNotPrompt);
|
||||
VssCredentials creds = new VssCredentials(new VssOAuthAccessTokenCredential(token), CredentialPromptType.DoNotPrompt);
|
||||
trace.Info("cred created");
|
||||
|
||||
return creds;
|
||||
|
||||
@@ -6,7 +6,7 @@ using GitHub.Runner.Common;
|
||||
namespace GitHub.Runner.Listener.Configuration
|
||||
{
|
||||
/// <summary>
|
||||
/// Manages an RSA key for the agent using the most appropriate store for the target platform.
|
||||
/// Manages an RSA key for the runner using the most appropriate store for the target platform.
|
||||
/// </summary>
|
||||
#if OS_WINDOWS
|
||||
[ServiceLocator(Default = typeof(RSAEncryptedFileKeyManager))]
|
||||
@@ -16,10 +16,10 @@ namespace GitHub.Runner.Listener.Configuration
|
||||
public interface IRSAKeyManager : IRunnerService
|
||||
{
|
||||
/// <summary>
|
||||
/// Creates a new <c>RSACryptoServiceProvider</c> instance for the current agent. If a key file is found then the current
|
||||
/// Creates a new <c>RSACryptoServiceProvider</c> instance for the current runner. If a key file is found then the current
|
||||
/// key is returned to the caller.
|
||||
/// </summary>
|
||||
/// <returns>An <c>RSACryptoServiceProvider</c> instance representing the key for the agent</returns>
|
||||
/// <returns>An <c>RSACryptoServiceProvider</c> instance representing the key for the runner</returns>
|
||||
RSACryptoServiceProvider CreateKey();
|
||||
|
||||
/// <summary>
|
||||
@@ -30,7 +30,7 @@ namespace GitHub.Runner.Listener.Configuration
|
||||
/// <summary>
|
||||
/// Gets the <c>RSACryptoServiceProvider</c> instance currently stored by the key manager.
|
||||
/// </summary>
|
||||
/// <returns>An <c>RSACryptoServiceProvider</c> instance representing the key for the agent</returns>
|
||||
/// <returns>An <c>RSACryptoServiceProvider</c> instance representing the key for the runner</returns>
|
||||
/// <exception cref="CryptographicException">No key exists in the store</exception>
|
||||
RSACryptoServiceProvider GetKey();
|
||||
}
|
||||
|
||||
@@ -447,7 +447,7 @@ namespace GitHub.Runner.Listener.Configuration
|
||||
{
|
||||
Trace.Entering();
|
||||
|
||||
string agentServiceExecutable = "\"" + Path.Combine(HostContext.GetDirectory(WellKnownDirectory.Bin), WindowsServiceControlManager.WindowsServiceControllerName) + "\"";
|
||||
string runnerServiceExecutable = "\"" + Path.Combine(HostContext.GetDirectory(WellKnownDirectory.Bin), WindowsServiceControlManager.WindowsServiceControllerName) + "\"";
|
||||
IntPtr scmHndl = IntPtr.Zero;
|
||||
IntPtr svcHndl = IntPtr.Zero;
|
||||
IntPtr tmpBuf = IntPtr.Zero;
|
||||
@@ -468,7 +468,7 @@ namespace GitHub.Runner.Listener.Configuration
|
||||
};
|
||||
|
||||
processInvoker.ExecuteAsync(workingDirectory: string.Empty,
|
||||
fileName: agentServiceExecutable,
|
||||
fileName: runnerServiceExecutable,
|
||||
arguments: "init",
|
||||
environment: null,
|
||||
requireExitCodeZero: true,
|
||||
@@ -490,7 +490,7 @@ namespace GitHub.Runner.Listener.Configuration
|
||||
SERVICE_WIN32_OWN_PROCESS,
|
||||
ServiceBootFlag.AutoStart,
|
||||
ServiceError.Normal,
|
||||
agentServiceExecutable,
|
||||
runnerServiceExecutable,
|
||||
null,
|
||||
IntPtr.Zero,
|
||||
null,
|
||||
|
||||
@@ -43,7 +43,7 @@ namespace GitHub.Runner.Listener.Configuration
|
||||
|
||||
// Construct a credentials cache with a single OAuth credential for communication. The windows credential
|
||||
// is explicitly set to null to ensure we never do that negotiation.
|
||||
return new VssCredentials(null, agentCredential, CredentialPromptType.DoNotPrompt);
|
||||
return new VssCredentials(agentCredential, CredentialPromptType.DoNotPrompt);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -38,25 +38,6 @@ namespace GitHub.Runner.Listener.Configuration
|
||||
return CredentialManager.CredentialTypes.ContainsKey(value);
|
||||
}
|
||||
|
||||
public static bool FilePathValidator(string value)
|
||||
{
|
||||
var directoryInfo = new DirectoryInfo(value);
|
||||
|
||||
if (!directoryInfo.Exists)
|
||||
{
|
||||
try
|
||||
{
|
||||
Directory.CreateDirectory(value);
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
public static bool BoolValidator(string value)
|
||||
{
|
||||
return string.Equals(value, "true", StringComparison.OrdinalIgnoreCase) ||
|
||||
|
||||
@@ -80,7 +80,7 @@ namespace GitHub.Runner.Listener
|
||||
Trace.Info($"Attempt to create session.");
|
||||
try
|
||||
{
|
||||
Trace.Info("Connecting to the Agent Server...");
|
||||
Trace.Info("Connecting to the Runner Server...");
|
||||
await _runnerServer.ConnectAsync(new Uri(serverUrl), creds);
|
||||
Trace.Info("VssConnection created");
|
||||
|
||||
@@ -110,7 +110,7 @@ namespace GitHub.Runner.Listener
|
||||
}
|
||||
catch (TaskAgentAccessTokenExpiredException)
|
||||
{
|
||||
Trace.Info("Agent OAuth token has been revoked. Session creation failed.");
|
||||
Trace.Info("Runner OAuth token has been revoked. Session creation failed.");
|
||||
throw;
|
||||
}
|
||||
catch (Exception ex)
|
||||
@@ -190,7 +190,7 @@ namespace GitHub.Runner.Listener
|
||||
}
|
||||
catch (TaskAgentAccessTokenExpiredException)
|
||||
{
|
||||
Trace.Info("Agent OAuth token has been revoked. Unable to pull message.");
|
||||
Trace.Info("Runner OAuth token has been revoked. Unable to pull message.");
|
||||
throw;
|
||||
}
|
||||
catch (Exception ex)
|
||||
@@ -336,7 +336,7 @@ namespace GitHub.Runner.Listener
|
||||
{
|
||||
if (ex is TaskAgentNotFoundException)
|
||||
{
|
||||
Trace.Info("The agent no longer exists on the server. Stopping the runner.");
|
||||
Trace.Info("The runner no longer exists on the server. Stopping the runner.");
|
||||
_term.WriteError("The runner no longer exists on the server. Please reconfigure the runner.");
|
||||
return false;
|
||||
}
|
||||
@@ -364,7 +364,7 @@ namespace GitHub.Runner.Listener
|
||||
}
|
||||
else if (ex is VssOAuthTokenRequestException && ex.Message.Contains("Current server time is"))
|
||||
{
|
||||
Trace.Info("Local clock might skewed.");
|
||||
Trace.Info("Local clock might be skewed.");
|
||||
_term.WriteError("The local machine's clock may be out of sync with the server time by more than five minutes. Please sync your clock with your domain or internet time and try again.");
|
||||
if (_sessionCreationExceptionTracker.ContainsKey(nameof(VssOAuthTokenRequestException)))
|
||||
{
|
||||
|
||||
@@ -4,6 +4,7 @@ using GitHub.Runner.Sdk;
|
||||
using System;
|
||||
using System.Globalization;
|
||||
using System.IO;
|
||||
using System.Reflection;
|
||||
using System.Runtime.InteropServices;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
@@ -14,6 +15,9 @@ namespace GitHub.Runner.Listener
|
||||
{
|
||||
public static int Main(string[] args)
|
||||
{
|
||||
// Add environment variables from .env file
|
||||
LoadAndSetEnv();
|
||||
|
||||
using (HostContext context = new HostContext("Runner"))
|
||||
{
|
||||
return MainAsync(context, args).GetAwaiter().GetResult();
|
||||
@@ -25,7 +29,7 @@ namespace GitHub.Runner.Listener
|
||||
// 1: Terminate failure
|
||||
// 2: Retriable failure
|
||||
// 3: Exit for self update
|
||||
public async static Task<int> MainAsync(IHostContext context, string[] args)
|
||||
private async static Task<int> MainAsync(IHostContext context, string[] args)
|
||||
{
|
||||
Tracing trace = context.GetTrace(nameof(GitHub.Runner.Listener));
|
||||
trace.Info($"Runner is built for {Constants.Runner.Platform} ({Constants.Runner.PlatformArchitecture}) - {BuildConstants.RunnerPackage.PackageName}.");
|
||||
@@ -83,22 +87,6 @@ namespace GitHub.Runner.Listener
|
||||
return Constants.Runner.ReturnCode.TerminatedError;
|
||||
}
|
||||
|
||||
// Add environment variables from .env file
|
||||
string envFile = Path.Combine(context.GetDirectory(WellKnownDirectory.Root), ".env");
|
||||
if (File.Exists(envFile))
|
||||
{
|
||||
var envContents = File.ReadAllLines(envFile);
|
||||
foreach (var env in envContents)
|
||||
{
|
||||
if (!string.IsNullOrEmpty(env) && env.IndexOf('=') > 0)
|
||||
{
|
||||
string envKey = env.Substring(0, env.IndexOf('='));
|
||||
string envValue = env.Substring(env.IndexOf('=') + 1);
|
||||
Environment.SetEnvironmentVariable(envKey, envValue);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Parse the command line args.
|
||||
var command = new CommandSettings(context, args);
|
||||
trace.Info("Arguments parsed");
|
||||
@@ -136,5 +124,34 @@ namespace GitHub.Runner.Listener
|
||||
return Constants.Runner.ReturnCode.RetryableError;
|
||||
}
|
||||
}
|
||||
|
||||
private static void LoadAndSetEnv()
|
||||
{
|
||||
var binDir = Path.GetDirectoryName(Assembly.GetEntryAssembly().Location);
|
||||
var rootDir = new DirectoryInfo(binDir).Parent.FullName;
|
||||
string envFile = Path.Combine(rootDir, ".env");
|
||||
if (File.Exists(envFile))
|
||||
{
|
||||
var envContents = File.ReadAllLines(envFile);
|
||||
foreach (var env in envContents)
|
||||
{
|
||||
if (!string.IsNullOrEmpty(env))
|
||||
{
|
||||
var separatorIndex = env.IndexOf('=');
|
||||
if (separatorIndex > 0)
|
||||
{
|
||||
string envKey = env.Substring(0, separatorIndex);
|
||||
string envValue = null;
|
||||
if (env.Length > separatorIndex + 1)
|
||||
{
|
||||
envValue = env.Substring(separatorIndex + 1);
|
||||
}
|
||||
|
||||
Environment.SetEnvironmentVariable(envKey, envValue);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>netcoreapp3.0</TargetFramework>
|
||||
<TargetFramework>netcoreapp3.1</TargetFramework>
|
||||
<OutputType>Exe</OutputType>
|
||||
<RuntimeIdentifiers>win-x64;win-x86;linux-x64;linux-arm64;linux-arm;osx-x64</RuntimeIdentifiers>
|
||||
<TargetLatestRuntimePatch>true</TargetLatestRuntimePatch>
|
||||
|
||||
@@ -37,8 +37,7 @@ namespace GitHub.Runner.Listener
|
||||
{
|
||||
try
|
||||
{
|
||||
var runnerCertManager = HostContext.GetService<IRunnerCertificateManager>();
|
||||
VssUtil.InitializeVssClientSettings(HostContext.UserAgent, HostContext.WebProxy, runnerCertManager.VssClientCertificateManager);
|
||||
VssUtil.InitializeVssClientSettings(HostContext.UserAgent, HostContext.WebProxy);
|
||||
|
||||
_inConfigStage = true;
|
||||
_completedCommand.Reset();
|
||||
@@ -434,7 +433,7 @@ namespace GitHub.Runner.Listener
|
||||
}
|
||||
catch (TaskAgentAccessTokenExpiredException)
|
||||
{
|
||||
Trace.Info("Agent OAuth token has been revoked. Shutting down.");
|
||||
Trace.Info("Runner OAuth token has been revoked. Shutting down.");
|
||||
}
|
||||
|
||||
return Constants.Runner.ReturnCode.Success;
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>netcoreapp3.0</TargetFramework>
|
||||
<TargetFramework>netcoreapp3.1</TargetFramework>
|
||||
<OutputType>Exe</OutputType>
|
||||
<RuntimeIdentifiers>win-x64;win-x86;linux-x64;linux-arm64;linux-arm;osx-x64</RuntimeIdentifiers>
|
||||
<TargetLatestRuntimePatch>true</TargetLatestRuntimePatch>
|
||||
|
||||
@@ -79,11 +79,6 @@ namespace GitHub.Runner.Plugins.Repository.v1_0
|
||||
{
|
||||
// Validate args.
|
||||
ArgUtil.NotNull(executionContext, nameof(executionContext));
|
||||
bool useSelfSignedCACert = false;
|
||||
bool useClientCert = false;
|
||||
string clientCertPrivateKeyAskPassFile = null;
|
||||
bool acceptUntrustedCerts = false;
|
||||
|
||||
executionContext.Output($"Syncing repository: {repoFullName}");
|
||||
Uri repositoryUrl = new Uri($"https://github.com/{repoFullName}");
|
||||
if (!repositoryUrl.IsAbsoluteUri)
|
||||
@@ -112,9 +107,6 @@ namespace GitHub.Runner.Plugins.Repository.v1_0
|
||||
}
|
||||
}
|
||||
|
||||
var runnerCert = executionContext.GetCertConfiguration();
|
||||
acceptUntrustedCerts = runnerCert?.SkipServerCertificateValidation ?? false;
|
||||
|
||||
executionContext.Debug($"repository url={repositoryUrl}");
|
||||
executionContext.Debug($"targetPath={targetPath}");
|
||||
executionContext.Debug($"sourceBranch={sourceBranch}");
|
||||
@@ -124,12 +116,6 @@ namespace GitHub.Runner.Plugins.Repository.v1_0
|
||||
executionContext.Debug($"checkoutNestedSubmodules={checkoutNestedSubmodules}");
|
||||
executionContext.Debug($"fetchDepth={fetchDepth}");
|
||||
executionContext.Debug($"gitLfsSupport={gitLfsSupport}");
|
||||
executionContext.Debug($"acceptUntrustedCerts={acceptUntrustedCerts}");
|
||||
|
||||
#if OS_WINDOWS
|
||||
bool schannelSslBackend = StringUtil.ConvertToBoolean(executionContext.GetRunnerContext("gituseschannel"));
|
||||
executionContext.Debug($"schannelSslBackend={schannelSslBackend}");
|
||||
#endif
|
||||
|
||||
// Initialize git command manager with additional environment variables.
|
||||
Dictionary<string, string> gitEnv = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
|
||||
@@ -164,54 +150,6 @@ namespace GitHub.Runner.Plugins.Repository.v1_0
|
||||
|
||||
// prepare askpass for client cert private key, if the repository's endpoint url match the runner config url
|
||||
var systemConnection = executionContext.Endpoints.Single(x => string.Equals(x.Name, WellKnownServiceEndpointNames.SystemVssConnection, StringComparison.OrdinalIgnoreCase));
|
||||
if (runnerCert != null && Uri.Compare(repositoryUrl, systemConnection.Url, UriComponents.SchemeAndServer, UriFormat.Unescaped, StringComparison.OrdinalIgnoreCase) == 0)
|
||||
{
|
||||
if (!string.IsNullOrEmpty(runnerCert.CACertificateFile))
|
||||
{
|
||||
useSelfSignedCACert = true;
|
||||
}
|
||||
|
||||
if (!string.IsNullOrEmpty(runnerCert.ClientCertificateFile) &&
|
||||
!string.IsNullOrEmpty(runnerCert.ClientCertificatePrivateKeyFile))
|
||||
{
|
||||
useClientCert = true;
|
||||
|
||||
// prepare askpass for client cert password
|
||||
if (!string.IsNullOrEmpty(runnerCert.ClientCertificatePassword))
|
||||
{
|
||||
clientCertPrivateKeyAskPassFile = Path.Combine(executionContext.GetRunnerContext("temp"), $"{Guid.NewGuid()}.sh");
|
||||
List<string> askPass = new List<string>();
|
||||
askPass.Add("#!/bin/sh");
|
||||
askPass.Add($"echo \"{runnerCert.ClientCertificatePassword}\"");
|
||||
File.WriteAllLines(clientCertPrivateKeyAskPassFile, askPass);
|
||||
|
||||
#if !OS_WINDOWS
|
||||
string toolPath = WhichUtil.Which("chmod", true);
|
||||
string argLine = $"775 {clientCertPrivateKeyAskPassFile}";
|
||||
executionContext.Command($"chmod {argLine}");
|
||||
|
||||
var processInvoker = new ProcessInvoker(executionContext);
|
||||
processInvoker.OutputDataReceived += (object sender, ProcessDataReceivedEventArgs args) =>
|
||||
{
|
||||
if (!string.IsNullOrEmpty(args.Data))
|
||||
{
|
||||
executionContext.Output(args.Data);
|
||||
}
|
||||
};
|
||||
processInvoker.ErrorDataReceived += (object sender, ProcessDataReceivedEventArgs args) =>
|
||||
{
|
||||
if (!string.IsNullOrEmpty(args.Data))
|
||||
{
|
||||
executionContext.Output(args.Data);
|
||||
}
|
||||
};
|
||||
|
||||
string workingDirectory = executionContext.GetRunnerContext("workspace");
|
||||
await processInvoker.ExecuteAsync(workingDirectory, toolPath, argLine, null, true, CancellationToken.None);
|
||||
#endif
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Check the current contents of the root folder to see if there is already a repo
|
||||
// If there is a repo, see if it matches the one we are expecting to be there based on the remote fetch url
|
||||
@@ -361,46 +299,6 @@ namespace GitHub.Runner.Plugins.Repository.v1_0
|
||||
additionalFetchArgs.Add($"-c http.extraheader=\"AUTHORIZATION: {GenerateBasicAuthHeader(executionContext, accessToken)}\"");
|
||||
}
|
||||
|
||||
// Prepare ignore ssl cert error config for fetch.
|
||||
if (acceptUntrustedCerts)
|
||||
{
|
||||
additionalFetchArgs.Add($"-c http.sslVerify=false");
|
||||
additionalLfsFetchArgs.Add($"-c http.sslVerify=false");
|
||||
}
|
||||
|
||||
// Prepare self-signed CA cert config for fetch from server.
|
||||
if (useSelfSignedCACert)
|
||||
{
|
||||
executionContext.Debug($"Use self-signed certificate '{runnerCert.CACertificateFile}' for git fetch.");
|
||||
additionalFetchArgs.Add($"-c http.sslcainfo=\"{runnerCert.CACertificateFile}\"");
|
||||
additionalLfsFetchArgs.Add($"-c http.sslcainfo=\"{runnerCert.CACertificateFile}\"");
|
||||
}
|
||||
|
||||
// Prepare client cert config for fetch from server.
|
||||
if (useClientCert)
|
||||
{
|
||||
executionContext.Debug($"Use client certificate '{runnerCert.ClientCertificateFile}' for git fetch.");
|
||||
|
||||
if (!string.IsNullOrEmpty(clientCertPrivateKeyAskPassFile))
|
||||
{
|
||||
additionalFetchArgs.Add($"-c http.sslcert=\"{runnerCert.ClientCertificateFile}\" -c http.sslkey=\"{runnerCert.ClientCertificatePrivateKeyFile}\" -c http.sslCertPasswordProtected=true -c core.askpass=\"{clientCertPrivateKeyAskPassFile}\"");
|
||||
additionalLfsFetchArgs.Add($"-c http.sslcert=\"{runnerCert.ClientCertificateFile}\" -c http.sslkey=\"{runnerCert.ClientCertificatePrivateKeyFile}\" -c http.sslCertPasswordProtected=true -c core.askpass=\"{clientCertPrivateKeyAskPassFile}\"");
|
||||
}
|
||||
else
|
||||
{
|
||||
additionalFetchArgs.Add($"-c http.sslcert=\"{runnerCert.ClientCertificateFile}\" -c http.sslkey=\"{runnerCert.ClientCertificatePrivateKeyFile}\"");
|
||||
additionalLfsFetchArgs.Add($"-c http.sslcert=\"{runnerCert.ClientCertificateFile}\" -c http.sslkey=\"{runnerCert.ClientCertificatePrivateKeyFile}\"");
|
||||
}
|
||||
}
|
||||
|
||||
#if OS_WINDOWS
|
||||
if (schannelSslBackend)
|
||||
{
|
||||
executionContext.Debug("Use SChannel SslBackend for git fetch.");
|
||||
additionalFetchArgs.Add("-c http.sslbackend=\"schannel\"");
|
||||
additionalLfsFetchArgs.Add("-c http.sslbackend=\"schannel\"");
|
||||
}
|
||||
#endif
|
||||
// Prepare gitlfs url for fetch and checkout
|
||||
if (gitLfsSupport)
|
||||
{
|
||||
@@ -502,55 +400,12 @@ namespace GitHub.Runner.Plugins.Repository.v1_0
|
||||
additionalSubmoduleUpdateArgs.Add($"-c http.{authorityUrl}.extraheader=\"AUTHORIZATION: {GenerateBasicAuthHeader(executionContext, accessToken)}\"");
|
||||
}
|
||||
|
||||
// Prepare ignore ssl cert error config for fetch.
|
||||
if (acceptUntrustedCerts)
|
||||
{
|
||||
additionalSubmoduleUpdateArgs.Add($"-c http.sslVerify=false");
|
||||
}
|
||||
|
||||
// Prepare self-signed CA cert config for submodule update.
|
||||
if (useSelfSignedCACert)
|
||||
{
|
||||
executionContext.Debug($"Use self-signed CA certificate '{runnerCert.CACertificateFile}' for git submodule update.");
|
||||
string authorityUrl = repositoryUrl.AbsoluteUri.Replace(repositoryUrl.PathAndQuery, string.Empty);
|
||||
additionalSubmoduleUpdateArgs.Add($"-c http.{authorityUrl}.sslcainfo=\"{runnerCert.CACertificateFile}\"");
|
||||
}
|
||||
|
||||
// Prepare client cert config for submodule update.
|
||||
if (useClientCert)
|
||||
{
|
||||
executionContext.Debug($"Use client certificate '{runnerCert.ClientCertificateFile}' for git submodule update.");
|
||||
string authorityUrl = repositoryUrl.AbsoluteUri.Replace(repositoryUrl.PathAndQuery, string.Empty);
|
||||
|
||||
if (!string.IsNullOrEmpty(clientCertPrivateKeyAskPassFile))
|
||||
{
|
||||
additionalSubmoduleUpdateArgs.Add($"-c http.{authorityUrl}.sslcert=\"{runnerCert.ClientCertificateFile}\" -c http.{authorityUrl}.sslkey=\"{runnerCert.ClientCertificatePrivateKeyFile}\" -c http.{authorityUrl}.sslCertPasswordProtected=true -c core.askpass=\"{clientCertPrivateKeyAskPassFile}\"");
|
||||
}
|
||||
else
|
||||
{
|
||||
additionalSubmoduleUpdateArgs.Add($"-c http.{authorityUrl}.sslcert=\"{runnerCert.ClientCertificateFile}\" -c http.{authorityUrl}.sslkey=\"{runnerCert.ClientCertificatePrivateKeyFile}\"");
|
||||
}
|
||||
}
|
||||
#if OS_WINDOWS
|
||||
if (schannelSslBackend)
|
||||
{
|
||||
executionContext.Debug("Use SChannel SslBackend for git submodule update.");
|
||||
additionalSubmoduleUpdateArgs.Add("-c http.sslbackend=\"schannel\"");
|
||||
}
|
||||
#endif
|
||||
|
||||
int exitCode_submoduleUpdate = await gitCommandManager.GitSubmoduleUpdate(executionContext, targetPath, fetchDepth, string.Join(" ", additionalSubmoduleUpdateArgs), checkoutNestedSubmodules, cancellationToken);
|
||||
if (exitCode_submoduleUpdate != 0)
|
||||
{
|
||||
throw new InvalidOperationException($"Git submodule update failed with exit code: {exitCode_submoduleUpdate}");
|
||||
}
|
||||
}
|
||||
|
||||
if (useClientCert && !string.IsNullOrEmpty(clientCertPrivateKeyAskPassFile))
|
||||
{
|
||||
executionContext.Debug("Remove git.sslkey askpass file.");
|
||||
IOUtil.DeleteFile(clientCertPrivateKeyAskPassFile);
|
||||
}
|
||||
}
|
||||
|
||||
private async Task<bool> IsRepositoryOriginUrlMatch(RunnerActionPluginExecutionContext context, GitCliManager gitCommandManager, string repositoryPath, Uri expectedRepositoryOriginUrl)
|
||||
|
||||
@@ -65,11 +65,6 @@ namespace GitHub.Runner.Plugins.Repository.v1_1
|
||||
// Validate args.
|
||||
ArgUtil.NotNull(executionContext, nameof(executionContext));
|
||||
Dictionary<string, string> configModifications = new Dictionary<string, string>();
|
||||
bool useSelfSignedCACert = false;
|
||||
bool useClientCert = false;
|
||||
string clientCertPrivateKeyAskPassFile = null;
|
||||
bool acceptUntrustedCerts = false;
|
||||
|
||||
executionContext.Output($"Syncing repository: {repoFullName}");
|
||||
Uri repositoryUrl = new Uri($"https://github.com/{repoFullName}");
|
||||
if (!repositoryUrl.IsAbsoluteUri)
|
||||
@@ -98,9 +93,6 @@ namespace GitHub.Runner.Plugins.Repository.v1_1
|
||||
}
|
||||
}
|
||||
|
||||
var runnerCert = executionContext.GetCertConfiguration();
|
||||
acceptUntrustedCerts = runnerCert?.SkipServerCertificateValidation ?? false;
|
||||
|
||||
executionContext.Debug($"repository url={repositoryUrl}");
|
||||
executionContext.Debug($"targetPath={targetPath}");
|
||||
executionContext.Debug($"sourceBranch={sourceBranch}");
|
||||
@@ -110,12 +102,6 @@ namespace GitHub.Runner.Plugins.Repository.v1_1
|
||||
executionContext.Debug($"checkoutNestedSubmodules={checkoutNestedSubmodules}");
|
||||
executionContext.Debug($"fetchDepth={fetchDepth}");
|
||||
executionContext.Debug($"gitLfsSupport={gitLfsSupport}");
|
||||
executionContext.Debug($"acceptUntrustedCerts={acceptUntrustedCerts}");
|
||||
|
||||
#if OS_WINDOWS
|
||||
bool schannelSslBackend = StringUtil.ConvertToBoolean(executionContext.GetRunnerContext("gituseschannel"));
|
||||
executionContext.Debug($"schannelSslBackend={schannelSslBackend}");
|
||||
#endif
|
||||
|
||||
// Initialize git command manager with additional environment variables.
|
||||
Dictionary<string, string> gitEnv = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
|
||||
@@ -153,54 +139,6 @@ namespace GitHub.Runner.Plugins.Repository.v1_1
|
||||
|
||||
// prepare askpass for client cert private key, if the repository's endpoint url match the runner config url
|
||||
var systemConnection = executionContext.Endpoints.Single(x => string.Equals(x.Name, WellKnownServiceEndpointNames.SystemVssConnection, StringComparison.OrdinalIgnoreCase));
|
||||
if (runnerCert != null && Uri.Compare(repositoryUrl, systemConnection.Url, UriComponents.SchemeAndServer, UriFormat.Unescaped, StringComparison.OrdinalIgnoreCase) == 0)
|
||||
{
|
||||
if (!string.IsNullOrEmpty(runnerCert.CACertificateFile))
|
||||
{
|
||||
useSelfSignedCACert = true;
|
||||
}
|
||||
|
||||
if (!string.IsNullOrEmpty(runnerCert.ClientCertificateFile) &&
|
||||
!string.IsNullOrEmpty(runnerCert.ClientCertificatePrivateKeyFile))
|
||||
{
|
||||
useClientCert = true;
|
||||
|
||||
// prepare askpass for client cert password
|
||||
if (!string.IsNullOrEmpty(runnerCert.ClientCertificatePassword))
|
||||
{
|
||||
clientCertPrivateKeyAskPassFile = Path.Combine(executionContext.GetRunnerContext("temp"), $"{Guid.NewGuid()}.sh");
|
||||
List<string> askPass = new List<string>();
|
||||
askPass.Add("#!/bin/sh");
|
||||
askPass.Add($"echo \"{runnerCert.ClientCertificatePassword}\"");
|
||||
File.WriteAllLines(clientCertPrivateKeyAskPassFile, askPass);
|
||||
|
||||
#if !OS_WINDOWS
|
||||
string toolPath = WhichUtil.Which("chmod", true);
|
||||
string argLine = $"775 {clientCertPrivateKeyAskPassFile}";
|
||||
executionContext.Command($"chmod {argLine}");
|
||||
|
||||
var processInvoker = new ProcessInvoker(executionContext);
|
||||
processInvoker.OutputDataReceived += (object sender, ProcessDataReceivedEventArgs args) =>
|
||||
{
|
||||
if (!string.IsNullOrEmpty(args.Data))
|
||||
{
|
||||
executionContext.Output(args.Data);
|
||||
}
|
||||
};
|
||||
processInvoker.ErrorDataReceived += (object sender, ProcessDataReceivedEventArgs args) =>
|
||||
{
|
||||
if (!string.IsNullOrEmpty(args.Data))
|
||||
{
|
||||
executionContext.Output(args.Data);
|
||||
}
|
||||
};
|
||||
|
||||
string workingDirectory = executionContext.GetRunnerContext("workspace");
|
||||
await processInvoker.ExecuteAsync(workingDirectory, toolPath, argLine, null, true, CancellationToken.None);
|
||||
#endif
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Check the current contents of the root folder to see if there is already a repo
|
||||
// If there is a repo, see if it matches the one we are expecting to be there based on the remote fetch url
|
||||
@@ -355,46 +293,6 @@ namespace GitHub.Runner.Plugins.Repository.v1_1
|
||||
throw new InvalidOperationException($"Git config failed with exit code: {exitCode_config}");
|
||||
}
|
||||
|
||||
// Prepare ignore ssl cert error config for fetch.
|
||||
if (acceptUntrustedCerts)
|
||||
{
|
||||
additionalFetchArgs.Add($"-c http.sslVerify=false");
|
||||
additionalLfsFetchArgs.Add($"-c http.sslVerify=false");
|
||||
}
|
||||
|
||||
// Prepare self-signed CA cert config for fetch from server.
|
||||
if (useSelfSignedCACert)
|
||||
{
|
||||
executionContext.Debug($"Use self-signed certificate '{runnerCert.CACertificateFile}' for git fetch.");
|
||||
additionalFetchArgs.Add($"-c http.sslcainfo=\"{runnerCert.CACertificateFile}\"");
|
||||
additionalLfsFetchArgs.Add($"-c http.sslcainfo=\"{runnerCert.CACertificateFile}\"");
|
||||
}
|
||||
|
||||
// Prepare client cert config for fetch from server.
|
||||
if (useClientCert)
|
||||
{
|
||||
executionContext.Debug($"Use client certificate '{runnerCert.ClientCertificateFile}' for git fetch.");
|
||||
|
||||
if (!string.IsNullOrEmpty(clientCertPrivateKeyAskPassFile))
|
||||
{
|
||||
additionalFetchArgs.Add($"-c http.sslcert=\"{runnerCert.ClientCertificateFile}\" -c http.sslkey=\"{runnerCert.ClientCertificatePrivateKeyFile}\" -c http.sslCertPasswordProtected=true -c core.askpass=\"{clientCertPrivateKeyAskPassFile}\"");
|
||||
additionalLfsFetchArgs.Add($"-c http.sslcert=\"{runnerCert.ClientCertificateFile}\" -c http.sslkey=\"{runnerCert.ClientCertificatePrivateKeyFile}\" -c http.sslCertPasswordProtected=true -c core.askpass=\"{clientCertPrivateKeyAskPassFile}\"");
|
||||
}
|
||||
else
|
||||
{
|
||||
additionalFetchArgs.Add($"-c http.sslcert=\"{runnerCert.ClientCertificateFile}\" -c http.sslkey=\"{runnerCert.ClientCertificatePrivateKeyFile}\"");
|
||||
additionalLfsFetchArgs.Add($"-c http.sslcert=\"{runnerCert.ClientCertificateFile}\" -c http.sslkey=\"{runnerCert.ClientCertificatePrivateKeyFile}\"");
|
||||
}
|
||||
}
|
||||
|
||||
#if OS_WINDOWS
|
||||
if (schannelSslBackend)
|
||||
{
|
||||
executionContext.Debug("Use SChannel SslBackend for git fetch.");
|
||||
additionalFetchArgs.Add("-c http.sslbackend=\"schannel\"");
|
||||
additionalLfsFetchArgs.Add("-c http.sslbackend=\"schannel\"");
|
||||
}
|
||||
#endif
|
||||
// Prepare gitlfs url for fetch and checkout
|
||||
if (gitLfsSupport)
|
||||
{
|
||||
@@ -484,43 +382,6 @@ namespace GitHub.Runner.Plugins.Repository.v1_1
|
||||
|
||||
List<string> additionalSubmoduleUpdateArgs = new List<string>();
|
||||
|
||||
// Prepare ignore ssl cert error config for fetch.
|
||||
if (acceptUntrustedCerts)
|
||||
{
|
||||
additionalSubmoduleUpdateArgs.Add($"-c http.sslVerify=false");
|
||||
}
|
||||
|
||||
// Prepare self-signed CA cert config for submodule update.
|
||||
if (useSelfSignedCACert)
|
||||
{
|
||||
executionContext.Debug($"Use self-signed CA certificate '{runnerCert.CACertificateFile}' for git submodule update.");
|
||||
string authorityUrl = repositoryUrl.AbsoluteUri.Replace(repositoryUrl.PathAndQuery, string.Empty);
|
||||
additionalSubmoduleUpdateArgs.Add($"-c http.{authorityUrl}.sslcainfo=\"{runnerCert.CACertificateFile}\"");
|
||||
}
|
||||
|
||||
// Prepare client cert config for submodule update.
|
||||
if (useClientCert)
|
||||
{
|
||||
executionContext.Debug($"Use client certificate '{runnerCert.ClientCertificateFile}' for git submodule update.");
|
||||
string authorityUrl = repositoryUrl.AbsoluteUri.Replace(repositoryUrl.PathAndQuery, string.Empty);
|
||||
|
||||
if (!string.IsNullOrEmpty(clientCertPrivateKeyAskPassFile))
|
||||
{
|
||||
additionalSubmoduleUpdateArgs.Add($"-c http.{authorityUrl}.sslcert=\"{runnerCert.ClientCertificateFile}\" -c http.{authorityUrl}.sslkey=\"{runnerCert.ClientCertificatePrivateKeyFile}\" -c http.{authorityUrl}.sslCertPasswordProtected=true -c core.askpass=\"{clientCertPrivateKeyAskPassFile}\"");
|
||||
}
|
||||
else
|
||||
{
|
||||
additionalSubmoduleUpdateArgs.Add($"-c http.{authorityUrl}.sslcert=\"{runnerCert.ClientCertificateFile}\" -c http.{authorityUrl}.sslkey=\"{runnerCert.ClientCertificatePrivateKeyFile}\"");
|
||||
}
|
||||
}
|
||||
#if OS_WINDOWS
|
||||
if (schannelSslBackend)
|
||||
{
|
||||
executionContext.Debug("Use SChannel SslBackend for git submodule update.");
|
||||
additionalSubmoduleUpdateArgs.Add("-c http.sslbackend=\"schannel\"");
|
||||
}
|
||||
#endif
|
||||
|
||||
int exitCode_submoduleUpdate = await gitCommandManager.GitSubmoduleUpdate(executionContext, targetPath, fetchDepth, string.Join(" ", additionalSubmoduleUpdateArgs), checkoutNestedSubmodules, cancellationToken);
|
||||
if (exitCode_submoduleUpdate != 0)
|
||||
{
|
||||
@@ -528,12 +389,6 @@ namespace GitHub.Runner.Plugins.Repository.v1_1
|
||||
}
|
||||
}
|
||||
|
||||
if (useClientCert && !string.IsNullOrEmpty(clientCertPrivateKeyAskPassFile))
|
||||
{
|
||||
executionContext.Debug("Remove git.sslkey askpass file.");
|
||||
IOUtil.DeleteFile(clientCertPrivateKeyAskPassFile);
|
||||
}
|
||||
|
||||
// Set intra-task variable for post job cleanup
|
||||
executionContext.SetIntraActionState("repositoryPath", targetPath);
|
||||
executionContext.SetIntraActionState("modifiedgitconfig", JsonUtility.ToString(configModifications.Keys));
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>netcoreapp3.0</TargetFramework>
|
||||
<TargetFramework>netcoreapp3.1</TargetFramework>
|
||||
<OutputType>Library</OutputType>
|
||||
<RuntimeIdentifiers>win-x64;win-x86;linux-x64;linux-arm64;linux-arm;osx-x64</RuntimeIdentifiers>
|
||||
<TargetLatestRuntimePatch>true</TargetLatestRuntimePatch>
|
||||
|
||||
@@ -83,21 +83,6 @@ namespace GitHub.Runner.Sdk
|
||||
}
|
||||
|
||||
VssClientHttpRequestSettings.Default.UserAgent = headerValues;
|
||||
|
||||
var certSetting = GetCertConfiguration();
|
||||
if (certSetting != null)
|
||||
{
|
||||
if (!string.IsNullOrEmpty(certSetting.ClientCertificateArchiveFile))
|
||||
{
|
||||
VssClientHttpRequestSettings.Default.ClientCertificateManager = new RunnerClientCertificateManager(certSetting.ClientCertificateArchiveFile, certSetting.ClientCertificatePassword);
|
||||
}
|
||||
|
||||
if (certSetting.SkipServerCertificateValidation)
|
||||
{
|
||||
VssClientHttpRequestSettings.Default.ServerCertificateValidationCallback = HttpClientHandler.DangerousAcceptAnyServerCertificateValidator;
|
||||
}
|
||||
}
|
||||
|
||||
VssHttpMessageHandler.DefaultWebProxy = this.WebProxy;
|
||||
ServiceEndpoint systemConnection = this.Endpoints.FirstOrDefault(e => string.Equals(e.Name, WellKnownServiceEndpointNames.SystemVssConnection, StringComparison.OrdinalIgnoreCase));
|
||||
ArgUtil.NotNull(systemConnection, nameof(systemConnection));
|
||||
@@ -227,40 +212,6 @@ namespace GitHub.Runner.Sdk
|
||||
}
|
||||
}
|
||||
|
||||
public RunnerCertificateSettings GetCertConfiguration()
|
||||
{
|
||||
bool skipCertValidation = StringUtil.ConvertToBoolean(GetRunnerContext("SkipCertValidation"));
|
||||
string caFile = GetRunnerContext("CAInfo");
|
||||
string clientCertFile = GetRunnerContext("ClientCert");
|
||||
|
||||
if (!string.IsNullOrEmpty(caFile) || !string.IsNullOrEmpty(clientCertFile) || skipCertValidation)
|
||||
{
|
||||
var certConfig = new RunnerCertificateSettings();
|
||||
certConfig.SkipServerCertificateValidation = skipCertValidation;
|
||||
certConfig.CACertificateFile = caFile;
|
||||
|
||||
if (!string.IsNullOrEmpty(clientCertFile))
|
||||
{
|
||||
certConfig.ClientCertificateFile = clientCertFile;
|
||||
string clientCertKey = GetRunnerContext("ClientCertKey");
|
||||
string clientCertArchive = GetRunnerContext("ClientCertArchive");
|
||||
string clientCertPassword = GetRunnerContext("ClientCertPassword");
|
||||
|
||||
certConfig.ClientCertificatePrivateKeyFile = clientCertKey;
|
||||
certConfig.ClientCertificateArchiveFile = clientCertArchive;
|
||||
certConfig.ClientCertificatePassword = clientCertPassword;
|
||||
|
||||
certConfig.VssClientCertificateManager = new RunnerClientCertificateManager(clientCertArchive, clientCertPassword);
|
||||
}
|
||||
|
||||
return certConfig;
|
||||
}
|
||||
else
|
||||
{
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private string Escape(string input)
|
||||
{
|
||||
foreach (var mapping in _commandEscapeMappings)
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>netcoreapp3.0</TargetFramework>
|
||||
<TargetFramework>netcoreapp3.1</TargetFramework>
|
||||
<OutputType>Library</OutputType>
|
||||
<RuntimeIdentifiers>win-x64;win-x86;linux-x64;linux-arm64;linux-arm;osx-x64</RuntimeIdentifiers>
|
||||
<TargetLatestRuntimePatch>true</TargetLatestRuntimePatch>
|
||||
|
||||
@@ -1,40 +0,0 @@
|
||||
|
||||
using System.Security.Cryptography.X509Certificates;
|
||||
using GitHub.Services.Common;
|
||||
|
||||
namespace GitHub.Runner.Sdk
|
||||
{
|
||||
public class RunnerCertificateSettings
|
||||
{
|
||||
public bool SkipServerCertificateValidation { get; set; }
|
||||
public string CACertificateFile { get; set; }
|
||||
public string ClientCertificateFile { get; set; }
|
||||
public string ClientCertificatePrivateKeyFile { get; set; }
|
||||
public string ClientCertificateArchiveFile { get; set; }
|
||||
public string ClientCertificatePassword { get; set; }
|
||||
public IVssClientCertificateManager VssClientCertificateManager { get; set; }
|
||||
}
|
||||
|
||||
public class RunnerClientCertificateManager : IVssClientCertificateManager
|
||||
{
|
||||
private readonly X509Certificate2Collection _clientCertificates = new X509Certificate2Collection();
|
||||
public X509Certificate2Collection ClientCertificates => _clientCertificates;
|
||||
|
||||
public RunnerClientCertificateManager()
|
||||
{
|
||||
}
|
||||
|
||||
public RunnerClientCertificateManager(string clientCertificateArchiveFile, string clientCertificatePassword)
|
||||
{
|
||||
AddClientCertificate(clientCertificateArchiveFile, clientCertificatePassword);
|
||||
}
|
||||
|
||||
public void AddClientCertificate(string clientCertificateArchiveFile, string clientCertificatePassword)
|
||||
{
|
||||
if (!string.IsNullOrEmpty(clientCertificateArchiveFile))
|
||||
{
|
||||
_clientCertificates.Add(new X509Certificate2(clientCertificateArchiveFile, clientCertificatePassword));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -14,7 +14,7 @@ namespace GitHub.Runner.Sdk
|
||||
{
|
||||
public static class VssUtil
|
||||
{
|
||||
public static void InitializeVssClientSettings(ProductInfoHeaderValue additionalUserAgent, IWebProxy proxy, IVssClientCertificateManager clientCert)
|
||||
public static void InitializeVssClientSettings(ProductInfoHeaderValue additionalUserAgent, IWebProxy proxy)
|
||||
{
|
||||
var headerValues = new List<ProductInfoHeaderValue>();
|
||||
headerValues.Add(additionalUserAgent);
|
||||
@@ -26,7 +26,6 @@ namespace GitHub.Runner.Sdk
|
||||
}
|
||||
|
||||
VssClientHttpRequestSettings.Default.UserAgent = headerValues;
|
||||
VssClientHttpRequestSettings.Default.ClientCertificateManager = clientCert;
|
||||
VssHttpMessageHandler.DefaultWebProxy = proxy;
|
||||
}
|
||||
|
||||
@@ -83,7 +82,7 @@ namespace GitHub.Runner.Sdk
|
||||
if (serviceEndpoint.Authorization.Scheme == EndpointAuthorizationSchemes.OAuth &&
|
||||
serviceEndpoint.Authorization.Parameters.TryGetValue(EndpointAuthorizationParameters.AccessToken, out accessToken))
|
||||
{
|
||||
credentials = new VssCredentials(null, new VssOAuthAccessTokenCredential(accessToken), CredentialPromptType.DoNotPrompt);
|
||||
credentials = new VssCredentials(new VssOAuthAccessTokenCredential(accessToken), CredentialPromptType.DoNotPrompt);
|
||||
}
|
||||
|
||||
return credentials;
|
||||
|
||||
@@ -7,7 +7,7 @@ namespace GitHub.Runner.Sdk
|
||||
{
|
||||
public static class WhichUtil
|
||||
{
|
||||
public static string Which(string command, bool require = false, ITraceWriter trace = null)
|
||||
public static string Which(string command, bool require = false, ITraceWriter trace = null, string prependPath = null)
|
||||
{
|
||||
ArgUtil.NotNullOrEmpty(command, nameof(command));
|
||||
trace?.Info($"Which: '{command}'");
|
||||
@@ -17,6 +17,10 @@ namespace GitHub.Runner.Sdk
|
||||
trace?.Info("PATH environment variable not defined.");
|
||||
path = path ?? string.Empty;
|
||||
}
|
||||
if (!string.IsNullOrEmpty(prependPath))
|
||||
{
|
||||
path = PathUtil.PrependPath(prependPath, path);
|
||||
}
|
||||
|
||||
string[] pathSegments = path.Split(new Char[] { Path.PathSeparator }, StringSplitOptions.RemoveEmptyEntries);
|
||||
for (int i = 0; i < pathSegments.Length; i++)
|
||||
|
||||
@@ -276,9 +276,7 @@ namespace GitHub.Runner.Worker.Container
|
||||
return await ExecuteDockerCommandAsync(context, "exec", $"{options} {containerId} {command}", context.CancellationToken);
|
||||
}
|
||||
|
||||
#pragma warning disable CS1998 // Async method lacks 'await' operators and will run synchronously (method has async logic on only certain platforms)
|
||||
public async Task<int> DockerExec(IExecutionContext context, string containerId, string options, string command, List<string> output)
|
||||
#pragma warning restore CS1998 // Async method lacks 'await' operators and will run synchronously
|
||||
{
|
||||
ArgUtil.NotNull(output, nameof(output));
|
||||
|
||||
@@ -309,9 +307,10 @@ namespace GitHub.Runner.Worker.Container
|
||||
}
|
||||
};
|
||||
|
||||
#if OS_WINDOWS || OS_OSX
|
||||
throw new NotSupportedException($"Container operation is only supported on Linux");
|
||||
#else
|
||||
if (!Constants.Runner.Platform.Equals(Constants.OSPlatform.Linux))
|
||||
{
|
||||
throw new NotSupportedException("Container operations are only supported on Linux runners");
|
||||
}
|
||||
return await processInvoker.ExecuteAsync(
|
||||
workingDirectory: HostContext.GetDirectory(WellKnownDirectory.Work),
|
||||
fileName: DockerPath,
|
||||
@@ -320,7 +319,6 @@ namespace GitHub.Runner.Worker.Container
|
||||
requireExitCodeZero: false,
|
||||
outputEncoding: null,
|
||||
cancellationToken: CancellationToken.None);
|
||||
#endif
|
||||
}
|
||||
|
||||
public async Task<List<string>> DockerInspect(IExecutionContext context, string dockerObject, string options)
|
||||
@@ -339,9 +337,7 @@ namespace GitHub.Runner.Worker.Container
|
||||
return ExecuteDockerCommandAsync(context, command, options, null, cancellationToken);
|
||||
}
|
||||
|
||||
#pragma warning disable CS1998 // Async method lacks 'await' operators and will run synchronously (method has async logic on only certain platforms)
|
||||
private async Task<int> ExecuteDockerCommandAsync(IExecutionContext context, string command, string options, IDictionary<string, string> environment, EventHandler<ProcessDataReceivedEventArgs> stdoutDataReceived, EventHandler<ProcessDataReceivedEventArgs> stderrDataReceived, CancellationToken cancellationToken = default(CancellationToken))
|
||||
#pragma warning restore CS1998 // Async method lacks 'await' operators and will run synchronously
|
||||
{
|
||||
string arg = $"{command} {options}".Trim();
|
||||
context.Command($"{DockerPath} {arg}");
|
||||
@@ -351,9 +347,10 @@ namespace GitHub.Runner.Worker.Container
|
||||
processInvoker.ErrorDataReceived += stderrDataReceived;
|
||||
|
||||
|
||||
#if OS_WINDOWS || OS_OSX
|
||||
throw new NotSupportedException($"Container operation is only supported on Linux");
|
||||
#else
|
||||
if (!Constants.Runner.Platform.Equals(Constants.OSPlatform.Linux))
|
||||
{
|
||||
throw new NotSupportedException("Container operations are only supported on Linux runners");
|
||||
}
|
||||
return await processInvoker.ExecuteAsync(
|
||||
workingDirectory: context.GetGitHubContext("workspace"),
|
||||
fileName: DockerPath,
|
||||
@@ -363,12 +360,9 @@ namespace GitHub.Runner.Worker.Container
|
||||
outputEncoding: null,
|
||||
killProcessOnCancel: false,
|
||||
cancellationToken: cancellationToken);
|
||||
#endif
|
||||
}
|
||||
|
||||
#pragma warning disable CS1998 // Async method lacks 'await' operators and will run synchronously (method has async logic on only certain platforms)
|
||||
private async Task<int> ExecuteDockerCommandAsync(IExecutionContext context, string command, string options, string workingDirectory, CancellationToken cancellationToken = default(CancellationToken))
|
||||
#pragma warning restore CS1998 // Async method lacks 'await' operators and will run synchronously
|
||||
{
|
||||
string arg = $"{command} {options}".Trim();
|
||||
context.Command($"{DockerPath} {arg}");
|
||||
@@ -384,9 +378,10 @@ namespace GitHub.Runner.Worker.Container
|
||||
context.Output(message.Data);
|
||||
};
|
||||
|
||||
#if OS_WINDOWS || OS_OSX
|
||||
throw new NotSupportedException($"Container operation is only supported on Linux");
|
||||
#else
|
||||
if (!Constants.Runner.Platform.Equals(Constants.OSPlatform.Linux))
|
||||
{
|
||||
throw new NotSupportedException("Container operations are only supported on Linux runners");
|
||||
}
|
||||
return await processInvoker.ExecuteAsync(
|
||||
workingDirectory: workingDirectory ?? context.GetGitHubContext("workspace"),
|
||||
fileName: DockerPath,
|
||||
@@ -397,7 +392,6 @@ namespace GitHub.Runner.Worker.Container
|
||||
killProcessOnCancel: false,
|
||||
redirectStandardIn: null,
|
||||
cancellationToken: cancellationToken);
|
||||
#endif
|
||||
}
|
||||
|
||||
private async Task<List<string>> ExecuteDockerCommandAsync(IExecutionContext context, string command, string options)
|
||||
|
||||
@@ -35,6 +35,10 @@ namespace GitHub.Runner.Worker
|
||||
public async Task StartContainersAsync(IExecutionContext executionContext, object data)
|
||||
{
|
||||
Trace.Entering();
|
||||
if (!Constants.Runner.Platform.Equals(Constants.OSPlatform.Linux))
|
||||
{
|
||||
throw new NotSupportedException("Container operations are only supported on Linux runners");
|
||||
}
|
||||
ArgUtil.NotNull(executionContext, nameof(executionContext));
|
||||
List<ContainerInfo> containers = data as List<ContainerInfo>;
|
||||
ArgUtil.NotNull(containers, nameof(containers));
|
||||
@@ -44,7 +48,7 @@ namespace GitHub.Runner.Worker
|
||||
displayName: "Stop containers",
|
||||
data: data);
|
||||
|
||||
executionContext.Debug($"Register post job cleanup for stoping/deleting containers.");
|
||||
executionContext.Debug($"Register post job cleanup for stopping/deleting containers.");
|
||||
executionContext.RegisterPostJobStep(nameof(StopContainersAsync), postJobStep);
|
||||
|
||||
// Check whether we are inside a container.
|
||||
@@ -125,7 +129,7 @@ namespace GitHub.Runner.Worker
|
||||
executionContext.Warning($"Delete stale container networks failed, docker network prune fail with exit code {networkPruneExitCode}");
|
||||
}
|
||||
|
||||
// Create local docker network for this job to avoid port conflict when multiple agents run on same machine.
|
||||
// Create local docker network for this job to avoid port conflict when multiple runners run on same machine.
|
||||
// All containers within a job join the same network
|
||||
var containerNetwork = $"github_network_{Guid.NewGuid().ToString("N")}";
|
||||
await CreateContainerNetworkAsync(executionContext, containerNetwork);
|
||||
|
||||
@@ -610,44 +610,6 @@ namespace GitHub.Runner.Worker
|
||||
|
||||
// PostJobSteps for job ExecutionContext
|
||||
PostJobSteps = new Stack<IStep>();
|
||||
// // Certificate variables
|
||||
// var agentCert = HostContext.GetService<IRunnerCertificateManager>();
|
||||
// if (agentCert.SkipServerCertificateValidation)
|
||||
// {
|
||||
// SetRunnerContext("sslskipcertvalidation", bool.TrueString);
|
||||
// }
|
||||
|
||||
// if (!string.IsNullOrEmpty(agentCert.CACertificateFile))
|
||||
// {
|
||||
// SetRunnerContext("sslcainfo", agentCert.CACertificateFile);
|
||||
// }
|
||||
|
||||
// if (!string.IsNullOrEmpty(agentCert.ClientCertificateFile) &&
|
||||
// !string.IsNullOrEmpty(agentCert.ClientCertificatePrivateKeyFile) &&
|
||||
// !string.IsNullOrEmpty(agentCert.ClientCertificateArchiveFile))
|
||||
// {
|
||||
// SetRunnerContext("clientcertfile", agentCert.ClientCertificateFile);
|
||||
// SetRunnerContext("clientcertprivatekey", agentCert.ClientCertificatePrivateKeyFile);
|
||||
// SetRunnerContext("clientcertarchive", agentCert.ClientCertificateArchiveFile);
|
||||
|
||||
// if (!string.IsNullOrEmpty(agentCert.ClientCertificatePassword))
|
||||
// {
|
||||
// HostContext.SecretMasker.AddValue(agentCert.ClientCertificatePassword);
|
||||
// SetRunnerContext("clientcertpassword", agentCert.ClientCertificatePassword);
|
||||
// }
|
||||
// }
|
||||
|
||||
// // Runtime option variables
|
||||
// var runtimeOptions = HostContext.GetService<IConfigurationStore>().GetRunnerRuntimeOptions();
|
||||
// if (runtimeOptions != null)
|
||||
// {
|
||||
// #if OS_WINDOWS
|
||||
// if (runtimeOptions.GitUseSecureChannel)
|
||||
// {
|
||||
// SetRunnerContext("gituseschannel", runtimeOptions.GitUseSecureChannel.ToString());
|
||||
// }
|
||||
// #endif
|
||||
// }
|
||||
|
||||
// Job timeline record.
|
||||
InitializeTimelineRecord(
|
||||
|
||||
@@ -16,6 +16,8 @@ namespace GitHub.Runner.Worker
|
||||
"head_ref",
|
||||
"ref",
|
||||
"repository",
|
||||
"run_id",
|
||||
"run_number",
|
||||
"sha",
|
||||
"workflow",
|
||||
"workspace",
|
||||
|
||||
@@ -62,7 +62,7 @@ namespace GitHub.Runner.Worker.Handlers
|
||||
}
|
||||
else if (data.ExecutionType == ActionExecutionType.Plugin)
|
||||
{
|
||||
// Agent plugin
|
||||
// Runner plugin
|
||||
handler = HostContext.CreateService<IRunnerPluginHandler>();
|
||||
(handler as IRunnerPluginHandler).Data = data as PluginActionExecutionData;
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
using System.IO;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using System.Linq;
|
||||
using GitHub.DistributedTask.Pipelines.ContextData;
|
||||
using GitHub.Runner.Common;
|
||||
using GitHub.Runner.Sdk;
|
||||
@@ -56,6 +57,7 @@ namespace GitHub.Runner.Worker.Handlers
|
||||
string shellCommand;
|
||||
string shellCommandPath = null;
|
||||
bool validateShellOnHost = !(StepHost is ContainerStepHost);
|
||||
string prependPath = string.Join(Path.PathSeparator.ToString(), ExecutionContext.PrependPath.Reverse<string>());
|
||||
Inputs.TryGetValue("shell", out var shell);
|
||||
if (string.IsNullOrEmpty(shell))
|
||||
{
|
||||
@@ -63,19 +65,19 @@ namespace GitHub.Runner.Worker.Handlers
|
||||
shellCommand = "pwsh";
|
||||
if(validateShellOnHost)
|
||||
{
|
||||
shellCommandPath = WhichUtil.Which(shellCommand, require: false, Trace);
|
||||
shellCommandPath = WhichUtil.Which(shellCommand, require: false, Trace, prependPath);
|
||||
if (string.IsNullOrEmpty(shellCommandPath))
|
||||
{
|
||||
shellCommand = "powershell";
|
||||
Trace.Info($"Defaulting to {shellCommand}");
|
||||
shellCommandPath = WhichUtil.Which(shellCommand, require: true, Trace);
|
||||
shellCommandPath = WhichUtil.Which(shellCommand, require: true, Trace, prependPath);
|
||||
}
|
||||
}
|
||||
#else
|
||||
shellCommand = "sh";
|
||||
if (validateShellOnHost)
|
||||
{
|
||||
shellCommandPath = WhichUtil.Which("bash") ?? WhichUtil.Which("sh", true, Trace);
|
||||
shellCommandPath = WhichUtil.Which("bash", false, Trace, prependPath) ?? WhichUtil.Which("sh", true, Trace, prependPath);
|
||||
}
|
||||
#endif
|
||||
argFormat = ScriptHandlerHelpers.GetScriptArgumentsFormat(shellCommand);
|
||||
@@ -86,7 +88,7 @@ namespace GitHub.Runner.Worker.Handlers
|
||||
shellCommand = parsed.shellCommand;
|
||||
if (validateShellOnHost)
|
||||
{
|
||||
shellCommandPath = WhichUtil.Which(parsed.shellCommand, true, Trace);
|
||||
shellCommandPath = WhichUtil.Which(parsed.shellCommand, true, Trace, prependPath);
|
||||
}
|
||||
|
||||
argFormat = $"{parsed.shellArgs}".TrimStart();
|
||||
@@ -144,23 +146,24 @@ namespace GitHub.Runner.Worker.Handlers
|
||||
Inputs.TryGetValue("shell", out var shell);
|
||||
var isContainerStepHost = StepHost is ContainerStepHost;
|
||||
|
||||
string prependPath = string.Join(Path.PathSeparator.ToString(), ExecutionContext.PrependPath.Reverse<string>());
|
||||
string commandPath, argFormat, shellCommand;
|
||||
// Set up default command and arguments
|
||||
if (string.IsNullOrEmpty(shell))
|
||||
{
|
||||
#if OS_WINDOWS
|
||||
shellCommand = "pwsh";
|
||||
commandPath = WhichUtil.Which(shellCommand, require: false, Trace);
|
||||
commandPath = WhichUtil.Which(shellCommand, require: false, Trace, prependPath);
|
||||
if (string.IsNullOrEmpty(commandPath))
|
||||
{
|
||||
shellCommand = "powershell";
|
||||
Trace.Info($"Defaulting to {shellCommand}");
|
||||
commandPath = WhichUtil.Which(shellCommand, require: true, Trace);
|
||||
commandPath = WhichUtil.Which(shellCommand, require: true, Trace, prependPath);
|
||||
}
|
||||
ArgUtil.NotNullOrEmpty(commandPath, "Default Shell");
|
||||
#else
|
||||
shellCommand = "sh";
|
||||
commandPath = WhichUtil.Which("bash", false, Trace) ?? WhichUtil.Which("sh", true, Trace);
|
||||
commandPath = WhichUtil.Which("bash", false, Trace, prependPath) ?? WhichUtil.Which("sh", true, Trace, prependPath);
|
||||
#endif
|
||||
argFormat = ScriptHandlerHelpers.GetScriptArgumentsFormat(shellCommand);
|
||||
}
|
||||
@@ -169,7 +172,7 @@ namespace GitHub.Runner.Worker.Handlers
|
||||
var parsed = ScriptHandlerHelpers.ParseShellOptionString(shell);
|
||||
shellCommand = parsed.shellCommand;
|
||||
// For non-ContainerStepHost, the command must be located on the host by Which
|
||||
commandPath = WhichUtil.Which(parsed.shellCommand, !isContainerStepHost, Trace);
|
||||
commandPath = WhichUtil.Which(parsed.shellCommand, !isContainerStepHost, Trace, prependPath);
|
||||
argFormat = $"{parsed.shellArgs}".TrimStart();
|
||||
if (string.IsNullOrEmpty(argFormat))
|
||||
{
|
||||
|
||||
@@ -141,6 +141,13 @@ namespace GitHub.Runner.Worker.Handlers
|
||||
executionContext.Debug(line);
|
||||
if (line.ToLower().Contains("alpine"))
|
||||
{
|
||||
if (!Constants.Runner.PlatformArchitecture.Equals(Constants.Architecture.X64))
|
||||
{
|
||||
var os = Constants.Runner.Platform.ToString();
|
||||
var arch = Constants.Runner.PlatformArchitecture.ToString();
|
||||
var msg = $"JavaScript Actions in Alpine containers are only supported on x64 Linux runners. Detected {os} {arch}";
|
||||
throw new NotSupportedException(msg);
|
||||
}
|
||||
nodeExternal = "node12_alpine";
|
||||
executionContext.Output($"Container distribution is alpine. Running JavaScript Action with external tool: {nodeExternal}");
|
||||
return nodeExternal;
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>netcoreapp3.0</TargetFramework>
|
||||
<TargetFramework>netcoreapp3.1</TargetFramework>
|
||||
<OutputType>Exe</OutputType>
|
||||
<RuntimeIdentifiers>win-x64;win-x86;linux-x64;linux-arm64;linux-arm;osx-x64</RuntimeIdentifiers>
|
||||
<TargetLatestRuntimePatch>true</TargetLatestRuntimePatch>
|
||||
|
||||
@@ -40,8 +40,7 @@ namespace GitHub.Runner.Worker
|
||||
// Validate args.
|
||||
ArgUtil.NotNullOrEmpty(pipeIn, nameof(pipeIn));
|
||||
ArgUtil.NotNullOrEmpty(pipeOut, nameof(pipeOut));
|
||||
var runnerCertManager = HostContext.GetService<IRunnerCertificateManager>();
|
||||
VssUtil.InitializeVssClientSettings(HostContext.UserAgent, HostContext.WebProxy, runnerCertManager.VssClientCertificateManager);
|
||||
VssUtil.InitializeVssClientSettings(HostContext.UserAgent, HostContext.WebProxy);
|
||||
var jobRunner = HostContext.CreateService<IJobRunner>();
|
||||
|
||||
using (var channel = HostContext.CreateService<IProcessChannel>())
|
||||
|
||||
35
src/Sdk/Common/Common/Authentication/FederatedCredential.cs
Normal file
35
src/Sdk/Common/Common/Authentication/FederatedCredential.cs
Normal file
@@ -0,0 +1,35 @@
|
||||
using System;
|
||||
using System.Linq;
|
||||
using System.Net;
|
||||
using GitHub.Services.Common.Internal;
|
||||
|
||||
namespace GitHub.Services.Common
|
||||
{
|
||||
/// <summary>
|
||||
/// Provides a common implementation for federated credentials.
|
||||
/// </summary>
|
||||
[Serializable]
|
||||
public abstract class FederatedCredential : IssuedTokenCredential
|
||||
{
|
||||
protected FederatedCredential(IssuedToken initialToken)
|
||||
: base(initialToken)
|
||||
{
|
||||
}
|
||||
|
||||
public override bool IsAuthenticationChallenge(IHttpResponse webResponse)
|
||||
{
|
||||
if (webResponse == null)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (webResponse.StatusCode == HttpStatusCode.Found ||
|
||||
webResponse.StatusCode == HttpStatusCode.Redirect)
|
||||
{
|
||||
return webResponse.Headers.GetValues(HttpHeaders.TfsFedAuthRealm).Any();
|
||||
}
|
||||
|
||||
return webResponse.StatusCode == HttpStatusCode.Unauthorized;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,66 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Net.Http;
|
||||
|
||||
namespace GitHub.Services.Common
|
||||
{
|
||||
internal struct HttpRequestMessageWrapper : IHttpRequest, IHttpHeaders
|
||||
{
|
||||
public HttpRequestMessageWrapper(HttpRequestMessage request)
|
||||
{
|
||||
m_request = request;
|
||||
}
|
||||
|
||||
public IHttpHeaders Headers
|
||||
{
|
||||
get
|
||||
{
|
||||
return this;
|
||||
}
|
||||
}
|
||||
|
||||
public Uri RequestUri
|
||||
{
|
||||
get
|
||||
{
|
||||
return m_request.RequestUri;
|
||||
}
|
||||
}
|
||||
|
||||
public IDictionary<string, object> Properties
|
||||
{
|
||||
get
|
||||
{
|
||||
return m_request.Properties;
|
||||
}
|
||||
}
|
||||
|
||||
IEnumerable<String> IHttpHeaders.GetValues(String name)
|
||||
{
|
||||
IEnumerable<String> values;
|
||||
if (!m_request.Headers.TryGetValues(name, out values))
|
||||
{
|
||||
values = Enumerable.Empty<String>();
|
||||
}
|
||||
return values;
|
||||
}
|
||||
|
||||
void IHttpHeaders.SetValue(
|
||||
String name,
|
||||
String value)
|
||||
{
|
||||
m_request.Headers.Remove(name);
|
||||
m_request.Headers.Add(name, value);
|
||||
}
|
||||
|
||||
Boolean IHttpHeaders.TryGetValues(
|
||||
String name,
|
||||
out IEnumerable<String> values)
|
||||
{
|
||||
return m_request.Headers.TryGetValues(name, out values);
|
||||
}
|
||||
|
||||
private readonly HttpRequestMessage m_request;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Net;
|
||||
using System.Net.Http;
|
||||
|
||||
namespace GitHub.Services.Common
|
||||
{
|
||||
internal struct HttpResponseMessageWrapper : IHttpResponse, IHttpHeaders
|
||||
{
|
||||
public HttpResponseMessageWrapper(HttpResponseMessage response)
|
||||
{
|
||||
m_response = response;
|
||||
}
|
||||
|
||||
public IHttpHeaders Headers
|
||||
{
|
||||
get
|
||||
{
|
||||
return this;
|
||||
}
|
||||
}
|
||||
|
||||
public HttpStatusCode StatusCode
|
||||
{
|
||||
get
|
||||
{
|
||||
return m_response.StatusCode;
|
||||
}
|
||||
}
|
||||
|
||||
IEnumerable<String> IHttpHeaders.GetValues(String name)
|
||||
{
|
||||
IEnumerable<String> values;
|
||||
if (!m_response.Headers.TryGetValues(name, out values))
|
||||
{
|
||||
values = Enumerable.Empty<String>();
|
||||
}
|
||||
return values;
|
||||
}
|
||||
|
||||
void IHttpHeaders.SetValue(
|
||||
String name,
|
||||
String value)
|
||||
{
|
||||
throw new NotSupportedException();
|
||||
}
|
||||
|
||||
Boolean IHttpHeaders.TryGetValues(
|
||||
String name,
|
||||
out IEnumerable<String> values)
|
||||
{
|
||||
return m_response.Headers.TryGetValues(name, out values);
|
||||
}
|
||||
|
||||
private readonly HttpResponseMessage m_response;
|
||||
}
|
||||
}
|
||||
14
src/Sdk/Common/Common/Authentication/IHttpHeaders.cs
Normal file
14
src/Sdk/Common/Common/Authentication/IHttpHeaders.cs
Normal file
@@ -0,0 +1,14 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace GitHub.Services.Common
|
||||
{
|
||||
public interface IHttpHeaders
|
||||
{
|
||||
IEnumerable<String> GetValues(String name);
|
||||
|
||||
void SetValue(String name, String value);
|
||||
|
||||
Boolean TryGetValues(String name, out IEnumerable<String> values);
|
||||
}
|
||||
}
|
||||
23
src/Sdk/Common/Common/Authentication/IHttpRequest.cs
Normal file
23
src/Sdk/Common/Common/Authentication/IHttpRequest.cs
Normal file
@@ -0,0 +1,23 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace GitHub.Services.Common
|
||||
{
|
||||
public interface IHttpRequest
|
||||
{
|
||||
IHttpHeaders Headers
|
||||
{
|
||||
get;
|
||||
}
|
||||
|
||||
Uri RequestUri
|
||||
{
|
||||
get;
|
||||
}
|
||||
|
||||
IDictionary<string, object> Properties
|
||||
{
|
||||
get;
|
||||
}
|
||||
}
|
||||
}
|
||||
17
src/Sdk/Common/Common/Authentication/IHttpResponse.cs
Normal file
17
src/Sdk/Common/Common/Authentication/IHttpResponse.cs
Normal file
@@ -0,0 +1,17 @@
|
||||
using System.Net;
|
||||
|
||||
namespace GitHub.Services.Common
|
||||
{
|
||||
public interface IHttpResponse
|
||||
{
|
||||
IHttpHeaders Headers
|
||||
{
|
||||
get;
|
||||
}
|
||||
|
||||
HttpStatusCode StatusCode
|
||||
{
|
||||
get;
|
||||
}
|
||||
}
|
||||
}
|
||||
29
src/Sdk/Common/Common/Authentication/IVssCredentialPrompt.cs
Normal file
29
src/Sdk/Common/Common/Authentication/IVssCredentialPrompt.cs
Normal file
@@ -0,0 +1,29 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace GitHub.Services.Common
|
||||
{
|
||||
/// <summary>
|
||||
/// Provide an interface to get a new token for the credentials.
|
||||
/// </summary>
|
||||
public interface IVssCredentialPrompt
|
||||
{
|
||||
/// <summary>
|
||||
/// Get a new token using the specified provider and the previously failed token.
|
||||
/// </summary>
|
||||
/// <param name="provider">The provider for the token to be retrieved</param>
|
||||
/// <param name="failedToken">The token which previously failed authentication, if available</param>
|
||||
/// <returns>The new token</returns>
|
||||
Task<IssuedToken> GetTokenAsync(IssuedTokenProvider provider, IssuedToken failedToken);
|
||||
|
||||
IDictionary<string, string> Parameters { get; set; }
|
||||
}
|
||||
|
||||
public interface IVssCredentialPrompts : IVssCredentialPrompt
|
||||
{
|
||||
IVssCredentialPrompt FederatedPrompt
|
||||
{
|
||||
get;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
using System;
|
||||
|
||||
namespace GitHub.Services.Common
|
||||
{
|
||||
public interface IVssCredentialStorage
|
||||
{
|
||||
IssuedToken RetrieveToken(
|
||||
Uri serverUrl,
|
||||
VssCredentialsType credentialsType);
|
||||
|
||||
void StoreToken(
|
||||
Uri serverUrl,
|
||||
IssuedToken token);
|
||||
|
||||
void RemoveToken(
|
||||
Uri serverUrl,
|
||||
IssuedToken token);
|
||||
|
||||
bool RemoveTokenValue(
|
||||
Uri serverUrl,
|
||||
IssuedToken token);
|
||||
}
|
||||
}
|
||||
113
src/Sdk/Common/Common/Authentication/IssuedToken.cs
Normal file
113
src/Sdk/Common/Common/Authentication/IssuedToken.cs
Normal file
@@ -0,0 +1,113 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using GitHub.Services.Common.Internal;
|
||||
|
||||
namespace GitHub.Services.Common
|
||||
{
|
||||
/// <summary>
|
||||
/// Provides a common base class for issued tokens.
|
||||
/// </summary>
|
||||
[Serializable]
|
||||
public abstract class IssuedToken
|
||||
{
|
||||
internal IssuedToken()
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets a value indicating whether or not this token has been successfully authenticated with the remote
|
||||
/// server.
|
||||
/// </summary>
|
||||
public bool IsAuthenticated
|
||||
{
|
||||
get
|
||||
{
|
||||
return m_authenticated == 1;
|
||||
}
|
||||
}
|
||||
|
||||
protected internal abstract VssCredentialsType CredentialType
|
||||
{
|
||||
get;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// True if the token is retrieved from token storage.
|
||||
/// </summary>
|
||||
internal bool FromStorage
|
||||
{
|
||||
get;
|
||||
set;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Metadata about the token in a collection of properties.
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
public IDictionary<string, string> Properties
|
||||
{
|
||||
get;
|
||||
set;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Id of the owner of the token.
|
||||
/// </summary>
|
||||
internal Guid UserId
|
||||
{
|
||||
get;
|
||||
set;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Name of the owner of the token.
|
||||
/// </summary>
|
||||
internal string UserName
|
||||
{
|
||||
get;
|
||||
set;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Invoked when the issued token has been validated by successfully authenticated with the remote server.
|
||||
/// </summary>
|
||||
internal bool Authenticated()
|
||||
{
|
||||
return Interlocked.CompareExchange(ref m_authenticated, 1, 0) == 0;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get the value of the <c>HttpHeaders.VssUserData</c> response header and
|
||||
/// populate the <c>UserId</c> and <c>UserName</c> properties.
|
||||
/// </summary>
|
||||
internal void GetUserData(IHttpResponse response)
|
||||
{
|
||||
IEnumerable<string> headerValues;
|
||||
if (response.Headers.TryGetValues(HttpHeaders.VssUserData, out headerValues))
|
||||
{
|
||||
string userData = headerValues.FirstOrDefault();
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(userData))
|
||||
{
|
||||
string[] split = userData.Split(':');
|
||||
|
||||
if (split.Length >= 2)
|
||||
{
|
||||
UserId = Guid.Parse(split[0]);
|
||||
UserName = split[1];
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Applies the token to the HTTP request message.
|
||||
/// </summary>
|
||||
/// <param name="request">The HTTP request message</param>
|
||||
internal abstract void ApplyTo(IHttpRequest request);
|
||||
|
||||
private int m_authenticated;
|
||||
}
|
||||
}
|
||||
148
src/Sdk/Common/Common/Authentication/IssuedTokenCredential.cs
Normal file
148
src/Sdk/Common/Common/Authentication/IssuedTokenCredential.cs
Normal file
@@ -0,0 +1,148 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace GitHub.Services.Common
|
||||
{
|
||||
/// <summary>
|
||||
/// Provides a common base class for issued token credentials.
|
||||
/// </summary>
|
||||
[Serializable]
|
||||
public abstract class IssuedTokenCredential
|
||||
{
|
||||
protected IssuedTokenCredential(IssuedToken initialToken)
|
||||
{
|
||||
InitialToken = initialToken;
|
||||
}
|
||||
|
||||
public abstract VssCredentialsType CredentialType
|
||||
{
|
||||
get;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The initial token to use to authenticate if available.
|
||||
/// </summary>
|
||||
internal IssuedToken InitialToken
|
||||
{
|
||||
get;
|
||||
set;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the synchronization context which should be used for UI prompts.
|
||||
/// </summary>
|
||||
internal TaskScheduler Scheduler
|
||||
{
|
||||
get
|
||||
{
|
||||
return m_scheduler;
|
||||
}
|
||||
set
|
||||
{
|
||||
m_scheduler = value;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The credentials prompt which is used for retrieving a new token.
|
||||
/// </summary>
|
||||
internal IVssCredentialPrompt Prompt
|
||||
{
|
||||
get
|
||||
{
|
||||
return m_prompt;
|
||||
}
|
||||
set
|
||||
{
|
||||
m_prompt = value;
|
||||
}
|
||||
}
|
||||
|
||||
internal IVssCredentialStorage Storage
|
||||
{
|
||||
get
|
||||
{
|
||||
return m_storage;
|
||||
}
|
||||
set
|
||||
{
|
||||
m_storage = value;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The base url for the vssconnection to be used in the token storage key.
|
||||
/// </summary>
|
||||
internal Uri TokenStorageUrl { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Creates a token provider suitable for handling the challenge presented in the response.
|
||||
/// </summary>
|
||||
/// <param name="serverUrl">The targeted server</param>
|
||||
/// <param name="response">The challenge response</param>
|
||||
/// <param name="failedToken">The failed token</param>
|
||||
/// <returns>An issued token provider instance</returns>
|
||||
internal IssuedTokenProvider CreateTokenProvider(
|
||||
Uri serverUrl,
|
||||
IHttpResponse response,
|
||||
IssuedToken failedToken)
|
||||
{
|
||||
if (response != null && !IsAuthenticationChallenge(response))
|
||||
{
|
||||
throw new InvalidOperationException();
|
||||
}
|
||||
|
||||
if (InitialToken == null && Storage != null)
|
||||
{
|
||||
if (TokenStorageUrl == null)
|
||||
{
|
||||
throw new InvalidOperationException($"The {nameof(TokenStorageUrl)} property must have a value if the {nameof(Storage)} property is set on this instance of {GetType().Name}.");
|
||||
}
|
||||
InitialToken = Storage.RetrieveToken(TokenStorageUrl, CredentialType);
|
||||
}
|
||||
|
||||
IssuedTokenProvider provider = OnCreateTokenProvider(serverUrl, response);
|
||||
if (provider != null)
|
||||
{
|
||||
provider.TokenStorageUrl = TokenStorageUrl;
|
||||
}
|
||||
|
||||
// If the initial token is the one which failed to authenticate, don't
|
||||
// use it again and let the token provider get a new token.
|
||||
if (provider != null)
|
||||
{
|
||||
if (InitialToken != null && !Object.ReferenceEquals(InitialToken, failedToken))
|
||||
{
|
||||
provider.CurrentToken = InitialToken;
|
||||
}
|
||||
}
|
||||
|
||||
return provider;
|
||||
}
|
||||
|
||||
internal virtual string GetAuthenticationChallenge(IHttpResponse webResponse)
|
||||
{
|
||||
IEnumerable<String> values;
|
||||
if (!webResponse.Headers.TryGetValues(Internal.HttpHeaders.WwwAuthenticate, out values))
|
||||
{
|
||||
return String.Empty;
|
||||
}
|
||||
|
||||
return String.Join(", ", values);
|
||||
}
|
||||
|
||||
public abstract bool IsAuthenticationChallenge(IHttpResponse webResponse);
|
||||
|
||||
protected abstract IssuedTokenProvider OnCreateTokenProvider(Uri serverUrl, IHttpResponse response);
|
||||
|
||||
[NonSerialized]
|
||||
private TaskScheduler m_scheduler;
|
||||
|
||||
[NonSerialized]
|
||||
private IVssCredentialPrompt m_prompt;
|
||||
|
||||
[NonSerialized]
|
||||
private IVssCredentialStorage m_storage;
|
||||
}
|
||||
}
|
||||
545
src/Sdk/Common/Common/Authentication/IssuedTokenProvider.cs
Normal file
545
src/Sdk/Common/Common/Authentication/IssuedTokenProvider.cs
Normal file
@@ -0,0 +1,545 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.ComponentModel;
|
||||
using System.Diagnostics;
|
||||
using System.Globalization;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using GitHub.Services.Common.Diagnostics;
|
||||
|
||||
namespace GitHub.Services.Common
|
||||
{
|
||||
internal interface ISupportSignOut
|
||||
{
|
||||
void SignOut(Uri serverUrl, Uri replyToUrl, string identityProvider);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Provides a common base class for providers of the token authentication model.
|
||||
/// </summary>
|
||||
[EditorBrowsable(EditorBrowsableState.Never)]
|
||||
public abstract class IssuedTokenProvider
|
||||
{
|
||||
private const double c_slowTokenAcquisitionTimeInSeconds = 2.0;
|
||||
|
||||
protected IssuedTokenProvider(
|
||||
IssuedTokenCredential credential,
|
||||
Uri serverUrl,
|
||||
Uri signInUrl)
|
||||
{
|
||||
ArgumentUtility.CheckForNull(credential, "credential");
|
||||
|
||||
this.SignInUrl = signInUrl;
|
||||
this.Credential = credential;
|
||||
this.ServerUrl = serverUrl;
|
||||
|
||||
m_thisLock = new object();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the authentication scheme used to create this token provider.
|
||||
/// </summary>
|
||||
protected virtual String AuthenticationScheme
|
||||
{
|
||||
get
|
||||
{
|
||||
return String.Empty;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the authentication parameter or parameters used to create this token provider.
|
||||
/// </summary>
|
||||
protected virtual String AuthenticationParameter
|
||||
{
|
||||
get
|
||||
{
|
||||
return String.Empty;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the credential associated with the provider.
|
||||
/// </summary>
|
||||
protected internal IssuedTokenCredential Credential
|
||||
{
|
||||
get;
|
||||
}
|
||||
|
||||
internal VssCredentialsType CredentialType => this.Credential.CredentialType;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the current token.
|
||||
/// </summary>
|
||||
public IssuedToken CurrentToken
|
||||
{
|
||||
get;
|
||||
internal set;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets a value indicating whether or not a call to get token will require interactivity.
|
||||
/// </summary>
|
||||
public abstract bool GetTokenIsInteractive
|
||||
{
|
||||
get;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets a value indicating whether or not an ISynchronizeInvoke call is required.
|
||||
/// </summary>
|
||||
private Boolean InvokeRequired
|
||||
{
|
||||
get
|
||||
{
|
||||
return this.GetTokenIsInteractive && this.Credential.Scheduler != null;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the sign-in URL for the token provider.
|
||||
/// </summary>
|
||||
public Uri SignInUrl { get; private set; }
|
||||
|
||||
protected Uri ServerUrl { get; }
|
||||
|
||||
/// <summary>
|
||||
/// The base url for the vssconnection to be used in the token storage key.
|
||||
/// </summary>
|
||||
internal Uri TokenStorageUrl { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Determines whether the specified web response is an authentication challenge.
|
||||
/// </summary>
|
||||
/// <param name="webResponse">The web response</param>
|
||||
/// <returns>True if the web response is a challenge for token authentication; otherwise, false</returns>
|
||||
protected internal virtual bool IsAuthenticationChallenge(IHttpResponse webResponse)
|
||||
{
|
||||
return this.Credential.IsAuthenticationChallenge(webResponse);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Formats the authentication challenge string which this token provider handles.
|
||||
/// </summary>
|
||||
/// <returns>A string representing the handled authentication challenge</returns>
|
||||
internal string GetAuthenticationParameters()
|
||||
{
|
||||
if (string.IsNullOrEmpty(this.AuthenticationParameter))
|
||||
{
|
||||
return this.AuthenticationScheme;
|
||||
}
|
||||
else
|
||||
{
|
||||
return string.Format(CultureInfo.InvariantCulture, this.AuthenticationScheme, this.AuthenticationParameter);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Validates the current token if the provided reference is the current token and it
|
||||
/// has not been validated before.
|
||||
/// </summary>
|
||||
/// <param name="token">The token which should be validated</param>
|
||||
/// <param name="webResponse">The web response which used the token</param>
|
||||
internal void ValidateToken(
|
||||
IssuedToken token,
|
||||
IHttpResponse webResponse)
|
||||
{
|
||||
if (token == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
lock (m_thisLock)
|
||||
{
|
||||
IssuedToken tokenToValidate = OnValidatingToken(token, webResponse);
|
||||
|
||||
if (tokenToValidate.IsAuthenticated)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
// Perform validation which may include matching user information from the response
|
||||
// with that from the stored connection. If user information mismatch, an exception
|
||||
// will be thrown and the token will not be authenticated, which means if the same
|
||||
// token is ever used again in a different request it will be revalidated and fail.
|
||||
tokenToValidate.GetUserData(webResponse);
|
||||
OnTokenValidated(tokenToValidate);
|
||||
|
||||
// Set the token to be authenticated.
|
||||
tokenToValidate.Authenticated();
|
||||
}
|
||||
finally
|
||||
{
|
||||
// When the token fails validation, we null its reference from the token provider so it
|
||||
// would not be used again by the consumers of both. Note that we only update the current
|
||||
// token of the provider if it is the original token being validated, because we do not
|
||||
// want to overwrite a different token.
|
||||
if (object.ReferenceEquals(this.CurrentToken, token))
|
||||
{
|
||||
this.CurrentToken = tokenToValidate.IsAuthenticated ? tokenToValidate : null;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Invalidates the current token if the provided reference is the current token.
|
||||
/// </summary>
|
||||
/// <param name="token">The token reference which should be invalidated</param>
|
||||
internal void InvalidateToken(IssuedToken token)
|
||||
{
|
||||
bool invalidated = false;
|
||||
lock (m_thisLock)
|
||||
{
|
||||
if (token != null && object.ReferenceEquals(this.CurrentToken, token))
|
||||
{
|
||||
this.CurrentToken = null;
|
||||
invalidated = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (invalidated)
|
||||
{
|
||||
OnTokenInvalidated(token);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Retrieves a token for the credentials.
|
||||
/// </summary>
|
||||
/// <param name="failedToken">The token which previously failed authentication, if available</param>
|
||||
/// <param name="cancellationToken">The <c>CancellationToken</c>that will be assigned to the new task</param>
|
||||
/// <returns>A security token for the current credentials</returns>
|
||||
public async Task<IssuedToken> GetTokenAsync(
|
||||
IssuedToken failedToken,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
IssuedToken currentToken = this.CurrentToken;
|
||||
VssTraceActivity traceActivity = VssTraceActivity.Current;
|
||||
Stopwatch aadAuthTokenTimer = Stopwatch.StartNew();
|
||||
try
|
||||
{
|
||||
VssHttpEventSource.Log.AuthenticationStart(traceActivity);
|
||||
|
||||
if (currentToken != null)
|
||||
{
|
||||
VssHttpEventSource.Log.IssuedTokenRetrievedFromCache(traceActivity, this, currentToken);
|
||||
return currentToken;
|
||||
}
|
||||
else
|
||||
{
|
||||
GetTokenOperation operation = null;
|
||||
try
|
||||
{
|
||||
GetTokenOperation operationInProgress;
|
||||
operation = CreateOperation(traceActivity, failedToken, cancellationToken, out operationInProgress);
|
||||
if (operationInProgress == null)
|
||||
{
|
||||
return await operation.GetTokenAsync(traceActivity).ConfigureAwait(false);
|
||||
}
|
||||
else
|
||||
{
|
||||
return await operationInProgress.WaitForTokenAsync(traceActivity, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
lock (m_thisLock)
|
||||
{
|
||||
m_operations.Remove(operation);
|
||||
}
|
||||
|
||||
operation?.Dispose();
|
||||
}
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
VssHttpEventSource.Log.AuthenticationStop(traceActivity);
|
||||
|
||||
aadAuthTokenTimer.Stop();
|
||||
TimeSpan getTokenTime = aadAuthTokenTimer.Elapsed;
|
||||
|
||||
if(getTokenTime.TotalSeconds >= c_slowTokenAcquisitionTimeInSeconds)
|
||||
{
|
||||
// It may seem strange to pass the string value of TotalSeconds into this method, but testing
|
||||
// showed that ETW is persnickety when you register a method in an EventSource that doesn't
|
||||
// use strings or integers as its parameters. It is easier to simply give the method a string
|
||||
// than figure out to get ETW to reliably accept a double or TimeSpan.
|
||||
VssHttpEventSource.Log.AuthorizationDelayed(getTokenTime.TotalSeconds.ToString());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Retrieves a token for the credentials.
|
||||
/// </summary>
|
||||
/// <param name="failedToken">The token which previously failed authentication, if available</param>
|
||||
/// <param name="cancellationToken">The <c>CancellationToken</c>that will be assigned to the new task</param>
|
||||
/// <returns>A security token for the current credentials</returns>
|
||||
protected virtual Task<IssuedToken> OnGetTokenAsync(
|
||||
IssuedToken failedToken,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
if (this.Credential.Prompt != null)
|
||||
{
|
||||
return this.Credential.Prompt.GetTokenAsync(this, failedToken);
|
||||
}
|
||||
else
|
||||
{
|
||||
return Task.FromResult<IssuedToken>(null);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Invoked when the current token is being validated. When overriden in a derived class,
|
||||
/// validate and return the validated token.
|
||||
/// </summary>
|
||||
/// <remarks>Is called inside a lock in <c>ValidateToken</c></remarks>
|
||||
/// <param name="token">The token to validate</param>
|
||||
/// <param name="webResponse">The web response which used the token</param>
|
||||
/// <returns>The validated token</returns>
|
||||
protected virtual IssuedToken OnValidatingToken(
|
||||
IssuedToken token,
|
||||
IHttpResponse webResponse)
|
||||
{
|
||||
return token;
|
||||
}
|
||||
|
||||
protected virtual void OnTokenValidated(IssuedToken token)
|
||||
{
|
||||
// Store the validated token to the token storage if it is not originally from there.
|
||||
if (!token.FromStorage && TokenStorageUrl != null)
|
||||
{
|
||||
Credential.Storage?.StoreToken(TokenStorageUrl, token);
|
||||
}
|
||||
|
||||
VssHttpEventSource.Log.IssuedTokenValidated(VssTraceActivity.Current, this, token);
|
||||
}
|
||||
|
||||
protected virtual void OnTokenInvalidated(IssuedToken token)
|
||||
{
|
||||
if (Credential.Storage != null && TokenStorageUrl != null)
|
||||
{
|
||||
Credential.Storage.RemoveTokenValue(TokenStorageUrl, token);
|
||||
}
|
||||
|
||||
VssHttpEventSource.Log.IssuedTokenInvalidated(VssTraceActivity.Current, this, token);
|
||||
}
|
||||
|
||||
private GetTokenOperation CreateOperation(
|
||||
VssTraceActivity traceActivity,
|
||||
IssuedToken failedToken,
|
||||
CancellationToken cancellationToken,
|
||||
out GetTokenOperation operationInProgress)
|
||||
{
|
||||
operationInProgress = null;
|
||||
GetTokenOperation operation = null;
|
||||
lock (m_thisLock)
|
||||
{
|
||||
if (m_operations == null)
|
||||
{
|
||||
m_operations = new List<GetTokenOperation>();
|
||||
}
|
||||
|
||||
// Grab the main operation which is doing the work (if any)
|
||||
if (m_operations.Count > 0)
|
||||
{
|
||||
operationInProgress = m_operations[0];
|
||||
|
||||
// Use the existing completion source when creating the new operation
|
||||
operation = new GetTokenOperation(traceActivity, this, failedToken, cancellationToken, operationInProgress.CompletionSource);
|
||||
}
|
||||
else
|
||||
{
|
||||
operation = new GetTokenOperation(traceActivity, this, failedToken, cancellationToken);
|
||||
}
|
||||
|
||||
m_operations.Add(operation);
|
||||
}
|
||||
|
||||
return operation;
|
||||
}
|
||||
|
||||
private object m_thisLock;
|
||||
private List<GetTokenOperation> m_operations;
|
||||
|
||||
private class DisposableTaskCompletionSource<T> : TaskCompletionSource<T>, IDisposable
|
||||
{
|
||||
public DisposableTaskCompletionSource()
|
||||
{
|
||||
this.Task.ConfigureAwait(false).GetAwaiter().OnCompleted(() => { m_completed = true; });
|
||||
}
|
||||
|
||||
~DisposableTaskCompletionSource()
|
||||
{
|
||||
TraceErrorIfNotCompleted();
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
if (m_disposed)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
TraceErrorIfNotCompleted();
|
||||
|
||||
m_disposed = true;
|
||||
GC.SuppressFinalize(this);
|
||||
}
|
||||
|
||||
private void TraceErrorIfNotCompleted()
|
||||
{
|
||||
if (!m_completed)
|
||||
{
|
||||
VssHttpEventSource.Log.TokenSourceNotCompleted();
|
||||
}
|
||||
}
|
||||
|
||||
private Boolean m_disposed;
|
||||
private Boolean m_completed;
|
||||
}
|
||||
|
||||
private sealed class GetTokenOperation : IDisposable
|
||||
{
|
||||
public GetTokenOperation(
|
||||
VssTraceActivity activity,
|
||||
IssuedTokenProvider provider,
|
||||
IssuedToken failedToken,
|
||||
CancellationToken cancellationToken)
|
||||
: this(activity, provider, failedToken, cancellationToken, new DisposableTaskCompletionSource<IssuedToken>(), true)
|
||||
{
|
||||
}
|
||||
|
||||
public GetTokenOperation(
|
||||
VssTraceActivity activity,
|
||||
IssuedTokenProvider provider,
|
||||
IssuedToken failedToken,
|
||||
CancellationToken cancellationToken,
|
||||
DisposableTaskCompletionSource<IssuedToken> completionSource,
|
||||
Boolean ownsCompletionSource = false)
|
||||
{
|
||||
this.Provider = provider;
|
||||
this.ActivityId = activity?.Id ?? Guid.Empty;
|
||||
this.FailedToken = failedToken;
|
||||
this.CancellationToken = cancellationToken;
|
||||
this.CompletionSource = completionSource;
|
||||
this.OwnsCompletionSource = ownsCompletionSource;
|
||||
}
|
||||
|
||||
public Guid ActivityId { get; }
|
||||
|
||||
public CancellationToken CancellationToken { get; }
|
||||
|
||||
public DisposableTaskCompletionSource<IssuedToken> CompletionSource { get; }
|
||||
|
||||
public Boolean OwnsCompletionSource { get; }
|
||||
|
||||
private IssuedToken FailedToken { get; }
|
||||
|
||||
private IssuedTokenProvider Provider { get; }
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
if (this.OwnsCompletionSource)
|
||||
{
|
||||
this.CompletionSource?.Dispose();
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<IssuedToken> GetTokenAsync(VssTraceActivity traceActivity)
|
||||
{
|
||||
IssuedToken token = null;
|
||||
try
|
||||
{
|
||||
VssHttpEventSource.Log.IssuedTokenAcquiring(traceActivity, this.Provider);
|
||||
if (this.Provider.InvokeRequired)
|
||||
{
|
||||
// Post to the UI thread using the scheduler. This may return a new task object which needs
|
||||
// to be awaited, since once we get to the UI thread there may be nothing to do if someone else
|
||||
// preempts us.
|
||||
|
||||
// The cancellation token source is used to handle race conditions between scheduling and
|
||||
// waiting for the UI task to begin execution. The callback is responsible for disposing of
|
||||
// the token source, since the thought here is that the callback will run eventually as the
|
||||
// typical reason for not starting execution within the timeout is due to a deadlock with
|
||||
// the scheduler being used.
|
||||
var timerTask = new TaskCompletionSource<Object>();
|
||||
var timeoutTokenSource = new CancellationTokenSource(TimeSpan.FromSeconds(3));
|
||||
timeoutTokenSource.Token.Register(() => timerTask.SetResult(null), false);
|
||||
|
||||
var uiTask = Task.Factory.StartNew((state) => PostCallback(state, timeoutTokenSource),
|
||||
this,
|
||||
this.CancellationToken,
|
||||
TaskCreationOptions.None,
|
||||
this.Provider.Credential.Scheduler).Unwrap();
|
||||
|
||||
var completedTask = await Task.WhenAny(timerTask.Task, uiTask).ConfigureAwait(false);
|
||||
if (completedTask == uiTask)
|
||||
{
|
||||
token = uiTask.Result;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
token = await this.Provider.OnGetTokenAsync(this.FailedToken, this.CancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
CompletionSource.TrySetResult(token);
|
||||
return token;
|
||||
}
|
||||
catch (Exception exception)
|
||||
{
|
||||
// Mark our completion source as failed so other waiters will get notified in all cases
|
||||
CompletionSource.TrySetException(exception);
|
||||
throw;
|
||||
}
|
||||
finally
|
||||
{
|
||||
this.Provider.CurrentToken = token ?? this.FailedToken;
|
||||
VssHttpEventSource.Log.IssuedTokenAcquired(traceActivity, this.Provider, token);
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<IssuedToken> WaitForTokenAsync(
|
||||
VssTraceActivity traceActivity,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
IssuedToken token = null;
|
||||
try
|
||||
{
|
||||
|
||||
VssHttpEventSource.Log.IssuedTokenWaitStart(traceActivity, this.Provider, this.ActivityId);
|
||||
token = await Task.Factory.ContinueWhenAll<IssuedToken>(new Task[] { CompletionSource.Task }, (x) => CompletionSource.Task.Result, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
finally
|
||||
{
|
||||
VssHttpEventSource.Log.IssuedTokenWaitStop(traceActivity, this.Provider, token);
|
||||
}
|
||||
|
||||
return token;
|
||||
}
|
||||
|
||||
private static Task<IssuedToken> PostCallback(
|
||||
Object state,
|
||||
CancellationTokenSource timeoutTokenSource)
|
||||
{
|
||||
// Make sure that we were not cancelled (timed out) before this callback is invoked.
|
||||
using (timeoutTokenSource)
|
||||
{
|
||||
timeoutTokenSource.CancelAfter(-1);
|
||||
if (timeoutTokenSource.IsCancellationRequested)
|
||||
{
|
||||
return Task.FromResult<IssuedToken>(null);
|
||||
}
|
||||
}
|
||||
|
||||
GetTokenOperation thisPtr = (GetTokenOperation)state;
|
||||
return thisPtr.Provider.OnGetTokenAsync(thisPtr.FailedToken, thisPtr.CancellationToken);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
400
src/Sdk/Common/Common/Authentication/VssCredentials.cs
Normal file
400
src/Sdk/Common/Common/Authentication/VssCredentials.cs
Normal file
@@ -0,0 +1,400 @@
|
||||
using System;
|
||||
using System.Collections.Concurrent;
|
||||
using System.Collections.Generic;
|
||||
using System.ComponentModel;
|
||||
using System.Globalization;
|
||||
using System.Text;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using GitHub.Services.Common.Diagnostics;
|
||||
using GitHub.Services.Common.Internal;
|
||||
|
||||
namespace GitHub.Services.Common
|
||||
{
|
||||
/// <summary>
|
||||
/// The type of credentials supported natively by the framework
|
||||
/// </summary>
|
||||
public enum VssCredentialsType
|
||||
{
|
||||
Windows = 0,
|
||||
Federated = 1,
|
||||
Basic = 2,
|
||||
ServiceIdentity = 3,
|
||||
OAuth = 4,
|
||||
S2S = 5,
|
||||
Other = 6,
|
||||
Aad = 7,
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Provides the ability to control when to show or hide the credential prompt user interface.
|
||||
/// </summary>
|
||||
public enum CredentialPromptType
|
||||
{
|
||||
/// <summary>
|
||||
/// Show the UI only if necessary to obtain credentials.
|
||||
/// </summary>
|
||||
PromptIfNeeded = 0,
|
||||
|
||||
/// <summary>
|
||||
/// Never show the UI, even if an error occurs.
|
||||
/// </summary>
|
||||
DoNotPrompt = 2,
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Provides credentials to use when connecting to a Visual Studio Service.
|
||||
/// </summary>
|
||||
public class VssCredentials
|
||||
{
|
||||
/// <summary>
|
||||
/// Initializes a new <c>VssCredentials</c> instance with default credentials.
|
||||
/// </summary>
|
||||
public VssCredentials()
|
||||
: this(null)
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new <c>VssCredentials</c> instance with the specified windows and issued token
|
||||
/// credential.
|
||||
/// </summary>
|
||||
/// <param name="federatedCredential">The federated credential to use for authentication</param>
|
||||
public VssCredentials(FederatedCredential federatedCredential)
|
||||
: this(federatedCredential, EnvironmentUserInteractive
|
||||
? CredentialPromptType.PromptIfNeeded : CredentialPromptType.DoNotPrompt)
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new <c>VssCredentials</c> instance with the specified windows and issued token
|
||||
/// credential.
|
||||
/// </summary>
|
||||
/// <param name="federatedCredential">The federated credential to use for authentication</param>
|
||||
/// <param name="promptType">CredentialPromptType.PromptIfNeeded if interactive prompts are allowed, otherwise CredentialProptType.DoNotPrompt</param>
|
||||
public VssCredentials(
|
||||
FederatedCredential federatedCredential,
|
||||
CredentialPromptType promptType)
|
||||
: this(federatedCredential, promptType, null)
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new <c>VssCredentials</c> instance with the specified windows and issued token
|
||||
/// credential.
|
||||
/// </summary>
|
||||
/// <param name="federatedCredential">The federated credential to use for authentication</param>
|
||||
/// <param name="promptType">CredentialPromptType.PromptIfNeeded if interactive prompts are allowed; otherwise, CredentialProptType.DoNotPrompt</param>
|
||||
/// <param name="scheduler">An optional <c>TaskScheduler</c> to ensure credentials prompting occurs on the UI thread</param>
|
||||
public VssCredentials(
|
||||
FederatedCredential federatedCredential,
|
||||
CredentialPromptType promptType,
|
||||
TaskScheduler scheduler)
|
||||
: this(federatedCredential, promptType, scheduler, null)
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new <c>VssCredentials</c> instance with the specified windows and issued token
|
||||
/// credential.
|
||||
/// </summary>
|
||||
/// <param name="federatedCredential">The federated credential to use for authentication</param>
|
||||
/// <param name="promptType">CredentialPromptType.PromptIfNeeded if interactive prompts are allowed; otherwise, CredentialProptType.DoNotPrompt</param>
|
||||
/// <param name="scheduler">An optional <c>TaskScheduler</c> to ensure credentials prompting occurs on the UI thread</param>
|
||||
/// <param name="credentialPrompt">An optional <c>IVssCredentialPrompt</c> to perform prompting for credentials</param>
|
||||
public VssCredentials(
|
||||
FederatedCredential federatedCredential,
|
||||
CredentialPromptType promptType,
|
||||
TaskScheduler scheduler,
|
||||
IVssCredentialPrompt credentialPrompt)
|
||||
{
|
||||
this.PromptType = promptType;
|
||||
|
||||
if (promptType == CredentialPromptType.PromptIfNeeded && scheduler == null)
|
||||
{
|
||||
// If we use TaskScheduler.FromCurrentSynchronizationContext() here and this is executing under the UI
|
||||
// thread, for example from an event handler in a WinForms applications, this TaskScheduler will capture
|
||||
// the UI SyncrhonizationContext whose MaximumConcurrencyLevel is 1 and only has a single thread to
|
||||
// execute queued work. Then, if the UI thread invokes one of our synchronous methods that are just
|
||||
// wrappers that block until the asynchronous overload returns, and if the async Task queues work to
|
||||
// this TaskScheduler, like GitHub.Services.CommonGetTokenOperation.GetTokenAsync does,
|
||||
// this will produce an immediate deadlock. It is a much safer choice to use TaskScheduler.Default here
|
||||
// as it uses the .NET Framework ThreadPool to execute queued work.
|
||||
scheduler = TaskScheduler.Default;
|
||||
}
|
||||
|
||||
if (federatedCredential != null)
|
||||
{
|
||||
m_federatedCredential = federatedCredential;
|
||||
m_federatedCredential.Scheduler = scheduler;
|
||||
m_federatedCredential.Prompt = credentialPrompt;
|
||||
}
|
||||
|
||||
m_thisLock = new object();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Implicitly converts a <c>FederatedCredential</c> instance into a <c>VssCredentials</c> instance.
|
||||
/// </summary>
|
||||
/// <param name="credential">The federated credential instance</param>
|
||||
/// <returns>A new <c>VssCredentials</c> instance which wraps the specified credential</returns>
|
||||
public static implicit operator VssCredentials(FederatedCredential credential)
|
||||
{
|
||||
return new VssCredentials(credential);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets a value indicating whether or not interactive prompts are allowed.
|
||||
/// </summary>
|
||||
public CredentialPromptType PromptType
|
||||
{
|
||||
get
|
||||
{
|
||||
return m_promptType;
|
||||
}
|
||||
set
|
||||
{
|
||||
if (value == CredentialPromptType.PromptIfNeeded && !EnvironmentUserInteractive)
|
||||
{
|
||||
throw new ArgumentException(CommonResources.CannotPromptIfNonInteractive(), "PromptType");
|
||||
}
|
||||
|
||||
m_promptType = value;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets a value indicating the issued token credentials to use for authentication with the server.
|
||||
/// </summary>
|
||||
public FederatedCredential Federated
|
||||
{
|
||||
get
|
||||
{
|
||||
return m_federatedCredential;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// A pluggable credential store.
|
||||
/// Simply assign a storage implementation to this property
|
||||
/// and the <c>VssCredentials</c> will use it to store and retrieve tokens
|
||||
/// during authentication.
|
||||
/// </summary>
|
||||
public IVssCredentialStorage Storage
|
||||
{
|
||||
get
|
||||
{
|
||||
return m_credentialStorage;
|
||||
}
|
||||
set
|
||||
{
|
||||
m_credentialStorage = value;
|
||||
|
||||
if (m_federatedCredential != null)
|
||||
{
|
||||
m_federatedCredential.Storage = value;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
///Attempts to find appropriate Access token for IDE user and add to prompt's parameter
|
||||
/// Actual implementation in override.
|
||||
/// </summary>
|
||||
internal virtual bool TryGetValidAdalToken(IVssCredentialPrompt prompt)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates a token provider for the configured issued token credentials.
|
||||
/// </summary>
|
||||
/// <param name="serverUrl">The targeted server</param>
|
||||
/// <param name="webResponse">The failed web response</param>
|
||||
/// <param name="failedToken">The failed token</param>
|
||||
/// <returns>A provider for retrieving tokens for the configured credential</returns>
|
||||
internal IssuedTokenProvider CreateTokenProvider(
|
||||
Uri serverUrl,
|
||||
IHttpResponse webResponse,
|
||||
IssuedToken failedToken)
|
||||
{
|
||||
ArgumentUtility.CheckForNull(serverUrl, "serverUrl");
|
||||
|
||||
IssuedTokenProvider tokenProvider = null;
|
||||
VssTraceActivity traceActivity = VssTraceActivity.Current;
|
||||
lock (m_thisLock)
|
||||
{
|
||||
tokenProvider = m_currentProvider;
|
||||
if (tokenProvider == null || !tokenProvider.IsAuthenticationChallenge(webResponse))
|
||||
{
|
||||
// Prefer federated authentication over Windows authentication.
|
||||
if (m_federatedCredential != null && m_federatedCredential.IsAuthenticationChallenge(webResponse))
|
||||
{
|
||||
if (tokenProvider != null)
|
||||
{
|
||||
VssHttpEventSource.Log.IssuedTokenProviderRemoved(traceActivity, tokenProvider);
|
||||
}
|
||||
|
||||
// TODO: This needs to be refactored or renamed to be more generic ...
|
||||
this.TryGetValidAdalToken(m_federatedCredential.Prompt);
|
||||
|
||||
tokenProvider = m_federatedCredential.CreateTokenProvider(serverUrl, webResponse, failedToken);
|
||||
|
||||
if (tokenProvider != null)
|
||||
{
|
||||
VssHttpEventSource.Log.IssuedTokenProviderCreated(traceActivity, tokenProvider);
|
||||
}
|
||||
}
|
||||
|
||||
m_currentProvider = tokenProvider;
|
||||
}
|
||||
|
||||
return tokenProvider;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Retrieves the token provider for the provided server URL if one has been created.
|
||||
/// </summary>
|
||||
/// <param name="serverUrl">The targeted server</param>
|
||||
/// <param name="provider">Stores the active token provider, if one exists</param>
|
||||
/// <returns>True if a token provider was found, false otherwise</returns>
|
||||
public bool TryGetTokenProvider(
|
||||
Uri serverUrl,
|
||||
out IssuedTokenProvider provider)
|
||||
{
|
||||
ArgumentUtility.CheckForNull(serverUrl, "serverUrl");
|
||||
|
||||
lock (m_thisLock)
|
||||
{
|
||||
// Ensure that we attempt to use the most appropriate authentication mechanism by default.
|
||||
if (m_currentProvider == null)
|
||||
{
|
||||
if (m_federatedCredential != null)
|
||||
{
|
||||
m_currentProvider = m_federatedCredential.CreateTokenProvider(serverUrl, null, null);
|
||||
}
|
||||
|
||||
if (m_currentProvider != null)
|
||||
{
|
||||
VssHttpEventSource.Log.IssuedTokenProviderCreated(VssTraceActivity.Current, m_currentProvider);
|
||||
}
|
||||
}
|
||||
|
||||
provider = m_currentProvider;
|
||||
}
|
||||
|
||||
return provider != null;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Determines if the web response is an authentication redirect for issued token providers.
|
||||
/// </summary>
|
||||
/// <param name="webResponse">The web response</param>
|
||||
/// <returns>True if this is an token authentication redirect, false otherwise</returns>
|
||||
internal bool IsAuthenticationChallenge(IHttpResponse webResponse)
|
||||
{
|
||||
if (webResponse == null)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
bool isChallenge = false;
|
||||
if (!isChallenge && m_federatedCredential != null)
|
||||
{
|
||||
isChallenge = m_federatedCredential.IsAuthenticationChallenge(webResponse);
|
||||
}
|
||||
|
||||
return isChallenge;
|
||||
}
|
||||
|
||||
internal void SignOut(
|
||||
Uri serverUrl,
|
||||
Uri serviceLocation,
|
||||
string identityProvider)
|
||||
{
|
||||
// Remove the token in the storage and the current token provider. Note that we don't
|
||||
// call InvalidateToken here because we want to remove the whole token not just its value
|
||||
if ((m_currentProvider != null) && (m_currentProvider.CurrentToken != null))
|
||||
{
|
||||
if (m_currentProvider.Credential.Storage != null && m_currentProvider.TokenStorageUrl != null)
|
||||
{
|
||||
m_currentProvider.Credential.Storage.RemoveToken(m_currentProvider.TokenStorageUrl, m_currentProvider.CurrentToken);
|
||||
}
|
||||
m_currentProvider.CurrentToken = null;
|
||||
}
|
||||
|
||||
// We need to make sure that the current provider actually supports the signout method
|
||||
ISupportSignOut tokenProviderWithSignOut = m_currentProvider as ISupportSignOut;
|
||||
if (tokenProviderWithSignOut == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// Replace the parameters from the service location
|
||||
if (serviceLocation != null)
|
||||
{
|
||||
string serviceLocationUri = serviceLocation.AbsoluteUri;
|
||||
serviceLocationUri = serviceLocationUri.Replace("{mode}", "SignOut");
|
||||
serviceLocationUri = serviceLocationUri.Replace("{redirectUrl}", serverUrl.AbsoluteUri);
|
||||
serviceLocation = new Uri(serviceLocationUri);
|
||||
}
|
||||
|
||||
// Now actually signout of the token provider
|
||||
tokenProviderWithSignOut.SignOut(serviceLocation, serverUrl, identityProvider);
|
||||
}
|
||||
|
||||
[EditorBrowsable(EditorBrowsableState.Never)]
|
||||
public static void WriteAuthorizationToken(
|
||||
string token,
|
||||
IDictionary<string, string> attributes)
|
||||
{
|
||||
int i = 0;
|
||||
for (int j = 0; j < token.Length; i++, j += 128)
|
||||
{
|
||||
attributes["AuthTokenSegment" + i] = token.Substring(j, Math.Min(128, token.Length - j));
|
||||
}
|
||||
|
||||
attributes["AuthTokenSegmentCount"] = i.ToString(CultureInfo.InvariantCulture);
|
||||
}
|
||||
|
||||
protected static string ReadAuthorizationToken(IDictionary<string, string> attributes)
|
||||
{
|
||||
string authTokenCountValue;
|
||||
if (attributes.TryGetValue("AuthTokenSegmentCount", out authTokenCountValue))
|
||||
{
|
||||
int authTokenCount = int.Parse(authTokenCountValue, CultureInfo.InvariantCulture);
|
||||
|
||||
StringBuilder sb = new StringBuilder();
|
||||
for (int i = 0; i < authTokenCount; i++)
|
||||
{
|
||||
string segmentName = "AuthTokenSegment" + i;
|
||||
|
||||
string segmentValue;
|
||||
if (attributes.TryGetValue(segmentName, out segmentValue))
|
||||
{
|
||||
sb.Append(segmentValue);
|
||||
}
|
||||
}
|
||||
|
||||
return sb.ToString();
|
||||
}
|
||||
|
||||
return string.Empty;
|
||||
}
|
||||
|
||||
protected static bool EnvironmentUserInteractive
|
||||
{
|
||||
get
|
||||
{
|
||||
return Environment.UserInteractive;
|
||||
}
|
||||
}
|
||||
|
||||
private object m_thisLock;
|
||||
private CredentialPromptType m_promptType;
|
||||
private IssuedTokenProvider m_currentProvider;
|
||||
protected FederatedCredential m_federatedCredential;
|
||||
private IVssCredentialStorage m_credentialStorage;
|
||||
}
|
||||
}
|
||||
80
src/Sdk/Common/Common/ClientStorage/IVssClientStorage.cs
Normal file
80
src/Sdk/Common/Common/ClientStorage/IVssClientStorage.cs
Normal file
@@ -0,0 +1,80 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.ComponentModel;
|
||||
|
||||
namespace GitHub.Services.Common.ClientStorage
|
||||
{
|
||||
/// <summary>
|
||||
/// An interface for accessing client data stored locally.
|
||||
/// </summary>
|
||||
[EditorBrowsable(EditorBrowsableState.Never)] // for internal use
|
||||
public interface IVssClientStorage : IVssClientStorageReader, IVssClientStorageWriter
|
||||
{
|
||||
/// <summary>
|
||||
/// Much like the System.IO.Path.Combine method, this method puts together path segments into a path using
|
||||
/// the appropriate path delimiter.
|
||||
/// </summary>
|
||||
/// <param name="paths"></param>
|
||||
/// <returns></returns>
|
||||
string PathKeyCombine(params string[] paths);
|
||||
|
||||
/// <summary>
|
||||
/// The path segment delimiter used by this storage mechanism.
|
||||
/// </summary>
|
||||
char PathSeparator { get; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// An interface for reading from local data storage
|
||||
/// </summary>
|
||||
public interface IVssClientStorageReader
|
||||
{
|
||||
/// <summary>
|
||||
/// Reads one entry from the storage.
|
||||
/// </summary>
|
||||
/// <typeparam name="T">The type to return.</typeparam>
|
||||
/// <param name="path">This is the path key for the data to retrieve.</param>
|
||||
/// <returns>Returns the value stored at the given path as type T</returns>
|
||||
T ReadEntry<T>(string path);
|
||||
|
||||
/// <summary>
|
||||
/// Reads one entry from the storage. If the entry does not exist or can not be converted to type T, the default value provided will be returned.
|
||||
/// When T is not a simple type, and there is extra logic to determine the default value, the pattern: ReadEntry<T>(path) && GetDefault(); is
|
||||
/// preferred, so that method to retrieve the default is not evaluated unless the entry does not exist.
|
||||
/// </summary>
|
||||
/// <typeparam name="T">The type to return.</typeparam>
|
||||
/// <param name="path">This is the path key for the data to retrieve.</param>
|
||||
/// <param name="defaultValue">The value to return if the key does not exist or the value can not be converted to type T</param>
|
||||
/// <returns></returns>
|
||||
T ReadEntry<T>(string path, T defaultValue);
|
||||
|
||||
/// <summary>
|
||||
/// Returns all entries under the path provided whose values can be converted to T. If path = "root\mydata", then this will return all entries where path begins with "root\mydata\".
|
||||
/// </summary>
|
||||
/// <typeparam name="T">The type for the entries to return.</typeparam>
|
||||
/// <param name="path">The path pointing to the branch of entries to return.</param>
|
||||
/// <returns></returns>
|
||||
IDictionary<string, T> ReadEntries<T>(string path);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// An interface for writing to local data storage
|
||||
/// </summary>
|
||||
public interface IVssClientStorageWriter
|
||||
{
|
||||
/// <summary>
|
||||
/// Write one entry into the local data storage.
|
||||
/// </summary>
|
||||
/// <param name="path">This is the key for the data to store. Providing a path allows data to be accessed hierarchicaly.</param>
|
||||
/// <param name="value">The value to store at the specified path. Setting his to NULL will remove the entry.</param>
|
||||
void WriteEntry(string path, object value);
|
||||
|
||||
/// <summary>
|
||||
/// Writes a set of entries to the writer, which provides efficiency benefits over writing each entry individually.
|
||||
/// It also ensures that the either all of the entries are written or in the case of an error, no entries are written.
|
||||
/// Setting a value to NULL, will remove the entry.
|
||||
/// </summary>
|
||||
/// <param name="entries"></param>
|
||||
void WriteEntries(IEnumerable<KeyValuePair<string, Object>> entries);
|
||||
}
|
||||
}
|
||||
623
src/Sdk/Common/Common/ClientStorage/VssFileStorage.cs
Normal file
623
src/Sdk/Common/Common/ClientStorage/VssFileStorage.cs
Normal file
@@ -0,0 +1,623 @@
|
||||
using System;
|
||||
using System.Collections.Concurrent;
|
||||
using System.Collections.Generic;
|
||||
using System.ComponentModel;
|
||||
using System.Diagnostics;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using GitHub.Services.Common.Internal;
|
||||
using Newtonsoft.Json;
|
||||
using Newtonsoft.Json.Linq;
|
||||
|
||||
namespace GitHub.Services.Common.ClientStorage
|
||||
{
|
||||
/// <summary>
|
||||
/// Class providing access to local file storage, so data can persist across processes.
|
||||
/// </summary>
|
||||
[EditorBrowsable(EditorBrowsableState.Never)] // for internal use
|
||||
public class VssFileStorage : IVssClientStorage, IDisposable
|
||||
{
|
||||
private readonly string m_filePath;
|
||||
private readonly VssFileStorageReader m_reader;
|
||||
private readonly IVssClientStorageWriter m_writer;
|
||||
|
||||
private const char c_defaultPathSeparator = '\\';
|
||||
private const bool c_defaultIgnoreCaseInPaths = false;
|
||||
|
||||
/// <summary>
|
||||
/// The separator to use between the path segments of the storage keys.
|
||||
/// </summary>
|
||||
public char PathSeparator { get; }
|
||||
|
||||
/// <summary>
|
||||
/// The StringComparer used to compare keys in the dictionary.
|
||||
/// </summary>
|
||||
public StringComparer PathComparer { get; }
|
||||
|
||||
/// <summary>
|
||||
/// This constructor should remain private. Use the factory method GetVssLocalFileStorage to ensure we only have one instance per file,
|
||||
/// which will reduce contention.
|
||||
/// </summary>
|
||||
/// <param name="filePath">This file path to store the settings.</param>
|
||||
/// <param name="pathSeparatorForKeys">The separator to use between the path segments of the storage keys.</param>
|
||||
/// <param name="ignoreCaseInPaths">If true the dictionary will use the OrdinalIgnoreCase StringComparer to compare keys.</param>
|
||||
private VssFileStorage(string filePath, char pathSeparatorForKeys = c_defaultPathSeparator, bool ignoreCaseInPaths = c_defaultIgnoreCaseInPaths) // This constructor should remain private.
|
||||
{
|
||||
PathSeparator = pathSeparatorForKeys;
|
||||
PathComparer = GetAppropriateStringComparer(ignoreCaseInPaths);
|
||||
m_filePath = filePath;
|
||||
m_reader = new VssFileStorageReader(m_filePath, pathSeparatorForKeys, PathComparer);
|
||||
m_writer = new VssFileStorageWriter(m_filePath, pathSeparatorForKeys, PathComparer);
|
||||
}
|
||||
|
||||
public T ReadEntry<T>(string path)
|
||||
{
|
||||
return m_reader.ReadEntry<T>(path);
|
||||
}
|
||||
|
||||
public T ReadEntry<T>(string path, T defaultValue)
|
||||
{
|
||||
return m_reader.ReadEntry<T>(path, defaultValue);
|
||||
}
|
||||
|
||||
public IDictionary<string, T> ReadEntries<T>(string pathPrefix)
|
||||
{
|
||||
return m_reader.ReadEntries<T>(pathPrefix);
|
||||
}
|
||||
|
||||
public void WriteEntries(IEnumerable<KeyValuePair<string, object>> entries)
|
||||
{
|
||||
m_writer.WriteEntries(entries);
|
||||
m_reader.NotifyChanged();
|
||||
}
|
||||
|
||||
public void WriteEntry(string key, object value)
|
||||
{
|
||||
m_writer.WriteEntry(key, value);
|
||||
m_reader.NotifyChanged();
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
m_reader.Dispose();
|
||||
}
|
||||
|
||||
public string PathKeyCombine(params string[] paths)
|
||||
{
|
||||
StringBuilder combinedPath = new StringBuilder();
|
||||
foreach (string segment in paths)
|
||||
{
|
||||
if (segment != null)
|
||||
{
|
||||
string trimmedSegment = segment.TrimEnd(PathSeparator);
|
||||
if (trimmedSegment.Length > 0)
|
||||
{
|
||||
if (combinedPath.Length > 0)
|
||||
{
|
||||
combinedPath.Append(PathSeparator);
|
||||
}
|
||||
combinedPath.Append(trimmedSegment);
|
||||
}
|
||||
}
|
||||
}
|
||||
return combinedPath.ToString();
|
||||
}
|
||||
|
||||
private static ConcurrentDictionary<string, VssFileStorage> s_storages = new ConcurrentDictionary<string, VssFileStorage>(StringComparer.OrdinalIgnoreCase);
|
||||
|
||||
/// <summary>
|
||||
/// Factory method to get a VssFileStorage instance ensuring that we don't have two instances for the same file.
|
||||
/// </summary>
|
||||
/// <param name="fullPath">The full path to the storage file. Ensure that the path used is in an appropriately secure location for the data you are storing.</param>
|
||||
/// <param name="pathSeparatorForKeys">The separator to use between the path segments of the storage keys.</param>
|
||||
/// <param name="ignoreCaseInPaths">If true the dictionary will use the OrdinalIgnoreCase StringComparer to compare keys.</param>
|
||||
/// <returns></returns>
|
||||
public static IVssClientStorage GetVssLocalFileStorage(string fullPath, char pathSeparatorForKeys = c_defaultPathSeparator, bool ignoreCaseInPaths = c_defaultIgnoreCaseInPaths)
|
||||
{
|
||||
string normalizedFullPath = Path.GetFullPath(fullPath);
|
||||
VssFileStorage storage = s_storages.GetOrAdd(normalizedFullPath, (key) => new VssFileStorage(key, pathSeparatorForKeys, ignoreCaseInPaths));
|
||||
|
||||
// we need to throw on mismatch if the cache contains a conflicting instance
|
||||
if (storage.PathSeparator != pathSeparatorForKeys)
|
||||
{
|
||||
throw new ArgumentException(CommonResources.ConflictingPathSeparatorForVssFileStorage(pathSeparatorForKeys, normalizedFullPath, storage.PathSeparator));
|
||||
}
|
||||
|
||||
StringComparer pathComparer = GetAppropriateStringComparer(ignoreCaseInPaths);
|
||||
{
|
||||
if (storage.PathComparer != pathComparer)
|
||||
{
|
||||
string caseSensitive = "Ordinal";
|
||||
string caseInsensitive = "OrdinalIgnoreCase";
|
||||
string requested = ignoreCaseInPaths ? caseInsensitive : caseSensitive;
|
||||
string previous = ignoreCaseInPaths ? caseSensitive : caseInsensitive;
|
||||
throw new ArgumentException(CommonResources.ConflictingStringComparerForVssFileStorage(requested, normalizedFullPath, previous));
|
||||
}
|
||||
}
|
||||
|
||||
#if DEBUG
|
||||
Debug.Assert(fullPath.Equals(storage.m_filePath), string.Format("The same storage file is being referenced with different casing. This will cause issues when running in cross patform environments where the file system may be case sensitive. {0} != {1}", storage.m_filePath, normalizedFullPath));
|
||||
#endif
|
||||
return storage;
|
||||
}
|
||||
|
||||
private static StringComparer GetAppropriateStringComparer(bool ignoreCase)
|
||||
{
|
||||
return ignoreCase ? StringComparer.OrdinalIgnoreCase : StringComparer.Ordinal;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets an instance of a VssLocalFileStorage under the current user directory.
|
||||
/// </summary>
|
||||
/// <param name="pathSuffix">This pathSuffix will be combined at the end of the current user data directory for VSS to make a full path. Something like: "%localappdata%\GitHub\ActionsService\[pathSuffix]"</param>
|
||||
/// <param name="storeByVssVersion">Adds the current product version as a path segment. ...\GitHub\ActionsService\v[GeneratedVersionInfo.ProductVersion]\[pathSuffix]"</param>
|
||||
/// <param name="pathSeparatorForKeys">The separator to use between the path segments of the storage keys.</param>
|
||||
/// <param name="ignoreCaseInPaths">If true the dictionary will use the OrdinalIgnoreCase StringComparer to compare keys.</param>
|
||||
/// <returns></returns>
|
||||
public static IVssClientStorage GetCurrentUserVssFileStorage(string pathSuffix, bool storeByVssVersion, char pathSeparatorForKeys = c_defaultPathSeparator, bool ignoreCaseInPaths = c_defaultIgnoreCaseInPaths)
|
||||
{
|
||||
return GetVssLocalFileStorage(Path.Combine(storeByVssVersion ? ClientSettingsDirectoryByVersion : ClientSettingsDirectory, pathSuffix), pathSeparatorForKeys, ignoreCaseInPaths);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Directory containing the client settings files.
|
||||
///
|
||||
/// This will look something like this:
|
||||
/// C:\Users\[user]\AppData\Local\GitHub\ActionsService\v[GeneratedVersionInfo.ProductVersion]
|
||||
/// </summary>
|
||||
internal static string ClientSettingsDirectoryByVersion
|
||||
{
|
||||
get
|
||||
{
|
||||
// We purposely do not cache this value. This value needs to change if
|
||||
// Windows Impersonation is being used.
|
||||
return Path.Combine(ClientSettingsDirectory, "v" + GeneratedVersionInfo.ProductVersion);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Directory containing the client settings files.
|
||||
///
|
||||
/// This will look something like this:
|
||||
/// C:\Users\[user]\AppData\Local\GitHub\ActionsService
|
||||
/// </summary>
|
||||
internal static string ClientSettingsDirectory
|
||||
{
|
||||
get
|
||||
{
|
||||
// We purposely do not cache this value. This value needs to change if
|
||||
// Windows Impersonation is being used.
|
||||
|
||||
// Check to see if we can find the user's local application data directory.
|
||||
string subDir = "GitHub\\ActionsService";
|
||||
string path = Environment.GetEnvironmentVariable("localappdata");
|
||||
SafeGetFolderPath(Environment.SpecialFolder.LocalApplicationData);
|
||||
if (string.IsNullOrEmpty(path))
|
||||
{
|
||||
// If the user has never logged onto this box they will not have a local application data directory.
|
||||
// Check to see if they have a roaming network directory that moves with them.
|
||||
path = SafeGetFolderPath(Environment.SpecialFolder.ApplicationData);
|
||||
if (string.IsNullOrEmpty(path))
|
||||
{
|
||||
// The user does not have a roaming network directory either. Just place the cache in the
|
||||
// common area.
|
||||
// If we are using the common dir, we might not have access to create a folder under "GitHub"
|
||||
// so we just create a top level folder.
|
||||
path = Environment.GetFolderPath(Environment.SpecialFolder.CommonApplicationData);
|
||||
subDir = "GitHubActionsService";
|
||||
}
|
||||
}
|
||||
|
||||
Debug.Assert(path != null, "folder path cannot be null");
|
||||
return Path.Combine(path, subDir);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets folder path and returns null in case the special folder in question doesn't exist (useful when the user has never logged on, which makes
|
||||
/// GetFolderPath throw)
|
||||
/// </summary>
|
||||
/// <param name="specialFolder">Folder to retrieve</param>
|
||||
/// <returns>Path if available, null othewise</returns>
|
||||
private static string SafeGetFolderPath(Environment.SpecialFolder specialFolder)
|
||||
{
|
||||
try
|
||||
{
|
||||
return Environment.GetFolderPath(specialFolder);
|
||||
}
|
||||
catch (ArgumentException)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private class VssFileStorageReader : VssLocalFile, IVssClientStorageReader, IDisposable
|
||||
{
|
||||
private readonly string m_path;
|
||||
private Dictionary<string, JRaw> m_settings;
|
||||
|
||||
private readonly FileSystemWatcher m_watcher;
|
||||
private readonly ReaderWriterLockSlim m_lock;
|
||||
private long m_completedRefreshId;
|
||||
private long m_outstandingRefreshId;
|
||||
|
||||
public VssFileStorageReader(string fullPath, char pathSeparator, StringComparer comparer)
|
||||
: base(fullPath, pathSeparator, comparer)
|
||||
{
|
||||
m_path = fullPath;
|
||||
m_lock = new ReaderWriterLockSlim(LockRecursionPolicy.NoRecursion);
|
||||
m_completedRefreshId = 0;
|
||||
m_outstandingRefreshId = 1;
|
||||
|
||||
// Set up the file system watcher
|
||||
{
|
||||
string directoryToWatch = Path.GetDirectoryName(m_path);
|
||||
|
||||
if (!Directory.Exists(directoryToWatch))
|
||||
{
|
||||
Directory.CreateDirectory(directoryToWatch);
|
||||
}
|
||||
|
||||
m_watcher = new FileSystemWatcher(directoryToWatch, Path.GetFileName(m_path));
|
||||
m_watcher.IncludeSubdirectories = false;
|
||||
m_watcher.NotifyFilter = NotifyFilters.LastWrite | NotifyFilters.CreationTime;
|
||||
m_watcher.Changed += OnCacheFileChanged;
|
||||
m_watcher.EnableRaisingEvents = true;
|
||||
}
|
||||
}
|
||||
|
||||
public T ReadEntry<T>(string path)
|
||||
{
|
||||
return ReadEntry<T>(path, default(T));
|
||||
}
|
||||
|
||||
public T ReadEntry<T>(string path, T defaultValue)
|
||||
{
|
||||
path = NormalizePath(path);
|
||||
RefreshIfNeeded();
|
||||
|
||||
Dictionary<string, JRaw> settings = m_settings; // use a pointer to m_settings, incase m_settings gets set to a new instance during the operation
|
||||
JRaw value;
|
||||
if (settings.TryGetValue(path, out value) && value != null)
|
||||
{
|
||||
return JsonConvert.DeserializeObject<T>(value.ToString());
|
||||
}
|
||||
return defaultValue;
|
||||
}
|
||||
|
||||
public IDictionary<string, T> ReadEntries<T>(string pathPrefix)
|
||||
{
|
||||
string prefix = NormalizePath(pathPrefix, true);
|
||||
RefreshIfNeeded();
|
||||
Dictionary<string, JRaw> settings = m_settings; // use a pointer to m_settings, incase m_settings gets set to a new instance during the operation
|
||||
Dictionary<string, T> matchingEntries = new Dictionary<string, T>();
|
||||
foreach (KeyValuePair<string, JRaw> kvp in settings.Where(kvp => kvp.Key == prefix || kvp.Key.StartsWith(prefix + PathSeparator)))
|
||||
{
|
||||
try
|
||||
{
|
||||
matchingEntries[kvp.Key] = JsonConvert.DeserializeObject<T>(kvp.Value.ToString());
|
||||
}
|
||||
catch (JsonSerializationException) { }
|
||||
catch (JsonReaderException) { }
|
||||
}
|
||||
return matchingEntries;
|
||||
}
|
||||
|
||||
private void OnCacheFileChanged(object sender, FileSystemEventArgs e)
|
||||
{
|
||||
NotifyChanged();
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
m_watcher.Dispose();
|
||||
}
|
||||
|
||||
public void NotifyChanged()
|
||||
{
|
||||
using (new ReadLockScope(m_lock))
|
||||
{
|
||||
Interlocked.Increment(ref m_outstandingRefreshId);
|
||||
}
|
||||
}
|
||||
|
||||
private void RefreshIfNeeded()
|
||||
{
|
||||
long requestedRefreshId;
|
||||
|
||||
using (new ReadLockScope(m_lock))
|
||||
{
|
||||
requestedRefreshId = Interlocked.Read(ref m_outstandingRefreshId);
|
||||
|
||||
if (m_completedRefreshId >= requestedRefreshId)
|
||||
{
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
Dictionary<string, JRaw> newSettings;
|
||||
using (GetNewMutexScope())
|
||||
{
|
||||
if (m_completedRefreshId >= requestedRefreshId)
|
||||
{
|
||||
return;
|
||||
}
|
||||
newSettings = LoadFile();
|
||||
}
|
||||
|
||||
using (new ReadLockScope(m_lock))
|
||||
{
|
||||
if (m_completedRefreshId >= requestedRefreshId)
|
||||
{
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
using (new WriteLockScope(m_lock))
|
||||
{
|
||||
if (m_completedRefreshId >= requestedRefreshId)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
m_completedRefreshId = requestedRefreshId;
|
||||
m_settings = newSettings;
|
||||
}
|
||||
}
|
||||
|
||||
private struct ReadLockScope : IDisposable
|
||||
{
|
||||
public ReadLockScope(ReaderWriterLockSlim @lock)
|
||||
{
|
||||
m_lock = @lock;
|
||||
|
||||
m_lock.EnterReadLock();
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
m_lock.ExitReadLock();
|
||||
}
|
||||
|
||||
private readonly ReaderWriterLockSlim m_lock;
|
||||
}
|
||||
|
||||
private struct WriteLockScope : IDisposable
|
||||
{
|
||||
public WriteLockScope(ReaderWriterLockSlim @lock)
|
||||
{
|
||||
m_lock = @lock;
|
||||
m_lock.EnterWriteLock();
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
m_lock.ExitWriteLock();
|
||||
}
|
||||
|
||||
private readonly ReaderWriterLockSlim m_lock;
|
||||
}
|
||||
}
|
||||
|
||||
private class VssFileStorageWriter : VssLocalFile, IVssClientStorageWriter
|
||||
{
|
||||
public VssFileStorageWriter(string fullPath, char pathSeparator, StringComparer comparer)
|
||||
: base(fullPath, pathSeparator, comparer)
|
||||
{
|
||||
}
|
||||
|
||||
public void WriteEntries(IEnumerable<KeyValuePair<string, object>> entries)
|
||||
{
|
||||
if (entries.Any())
|
||||
{
|
||||
using (GetNewMutexScope())
|
||||
{
|
||||
bool changesMade = false;
|
||||
Dictionary<string, JRaw> originalSettings = LoadFile();
|
||||
Dictionary<string, JRaw> newSettings = new Dictionary<string, JRaw>(PathComparer);
|
||||
if (originalSettings.Any())
|
||||
{
|
||||
originalSettings.Copy(newSettings);
|
||||
}
|
||||
foreach (KeyValuePair<string, object> kvp in entries)
|
||||
{
|
||||
string path = NormalizePath(kvp.Key);
|
||||
if (kvp.Value != null)
|
||||
{
|
||||
JRaw jRawValue = new JRaw(JsonConvert.SerializeObject(kvp.Value));
|
||||
if (!newSettings.ContainsKey(path) || !newSettings[path].Equals(jRawValue))
|
||||
{
|
||||
newSettings[path] = jRawValue;
|
||||
changesMade = true;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
if (newSettings.Remove(path))
|
||||
{
|
||||
changesMade = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (changesMade)
|
||||
{
|
||||
SaveFile(originalSettings, newSettings);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void WriteEntry(string path, object value)
|
||||
{
|
||||
WriteEntries(new KeyValuePair<string, object>[] { new KeyValuePair<string, object>(path, value) });
|
||||
}
|
||||
}
|
||||
|
||||
private class VssLocalFile
|
||||
{
|
||||
private readonly string m_filePath;
|
||||
private readonly string m_bckUpFilePath;
|
||||
private readonly string m_emptyPathSegment;
|
||||
|
||||
public VssLocalFile(string filePath, char pathSeparator, StringComparer comparer)
|
||||
{
|
||||
m_filePath = filePath;
|
||||
PathComparer = comparer;
|
||||
PathSeparator = pathSeparator;
|
||||
m_emptyPathSegment = new string(pathSeparator, 2);
|
||||
FileInfo fileInfo = new FileInfo(m_filePath);
|
||||
m_bckUpFilePath = Path.Combine(fileInfo.Directory.FullName, "~" + fileInfo.Name);
|
||||
}
|
||||
|
||||
protected char PathSeparator { get; }
|
||||
|
||||
protected string NormalizePath(string path, bool allowRootPath = false)
|
||||
{
|
||||
if (string.IsNullOrEmpty(path) || path[0] != PathSeparator || path.IndexOf(m_emptyPathSegment, StringComparison.Ordinal) >= 0 || (!allowRootPath && path.Length == 1))
|
||||
{
|
||||
throw new ArgumentException(CommonResources.InvalidClientStoragePath(path, PathSeparator), "path");
|
||||
}
|
||||
if (path[path.Length - 1] == PathSeparator)
|
||||
{
|
||||
path = path.Substring(0, path.Length - 1);
|
||||
}
|
||||
return path;
|
||||
}
|
||||
|
||||
protected StringComparer PathComparer { get; }
|
||||
|
||||
protected Dictionary<string, JRaw> LoadFile()
|
||||
{
|
||||
Dictionary<string, JRaw> settings = null;
|
||||
if (File.Exists(m_filePath))
|
||||
{
|
||||
settings = LoadFile(m_filePath);
|
||||
}
|
||||
if ((settings == null || !settings.Any()) && File.Exists(m_bckUpFilePath))
|
||||
{
|
||||
settings = LoadFile(m_bckUpFilePath);
|
||||
}
|
||||
return settings ?? new Dictionary<string, JRaw>(PathComparer);
|
||||
}
|
||||
|
||||
private Dictionary<string, JRaw> LoadFile(string path)
|
||||
{
|
||||
Dictionary<string, JRaw> settings = new Dictionary<string, JRaw>(PathComparer);
|
||||
try
|
||||
{
|
||||
string fileContent;
|
||||
using (var fs = new FileStream(path, FileMode.Open, FileAccess.Read, FileShare.Read | FileShare.Delete))
|
||||
{
|
||||
using (var sr = new StreamReader(fs, Encoding.UTF8))
|
||||
{
|
||||
fileContent = sr.ReadToEnd();
|
||||
}
|
||||
}
|
||||
IReadOnlyDictionary<string, JRaw> loadedSettings = JsonConvert.DeserializeObject<IReadOnlyDictionary<string, JRaw>>(fileContent);
|
||||
if (loadedSettings != null)
|
||||
{
|
||||
// Replay the settings into our dictionary one by one so that our uniqueness constraint
|
||||
// isn't violated based on the StringComparer for this instance.
|
||||
foreach (KeyValuePair<string, JRaw> setting in loadedSettings)
|
||||
{
|
||||
settings[setting.Key] = setting.Value;
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (DirectoryNotFoundException) { }
|
||||
catch (FileNotFoundException) { }
|
||||
catch (JsonReaderException) { }
|
||||
catch (JsonSerializationException) { }
|
||||
catch (InvalidCastException) { }
|
||||
|
||||
return settings;
|
||||
}
|
||||
|
||||
protected void SaveFile(IDictionary<string, JRaw> originalSettings, IDictionary<string, JRaw> newSettings)
|
||||
{
|
||||
string newContent = JValue.Parse(JsonConvert.SerializeObject(newSettings)).ToString(Formatting.Indented);
|
||||
if (originalSettings.Any())
|
||||
{
|
||||
// during testing, creating this backup provided reliability in the event of aborted threads, and
|
||||
// crashed processes. With this, I was not able to simulate a case where corruption happens, but there is no
|
||||
// 100% gaurantee against corruption.
|
||||
string originalContent = JValue.Parse(JsonConvert.SerializeObject(originalSettings)).ToString(Formatting.Indented);
|
||||
SaveFile(m_bckUpFilePath, originalContent);
|
||||
}
|
||||
SaveFile(m_filePath, newContent);
|
||||
if (File.Exists(m_bckUpFilePath))
|
||||
{
|
||||
File.Delete(m_bckUpFilePath);
|
||||
}
|
||||
}
|
||||
|
||||
private void SaveFile(string path, string content)
|
||||
{
|
||||
bool success = false;
|
||||
int tries = 0;
|
||||
int retryDelayMilliseconds = 10;
|
||||
const int maxNumberOfRetries = 6;
|
||||
do
|
||||
{
|
||||
try
|
||||
{
|
||||
using (var fs = new FileStream(path, FileMode.Create, FileAccess.Write, FileShare.Delete))
|
||||
{
|
||||
using (var sw = new StreamWriter(fs, Encoding.UTF8))
|
||||
{
|
||||
sw.Write(content);
|
||||
}
|
||||
}
|
||||
success = true;
|
||||
}
|
||||
catch (IOException)
|
||||
{
|
||||
if (++tries > maxNumberOfRetries)
|
||||
{
|
||||
throw;
|
||||
}
|
||||
Task.Delay(retryDelayMilliseconds).Wait();
|
||||
retryDelayMilliseconds *= 2;
|
||||
}
|
||||
}
|
||||
while (!success);
|
||||
}
|
||||
|
||||
protected MutexScope GetNewMutexScope()
|
||||
{
|
||||
return new MutexScope(m_filePath.Replace(Path.DirectorySeparatorChar, '_'));
|
||||
}
|
||||
|
||||
protected struct MutexScope : IDisposable
|
||||
{
|
||||
public MutexScope(string name)
|
||||
{
|
||||
m_mutex = new Mutex(false, name);
|
||||
|
||||
try
|
||||
{
|
||||
if (!m_mutex.WaitOne(s_mutexTimeout))
|
||||
{
|
||||
throw new TimeoutException();
|
||||
}
|
||||
}
|
||||
catch (AbandonedMutexException)
|
||||
{
|
||||
// If this is thrown, then we hold the mutex.
|
||||
}
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
m_mutex.ReleaseMutex();
|
||||
}
|
||||
|
||||
private readonly Mutex m_mutex;
|
||||
private static readonly TimeSpan s_mutexTimeout = TimeSpan.FromSeconds(10);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
using System;
|
||||
using System.ComponentModel;
|
||||
using System.Net.Http;
|
||||
|
||||
namespace GitHub.Services.Common.Diagnostics
|
||||
{
|
||||
[EditorBrowsable(EditorBrowsableState.Never)]
|
||||
internal static class HttpRequestMessageExtensions
|
||||
{
|
||||
public static VssHttpMethod GetHttpMethod(this HttpRequestMessage message)
|
||||
{
|
||||
String methodName = message.Method.Method;
|
||||
VssHttpMethod httpMethod = VssHttpMethod.UNKNOWN;
|
||||
if (!Enum.TryParse<VssHttpMethod>(methodName, true, out httpMethod))
|
||||
{
|
||||
httpMethod = VssHttpMethod.UNKNOWN;
|
||||
}
|
||||
return httpMethod;
|
||||
}
|
||||
|
||||
public static VssTraceActivity GetActivity(this HttpRequestMessage message)
|
||||
{
|
||||
Object traceActivity;
|
||||
if (!message.Properties.TryGetValue(VssTraceActivity.PropertyName, out traceActivity))
|
||||
{
|
||||
return VssTraceActivity.Empty;
|
||||
}
|
||||
return (VssTraceActivity)traceActivity;
|
||||
}
|
||||
}
|
||||
}
|
||||
1132
src/Sdk/Common/Common/Diagnostics/VssHttpEventSource.cs
Normal file
1132
src/Sdk/Common/Common/Diagnostics/VssHttpEventSource.cs
Normal file
File diff suppressed because it is too large
Load Diff
15
src/Sdk/Common/Common/Diagnostics/VssHttpMethod.cs
Normal file
15
src/Sdk/Common/Common/Diagnostics/VssHttpMethod.cs
Normal file
@@ -0,0 +1,15 @@
|
||||
|
||||
namespace GitHub.Services.Common.Diagnostics
|
||||
{
|
||||
internal enum VssHttpMethod
|
||||
{
|
||||
UNKNOWN,
|
||||
DELETE,
|
||||
HEAD,
|
||||
GET,
|
||||
OPTIONS,
|
||||
PATCH,
|
||||
POST,
|
||||
PUT,
|
||||
}
|
||||
}
|
||||
136
src/Sdk/Common/Common/Diagnostics/VssTraceActivity.cs
Normal file
136
src/Sdk/Common/Common/Diagnostics/VssTraceActivity.cs
Normal file
@@ -0,0 +1,136 @@
|
||||
using System;
|
||||
using System.Diagnostics;
|
||||
using System.Runtime.Serialization;
|
||||
|
||||
namespace GitHub.Services.Common.Diagnostics
|
||||
{
|
||||
/// <summary>
|
||||
/// Represents a trace activity for correlating diagnostic traces together.
|
||||
/// </summary>
|
||||
[DataContract]
|
||||
[Serializable]
|
||||
public sealed class VssTraceActivity
|
||||
{
|
||||
private VssTraceActivity()
|
||||
{
|
||||
}
|
||||
|
||||
private VssTraceActivity(Guid activityId)
|
||||
{
|
||||
this.Id = activityId;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the unique identifier for the trace activity.
|
||||
/// </summary>
|
||||
[DataMember]
|
||||
public Guid Id
|
||||
{
|
||||
get;
|
||||
private set;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the current trace activity if one is set on the current thread; otherwise, null.
|
||||
/// </summary>
|
||||
public static VssTraceActivity Current
|
||||
{
|
||||
get
|
||||
{
|
||||
return null;
|
||||
}
|
||||
set { }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the empty trace activity.
|
||||
/// </summary>
|
||||
public static VssTraceActivity Empty
|
||||
{
|
||||
get
|
||||
{
|
||||
return s_empty.Value;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates a disposable trace scope in which the current trace activity is activated for trace correlation.
|
||||
/// The call context state for <see cref="VssTraceActivity.Current"/> is updated within the scope to reference
|
||||
/// the activated activity.
|
||||
/// </summary>
|
||||
/// <returns>A trace scope for correlating multiple traces together</returns>
|
||||
public IDisposable EnterCorrelationScope()
|
||||
{
|
||||
return new CorrelationScope(this);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the current activity or, if no activity is active on the current thread, creates a new activity for
|
||||
/// trace correlation.
|
||||
/// </summary>
|
||||
/// <returns>The current trace activity or a new trace activity</returns>
|
||||
public static VssTraceActivity GetOrCreate()
|
||||
{
|
||||
if (VssTraceActivity.Current != null)
|
||||
{
|
||||
return VssTraceActivity.Current;
|
||||
}
|
||||
else if (Trace.CorrelationManager.ActivityId == Guid.Empty)
|
||||
{
|
||||
return new VssTraceActivity(Guid.NewGuid());
|
||||
}
|
||||
else
|
||||
{
|
||||
return new VssTraceActivity(Trace.CorrelationManager.ActivityId);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates a new trace activity optionally using the provided identifier.
|
||||
/// </summary>
|
||||
/// <param name="activityId">The activity identifier or none to have one generated</param>
|
||||
/// <returns>A new trace activity instance</returns>
|
||||
public static VssTraceActivity New(Guid activityId = default(Guid))
|
||||
{
|
||||
return new VssTraceActivity(activityId == default(Guid) ? Guid.NewGuid() : activityId);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the property name used to cache this object on extensible objects.
|
||||
/// </summary>
|
||||
public const String PropertyName = "MS.VSS.Diagnostics.TraceActivity";
|
||||
private static Lazy<VssTraceActivity> s_empty = new Lazy<VssTraceActivity>(() => new VssTraceActivity(Guid.Empty));
|
||||
|
||||
private sealed class CorrelationScope : IDisposable
|
||||
{
|
||||
public CorrelationScope(VssTraceActivity activity)
|
||||
{
|
||||
m_previousActivity = VssTraceActivity.Current;
|
||||
if (m_previousActivity == null || m_previousActivity.Id != activity.Id)
|
||||
{
|
||||
m_swap = true;
|
||||
VssTraceActivity.Current = activity;
|
||||
}
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
if (m_swap)
|
||||
{
|
||||
try
|
||||
{
|
||||
m_swap = false;
|
||||
}
|
||||
finally
|
||||
{
|
||||
// Perform in a finally block to ensure consistency between the two variables
|
||||
VssTraceActivity.Current = m_previousActivity;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private Boolean m_swap;
|
||||
private VssTraceActivity m_previousActivity;
|
||||
}
|
||||
}
|
||||
}
|
||||
54
src/Sdk/Common/Common/ExceptionMappingAttribute.cs
Normal file
54
src/Sdk/Common/Common/ExceptionMappingAttribute.cs
Normal file
@@ -0,0 +1,54 @@
|
||||
using System;
|
||||
using System.ComponentModel;
|
||||
|
||||
namespace GitHub.Services.Common
|
||||
{
|
||||
/// <summary>
|
||||
/// Matches Exception Types to back compatible TypeName and TypeKey for the specified range
|
||||
/// of REST Api versions. This allows the current server to send back compatible typename
|
||||
/// and type key json when talking to older clients. It also allows current clients to translate
|
||||
/// exceptions returned from older servers to a current client's exception type.
|
||||
/// </summary>
|
||||
[EditorBrowsable(EditorBrowsableState.Never)]
|
||||
[AttributeUsage(AttributeTargets.Class, AllowMultiple = true, Inherited = false)]
|
||||
public class ExceptionMappingAttribute : Attribute
|
||||
{
|
||||
/// <summary>
|
||||
/// Matches Exception Types to back compatible TypeName and TypeKey for the specified range
|
||||
/// of REST Api versions. This allows the current server to send back compatible typename
|
||||
/// and type key json when talking to older clients. It also allows current clients to translate
|
||||
/// exceptions returned from older servers to a current client's exception type.
|
||||
/// </summary>
|
||||
/// <param name="minApiVersion">The inclusive minimum REST Api version for this mapping.</param>
|
||||
/// <param name="exclusiveMaxApiVersion">The exclusive maximum REST Api version for this mapping.</param>
|
||||
/// <param name="typeKey">The original typekey to be returned by the server when processing requests within the REST Api range specified.</param>
|
||||
/// <param name="typeName">The original typeName to be returned by the server when processing requests within the REST Api range specified.</param>
|
||||
public ExceptionMappingAttribute(string minApiVersion, string exclusiveMaxApiVersion, string typeKey, string typeName)
|
||||
{
|
||||
MinApiVersion = new Version(minApiVersion);
|
||||
ExclusiveMaxApiVersion = new Version(exclusiveMaxApiVersion);
|
||||
TypeKey = typeKey;
|
||||
TypeName = typeName;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The inclusive minimum REST Api version for this mapping.
|
||||
/// </summary>
|
||||
public Version MinApiVersion { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// The exclusive maximum REST Api version for this mapping.
|
||||
/// </summary>
|
||||
public Version ExclusiveMaxApiVersion { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// The original typekey to be returned by the server when processing requests within the REST Api range specified.
|
||||
/// </summary>
|
||||
public string TypeKey { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// The original typeName to be returned by the server when processing requests within the REST Api range specified.
|
||||
/// </summary>
|
||||
public string TypeName { get; private set; }
|
||||
}
|
||||
}
|
||||
58
src/Sdk/Common/Common/Exceptions/AuthenticationExceptions.cs
Normal file
58
src/Sdk/Common/Common/Exceptions/AuthenticationExceptions.cs
Normal file
@@ -0,0 +1,58 @@
|
||||
using GitHub.Services.Common.Internal;
|
||||
using System;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.Runtime.Serialization;
|
||||
|
||||
namespace GitHub.Services.Common
|
||||
{
|
||||
[Serializable]
|
||||
[SuppressMessage("Microsoft.Usage", "CA2214:DoNotCallOverridableMethodsInConstructors")]
|
||||
[ExceptionMapping("0.0", "3.0", "VssAuthenticationException", "GitHub.Services.Common.VssAuthenticationException, GitHub.Services.Common, Version=14.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a")]
|
||||
public class VssAuthenticationException : VssException
|
||||
{
|
||||
public VssAuthenticationException()
|
||||
{
|
||||
}
|
||||
|
||||
public VssAuthenticationException(String message)
|
||||
: base(message)
|
||||
{
|
||||
}
|
||||
|
||||
public VssAuthenticationException(String message, Exception innerException)
|
||||
: base(message, innerException)
|
||||
{
|
||||
}
|
||||
|
||||
protected VssAuthenticationException(SerializationInfo info, StreamingContext context)
|
||||
: base(info, context)
|
||||
{
|
||||
}
|
||||
}
|
||||
|
||||
[Serializable]
|
||||
[SuppressMessage("Microsoft.Usage", "CA2214:DoNotCallOverridableMethodsInConstructors")]
|
||||
[ExceptionMapping("0.0", "3.0", "VssUnauthorizedException", "GitHub.Services.Common.VssUnauthorizedException, GitHub.Services.Common, Version=14.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a")]
|
||||
public class VssUnauthorizedException : VssException
|
||||
{
|
||||
public VssUnauthorizedException()
|
||||
: this(CommonResources.VssUnauthorizedUnknownServer())
|
||||
{
|
||||
}
|
||||
|
||||
public VssUnauthorizedException(String message)
|
||||
: base(message)
|
||||
{
|
||||
}
|
||||
|
||||
public VssUnauthorizedException(String message, Exception innerException)
|
||||
: base(message, innerException)
|
||||
{
|
||||
}
|
||||
|
||||
protected VssUnauthorizedException(SerializationInfo info, StreamingContext context)
|
||||
: base(info, context)
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
||||
70
src/Sdk/Common/Common/Exceptions/CommonExceptions.cs
Normal file
70
src/Sdk/Common/Common/Exceptions/CommonExceptions.cs
Normal file
@@ -0,0 +1,70 @@
|
||||
using System;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.Runtime.Serialization;
|
||||
|
||||
namespace GitHub.Services.Common
|
||||
{
|
||||
/// <summary>
|
||||
/// Thrown when a config file fails to load
|
||||
/// </summary
|
||||
[Serializable]
|
||||
[SuppressMessage("Microsoft.Design", "CA1032:ImplementStandardExceptionConstructors")]
|
||||
[ExceptionMapping("0.0", "3.0", "ConfigFileException", "GitHub.Services.Common.ConfigFileException, GitHub.Services.Common, Version=14.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a")]
|
||||
public class ConfigFileException : VssException
|
||||
{
|
||||
public ConfigFileException(String message)
|
||||
: base(message)
|
||||
{
|
||||
}
|
||||
|
||||
public ConfigFileException(String message, Exception innerException)
|
||||
: base(message, innerException)
|
||||
{
|
||||
}
|
||||
|
||||
protected ConfigFileException(SerializationInfo info, StreamingContext context)
|
||||
: base(info, context)
|
||||
{
|
||||
}
|
||||
}
|
||||
|
||||
[Serializable]
|
||||
[SuppressMessage("Microsoft.Design", "CA1032:ImplementStandardExceptionConstructors")]
|
||||
[ExceptionMapping("0.0", "3.0", "VssServiceException", "GitHub.Services.Common.VssServiceException, GitHub.Services.Common, Version=14.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a")]
|
||||
public class VssServiceException : VssException
|
||||
{
|
||||
public VssServiceException()
|
||||
: base()
|
||||
{
|
||||
}
|
||||
|
||||
public VssServiceException(String message, Exception innerException)
|
||||
: base(message, innerException)
|
||||
{
|
||||
}
|
||||
|
||||
public VssServiceException(String message)
|
||||
: base(message)
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initializes an exception from serialized data
|
||||
/// </summary>
|
||||
/// <param name="info">object holding the serialized data</param>
|
||||
/// <param name="context">context info about the source or destination</param>
|
||||
protected VssServiceException(SerializationInfo info, StreamingContext context)
|
||||
: base(info, context)
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the type name and key for serialization of this exception.
|
||||
/// If not provided, the serializer will provide default values.
|
||||
/// </summary>
|
||||
public virtual void GetTypeNameAndKey(Version restApiVersion, out String typeName, out String typeKey)
|
||||
{
|
||||
GetTypeNameAndKeyForExceptionType(GetType(), restApiVersion, out typeName, out typeKey);
|
||||
}
|
||||
}
|
||||
}
|
||||
63
src/Sdk/Common/Common/Exceptions/PropertyExceptions.cs
Normal file
63
src/Sdk/Common/Common/Exceptions/PropertyExceptions.cs
Normal file
@@ -0,0 +1,63 @@
|
||||
using GitHub.Services.Common.Internal;
|
||||
using System;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.Runtime.Serialization;
|
||||
using System.Security;
|
||||
|
||||
namespace GitHub.Services.Common
|
||||
{
|
||||
/// <summary>
|
||||
/// Thrown when validating user input. Similar to ArgumentException but doesn't require the property to be an input parameter.
|
||||
/// </summary>
|
||||
[Serializable]
|
||||
[SuppressMessage("Microsoft.Design", "CA1032:ImplementStandardExceptionConstructors")]
|
||||
[ExceptionMapping("0.0", "3.0", "VssPropertyValidationException", "GitHub.Services.Common.VssPropertyValidationException, GitHub.Services.Common, Version=14.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a")]
|
||||
public class VssPropertyValidationException : VssServiceException
|
||||
{
|
||||
public VssPropertyValidationException(String propertyName, String message)
|
||||
: base(message)
|
||||
{
|
||||
PropertyName = propertyName;
|
||||
}
|
||||
|
||||
public VssPropertyValidationException(String propertyName, String message, Exception innerException)
|
||||
: base(message, innerException)
|
||||
{
|
||||
PropertyName = propertyName;
|
||||
}
|
||||
|
||||
protected VssPropertyValidationException(SerializationInfo info, StreamingContext context)
|
||||
: base(info, context)
|
||||
{
|
||||
PropertyName = info.GetString("PropertyName");
|
||||
}
|
||||
|
||||
public String PropertyName { get; set; }
|
||||
|
||||
[SecurityCritical]
|
||||
public override void GetObjectData(SerializationInfo info, StreamingContext context)
|
||||
{
|
||||
base.GetObjectData(info, context);
|
||||
info.AddValue("PropertyName", PropertyName);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// PropertyTypeNotSupportedException - this is thrown when a type is DBNull or an Object type other than a Byte array.
|
||||
/// </summary>
|
||||
[Serializable]
|
||||
[SuppressMessage("Microsoft.Design", "CA1032:ImplementStandardExceptionConstructors")]
|
||||
[ExceptionMapping("0.0", "3.0", "PropertyTypeNotSupportedException", "GitHub.Services.Common.PropertyTypeNotSupportedException, GitHub.Services.Common, Version=14.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a")]
|
||||
public class PropertyTypeNotSupportedException : VssPropertyValidationException
|
||||
{
|
||||
public PropertyTypeNotSupportedException(String propertyName, Type type)
|
||||
: base(propertyName, CommonResources.VssUnsupportedPropertyValueType(propertyName, type.FullName))
|
||||
{
|
||||
}
|
||||
|
||||
protected PropertyTypeNotSupportedException(SerializationInfo info, StreamingContext context)
|
||||
: base(info, context)
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
||||
98
src/Sdk/Common/Common/GenerateConstantAttributes.cs
Normal file
98
src/Sdk/Common/Common/GenerateConstantAttributes.cs
Normal file
@@ -0,0 +1,98 @@
|
||||
// Microsoft Confidential
|
||||
// Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
|
||||
using System;
|
||||
using System.ComponentModel;
|
||||
|
||||
namespace GitHub.Services.Common
|
||||
{
|
||||
/// <summary>
|
||||
/// Base class for constant generation. Allows types/fields to be generated
|
||||
/// with an alternate name.
|
||||
/// </summary>
|
||||
[EditorBrowsable(EditorBrowsableState.Never)]
|
||||
public abstract class GenerateConstantAttributeBase : Attribute
|
||||
{
|
||||
protected GenerateConstantAttributeBase(string alternateName = null)
|
||||
{
|
||||
AlternateName = alternateName;
|
||||
}
|
||||
|
||||
public string AlternateName { get; private set; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Can be applied to a const/readonly-static field of a class/enum/struct, but is
|
||||
/// only used when the containing type has the 'GenerateSpecificConstants' attribute applied.
|
||||
/// This allows the developer to specify exactly what constants to include out of the containing type.
|
||||
/// </summary>
|
||||
[AttributeUsage(AttributeTargets.Field)]
|
||||
[EditorBrowsable(EditorBrowsableState.Never)]
|
||||
public class GenerateConstantAttribute : GenerateConstantAttributeBase
|
||||
{
|
||||
public GenerateConstantAttribute(string alternateName = null)
|
||||
: base(alternateName)
|
||||
{
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Applied to any enum/class/struct. Causes the constants generator to create javascript constants
|
||||
/// for all const/readonly-static fields contained by the type.
|
||||
/// </summary>
|
||||
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Enum | AttributeTargets.Struct)]
|
||||
[EditorBrowsable(EditorBrowsableState.Never)]
|
||||
public class GenerateAllConstantsAttribute : GenerateConstantAttribute
|
||||
{
|
||||
public GenerateAllConstantsAttribute(string alternateName = null)
|
||||
: base(alternateName)
|
||||
{
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Applied to any enum/class/struct. Causes the constants generator to create javascript constants at runtime
|
||||
/// for the type for any member constants/enumerated values that are tagged with the 'GenerateConstant' attribute.
|
||||
/// </summary>
|
||||
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Enum | AttributeTargets.Struct)]
|
||||
[EditorBrowsable(EditorBrowsableState.Never)]
|
||||
public class GenerateSpecificConstantsAttribute : GenerateConstantAttribute
|
||||
{
|
||||
public GenerateSpecificConstantsAttribute(string alternateName = null)
|
||||
: base(alternateName)
|
||||
{
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Applied to a class that represents a data model which is serialized to javascript.
|
||||
/// This attribute controls how TypeScript interfaces are generated for the class that
|
||||
/// this is applied to.
|
||||
/// </summary>
|
||||
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Struct | AttributeTargets.Enum | AttributeTargets.Interface)]
|
||||
[EditorBrowsable(EditorBrowsableState.Never)]
|
||||
public class GenerateInterfaceAttribute : GenerateConstantAttributeBase
|
||||
{
|
||||
public GenerateInterfaceAttribute()
|
||||
: this(true)
|
||||
{
|
||||
}
|
||||
|
||||
public GenerateInterfaceAttribute(string alternateName)
|
||||
: base(alternateName)
|
||||
{
|
||||
GenerateInterface = true;
|
||||
}
|
||||
|
||||
public GenerateInterfaceAttribute(bool generateInterface)
|
||||
: base()
|
||||
{
|
||||
GenerateInterface = generateInterface;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Whether or not to generate a typescript interface for this type
|
||||
/// </summary>
|
||||
public bool GenerateInterface { get; set; }
|
||||
}
|
||||
}
|
||||
13
src/Sdk/Common/Common/IVssClientCertificateManager.cs
Normal file
13
src/Sdk/Common/Common/IVssClientCertificateManager.cs
Normal file
@@ -0,0 +1,13 @@
|
||||
using System.Security.Cryptography.X509Certificates;
|
||||
|
||||
namespace GitHub.Services.Common
|
||||
{
|
||||
/// <summary>
|
||||
/// An interface to allow custom implementations to
|
||||
/// gather client certificates when necessary.
|
||||
/// </summary>
|
||||
public interface IVssClientCertificateManager
|
||||
{
|
||||
X509Certificate2Collection ClientCertificates { get; }
|
||||
}
|
||||
}
|
||||
19
src/Sdk/Common/Common/IVssHttpRetryInfo.cs
Normal file
19
src/Sdk/Common/Common/IVssHttpRetryInfo.cs
Normal file
@@ -0,0 +1,19 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Net;
|
||||
using System.Net.Http;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace GitHub.Services.Common
|
||||
{
|
||||
public interface IVssHttpRetryInfo
|
||||
{
|
||||
void InitialAttempt(HttpRequestMessage request);
|
||||
|
||||
void Retry(TimeSpan sleep);
|
||||
|
||||
void Reset();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
using System;
|
||||
|
||||
namespace GitHub.Services.Common
|
||||
{
|
||||
public static class PerformanceTimerConstants
|
||||
{
|
||||
public const string Header = "X-VSS-PerfData";
|
||||
public const string PerfTimingKey = "PerformanceTimings";
|
||||
|
||||
[Obsolete]
|
||||
public const string Aad = "AAD"; // Previous timer, broken into Token and Graph below
|
||||
|
||||
public const string AadToken = "AadToken";
|
||||
public const string AadGraph = "AadGraph";
|
||||
public const string BlobStorage = "BlobStorage";
|
||||
public const string FinalSqlCommand = "FinalSQLCommand";
|
||||
public const string Redis = "Redis";
|
||||
public const string ServiceBus = "ServiceBus";
|
||||
public const string Sql = "SQL";
|
||||
public const string SqlReadOnly = "SQLReadOnly";
|
||||
public const string SqlRetries = "SQLRetries";
|
||||
public const string TableStorage = "TableStorage";
|
||||
public const string VssClient = "VssClient";
|
||||
public const string DocumentDB = "DocumentDB";
|
||||
}
|
||||
}
|
||||
61
src/Sdk/Common/Common/Performance/PerformanceTimingGroup.cs
Normal file
61
src/Sdk/Common/Common/Performance/PerformanceTimingGroup.cs
Normal file
@@ -0,0 +1,61 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Runtime.Serialization;
|
||||
|
||||
namespace GitHub.Services.Common
|
||||
{
|
||||
/// <summary>
|
||||
/// A set of performance timings all keyed off of the same string
|
||||
/// </summary>
|
||||
[DataContract]
|
||||
public class PerformanceTimingGroup
|
||||
{
|
||||
public PerformanceTimingGroup()
|
||||
{
|
||||
this.Timings = new List<PerformanceTimingEntry>();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Overall duration of all entries in this group in ticks
|
||||
/// </summary>
|
||||
[DataMember(EmitDefaultValue = false)]
|
||||
public long ElapsedTicks { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The total number of timing entries associated with this group
|
||||
/// </summary>
|
||||
[DataMember(EmitDefaultValue = false)]
|
||||
public int Count { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// A list of timing entries in this group. Only the first few entries in each group are collected.
|
||||
/// </summary>
|
||||
[DataMember]
|
||||
public List<PerformanceTimingEntry> Timings { get; private set; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// A single timing consisting of a duration and start time
|
||||
/// </summary>
|
||||
[DataContract]
|
||||
public struct PerformanceTimingEntry
|
||||
{
|
||||
/// <summary>
|
||||
/// Duration of the entry in ticks
|
||||
/// </summary>
|
||||
[DataMember]
|
||||
public long ElapsedTicks { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Offset from Server Request Context start time in microseconds
|
||||
/// </summary>
|
||||
[DataMember]
|
||||
public long StartOffset { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Properties to distinguish timings within the same group or to provide data to send with telemetry
|
||||
/// </summary>
|
||||
[DataMember(EmitDefaultValue = false)]
|
||||
public IDictionary<String, Object> Properties { get; set; }
|
||||
}
|
||||
}
|
||||
107
src/Sdk/Common/Common/TaskCancellationExtensions.cs
Normal file
107
src/Sdk/Common/Common/TaskCancellationExtensions.cs
Normal file
@@ -0,0 +1,107 @@
|
||||
using System;
|
||||
using System.Globalization;
|
||||
using System.Runtime.CompilerServices;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace GitHub.Services.Common
|
||||
{
|
||||
public static class TaskCancellationExtensions
|
||||
{
|
||||
private struct Void { }
|
||||
|
||||
/// <summary>
|
||||
/// Some APIs (e.g. HttpClient) don't honor cancellation tokens. This wrapper adds an extra layer of cancellation checking.
|
||||
/// </summary>
|
||||
public static Task EnforceCancellation(
|
||||
this Task task,
|
||||
CancellationToken cancellationToken,
|
||||
Func<string> makeMessage = null,
|
||||
[CallerFilePath] string file = "",
|
||||
[CallerMemberName] string member = "",
|
||||
[CallerLineNumber] int line = -1)
|
||||
{
|
||||
Func<Task<Void>> task2 = async () =>
|
||||
{
|
||||
await task.ConfigureAwait(false);
|
||||
return new Void();
|
||||
};
|
||||
|
||||
return task2().EnforceCancellation<Void>(cancellationToken, makeMessage, file, member, line);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Some APIs (e.g. HttpClient) don't honor cancellation tokens. This wrapper adds an extra layer of cancellation checking.
|
||||
/// </summary>
|
||||
public static async Task<TResult> EnforceCancellation<TResult>(
|
||||
this Task<TResult> task,
|
||||
CancellationToken cancellationToken,
|
||||
Func<string> makeMessage = null,
|
||||
[CallerFilePath] string file = "",
|
||||
[CallerMemberName] string member = "",
|
||||
[CallerLineNumber] int line = -1)
|
||||
{
|
||||
ArgumentUtility.CheckForNull(task, nameof(task));
|
||||
|
||||
// IsCompleted will return true when the task is in one of the three final states: RanToCompletion, Faulted, or Canceled.
|
||||
if (task.IsCompleted)
|
||||
{
|
||||
return await task;
|
||||
}
|
||||
|
||||
var cancellationTcs = new TaskCompletionSource<bool>(RUN_CONTINUATIONS_ASYNCHRONOUSLY);
|
||||
using (cancellationToken.Register(() => cancellationTcs.SetResult(false)))
|
||||
{
|
||||
var completedTask = await Task.WhenAny(task, cancellationTcs.Task).ConfigureAwait(false);
|
||||
if (completedTask == task)
|
||||
{
|
||||
return await task;
|
||||
}
|
||||
}
|
||||
|
||||
// Even if our actual task actually did honor the cancellation token, there's still a race that our WaitForCancellation
|
||||
// task may have handled the cancellation more quickly.
|
||||
if (!cancellationToken.IsCancellationRequested)
|
||||
{
|
||||
throw new InvalidOperationException("Task ended but cancellation token is not marked for cancellation.");
|
||||
}
|
||||
|
||||
// However, we'd ideally like to throw the cancellation exception from the original task if we can.
|
||||
// Thus, we'll give that task a few seconds to coallesce (e.g. write to a log) before we give up on it.
|
||||
int seconds = 3;
|
||||
var lastChanceTcs = new TaskCompletionSource<bool>(RUN_CONTINUATIONS_ASYNCHRONOUSLY);
|
||||
using (var lastChanceTimer = new CancellationTokenSource(TimeSpan.FromSeconds(seconds)))
|
||||
using (lastChanceTimer.Token.Register(() => lastChanceTcs.SetResult(false)))
|
||||
{
|
||||
var completedTask = await Task.WhenAny(task, lastChanceTcs.Task).ConfigureAwait(false);
|
||||
if (completedTask == task)
|
||||
{
|
||||
return await task;
|
||||
}
|
||||
}
|
||||
|
||||
// At this point, we've given up on waiting for this task.
|
||||
ObserveExceptionIfNeeded(task);
|
||||
|
||||
string errorString = $"Task in function {member} at {file}:{line} was still active {seconds} seconds after operation was cancelled.";
|
||||
if (makeMessage != null)
|
||||
{
|
||||
errorString += $" {makeMessage()}";
|
||||
}
|
||||
|
||||
throw new OperationCanceledException(errorString, cancellationToken);
|
||||
}
|
||||
|
||||
private static void ObserveExceptionIfNeeded(Task task)
|
||||
{
|
||||
task.ContinueWith(t => t.Exception, TaskContinuationOptions.OnlyOnFaulted);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// This is a flag exposed by TaskCreationOptions and TaskContinuationOptions but it's not in .Net 4.5
|
||||
/// In Azure we have latest .Net loaded which will consume this flag.
|
||||
/// Client environments using earlier .Net would ignore it.
|
||||
/// </summary>
|
||||
private const int RUN_CONTINUATIONS_ASYNCHRONOUSLY = 0x40;
|
||||
}
|
||||
}
|
||||
1248
src/Sdk/Common/Common/Utility/ArgumentUtility.cs
Normal file
1248
src/Sdk/Common/Common/Utility/ArgumentUtility.cs
Normal file
File diff suppressed because it is too large
Load Diff
148
src/Sdk/Common/Common/Utility/ArrayUtility.cs
Normal file
148
src/Sdk/Common/Common/Utility/ArrayUtility.cs
Normal file
@@ -0,0 +1,148 @@
|
||||
//*************************************************************************************************
|
||||
// ArrayUtil.cs
|
||||
//
|
||||
// A class with random array processing helper routines.
|
||||
//
|
||||
// Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
//*************************************************************************************************
|
||||
using System;
|
||||
using System.Diagnostics;
|
||||
using System.Text;
|
||||
|
||||
namespace GitHub.Services.Common
|
||||
{
|
||||
//********************************************************************************************
|
||||
/// <summary>
|
||||
/// A class with random array processing helper routines.
|
||||
/// </summary>
|
||||
//********************************************************************************************
|
||||
public static class ArrayUtility
|
||||
{
|
||||
//****************************************************************************************
|
||||
/// <summary>
|
||||
/// Compare two byte arrays to determine if they contain the same data.
|
||||
/// </summary>
|
||||
/// <param name="a1">First array to compare.</param>
|
||||
/// <param name="a2">Second array to compare.</param>
|
||||
/// <returns>true if the arrays are equal and false if not.</returns>
|
||||
//****************************************************************************************
|
||||
public unsafe static bool Equals(byte[] a1, byte[] a2)
|
||||
{
|
||||
Debug.Assert(a1 != null, "a1 was null");
|
||||
Debug.Assert(a2 != null, "a2 was null");
|
||||
|
||||
// Check if the lengths are the same.
|
||||
if (a1.Length != a2.Length)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
if (a1.Length == 0)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
return Equals(a1, a2, a1.Length);
|
||||
}
|
||||
|
||||
//****************************************************************************************
|
||||
/// <summary>
|
||||
/// Generate hash code for a byte array.
|
||||
/// </summary>
|
||||
/// <param name="array">array to generate hash code for.</param>
|
||||
/// <returns>hash generated from the array members.</returns>
|
||||
//****************************************************************************************
|
||||
public static int GetHashCode(byte[] array)
|
||||
{
|
||||
Debug.Assert(array != null, "array was null");
|
||||
|
||||
int hash = 0;
|
||||
// the C# compiler defaults to unchecked behavior, so this will
|
||||
// wrap silently. Since this is a hash code and not a count, this
|
||||
// is fine with us.
|
||||
foreach (byte item in array)
|
||||
{
|
||||
hash += item;
|
||||
}
|
||||
|
||||
return hash;
|
||||
}
|
||||
|
||||
//****************************************************************************************
|
||||
/// <summary>
|
||||
/// Compare two byte arrays to determine if they contain the same data.
|
||||
/// </summary>
|
||||
/// <param name="a1">First array to compare.</param>
|
||||
/// <param name="a2">Second array to compare.</param>
|
||||
/// <param name="length"># of bytes to compare.</param>
|
||||
/// <returns>true if the arrays are equal and false if not.</returns>
|
||||
//****************************************************************************************
|
||||
public unsafe static bool Equals(byte[] a1, byte[] a2, int length)
|
||||
{
|
||||
// Pin the arrays so that we can use unsafe pointers to compare an int at a time.
|
||||
fixed (byte* p1 = &a1[0])
|
||||
{
|
||||
fixed (byte* p2 = &a2[0])
|
||||
{
|
||||
// Get temps for the pointers because you can't change fixed pointers.
|
||||
byte* q1 = p1, q2 = p2;
|
||||
|
||||
// Compare an int at a time for as long as we can. We divide by four because an int
|
||||
// is always 32 bits in C# regardless of platform.
|
||||
int i;
|
||||
for (i = length >> 2; i > 0; --i)
|
||||
{
|
||||
if (*((int*) q1) != *((int*) q2))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
q1 += sizeof(int);
|
||||
q2 += sizeof(int);
|
||||
}
|
||||
|
||||
// Compare a byte at a time for the remaining bytes (0 - 3 of them). This also
|
||||
// depends on ints being 32 bits.
|
||||
for (i = length & 0x3; i > 0; --i)
|
||||
{
|
||||
if (*q1 != *q2)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
++q1;
|
||||
++q2;
|
||||
}
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
//****************************************************************************************
|
||||
/// <summary>
|
||||
/// Convert the byte array to a lower case hex string.
|
||||
/// </summary>
|
||||
/// <param name="bytes">byte array to be converted.</param>
|
||||
/// <returns>hex string converted from byte array.</returns>
|
||||
//****************************************************************************************
|
||||
public static String StringFromByteArray(byte[] bytes)
|
||||
{
|
||||
if (bytes == null || bytes.Length == 0)
|
||||
{
|
||||
return "null";
|
||||
}
|
||||
|
||||
StringBuilder sb = new StringBuilder(bytes.Length * 2);
|
||||
|
||||
for (int i = 0; i < bytes.Length; i++)
|
||||
{
|
||||
byte b = bytes[i];
|
||||
|
||||
char first = (char)(((b >> 4) & 0x0F) + 0x30);
|
||||
char second = (char)((b & 0x0F) + 0x30);
|
||||
|
||||
sb.Append(first >= 0x3A ? (char)(first + 0x27) : first);
|
||||
sb.Append(second >= 0x3A ? (char)(second + 0x27) : second);
|
||||
}
|
||||
|
||||
return sb.ToString();
|
||||
}
|
||||
}
|
||||
} // namespace
|
||||
38
src/Sdk/Common/Common/Utility/BackoffTimerHelper.cs
Normal file
38
src/Sdk/Common/Common/Utility/BackoffTimerHelper.cs
Normal file
@@ -0,0 +1,38 @@
|
||||
using System;
|
||||
using System.ComponentModel;
|
||||
|
||||
namespace GitHub.Services.Common
|
||||
{
|
||||
[EditorBrowsable(EditorBrowsableState.Never)]
|
||||
public static class BackoffTimerHelper
|
||||
{
|
||||
public static TimeSpan GetRandomBackoff(
|
||||
TimeSpan minBackoff,
|
||||
TimeSpan maxBackoff,
|
||||
TimeSpan? previousBackoff = null)
|
||||
{
|
||||
Random random = null;
|
||||
if (previousBackoff.HasValue)
|
||||
{
|
||||
random = new Random((Int32)previousBackoff.Value.TotalMilliseconds);
|
||||
}
|
||||
else
|
||||
{
|
||||
random = new Random();
|
||||
}
|
||||
|
||||
return TimeSpan.FromMilliseconds(random.Next((Int32)minBackoff.TotalMilliseconds, (Int32)maxBackoff.TotalMilliseconds));
|
||||
}
|
||||
|
||||
public static TimeSpan GetExponentialBackoff(
|
||||
Int32 attempt,
|
||||
TimeSpan minBackoff,
|
||||
TimeSpan maxBackoff,
|
||||
TimeSpan deltaBackoff)
|
||||
{
|
||||
Double randomBackoff = (Double)new Random().Next((Int32)(deltaBackoff.TotalMilliseconds * 0.8), (Int32)(deltaBackoff.TotalMilliseconds * 1.2));
|
||||
Double additionalBackoff = attempt < 0 ? (Math.Pow(2.0, (Double)attempt)) * randomBackoff : (Math.Pow(2.0, (Double)attempt) - 1.0) * randomBackoff;
|
||||
return TimeSpan.FromMilliseconds(Math.Min(minBackoff.TotalMilliseconds + additionalBackoff, maxBackoff.TotalMilliseconds));
|
||||
}
|
||||
}
|
||||
}
|
||||
38
src/Sdk/Common/Common/Utility/CollectionsExtensions.cs
Normal file
38
src/Sdk/Common/Common/Utility/CollectionsExtensions.cs
Normal file
@@ -0,0 +1,38 @@
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace GitHub.Services.Common
|
||||
{
|
||||
public static class CollectionsExtensions
|
||||
{
|
||||
/// <summary>
|
||||
/// Adds all of the given values to this collection.
|
||||
/// Can be used with dictionaries, which implement <see cref="ICollection{T}"/> and <see cref="IEnumerable{T}"/> where T is <see cref="KeyValuePair{TKey, TValue}"/>.
|
||||
/// For dictionaries, also see <see cref="DictionaryExtensions.SetRange{K, V, TDictionary}(TDictionary, IEnumerable{KeyValuePair{K, V}})"/>
|
||||
/// </summary>
|
||||
public static TCollection AddRange<T, TCollection>(this TCollection collection, IEnumerable<T> values)
|
||||
where TCollection : ICollection<T>
|
||||
{
|
||||
foreach (var value in values)
|
||||
{
|
||||
collection.Add(value);
|
||||
}
|
||||
|
||||
return collection;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Adds all of the given values to this collection if and only if the values object is not null.
|
||||
/// See <see cref="AddRange{T, TCollection}(TCollection, IEnumerable{T})"/> for more details.
|
||||
/// </summary>
|
||||
public static TCollection AddRangeIfRangeNotNull<T, TCollection>(this TCollection collection, IEnumerable<T> values)
|
||||
where TCollection : ICollection<T>
|
||||
{
|
||||
if (values != null)
|
||||
{
|
||||
collection.AddRange(values);
|
||||
}
|
||||
|
||||
return collection;
|
||||
}
|
||||
}
|
||||
}
|
||||
29
src/Sdk/Common/Common/Utility/ConvertUtility.cs
Normal file
29
src/Sdk/Common/Common/Utility/ConvertUtility.cs
Normal file
@@ -0,0 +1,29 @@
|
||||
using System;
|
||||
using System.ComponentModel;
|
||||
using System.Globalization;
|
||||
using System.Reflection;
|
||||
|
||||
namespace GitHub.Services.Common
|
||||
{
|
||||
/// <summary>
|
||||
/// Utility class for wrapping Convert.ChangeType to handle nullable values.
|
||||
/// </summary>
|
||||
public class ConvertUtility
|
||||
{
|
||||
public static object ChangeType(object value, Type type)
|
||||
{
|
||||
return ChangeType(value, type, CultureInfo.CurrentCulture);
|
||||
}
|
||||
|
||||
public static object ChangeType(object value, Type type, IFormatProvider provider)
|
||||
{
|
||||
if (type.IsOfType(typeof(Nullable<>)))
|
||||
{
|
||||
var nullableConverter = new NullableConverter(type);
|
||||
return nullableConverter.ConvertTo(value, nullableConverter.UnderlyingType);
|
||||
}
|
||||
|
||||
return Convert.ChangeType(value, type, provider);
|
||||
}
|
||||
}
|
||||
}
|
||||
655
src/Sdk/Common/Common/Utility/DictionaryExtensions.cs
Normal file
655
src/Sdk/Common/Common/Utility/DictionaryExtensions.cs
Normal file
@@ -0,0 +1,655 @@
|
||||
using GitHub.Services.Common.Internal;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Globalization;
|
||||
using System.Reflection;
|
||||
using System.Linq;
|
||||
|
||||
namespace GitHub.Services.Common
|
||||
{
|
||||
public static class DictionaryExtensions
|
||||
{
|
||||
/// <summary>
|
||||
/// Adds a new value to the dictionary or updates the value if the entry already exists.
|
||||
/// Returns the updated value inserted into the dictionary.
|
||||
/// </summary>
|
||||
public static V AddOrUpdate<K, V>(this IDictionary<K, V> dictionary,
|
||||
K key, V addValue, Func<V, V, V> updateValueFactory)
|
||||
{
|
||||
if (dictionary.TryGetValue(key, out V returnValue))
|
||||
{
|
||||
addValue = updateValueFactory(returnValue, addValue);
|
||||
}
|
||||
|
||||
dictionary[key] = addValue;
|
||||
return addValue;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns the value in an IDictionary at the given key, or the default
|
||||
/// value for that type if it is not present.
|
||||
/// </summary>
|
||||
public static V GetValueOrDefault<K, V>(this IDictionary<K, V> dictionary, K key, V @default = default(V))
|
||||
{
|
||||
V value;
|
||||
return dictionary.TryGetValue(key, out value) ? value : @default;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns the value in an IReadOnlyDictionary at the given key, or the default
|
||||
/// value for that type if it is not present.
|
||||
/// </summary>
|
||||
public static V GetValueOrDefault<K, V>(this IReadOnlyDictionary<K, V> dictionary, K key, V @default = default(V))
|
||||
{
|
||||
V value;
|
||||
return dictionary.TryGetValue(key, out value) ? value : @default;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns the value in a Dictionary at the given key, or the default
|
||||
/// value for that type if it is not present.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// This overload is necessary to prevent Ambiguous Match issues, as Dictionary implements both
|
||||
/// IDictionary and IReadonlyDictionary, but neither interface implements the other
|
||||
/// </remarks>
|
||||
public static V GetValueOrDefault<K, V>(this Dictionary<K, V> dictionary, K key, V @default = default(V))
|
||||
{
|
||||
V value;
|
||||
return dictionary.TryGetValue(key, out value) ? value : @default;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns the value in an IDictionary at the given key, or the default
|
||||
/// nullable value for that type if it is not present.
|
||||
/// </summary>
|
||||
public static V? GetNullableValueOrDefault<K, V>(this IDictionary<K, V> dictionary, K key, V? @default = default(V?)) where V : struct
|
||||
{
|
||||
V value;
|
||||
return dictionary.TryGetValue(key, out value) ? value : @default;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns the value in an IReadOnlyDictionary at the given key, or the default
|
||||
/// nullable value for that type if it is not present.
|
||||
/// </summary>
|
||||
public static V? GetNullableValueOrDefault<K, V>(this IReadOnlyDictionary<K, V> dictionary, K key, V? @default = default(V?)) where V : struct
|
||||
{
|
||||
V value;
|
||||
return dictionary.TryGetValue(key, out value) ? value : @default;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns the value in a Dictionary at the given key, or the default
|
||||
/// nullable value for that type if it is not present.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// This overload is necessary to prevent Ambiguous Match issues, as Dictionary implements both
|
||||
/// IDictionary and IReadonlyDictionary, but neither interface implements the other
|
||||
/// </remarks>
|
||||
public static V? GetNullableValueOrDefault<K, V>(this Dictionary<K, V> dictionary, K key, V? @default = default(V?)) where V : struct
|
||||
{
|
||||
V value;
|
||||
return dictionary.TryGetValue(key, out value) ? value : @default;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns the value in an IReadonlyDictionary with values of type <see cref="object"/>
|
||||
/// casted as values of requested type, or the defualt if the key is not found or
|
||||
/// if the value was found but not compatabile with the requested type.
|
||||
/// </summary>
|
||||
/// <typeparam name="K">The key type</typeparam>
|
||||
/// <typeparam name="V">The requested type of the stored value</typeparam>
|
||||
/// <param name="dictionary">the dictionary to perform the lookup on</param>
|
||||
/// <param name="key">The key to lookup</param>
|
||||
/// <param name="default">Optional: the default value to return if not found</param>
|
||||
/// <returns>The value at the key, or the default if it is not found or of the wrong type</returns>
|
||||
public static V GetCastedValueOrDefault<K, V>(this IReadOnlyDictionary<K, object> dictionary, K key, V @default = default(V))
|
||||
{
|
||||
object value;
|
||||
return dictionary.TryGetValue(key, out value) && value is V ? (V)value : @default;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns the value in an IDictionary with values of type <see cref="object"/>
|
||||
/// casted as values of requested type, or the defualt if the key is not found or
|
||||
/// if the value was found but not compatabile with the requested type.
|
||||
/// </summary>
|
||||
/// <typeparam name="K">The key type</typeparam>
|
||||
/// <typeparam name="V">The requested type of the stored value</typeparam>
|
||||
/// <param name="dictionary">the dictionary to perform the lookup on</param>
|
||||
/// <param name="key">The key to lookup</param>
|
||||
/// <param name="default">Optional: the default value to return if not found</param>
|
||||
/// <returns>The value at the key, or the default if it is not found or of the wrong type</returns>
|
||||
public static V GetCastedValueOrDefault<K, V>(this IDictionary<K, object> dictionary, K key, V @default = default(V))
|
||||
{
|
||||
object value;
|
||||
return dictionary.TryGetValue(key, out value) && value is V ? (V)value : @default;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns the value in a Dictionary with values of type <see cref="object"/>
|
||||
/// casted as values of requested type, or the defualt if the key is not found or
|
||||
/// if the value was found but not compatabile with the requested type.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// This overload is necessary to prevent Ambiguous Match issues, as Dictionary implements both
|
||||
/// IDictionary and IReadonlyDictionary, but neither interface implements the other
|
||||
/// </remarks>
|
||||
/// <typeparam name="K">The key type</typeparam>
|
||||
/// <typeparam name="V">The requested type of the stored value</typeparam>
|
||||
/// <param name="dictionary">the dictionary to perform the lookup on</param>
|
||||
/// <param name="key">The key to lookup</param>
|
||||
/// <param name="default">Optional: the default value to return if not found</param>
|
||||
/// <returns>The value at the key, or the default if it is not found or of the wrong type</returns>
|
||||
public static V GetCastedValueOrDefault<K, V>(this Dictionary<K, object> dictionary, K key, V @default = default(V))
|
||||
{
|
||||
return ((IReadOnlyDictionary<K, object>)dictionary).GetCastedValueOrDefault(key, @default);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns the value in an IDictionary at the given key, or creates a new value using the default constructor, adds it at the given key, and returns the new value.
|
||||
/// </summary>
|
||||
public static V GetOrAddValue<K, V>(this IDictionary<K, V> dictionary, K key) where V : new()
|
||||
{
|
||||
V value = default(V);
|
||||
|
||||
if (!dictionary.TryGetValue(key, out value))
|
||||
{
|
||||
value = new V();
|
||||
dictionary.Add(key, value);
|
||||
}
|
||||
|
||||
return value;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns the value in an IDictionary at the given key, or creates a new value using the given delegate, adds it at the given key, and returns the new value.
|
||||
/// </summary>
|
||||
public static V GetOrAddValue<K, V>(this IDictionary<K, V> dictionary, K key, Func<V> createValueToAdd)
|
||||
{
|
||||
V value = default(V);
|
||||
|
||||
if (!dictionary.TryGetValue(key, out value))
|
||||
{
|
||||
value = createValueToAdd();
|
||||
dictionary.Add(key, value);
|
||||
}
|
||||
|
||||
return value;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Adds all of the given key-value pairs (such as from another dictionary, since IDictionary implements IEnumerable<KeyValuePair>) to this dictionary.
|
||||
/// Overwrites preexisting values of the same key.
|
||||
/// To avoid overwriting values, use <see cref="CollectionsExtensions.AddRange{T, TCollection}(TCollection, IEnumerable{T})"/>.
|
||||
/// </summary>
|
||||
/// <returns>this dictionary</returns>
|
||||
public static TDictionary SetRange<K, V, TDictionary>(this TDictionary dictionary, IEnumerable<KeyValuePair<K, V>> keyValuePairs)
|
||||
where TDictionary : IDictionary<K, V>
|
||||
{
|
||||
foreach (var keyValuePair in keyValuePairs)
|
||||
{
|
||||
dictionary[keyValuePair.Key] = keyValuePair.Value;
|
||||
}
|
||||
|
||||
return dictionary;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Adds all of the given key-value pairs if and only if the key-value pairs object is not null.
|
||||
/// See <see cref="SetRange{K, V, TDictionary}(TDictionary, IEnumerable{KeyValuePair{K, V}})"/> for more details.
|
||||
/// </summary>
|
||||
/// <returns>this dictionary</returns>
|
||||
public static TDictionary SetRangeIfRangeNotNull<K, V, TDictionary>(this TDictionary dictionary, IEnumerable<KeyValuePair<K, V>> keyValuePairs)
|
||||
where TDictionary : IDictionary<K, V>
|
||||
{
|
||||
if (keyValuePairs != null)
|
||||
{
|
||||
dictionary.SetRange(keyValuePairs);
|
||||
}
|
||||
|
||||
return dictionary;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Adds all of the given key-value pairs to this lazily initialized dictionary if and only if the key-value pairs object is not null or empty.
|
||||
/// Does not initialize the dictionary otherwise.
|
||||
/// See <see cref="SetRange{K, V, TDictionary}(TDictionary, IEnumerable{KeyValuePair{K, V}})"/> for more details.
|
||||
/// </summary>
|
||||
/// <returns>this dictionary</returns>
|
||||
public static Lazy<TDictionary> SetRangeIfRangeNotNullOrEmpty<K, V, TDictionary>(this Lazy<TDictionary> lazyDictionary, IEnumerable<KeyValuePair<K, V>> keyValuePairs)
|
||||
where TDictionary : IDictionary<K, V>
|
||||
{
|
||||
if (keyValuePairs != null && keyValuePairs.Any())
|
||||
{
|
||||
lazyDictionary.Value.SetRange(keyValuePairs);
|
||||
}
|
||||
|
||||
return lazyDictionary;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Tries to add a key to the dictionary, if it does not already exist.
|
||||
/// </summary>
|
||||
/// <param name="dictionary">The <see cref="IDictionary{TKey,TValue}"/> instance where <c>TValue</c> is <c>object</c></param>
|
||||
/// <param name="key">The key to add</param>
|
||||
/// <param name="value">The value to add</param>
|
||||
/// <returns><c>true</c> if the key was added with the specified value. If the key already exists, the method returns <c>false</c> without updating the value.</returns>
|
||||
public static bool TryAdd<TKey, TValue>(this IDictionary<TKey, TValue> dictionary, TKey key, TValue value)
|
||||
{
|
||||
if (dictionary.ContainsKey(key))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
dictionary.Add(key, value);
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Tries to add all of the given key-values pairs to the dictionary, if they do not already exist.
|
||||
/// </summary>
|
||||
/// <param name="dictionary">The <see cref="IDictionary{TKey,TValue}"/> instance where <c>TValue</c> is <c>object</c></param>
|
||||
/// <param name="keyValuePairs">The values to try and add to the dictionary</param>
|
||||
/// <returns><c>true</c> if the all of the values were added. If any of the keys exists, the method returns <c>false</c> without updating the value.</returns>
|
||||
public static bool TryAddRange<TKey, TValue, TDictionary>(this TDictionary dictionary, IEnumerable<KeyValuePair<TKey, TValue>> keyValuePairs) where TDictionary : IDictionary<TKey, TValue>
|
||||
{
|
||||
bool rangeAdded = true;
|
||||
foreach (var keyValuePair in keyValuePairs)
|
||||
{
|
||||
rangeAdded &= dictionary.TryAdd(keyValuePair.Key, keyValuePair.Value);
|
||||
}
|
||||
|
||||
return rangeAdded;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the value of <typeparamref name="T"/> associated with the specified key or <c>default</c> value if
|
||||
/// either the key is not present or the value is not of type <typeparamref name="T"/>.
|
||||
/// </summary>
|
||||
/// <typeparam name="T">The type of the value associated with the specified key.</typeparam>
|
||||
/// <param name="dictionary">The <see cref="IDictionary{TKey,TValue}"/> instance where <c>TValue</c> is <c>object</c>.</param>
|
||||
/// <param name="key">The key whose value to get.</param>
|
||||
/// <param name="value">When this method returns, the value associated with the specified key, if the key is found; otherwise, the default value for the type of the value parameter.</param>
|
||||
/// <returns><c>true</c> if key was found, value is non-null, and value is of type <typeparamref name="T"/>; otherwise false.</returns>
|
||||
public static bool TryGetValue<T>(this IDictionary<string, object> dictionary, string key, out T value)
|
||||
{
|
||||
object valueObj;
|
||||
if (dictionary.TryGetValue(key, out valueObj))
|
||||
{
|
||||
//Handle Guids specially
|
||||
if (typeof(T) == typeof(Guid))
|
||||
{
|
||||
Guid guidVal;
|
||||
if (dictionary.TryGetGuid(key, out guidVal))
|
||||
{
|
||||
value = (T)(object)guidVal;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
//Handle Enums specially
|
||||
if (typeof(T).GetTypeInfo().IsEnum)
|
||||
{
|
||||
if (dictionary.TryGetEnum(key, out value))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
if (valueObj is T)
|
||||
{
|
||||
value = (T)valueObj;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
value = default(T);
|
||||
return false;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the value of T associated with the specified key if the value can be converted to T according to <see cref="PropertyValidation"/>.
|
||||
/// </summary>
|
||||
/// <typeparam name="T">the type of the value associated with the specified key</typeparam>
|
||||
/// <param name="dictionary">the dictionary from which we should retrieve the value</param>
|
||||
/// <param name="key">the key of the value to retrieve</param>
|
||||
/// <param name="value">when this method returns, the value associated with the specified key, if the key is found and the value is convertible to T,
|
||||
/// or default of T, if not</param>
|
||||
/// <returns>true if the value was retrieved successfully, otherwise false</returns>
|
||||
public static bool TryGetValidatedValue<T>(this IDictionary<string, object> dictionary, string key, out T value, bool allowNull = true)
|
||||
{
|
||||
value = default(T);
|
||||
//try to convert to T. T *must* be something with
|
||||
//TypeCode != TypeCode.object (and not DBNull) OR
|
||||
//byte[] or guid or object.
|
||||
if (!PropertyValidation.IsValidConvertibleType(typeof(T)))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
//special case guid...
|
||||
if (typeof(T) == typeof(Guid))
|
||||
{
|
||||
Guid guidVal;
|
||||
if (dictionary.TryGetGuid(key, out guidVal))
|
||||
{
|
||||
value = (T)(object)guidVal;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
object objValue = null;
|
||||
if (dictionary.TryGetValue(key, out objValue))
|
||||
{
|
||||
if (objValue == null)
|
||||
{
|
||||
//we found it and it is
|
||||
//null, which may be okay depending on the allowNull flag
|
||||
//value is already = default(T)
|
||||
return allowNull;
|
||||
}
|
||||
|
||||
if (typeof(T).GetTypeInfo().IsAssignableFrom(objValue.GetType().GetTypeInfo()))
|
||||
{
|
||||
value = (T)objValue;
|
||||
return true;
|
||||
}
|
||||
|
||||
if (typeof(T).GetTypeInfo().IsEnum)
|
||||
{
|
||||
if (dictionary.TryGetEnum(key, out value))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
if (objValue is string)
|
||||
{
|
||||
TypeCode typeCode = Type.GetTypeCode(typeof(T));
|
||||
|
||||
try
|
||||
{
|
||||
value = (T)Convert.ChangeType(objValue, typeCode, CultureInfo.CurrentCulture);
|
||||
return true;
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the Enum value associated with the specified key if the value can be converted to an Enum.
|
||||
/// </summary>
|
||||
public static bool TryGetEnum<T>(this IDictionary<string, object> dictionary, string key, out T value)
|
||||
{
|
||||
value = default(T);
|
||||
|
||||
object objValue = null;
|
||||
|
||||
if (dictionary.TryGetValue(key, out objValue))
|
||||
{
|
||||
if (objValue is string)
|
||||
{
|
||||
try
|
||||
{
|
||||
value = (T)Enum.Parse(typeof(T), (string)objValue, true);
|
||||
return true;
|
||||
}
|
||||
catch (ArgumentException)
|
||||
{
|
||||
// Provided string is not a member of enumeration
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
try
|
||||
{
|
||||
value = (T)objValue;
|
||||
return true;
|
||||
}
|
||||
catch (InvalidCastException)
|
||||
{
|
||||
// Value cannot be cast to the enum
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the Guid value associated with the specified key if the value can be converted to a Guid.
|
||||
/// </summary>
|
||||
public static bool TryGetGuid(this IDictionary<string, object> dictionary, string key, out Guid value)
|
||||
{
|
||||
value = Guid.Empty;
|
||||
|
||||
object objValue = null;
|
||||
|
||||
if (dictionary.TryGetValue(key, out objValue))
|
||||
{
|
||||
if (objValue is Guid)
|
||||
{
|
||||
value = (Guid)objValue;
|
||||
return true;
|
||||
}
|
||||
else if (objValue is string)
|
||||
{
|
||||
return Guid.TryParse((string)objValue, out value);
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Copies the values from this <see cref="IDictionary{TKey, TValue}"/> into a destination <see cref="IDictionary{TKey, TValue}"/>.
|
||||
/// </summary>
|
||||
/// <param name="source">The source dictionary from which to from.</param>
|
||||
/// <param name="dest">The destination dictionary to which to copy to.</param>
|
||||
/// <param name="filter">Optional filtering predicate.</param>
|
||||
/// <returns>The destination dictionary.</returns>
|
||||
/// <remarks>
|
||||
/// If <paramref name="dest"/> is <c>null</c>, no changes are made.
|
||||
/// </remarks>
|
||||
public static IDictionary<TKey, TValue> Copy<TKey, TValue>(this IDictionary<TKey, TValue> source, IDictionary<TKey, TValue> dest, Predicate<TKey> filter)
|
||||
{
|
||||
if (dest == null)
|
||||
{
|
||||
return dest;
|
||||
}
|
||||
|
||||
foreach (var key in source.Keys)
|
||||
{
|
||||
if (filter == null || filter(key))
|
||||
{
|
||||
dest[key] = source[key];
|
||||
}
|
||||
}
|
||||
|
||||
return dest;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Copies the values from this <see cref="IDictionary{TKey, TValue}"/> into a destination <see cref="IDictionary{TKey, TValue}"/>.
|
||||
/// </summary>
|
||||
/// <param name="source">The source dictionary from which to from.</param>
|
||||
/// <param name="dest">The destination dictionary to which to copy to.</param>
|
||||
/// <returns>The destination dictionary.</returns>
|
||||
/// <remarks>
|
||||
/// If <paramref name="dest"/> is <c>null</c>, no changes are made.
|
||||
/// </remarks>
|
||||
public static IDictionary<TKey, TValue> Copy<TKey, TValue>(this IDictionary<TKey, TValue> source, IDictionary<TKey, TValue> dest)
|
||||
{
|
||||
return source.Copy(dest, filter: null);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sets the given key-value pair if and only if the value is not null.
|
||||
/// </summary>
|
||||
public static IDictionary<TKey, TValue> SetIfNotNull<TKey, TValue>(
|
||||
this IDictionary<TKey, TValue> dictionary,
|
||||
TKey key,
|
||||
TValue value)
|
||||
where TValue : class
|
||||
{
|
||||
if (value != null)
|
||||
{
|
||||
dictionary[key] = value;
|
||||
}
|
||||
|
||||
return dictionary;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sets the given key-value pair on this lazily initialized dictionary if and only if the value is not null.
|
||||
/// Does not initialize the dictionary otherwise.
|
||||
/// </summary>
|
||||
public static Lazy<IDictionary<TKey, TValue>> SetIfNotNull<TKey, TValue>(
|
||||
this Lazy<IDictionary<TKey, TValue>> dictionary,
|
||||
TKey key,
|
||||
TValue value)
|
||||
where TValue : class
|
||||
{
|
||||
if (value != null)
|
||||
{
|
||||
dictionary.Value[key] = value;
|
||||
}
|
||||
|
||||
return dictionary;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Adds the given key-value pair to this dictionary if the value is nonnull
|
||||
/// and does not conflict with a preexisting value for the same key.
|
||||
/// No-ops if the value is null.
|
||||
/// No-ops if the preexisting value for the same key is equal to the given value.
|
||||
/// Throws <see cref="ArgumentException"/> if the preexisting value for the same key is not equal to the given value.
|
||||
/// </summary>
|
||||
public static IDictionary<TKey, TValue> SetIfNotNullAndNotConflicting<TKey, TValue>(
|
||||
this IDictionary<TKey, TValue> dictionary,
|
||||
TKey key,
|
||||
TValue value,
|
||||
string valuePropertyName = "value",
|
||||
string dictionaryName = "dictionary")
|
||||
where TValue : class
|
||||
{
|
||||
if (value == null)
|
||||
{
|
||||
return dictionary;
|
||||
}
|
||||
|
||||
dictionary.CheckForConflict(key, value, valuePropertyName, dictionaryName, ignoreDefaultValue: true);
|
||||
|
||||
dictionary[key] = value;
|
||||
|
||||
return dictionary;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Adds the given key-value pair to this dictionary if the value does not conflict with a preexisting value for the same key.
|
||||
/// No-ops if the preexisting value for the same key is equal to the given value.
|
||||
/// Throws <see cref="ArgumentException"/> if the preexisting value for the same key is not equal to the given value.
|
||||
/// </summary>
|
||||
public static IDictionary<TKey, TValue> SetIfNotConflicting<TKey, TValue>(
|
||||
this IDictionary<TKey, TValue> dictionary,
|
||||
TKey key,
|
||||
TValue value,
|
||||
string valuePropertyName = "value",
|
||||
string dictionaryName = "dictionary")
|
||||
{
|
||||
dictionary.CheckForConflict(key, value, valuePropertyName, dictionaryName, ignoreDefaultValue: false);
|
||||
|
||||
dictionary[key] = value;
|
||||
|
||||
return dictionary;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Throws <see cref="ArgumentException"/> if this IDictionary contains a preexisting value for the same key which is not equal to the given key.
|
||||
/// </summary>
|
||||
public static void CheckForConflict<TKey, TValue>(
|
||||
this IDictionary<TKey, TValue> dictionary,
|
||||
TKey key,
|
||||
TValue value,
|
||||
string valuePropertyName = "value",
|
||||
string dictionaryName = "dictionary",
|
||||
bool ignoreDefaultValue = true)
|
||||
{
|
||||
if (Equals(value, default(TValue)) && ignoreDefaultValue)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
TValue previousValue = default(TValue);
|
||||
|
||||
if (!dictionary.TryGetValue(key, out previousValue))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (Equals(previousValue, default(TValue)) && ignoreDefaultValue)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (Equals(value, previousValue))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
throw new ArgumentException(
|
||||
String.Format(CultureInfo.CurrentCulture,
|
||||
"Parameter {0} = '{1}' inconsistent with {2}['{3}'] => '{4}'",
|
||||
valuePropertyName, value, dictionaryName, key, previousValue));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Throws <see cref="ArgumentException"/> if this IReadOnlyDictionary contains a preexisting value for the same key which is not equal to the given key.
|
||||
/// </summary>
|
||||
public static void CheckForConflict<TKey, TValue>(
|
||||
this IReadOnlyDictionary<TKey, TValue> dictionary,
|
||||
TKey key,
|
||||
TValue value,
|
||||
string valuePropertyName = "value",
|
||||
string dictionaryName = "dictionary",
|
||||
bool ignoreDefaultValue = true)
|
||||
{
|
||||
if (Equals(value, default(TValue)) && ignoreDefaultValue)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
TValue previousValue = default(TValue);
|
||||
|
||||
if (!dictionary.TryGetValue(key, out previousValue))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (Equals(previousValue, default(TValue)) && ignoreDefaultValue)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (Equals(value, previousValue))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
throw new ArgumentException(
|
||||
String.Format(CultureInfo.CurrentCulture,
|
||||
"Parameter {0} = \"{1}\" is inconsistent with {2}[\"{3}\"] => \"{4}\"",
|
||||
valuePropertyName, value, dictionaryName, key, previousValue));
|
||||
}
|
||||
}
|
||||
}
|
||||
422
src/Sdk/Common/Common/Utility/EnumerableExtensions.cs
Normal file
422
src/Sdk/Common/Common/Utility/EnumerableExtensions.cs
Normal file
@@ -0,0 +1,422 @@
|
||||
using System;
|
||||
using System.Collections;
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.Specialized;
|
||||
using System.Linq;
|
||||
|
||||
namespace GitHub.Services.Common
|
||||
{
|
||||
public static class EnumerableExtensions
|
||||
{
|
||||
/// <summary>
|
||||
/// Returns an empty <see cref="IEnumerable{T}"/> if the supplied source is null.
|
||||
/// </summary>
|
||||
/// <typeparam name="T">The type of the elements of source.</typeparam>
|
||||
/// <param name="source">A sequence of values to return when not null.</param>
|
||||
/// <returns>The source sequence, or a new empty one if source was null.</returns>
|
||||
public static IEnumerable<T> AsEmptyIfNull<T>(this IEnumerable<T> source)
|
||||
=> source ?? Enumerable.Empty<T>();
|
||||
|
||||
/// <summary>
|
||||
/// If an enumerable is null, and it has a default constructor, return an empty collection by calling the
|
||||
/// default constructor.
|
||||
/// </summary>
|
||||
/// <typeparam name="TEnumerable">The type of the Enumerable</typeparam>
|
||||
/// <param name="source">A sequence of values to return when not null</param>
|
||||
/// <returns>The source sequence, or a new empty one if source was null.</returns>
|
||||
public static TEnumerable AsEmptyIfNull<TEnumerable>(this TEnumerable source) where TEnumerable : class, IEnumerable, new()
|
||||
=> source ?? new TEnumerable();
|
||||
|
||||
/// <summary>
|
||||
/// Splits a source <see cref="IEnumerable{T}"/> into several <see cref="IList{T}"/>s
|
||||
/// with a max size of batchSize.
|
||||
/// <remarks>Note that batchSize must be one or larger.</remarks>
|
||||
/// </summary>
|
||||
/// <param name="source">A sequence of values to split into smaller batches.</param>
|
||||
/// <param name="batchSize">The number of elements to place in each batch.</param>
|
||||
/// <returns>The original collection, split into batches.</returns>
|
||||
public static IEnumerable<IList<T>> Batch<T>(this IEnumerable<T> source, int batchSize)
|
||||
{
|
||||
ArgumentUtility.CheckForNull(source, nameof(source));
|
||||
ArgumentUtility.CheckBoundsInclusive(batchSize, 1, int.MaxValue, nameof(batchSize));
|
||||
|
||||
var nextBatch = new List<T>(batchSize);
|
||||
foreach (T item in source)
|
||||
{
|
||||
nextBatch.Add(item);
|
||||
if (nextBatch.Count == batchSize)
|
||||
{
|
||||
yield return nextBatch;
|
||||
nextBatch = new List<T>(batchSize);
|
||||
}
|
||||
}
|
||||
|
||||
if (nextBatch.Count > 0)
|
||||
{
|
||||
yield return nextBatch;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Splits an <see cref="IEnumerable{T}"/> into two partitions, determined by the supplied predicate. Those
|
||||
/// that follow the predicate are returned in the first, with the remaining elements in the second.
|
||||
/// </summary>
|
||||
/// <typeparam name="T">The type of the elements of source.</typeparam>
|
||||
/// <param name="source">The source enumerable to partition.</param>
|
||||
/// <param name="predicate">The predicate applied to filter the items into their partitions.</param>
|
||||
/// <returns>An object containing the matching and nonmatching results.</returns>
|
||||
public static PartitionResults<T> Partition<T>(this IEnumerable<T> source, Predicate<T> predicate)
|
||||
{
|
||||
ArgumentUtility.CheckForNull(source, nameof(source));
|
||||
ArgumentUtility.CheckForNull(predicate, nameof(predicate));
|
||||
|
||||
var results = new PartitionResults<T>();
|
||||
|
||||
foreach (var item in source)
|
||||
{
|
||||
if (predicate(item))
|
||||
{
|
||||
results.MatchingPartition.Add(item);
|
||||
}
|
||||
else
|
||||
{
|
||||
results.NonMatchingPartition.Add(item);
|
||||
}
|
||||
}
|
||||
|
||||
return results;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Partitions items from a source IEnumerable into N+1 lists, where the first N lists are determened
|
||||
/// by the sequential check of the provided predicates, with the N+1 list containing those items
|
||||
/// which matched none of the provided predicates.
|
||||
/// </summary>
|
||||
/// <typeparam name="T">The type of the elements in source.</typeparam>
|
||||
/// <param name="source">The source containing the elements to partition</param>
|
||||
/// <param name="predicates">The predicates to determine which list the results end up in</param>
|
||||
/// <returns>An item containing the matching collections and a collection containing the non-matching items.</returns>
|
||||
public static MultiPartitionResults<T> Partition<T>(this IEnumerable<T> source, params Predicate<T>[] predicates)
|
||||
{
|
||||
ArgumentUtility.CheckForNull(source, nameof(source));
|
||||
ArgumentUtility.CheckForNull(predicates, nameof(predicates));
|
||||
|
||||
var range = Enumerable.Range(0, predicates.Length).ToList();
|
||||
|
||||
var results = new MultiPartitionResults<T>();
|
||||
results.MatchingPartitions.AddRange(range.Select(_ => new List<T>()));
|
||||
|
||||
foreach (var item in source)
|
||||
{
|
||||
bool added = false;
|
||||
|
||||
foreach (var predicateIndex in range.Where(predicateIndex => predicates[predicateIndex](item)))
|
||||
{
|
||||
results.MatchingPartitions[predicateIndex].Add(item);
|
||||
added = true;
|
||||
break;
|
||||
}
|
||||
|
||||
if (!added)
|
||||
{
|
||||
results.NonMatchingPartition.Add(item);
|
||||
}
|
||||
}
|
||||
|
||||
return results;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Merges two sorted IEnumerables using the given comparison function which
|
||||
/// defines a total ordering of the data.
|
||||
/// </summary>
|
||||
public static IEnumerable<T> Merge<T>(
|
||||
this IEnumerable<T> first,
|
||||
IEnumerable<T> second,
|
||||
IComparer<T> comparer)
|
||||
{
|
||||
return Merge(first, second, comparer == null ? (Func<T, T, int>)null : comparer.Compare);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Merges two sorted IEnumerables using the given comparison function which
|
||||
/// defines a total ordering of the data.
|
||||
/// </summary>
|
||||
public static IEnumerable<T> Merge<T>(
|
||||
this IEnumerable<T> first,
|
||||
IEnumerable<T> second,
|
||||
Func<T, T, int> comparer)
|
||||
{
|
||||
ArgumentUtility.CheckForNull(first, nameof(first));
|
||||
ArgumentUtility.CheckForNull(second, nameof(second));
|
||||
ArgumentUtility.CheckForNull(comparer, nameof(comparer));
|
||||
|
||||
using (IEnumerator<T> e1 = first.GetEnumerator())
|
||||
using (IEnumerator<T> e2 = second.GetEnumerator())
|
||||
{
|
||||
bool e1Valid = e1.MoveNext();
|
||||
bool e2Valid = e2.MoveNext();
|
||||
|
||||
while (e1Valid && e2Valid)
|
||||
{
|
||||
if (comparer(e1.Current, e2.Current) <= 0)
|
||||
{
|
||||
yield return e1.Current;
|
||||
|
||||
e1Valid = e1.MoveNext();
|
||||
}
|
||||
else
|
||||
{
|
||||
yield return e2.Current;
|
||||
|
||||
e2Valid = e2.MoveNext();
|
||||
}
|
||||
}
|
||||
|
||||
while (e1Valid)
|
||||
{
|
||||
yield return e1.Current;
|
||||
|
||||
e1Valid = e1.MoveNext();
|
||||
}
|
||||
|
||||
while (e2Valid)
|
||||
{
|
||||
yield return e2.Current;
|
||||
|
||||
e2Valid = e2.MoveNext();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Merges two sorted IEnumerables using the given comparison function which defines a total ordering of the data. Unlike Merge, this method requires that
|
||||
/// both IEnumerables contain distinct elements. Likewise, the returned IEnumerable will only contain distinct elements. If the same element appears in both inputs,
|
||||
/// it will appear only once in the output.
|
||||
///
|
||||
/// Example:
|
||||
/// first: [1, 3, 5]
|
||||
/// second: [4, 5, 7]
|
||||
/// result: [1, 3, 4, 5, 7]
|
||||
/// </summary>
|
||||
public static IEnumerable<T> MergeDistinct<T>(
|
||||
this IEnumerable<T> first,
|
||||
IEnumerable<T> second,
|
||||
IComparer<T> comparer)
|
||||
{
|
||||
return MergeDistinct(first, second, comparer == null ? (Func<T, T, int>)null : comparer.Compare);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Merges two sorted IEnumerables using the given comparison function which defines a total ordering of the data. Unlike Merge, this method requires that
|
||||
/// both IEnumerables contain distinct elements. Likewise, the returned IEnumerable will only contain distinct elements. If the same element appears in both inputs,
|
||||
/// it will appear only once in the output.
|
||||
///
|
||||
/// Example:
|
||||
/// first: [1, 3, 5]
|
||||
/// second: [4, 5, 7]
|
||||
/// result: [1, 3, 4, 5, 7]
|
||||
/// </summary>
|
||||
public static IEnumerable<T> MergeDistinct<T>(
|
||||
this IEnumerable<T> first,
|
||||
IEnumerable<T> second,
|
||||
Func<T, T, int> comparer)
|
||||
{
|
||||
ArgumentUtility.CheckForNull(first, nameof(first));
|
||||
ArgumentUtility.CheckForNull(second, nameof(second));
|
||||
ArgumentUtility.CheckForNull(comparer, nameof(comparer));
|
||||
|
||||
using (IEnumerator<T> e1 = first.GetEnumerator())
|
||||
using (IEnumerator<T> e2 = second.GetEnumerator())
|
||||
{
|
||||
bool e1Valid = e1.MoveNext();
|
||||
bool e2Valid = e2.MoveNext();
|
||||
|
||||
while (e1Valid && e2Valid)
|
||||
{
|
||||
if (comparer(e1.Current, e2.Current) < 0)
|
||||
{
|
||||
yield return e1.Current;
|
||||
|
||||
e1Valid = e1.MoveNext();
|
||||
}
|
||||
else if (comparer(e1.Current, e2.Current) > 0)
|
||||
{
|
||||
yield return e2.Current;
|
||||
|
||||
e2Valid = e2.MoveNext();
|
||||
}
|
||||
else
|
||||
{
|
||||
yield return e1.Current;
|
||||
|
||||
e1Valid = e1.MoveNext();
|
||||
e2Valid = e2.MoveNext();
|
||||
}
|
||||
}
|
||||
|
||||
while (e1Valid)
|
||||
{
|
||||
yield return e1.Current;
|
||||
|
||||
e1Valid = e1.MoveNext();
|
||||
}
|
||||
|
||||
while (e2Valid)
|
||||
{
|
||||
yield return e2.Current;
|
||||
|
||||
e2Valid = e2.MoveNext();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates a HashSet based on the elements in <paramref name="source"/>.
|
||||
/// </summary>
|
||||
public static HashSet<T> ToHashSet<T>(
|
||||
IEnumerable<T> source)
|
||||
{
|
||||
return new HashSet<T>(source);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates a HashSet with equality comparer <paramref name="comparer"/> based on the elements
|
||||
/// in <paramref name="source"/>.
|
||||
/// </summary>
|
||||
public static HashSet<T> ToHashSet<T>(
|
||||
IEnumerable<T> source,
|
||||
IEqualityComparer<T> comparer)
|
||||
{
|
||||
return new HashSet<T>(source, comparer);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates a HashSet based on the elements in <paramref name="source"/>, using transformation
|
||||
/// function <paramref name="selector"/>.
|
||||
/// </summary>
|
||||
public static HashSet<TOut> ToHashSet<TIn, TOut>(
|
||||
this IEnumerable<TIn> source,
|
||||
Func<TIn, TOut> selector)
|
||||
{
|
||||
return new HashSet<TOut>(source.Select(selector));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates a HashSet with equality comparer <paramref name="comparer"/> based on the elements
|
||||
/// in <paramref name="source"/>, using transformation function <paramref name="selector"/>.
|
||||
/// </summary>
|
||||
public static HashSet<TOut> ToHashSet<TIn, TOut>(
|
||||
this IEnumerable<TIn> source,
|
||||
Func<TIn, TOut> selector,
|
||||
IEqualityComparer<TOut> comparer)
|
||||
{
|
||||
return new HashSet<TOut>(source.Select(selector), comparer);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Executes the specified action to each of the items in the collection
|
||||
/// <typeparam name="T">The type of the elements in the collection.</typeparam>
|
||||
/// <param name="collection">The collection on which the action will be performed</param>
|
||||
/// <param name="action">The action to be performed</param>
|
||||
/// </summary>
|
||||
public static void ForEach<T>(this IEnumerable<T> collection, Action<T> action)
|
||||
{
|
||||
ArgumentUtility.CheckForNull(action, nameof(action));
|
||||
ArgumentUtility.CheckForNull(collection, nameof(collection));
|
||||
|
||||
foreach (T item in collection)
|
||||
{
|
||||
action(item);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Add the item to the List if the condition is satisfied
|
||||
/// </summary>
|
||||
/// <typeparam name="T">The type of the elements in the collection.</typeparam>
|
||||
/// <param name="list">The collection on which the action will be performed</param>
|
||||
/// <param name="condition">The Condition under which the item will be added</param>
|
||||
/// <param name="element">The element to be added</param>
|
||||
public static void AddIf<T>(this List<T> list, bool condition, T element)
|
||||
{
|
||||
if (condition)
|
||||
{
|
||||
list.Add(element);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Converts a collection of key-value string pairs to a NameValueCollection.
|
||||
/// </summary>
|
||||
/// <param name="pairs">The key-value string pairs.</param>
|
||||
/// <returns>The NameValueCollection.</returns>
|
||||
public static NameValueCollection ToNameValueCollection(this IEnumerable<KeyValuePair<string, string>> pairs)
|
||||
{
|
||||
NameValueCollection collection = new NameValueCollection();
|
||||
|
||||
foreach (KeyValuePair<string, string> pair in pairs)
|
||||
{
|
||||
collection.Add(pair.Key, pair.Value);
|
||||
}
|
||||
|
||||
return collection;
|
||||
}
|
||||
|
||||
public static IList<P> PartitionSolveAndMergeBack<T, P>(this IList<T> source, Predicate<T> predicate, Func<IList<T>, IList<P>> matchingPartitionSolver, Func<IList<T>, IList<P>> nonMatchingPartitionSolver)
|
||||
{
|
||||
ArgumentUtility.CheckForNull(source, nameof(source));
|
||||
ArgumentUtility.CheckForNull(predicate, nameof(predicate));
|
||||
ArgumentUtility.CheckForNull(matchingPartitionSolver, nameof(matchingPartitionSolver));
|
||||
ArgumentUtility.CheckForNull(nonMatchingPartitionSolver, nameof(nonMatchingPartitionSolver));
|
||||
|
||||
var partitionedSource = new PartitionResults<Tuple<int, T>>();
|
||||
|
||||
for (int sourceCnt = 0; sourceCnt < source.Count; sourceCnt++)
|
||||
{
|
||||
var item = source[sourceCnt];
|
||||
|
||||
if (predicate(item))
|
||||
{
|
||||
partitionedSource.MatchingPartition.Add(new Tuple<int, T>(sourceCnt, item));
|
||||
}
|
||||
else
|
||||
{
|
||||
partitionedSource.NonMatchingPartition.Add(new Tuple<int, T>(sourceCnt, item));
|
||||
}
|
||||
}
|
||||
|
||||
var solvedResult = new List<P>(source.Count);
|
||||
if (partitionedSource.MatchingPartition.Any())
|
||||
{
|
||||
solvedResult.AddRange(matchingPartitionSolver(partitionedSource.MatchingPartition.Select(x => x.Item2).ToList()));
|
||||
}
|
||||
|
||||
if (partitionedSource.NonMatchingPartition.Any())
|
||||
{
|
||||
solvedResult.AddRange(nonMatchingPartitionSolver(partitionedSource.NonMatchingPartition.Select(x => x.Item2).ToList()));
|
||||
}
|
||||
|
||||
var result = Enumerable.Repeat(default(P), source.Count).ToList();
|
||||
|
||||
if (solvedResult.Count != source.Count)
|
||||
{
|
||||
return solvedResult; // either we can throw here or just return solvedResult and ignore!
|
||||
}
|
||||
|
||||
for (int resultCnt = 0; resultCnt < source.Count; resultCnt++)
|
||||
{
|
||||
if (resultCnt < partitionedSource.MatchingPartition.Count)
|
||||
{
|
||||
result[partitionedSource.MatchingPartition[resultCnt].Item1] = solvedResult[resultCnt];
|
||||
}
|
||||
else
|
||||
{
|
||||
result[partitionedSource.NonMatchingPartition[resultCnt - partitionedSource.MatchingPartition.Count].Item1] = solvedResult[resultCnt];
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
}
|
||||
}
|
||||
62
src/Sdk/Common/Common/Utility/ExpectedExceptionExtensions.cs
Normal file
62
src/Sdk/Common/Common/Utility/ExpectedExceptionExtensions.cs
Normal file
@@ -0,0 +1,62 @@
|
||||
using System;
|
||||
|
||||
namespace GitHub.Services.Common
|
||||
{
|
||||
public static class ExpectedExceptionExtensions
|
||||
{
|
||||
private const string c_expectedKey = "isExpected";
|
||||
|
||||
/// <summary>
|
||||
/// Mark the exception as expected when caused by user input in the provided area.
|
||||
/// If the exception thrower is the same area as the caller, the exception will be treated as expected.
|
||||
/// However, in the case of a service to service call, then the exception will be treated as unexpected.
|
||||
/// ex: GitRefsController throws ArgumentException called directly by a user then the exception will be expected
|
||||
/// GitRefsController throws ArgumentException called by BuildDefinitionController then the exception will not be expected.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// This allows for the use case "throw new ArgumentException().Expected(c_area)"
|
||||
/// This will overwrite the expected area if called a second time.
|
||||
/// This should not throw any exceptions as to avoid hiding the exception that was already caught.
|
||||
/// See https://vsowiki.com/index.php?title=Whitelisting_Expected_Commands_and_Exceptions
|
||||
/// </remarks>
|
||||
/// <param name="area">The area name where the exception is expected. This will be compared against IVssRequestContext.ServiceName. Area should be non-empty</param>
|
||||
/// <returns><paramref name="ex"/> after setting the area</returns>
|
||||
public static Exception Expected(this Exception ex, string area)
|
||||
{
|
||||
if (!string.IsNullOrEmpty(area))
|
||||
{
|
||||
ex.Data[c_expectedKey] = area;
|
||||
}
|
||||
|
||||
return ex;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Use this to "expect" an exception within the exception filtering syntax.
|
||||
/// ex:
|
||||
/// catch(ArgumentException ex) when (ex.ExpectedExceptionFilter(c_area))
|
||||
/// See <seealso cref="Expected(Exception, string)"/>
|
||||
/// </summary>
|
||||
/// <returns>false always</returns>
|
||||
public static bool ExpectedExceptionFilter(this Exception ex, string area)
|
||||
{
|
||||
ex.Expected(area);
|
||||
return false;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Determine if the exception is expected in the specified area.
|
||||
/// Case is ignored for the area comparison.
|
||||
/// </summary>
|
||||
public static bool IsExpected(this Exception ex, string area)
|
||||
{
|
||||
if (string.IsNullOrEmpty(area))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
// An exception's Data property is an IDictionary, which returns null for keys that do not exist.
|
||||
return area.Equals(ex.Data[c_expectedKey] as string, StringComparison.OrdinalIgnoreCase);
|
||||
}
|
||||
}
|
||||
}
|
||||
35
src/Sdk/Common/Common/Utility/HttpHeaders.cs
Normal file
35
src/Sdk/Common/Common/Utility/HttpHeaders.cs
Normal file
@@ -0,0 +1,35 @@
|
||||
using System;
|
||||
using System.ComponentModel;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
|
||||
namespace GitHub.Services.Common.Internal
|
||||
{
|
||||
[EditorBrowsable(EditorBrowsableState.Never)]
|
||||
public static class HttpHeaders
|
||||
{
|
||||
public const String ActivityId = "ActivityId";
|
||||
public const String TfsServiceError = "X-TFS-ServiceError";
|
||||
public const String TfsSessionHeader = "X-TFS-Session";
|
||||
public const String TfsFedAuthRealm = "X-TFS-FedAuthRealm";
|
||||
public const String TfsFedAuthIssuer = "X-TFS-FedAuthIssuer";
|
||||
public const String TfsFedAuthRedirect = "X-TFS-FedAuthRedirect";
|
||||
public const String VssE2EID = "X-VSS-E2EID";
|
||||
|
||||
public const String VssUserData = "X-VSS-UserData";
|
||||
public const String VssAgentHeader = "X-VSS-Agent";
|
||||
public const String VssAuthenticateError = "X-VSS-AuthenticateError";
|
||||
|
||||
public const String VssRateLimitDelay = "X-RateLimit-Delay";
|
||||
public const String VssRateLimitReset = "X-RateLimit-Reset";
|
||||
|
||||
public const String VssHostOfflineError = "X-VSS-HostOfflineError";
|
||||
|
||||
public const string VssRequestPriority = "X-VSS-RequestPriority";
|
||||
|
||||
public const string Authorization = "Authorization";
|
||||
public const string ProxyAuthenticate = "Proxy-Authenticate";
|
||||
public const string WwwAuthenticate = "WWW-Authenticate";
|
||||
|
||||
public const string AfdResponseRef = "X-MSEdge-Ref";
|
||||
}
|
||||
}
|
||||
26
src/Sdk/Common/Common/Utility/PartitioningResults.cs
Normal file
26
src/Sdk/Common/Common/Utility/PartitioningResults.cs
Normal file
@@ -0,0 +1,26 @@
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace GitHub.Services.Common
|
||||
{
|
||||
/// <summary>
|
||||
/// Contains results from two-way variant of EnuemrableExtensions.Partition()
|
||||
/// </summary>
|
||||
/// <typeparam name="T">The type of the elements in the contained lists.</typeparam>
|
||||
public sealed class PartitionResults<T>
|
||||
{
|
||||
public List<T> MatchingPartition { get; } = new List<T>();
|
||||
|
||||
public List<T> NonMatchingPartition { get; } = new List<T>();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Contains results from multi-partitioning variant of EnuemrableExtensions.Partition()
|
||||
/// </summary>
|
||||
/// <typeparam name="T">The type of the elements in the contained lists.</typeparam>
|
||||
public sealed class MultiPartitionResults<T>
|
||||
{
|
||||
public List<List<T>> MatchingPartitions { get; } = new List<List<T>>();
|
||||
|
||||
public List<T> NonMatchingPartition { get; } = new List<T>();
|
||||
}
|
||||
}
|
||||
40
src/Sdk/Common/Common/Utility/PathUtility.cs
Normal file
40
src/Sdk/Common/Common/Utility/PathUtility.cs
Normal file
@@ -0,0 +1,40 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.ComponentModel;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace GitHub.Services.Common
|
||||
{
|
||||
[EditorBrowsable(EditorBrowsableState.Never)]
|
||||
internal static class PathUtility
|
||||
{
|
||||
/// <summary>
|
||||
/// Replacement for Path.Combine.
|
||||
/// For URL please use UrlUtility.CombineUrl
|
||||
/// </summary>
|
||||
/// <param name="path1">The first half of the path.</param>
|
||||
/// <param name="path2">The second half of the path.</param>
|
||||
/// <returns>The concatenated string with and leading slashes or
|
||||
/// tildes removed from the second string.</returns>
|
||||
public static String Combine(String path1, String path2)
|
||||
{
|
||||
if (String.IsNullOrEmpty(path1))
|
||||
{
|
||||
return path2;
|
||||
}
|
||||
|
||||
if (String.IsNullOrEmpty(path2))
|
||||
{
|
||||
return path1;
|
||||
}
|
||||
|
||||
Char separator = path1.Contains("/") ? '/' : '\\';
|
||||
|
||||
Char[] trimChars = new Char[] { '\\', '/' };
|
||||
|
||||
return path1.TrimEnd(trimChars) + separator.ToString() + path2.TrimStart(trimChars);
|
||||
}
|
||||
}
|
||||
}
|
||||
89
src/Sdk/Common/Common/Utility/PrimitiveExtensions.cs
Normal file
89
src/Sdk/Common/Common/Utility/PrimitiveExtensions.cs
Normal file
@@ -0,0 +1,89 @@
|
||||
using GitHub.Services.Common.Internal;
|
||||
using System;
|
||||
using System.Text;
|
||||
|
||||
namespace GitHub.Services.Common
|
||||
{
|
||||
public static class PrimitiveExtensions
|
||||
{
|
||||
public static readonly DateTime UnixEpoch = new DateTime(1970, 1, 1, 0, 0, 0, 0, DateTimeKind.Utc);
|
||||
private static readonly long maxSecondsSinceUnixEpoch = (long)DateTime.MaxValue.Subtract(UnixEpoch).TotalSeconds;
|
||||
|
||||
//extension methods to convert to and from a Unix Epoch time to a DateTime
|
||||
public static Int64 ToUnixEpochTime(this DateTime dateTime)
|
||||
{
|
||||
return Convert.ToInt64((dateTime.ToUniversalTime() - UnixEpoch).TotalSeconds);
|
||||
}
|
||||
|
||||
public static DateTime FromUnixEpochTime(this Int64 unixTime)
|
||||
{
|
||||
if (unixTime >= maxSecondsSinceUnixEpoch)
|
||||
{
|
||||
return DateTime.MaxValue;
|
||||
}
|
||||
else
|
||||
{
|
||||
return UnixEpoch + TimeSpan.FromSeconds(unixTime);
|
||||
}
|
||||
}
|
||||
|
||||
public static string ToBase64StringNoPaddingFromString(string utf8String)
|
||||
{
|
||||
return ToBase64StringNoPadding(Encoding.UTF8.GetBytes(utf8String));
|
||||
}
|
||||
|
||||
public static string FromBase64StringNoPaddingToString(string base64String)
|
||||
{
|
||||
byte[] result = FromBase64StringNoPadding(base64String);
|
||||
|
||||
if (result == null || result.Length == 0)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
return Encoding.UTF8.GetString(result, 0, result.Length);
|
||||
}
|
||||
|
||||
//These methods convert To and From base64 strings without padding
|
||||
//for JWT scenarios
|
||||
//code taken from the JWS spec here:
|
||||
//http://tools.ietf.org/html/draft-ietf-jose-json-web-signature-08#appendix-C
|
||||
public static String ToBase64StringNoPadding(this byte[] bytes)
|
||||
{
|
||||
ArgumentUtility.CheckEnumerableForNullOrEmpty(bytes, "bytes");
|
||||
|
||||
string s = Convert.ToBase64String(bytes); // Regular base64 encoder
|
||||
s = s.Split('=')[0]; // Remove any trailing '='s
|
||||
s = s.Replace('+', '-'); // 62nd char of encoding
|
||||
s = s.Replace('/', '_'); // 63rd char of encoding
|
||||
return s;
|
||||
}
|
||||
|
||||
public static byte[] FromBase64StringNoPadding(this String base64String)
|
||||
{
|
||||
ArgumentUtility.CheckStringForNullOrEmpty(base64String, "base64String");
|
||||
|
||||
string s = base64String;
|
||||
s = s.Replace('-', '+'); // 62nd char of encoding
|
||||
s = s.Replace('_', '/'); // 63rd char of encoding
|
||||
switch (s.Length % 4) // Pad with trailing '='s
|
||||
{
|
||||
case 0: break; // No pad chars in this case
|
||||
case 2: s += "=="; break; // Two pad chars
|
||||
case 3: s += "="; break; // One pad char
|
||||
default:
|
||||
throw new ArgumentException(CommonResources.IllegalBase64String(), "base64String");
|
||||
}
|
||||
return Convert.FromBase64String(s); // Standard base64 decoder
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Converts base64 represented value into hex string representation.
|
||||
/// </summary>
|
||||
public static String ConvertToHex(String base64String)
|
||||
{
|
||||
var bytes = FromBase64StringNoPadding(base64String);
|
||||
return BitConverter.ToString(bytes).Replace("-", String.Empty);
|
||||
}
|
||||
}
|
||||
}
|
||||
361
src/Sdk/Common/Common/Utility/PropertyValidation.cs
Normal file
361
src/Sdk/Common/Common/Utility/PropertyValidation.cs
Normal file
@@ -0,0 +1,361 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.ComponentModel;
|
||||
using System.Globalization;
|
||||
using System.Reflection;
|
||||
|
||||
namespace GitHub.Services.Common.Internal
|
||||
{
|
||||
[EditorBrowsable(EditorBrowsableState.Never)]
|
||||
public class PropertyValidation
|
||||
{
|
||||
public static void ValidateDictionary(IDictionary<String, Object> source)
|
||||
{
|
||||
ArgumentUtility.CheckForNull(source, "source");
|
||||
|
||||
foreach (var entry in source)
|
||||
{
|
||||
ValidatePropertyName(entry.Key);
|
||||
ValidatePropertyValue(entry.Key, entry.Value);
|
||||
}
|
||||
}
|
||||
|
||||
public static Boolean IsValidConvertibleType(Type type)
|
||||
{
|
||||
return type != null && (type.GetTypeInfo().IsEnum ||
|
||||
type == typeof(Object) ||
|
||||
type == typeof(Byte[]) ||
|
||||
type == typeof(Guid) ||
|
||||
type == typeof(Boolean) ||
|
||||
type == typeof(Char) ||
|
||||
type == typeof(SByte) ||
|
||||
type == typeof(Byte) ||
|
||||
type == typeof(Int16) ||
|
||||
type == typeof(UInt16) ||
|
||||
type == typeof(Int32) ||
|
||||
type == typeof(UInt32) ||
|
||||
type == typeof(Int64) ||
|
||||
type == typeof(UInt64) ||
|
||||
type == typeof(Single) ||
|
||||
type == typeof(Double) ||
|
||||
type == typeof(Decimal) ||
|
||||
type == typeof(DateTime) ||
|
||||
type == typeof(String)
|
||||
);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Used for deserialization checks. Makes sure that
|
||||
/// the type string presented is in the inclusion list
|
||||
/// of valid types for the property service
|
||||
/// </summary>
|
||||
/// <param name="type"></param>
|
||||
/// <returns></returns>
|
||||
public static Boolean IsValidTypeString(String type)
|
||||
{
|
||||
return s_validPropertyTypeStrings.ContainsKey(type);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Used for deserialization checks. Looks up the
|
||||
/// type string presented in the inclusion list
|
||||
/// of valid types for the property service and returns the Type object
|
||||
/// </summary>
|
||||
/// <param name="type"></param>
|
||||
/// <param name="result">Resulting type that maps to the type string</param>
|
||||
/// <returns></returns>
|
||||
public static Boolean TryGetValidType(String type, out Type result)
|
||||
{
|
||||
return s_validPropertyTypeStrings.TryGetValue(type, out result);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Make sure the property name conforms to the requirements for a
|
||||
/// property name.
|
||||
/// </summary>
|
||||
/// <param name="propertyName"></param>
|
||||
public static void ValidatePropertyName(String propertyName)
|
||||
{
|
||||
ValidatePropertyString(propertyName, c_maxPropertyNameLengthInChars, "propertyName");
|
||||
|
||||
// Key must not start or end in whitespace. ValidatePropertyString() checks for null and empty strings,
|
||||
// which is why indexing on length without re-checking String.IsNullOrEmpty() is ok.
|
||||
if (Char.IsWhiteSpace(propertyName[0]) || Char.IsWhiteSpace(propertyName[propertyName.Length - 1]))
|
||||
{
|
||||
throw new VssPropertyValidationException(propertyName, CommonResources.InvalidPropertyName(propertyName));
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Make sure the property value is within the supported range of values
|
||||
/// for the type of the property specified.
|
||||
/// </summary>
|
||||
/// <param name="propertyName"></param>
|
||||
/// <param name="value"></param>
|
||||
public static void ValidatePropertyValue(String propertyName, Object value)
|
||||
{
|
||||
// Keep this consistent with XmlPropertyWriter.Write.
|
||||
if (null != value)
|
||||
{
|
||||
Type type = value.GetType();
|
||||
TypeCode typeCode = Type.GetTypeCode(type);
|
||||
|
||||
if (type.IsEnum)
|
||||
{
|
||||
ValidateStringValue(propertyName, ((Enum)value).ToString("D"));
|
||||
}
|
||||
else if (typeCode == TypeCode.Object && value is byte[])
|
||||
{
|
||||
ValidateByteArray(propertyName, (byte[])value);
|
||||
}
|
||||
else if (typeCode == TypeCode.Object && value is Guid)
|
||||
{
|
||||
//treat Guid like the other valid primitive types that
|
||||
//don't have explicit columns, e.g. it gets stored as a string
|
||||
ValidateStringValue(propertyName, ((Guid)value).ToString("N"));
|
||||
}
|
||||
else if (typeCode == TypeCode.Object)
|
||||
{
|
||||
throw new PropertyTypeNotSupportedException(propertyName, type);
|
||||
}
|
||||
else if (typeCode == TypeCode.DBNull)
|
||||
{
|
||||
throw new PropertyTypeNotSupportedException(propertyName, type);
|
||||
}
|
||||
else if (typeCode == TypeCode.Empty)
|
||||
{
|
||||
// should be impossible with null check above, but just in case.
|
||||
throw new PropertyTypeNotSupportedException(propertyName, type);
|
||||
}
|
||||
else if (typeCode == TypeCode.Int32)
|
||||
{
|
||||
ValidateInt32(propertyName, (int)value);
|
||||
}
|
||||
else if (typeCode == TypeCode.Double)
|
||||
{
|
||||
ValidateDouble(propertyName, (double)value);
|
||||
}
|
||||
else if (typeCode == TypeCode.DateTime)
|
||||
{
|
||||
ValidateDateTime(propertyName, (DateTime)value);
|
||||
}
|
||||
else if (typeCode == TypeCode.String)
|
||||
{
|
||||
ValidateStringValue(propertyName, (String)value);
|
||||
}
|
||||
else
|
||||
{
|
||||
// Here are the remaining types. All are supported over in DbArtifactPropertyValueColumns.
|
||||
// With a property definition they'll be strongly-typed when they're read back.
|
||||
// Otherwise they read back as strings.
|
||||
// Boolean
|
||||
// Char
|
||||
// SByte
|
||||
// Byte
|
||||
// Int16
|
||||
// UInt16
|
||||
// UInt32
|
||||
// Int64
|
||||
// UInt64
|
||||
// Single
|
||||
// Decimal
|
||||
ValidateStringValue(propertyName, value.ToString());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static void ValidateStringValue(String propertyName, String propertyValue)
|
||||
{
|
||||
if (propertyValue.Length > c_maxStringValueLength)
|
||||
{
|
||||
throw new VssPropertyValidationException("value", CommonResources.InvalidPropertyValueSize(propertyName, typeof(String).FullName, c_maxStringValueLength));
|
||||
}
|
||||
ArgumentUtility.CheckStringForInvalidCharacters(propertyValue, "value", true);
|
||||
}
|
||||
|
||||
private static void ValidateByteArray(String propertyName, Byte[] propertyValue)
|
||||
{
|
||||
if (propertyValue.Length > c_maxByteValueSize)
|
||||
{
|
||||
throw new VssPropertyValidationException("value", CommonResources.InvalidPropertyValueSize(propertyName, typeof(Byte[]).FullName, c_maxByteValueSize));
|
||||
}
|
||||
}
|
||||
|
||||
private static void ValidateDateTime(String propertyName, DateTime propertyValue)
|
||||
{
|
||||
// Let users get an out of range error for MinValue and MaxValue, not a DateTimeKind unspecified error.
|
||||
if (propertyValue != DateTime.MinValue
|
||||
&& propertyValue != DateTime.MaxValue)
|
||||
{
|
||||
if (propertyValue.Kind == DateTimeKind.Unspecified)
|
||||
{
|
||||
throw new VssPropertyValidationException("value", CommonResources.DateTimeKindMustBeSpecified());
|
||||
}
|
||||
|
||||
// Make sure the property value is in Universal time.
|
||||
if (propertyValue.Kind != DateTimeKind.Utc)
|
||||
{
|
||||
propertyValue = propertyValue.ToUniversalTime();
|
||||
}
|
||||
}
|
||||
|
||||
CheckRange(propertyValue, s_minAllowedDateTime, s_maxAllowedDateTime, propertyName, "value");
|
||||
}
|
||||
|
||||
private static void ValidateDouble(String propertyName, Double propertyValue)
|
||||
{
|
||||
if (Double.IsInfinity(propertyValue) || Double.IsNaN(propertyValue))
|
||||
{
|
||||
throw new VssPropertyValidationException("value", CommonResources.DoubleValueOutOfRange(propertyName, propertyValue));
|
||||
}
|
||||
|
||||
// SQL Server support: - 1.79E+308 to -2.23E-308, 0 and 2.23E-308 to 1.79E+308
|
||||
if (propertyValue < s_minNegative ||
|
||||
(propertyValue < 0 && propertyValue > s_maxNegative) ||
|
||||
propertyValue > s_maxPositive ||
|
||||
(propertyValue > 0 && propertyValue < s_minPositive))
|
||||
{
|
||||
throw new VssPropertyValidationException("value", CommonResources.DoubleValueOutOfRange(propertyName, propertyValue));
|
||||
}
|
||||
}
|
||||
|
||||
private static void ValidateInt32(String propertyName, Int32 propertyValue)
|
||||
{
|
||||
// All values allowed.
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Validation helper for validating all property strings.
|
||||
/// </summary>
|
||||
/// <param name="propertyString"></param>
|
||||
/// <param name="maxSize"></param>
|
||||
/// <param name="argumentName"></param>
|
||||
private static void ValidatePropertyString(String propertyString, Int32 maxSize, String argumentName)
|
||||
{
|
||||
ArgumentUtility.CheckStringForNullOrEmpty(propertyString, argumentName);
|
||||
if (propertyString.Length > maxSize)
|
||||
{
|
||||
throw new VssPropertyValidationException(argumentName, CommonResources.PropertyArgumentExceededMaximumSizeAllowed(argumentName, maxSize));
|
||||
}
|
||||
ArgumentUtility.CheckStringForInvalidCharacters(propertyString, argumentName, true);
|
||||
}
|
||||
|
||||
public static void CheckPropertyLength(String propertyValue, Boolean allowNull, Int32 minLength, Int32 maxLength, String propertyName, Type containerType, String topLevelParamName)
|
||||
{
|
||||
Boolean valueIsInvalid = false;
|
||||
|
||||
if (propertyValue == null)
|
||||
{
|
||||
if (!allowNull)
|
||||
{
|
||||
valueIsInvalid = true;
|
||||
}
|
||||
}
|
||||
else if ((propertyValue.Length < minLength) || (propertyValue.Length > maxLength))
|
||||
{
|
||||
valueIsInvalid = true;
|
||||
}
|
||||
|
||||
// throw exception if the value is invalid.
|
||||
if (valueIsInvalid)
|
||||
{
|
||||
// If the propertyValue is null, just print it like an empty string.
|
||||
if (propertyValue == null)
|
||||
{
|
||||
propertyValue = String.Empty;
|
||||
}
|
||||
|
||||
if (allowNull)
|
||||
{
|
||||
// paramName comes second for ArgumentException.
|
||||
throw new ArgumentException(CommonResources.InvalidStringPropertyValueNullAllowed(propertyValue, propertyName, containerType.Name, minLength, maxLength), topLevelParamName);
|
||||
}
|
||||
else
|
||||
{
|
||||
throw new ArgumentException(CommonResources.InvalidStringPropertyValueNullForbidden(propertyValue, propertyName, containerType.Name, minLength, maxLength), topLevelParamName);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Verify that a propery is within the bounds of the specified range.
|
||||
/// </summary>
|
||||
/// <param name="propertyValue">The property value</param>
|
||||
/// <param name="minValue">The minimum value allowed</param>
|
||||
/// <param name="maxValue">The maximum value allowed</param>
|
||||
/// <param name="propertyName">The name of the property</param>
|
||||
/// <param name="containerType">The container of the property</param>
|
||||
/// <param name="topLevelParamName">The top level parameter name</param>
|
||||
public static void CheckRange<T>(T propertyValue, T minValue, T maxValue, String propertyName, Type containerType, String topLevelParamName)
|
||||
where T : IComparable<T>
|
||||
{
|
||||
if (propertyValue.CompareTo(minValue) < 0 || propertyValue.CompareTo(maxValue) > 0)
|
||||
{
|
||||
// paramName comes first for ArgumentOutOfRangeException.
|
||||
throw new ArgumentOutOfRangeException(topLevelParamName, CommonResources.ValueTypeOutOfRange(propertyValue, propertyName, containerType.Name, minValue, maxValue));
|
||||
}
|
||||
}
|
||||
|
||||
private static void CheckRange<T>(T propertyValue, T minValue, T maxValue, String propertyName, String topLevelParamName)
|
||||
where T : IComparable<T>
|
||||
{
|
||||
if (propertyValue.CompareTo(minValue) < 0 || propertyValue.CompareTo(maxValue) > 0)
|
||||
{
|
||||
// paramName comes first for ArgumentOutOfRangeException.
|
||||
throw new ArgumentOutOfRangeException(topLevelParamName, CommonResources.VssPropertyValueOutOfRange(propertyName, propertyValue, minValue, maxValue));
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Make sure the property filter conforms to the requirements for a
|
||||
/// property filter.
|
||||
/// </summary>
|
||||
/// <param name="propertyNameFilter"></param>
|
||||
public static void ValidatePropertyFilter(String propertyNameFilter)
|
||||
{
|
||||
PropertyValidation.ValidatePropertyString(propertyNameFilter, c_maxPropertyNameLengthInChars, "propertyNameFilter");
|
||||
}
|
||||
|
||||
// Limits on the sizes of property values
|
||||
private const Int32 c_maxPropertyNameLengthInChars = 400;
|
||||
private const Int32 c_maxByteValueSize = 8 * 1024 * 1024;
|
||||
private const Int32 c_maxStringValueLength = 4 * 1024 * 1024;
|
||||
|
||||
// Minium date time allowed for a property value.
|
||||
private static readonly DateTime s_minAllowedDateTime = new DateTime(1753, 1, 1, 0, 0, 0, DateTimeKind.Utc);
|
||||
|
||||
// Maximum date time allowed for a property value.
|
||||
// We can't preserve DateTime.MaxValue faithfully because SQL's cut-off is 3 milliseconds lower. Also to handle UTC to Local shifts, we give ourselves a buffer of one day.
|
||||
private static readonly DateTime s_maxAllowedDateTime = DateTime.SpecifyKind(DateTime.MaxValue, DateTimeKind.Utc).AddDays(-1);
|
||||
|
||||
private static Double s_minNegative = Double.Parse("-1.79E+308", CultureInfo.InvariantCulture);
|
||||
private static Double s_maxNegative = Double.Parse("-2.23E-308", CultureInfo.InvariantCulture);
|
||||
private static Double s_minPositive = Double.Parse("2.23E-308", CultureInfo.InvariantCulture);
|
||||
private static Double s_maxPositive = Double.Parse("1.79E+308", CultureInfo.InvariantCulture);
|
||||
|
||||
private static readonly Dictionary<String, Type> s_validPropertyTypeStrings = new Dictionary<String, Type>(StringComparer.OrdinalIgnoreCase)
|
||||
{
|
||||
//primitive types:
|
||||
//(NO DBNull or Empty)
|
||||
{ "System.Boolean", typeof(Boolean) },
|
||||
{ "System.Byte", typeof(Byte) },
|
||||
{ "System.Char", typeof(Char) },
|
||||
{ "System.DateTime", typeof(DateTime) },
|
||||
{ "System.Decimal", typeof(Decimal) },
|
||||
{ "System.Double", typeof(Double) },
|
||||
{ "System.Int16", typeof(Int16) },
|
||||
{ "System.Int32", typeof(Int32) },
|
||||
{ "System.Int64", typeof(Int64) },
|
||||
{ "System.SByte", typeof(SByte) },
|
||||
{ "System.Single", typeof(Single) },
|
||||
{ "System.String", typeof(String) },
|
||||
{ "System.UInt16", typeof(UInt16) },
|
||||
{ "System.UInt32", typeof(UInt32) },
|
||||
{ "System.UInt64", typeof(UInt64) },
|
||||
|
||||
//other valid types
|
||||
{ "System.Byte[]", typeof(Byte[]) },
|
||||
{ "System.Guid", typeof(Guid) }
|
||||
};
|
||||
}
|
||||
}
|
||||
253
src/Sdk/Common/Common/Utility/SecretUtility.cs
Normal file
253
src/Sdk/Common/Common/Utility/SecretUtility.cs
Normal file
@@ -0,0 +1,253 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.ComponentModel;
|
||||
using System.Diagnostics;
|
||||
using System.Globalization;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace GitHub.Services.Common
|
||||
{
|
||||
/// <summary>
|
||||
/// Utility for masking common secret patterns
|
||||
/// </summary>
|
||||
[EditorBrowsable(EditorBrowsableState.Never)]
|
||||
public static class SecretUtility
|
||||
{
|
||||
/// <summary>
|
||||
/// The string to use to replace secrets when throwing exceptions, logging
|
||||
/// or otherwise risking exposure
|
||||
/// </summary>
|
||||
internal const string PasswordMask = "******";
|
||||
|
||||
/// <summary>
|
||||
/// The string used to mask newer secrets
|
||||
/// </summary>
|
||||
internal const string SecretMask = "<secret removed>";
|
||||
|
||||
//We use a different mask per token, to help track down suspicious mask sequences in error
|
||||
// strings that shouldn't obviously be masked
|
||||
// Internal for testing, please don't reuse
|
||||
internal const string PasswordRemovedMask = "**password-removed**";
|
||||
internal const string PwdRemovedMask = "**pwd-removed**";
|
||||
internal const string PasswordSpaceRemovedMask = "**password-space-removed**";
|
||||
internal const string PwdSpaceRemovedMask = "**pwd-space-removed**";
|
||||
internal const string AccountKeyRemovedMask = "**account-key-removed**";
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Whether this string contains an unmasked secret
|
||||
/// </summary>
|
||||
/// <param name="message">The message to check</param>
|
||||
/// <returns>True if a secret this class supports was found</returns>
|
||||
/// <remarks>This implementation is as least as expensive as a ScrubSecrets call</remarks>
|
||||
public static bool ContainsUnmaskedSecret(string message)
|
||||
{
|
||||
return !String.Equals(message, ScrubSecrets(message, false), StringComparison.Ordinal);
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Whether this string contains an unmasked secret
|
||||
/// </summary>
|
||||
/// <param name="message">The message to check</param>
|
||||
/// <param name="onlyJwtsFound">True if a secret was found and only jwts were found</param>
|
||||
/// <returns>True if a message this class supports was found</returns>
|
||||
/// <remarks>This method is a temporary workaround and should be removed in M136
|
||||
/// This implementation is as least as expensive as a ScrubSecrets call</remarks>
|
||||
public static bool ContainsUnmaskedSecret(string message, out bool onlyJwtsFound)
|
||||
{
|
||||
if (string.IsNullOrEmpty(message))
|
||||
{
|
||||
onlyJwtsFound = false;
|
||||
return false;
|
||||
}
|
||||
|
||||
string scrubbedMessage = ScrubJwts(message, assertOnDetection: false);
|
||||
bool jwtsFound = !String.Equals(message, scrubbedMessage, StringComparison.Ordinal);
|
||||
scrubbedMessage = ScrubTraditionalSecrets(message, assertOnDetection: false);
|
||||
bool secretsFound = !String.Equals(message, scrubbedMessage, StringComparison.Ordinal);
|
||||
onlyJwtsFound = !secretsFound && jwtsFound;
|
||||
return secretsFound || jwtsFound;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Scrub a message for any secrets(passwords, tokens) in known formats
|
||||
/// This method is called to scrub exception messages and traces to prevent any secrets
|
||||
/// from being leaked.
|
||||
/// </summary>
|
||||
/// <param name="message">The message to verify for secret data.</param>
|
||||
/// <param name="assertOnDetection">When true, if a message contains a
|
||||
/// secret in a known format the method will debug assert. Default = true.</param>
|
||||
/// <returns>The message with any detected secrets masked</returns>
|
||||
/// <remarks>This only does best effort pattern matching for a set of known patterns</remarks>
|
||||
public static string ScrubSecrets(string message, bool assertOnDetection = true)
|
||||
{
|
||||
if (string.IsNullOrEmpty(message))
|
||||
{
|
||||
return message;
|
||||
}
|
||||
|
||||
message = ScrubTraditionalSecrets(message, assertOnDetection);
|
||||
message = ScrubJwts(message, assertOnDetection);
|
||||
return message;
|
||||
}
|
||||
|
||||
private static string ScrubTraditionalSecrets(string message, bool assertOnDetection)
|
||||
{
|
||||
message = ScrubSecret(message, c_passwordToken, PasswordRemovedMask, assertOnDetection);
|
||||
message = ScrubSecret(message, c_pwdToken, PwdRemovedMask, assertOnDetection);
|
||||
message = ScrubSecret(message, c_passwordTokenSpaced, PasswordSpaceRemovedMask, assertOnDetection);
|
||||
message = ScrubSecret(message, c_pwdTokenSpaced, PwdSpaceRemovedMask, assertOnDetection);
|
||||
message = ScrubSecret(message, c_accountKeyToken, AccountKeyRemovedMask, assertOnDetection);
|
||||
|
||||
message = ScrubSecret(message, c_authBearerToken, SecretMask, assertOnDetection);
|
||||
return message;
|
||||
}
|
||||
|
||||
private static string ScrubJwts(string message, bool assertOnDetection)
|
||||
{
|
||||
//JWTs are sensitive and we need to scrub them, so this is a best effort attempt to
|
||||
// scrub them based on typical patterns we see
|
||||
message = ScrubSecret(message, c_jwtTypToken, SecretMask, assertOnDetection,
|
||||
maskToken: true);
|
||||
message = ScrubSecret(message, c_jwtAlgToken, SecretMask, assertOnDetection,
|
||||
maskToken: true);
|
||||
message = ScrubSecret(message, c_jwtX5tToken, SecretMask, assertOnDetection,
|
||||
maskToken: true);
|
||||
message = ScrubSecret(message, c_jwtKidToken, SecretMask, assertOnDetection,
|
||||
maskToken: true);
|
||||
return message;
|
||||
}
|
||||
|
||||
private static string ScrubSecret(string message, string token, string mask, bool assertOnDetection, bool maskToken=false)
|
||||
{
|
||||
int startIndex = -1;
|
||||
|
||||
do
|
||||
{
|
||||
startIndex = message.IndexOf(token, (startIndex < 0) ? 0 : startIndex, StringComparison.OrdinalIgnoreCase);
|
||||
if (startIndex < 0)
|
||||
{
|
||||
// Common case, there is not a password.
|
||||
break;
|
||||
}
|
||||
|
||||
//Explicitly check for original password mask so code that uses the orignal doesn't assert
|
||||
if (!maskToken && (
|
||||
message.IndexOf(token + mask, StringComparison.OrdinalIgnoreCase) == startIndex
|
||||
|| (message.IndexOf(token + PasswordMask, StringComparison.OrdinalIgnoreCase) == startIndex)))
|
||||
{
|
||||
// The password is already masked, move past this string.
|
||||
startIndex += token.Length + mask.Length;
|
||||
continue;
|
||||
}
|
||||
|
||||
// At this point we detected a password that is not masked, remove it!
|
||||
try
|
||||
{
|
||||
if (!maskToken)
|
||||
{
|
||||
startIndex += token.Length;
|
||||
}
|
||||
// Find the end of the password.
|
||||
int endIndex = message.Length - 1;
|
||||
|
||||
if (message[startIndex] == '"' || message[startIndex] == '\'')
|
||||
{
|
||||
// The password is wrapped in quotes. The end of the string will be the next unpaired quote.
|
||||
// Unless the message itself wrapped the connection string in quotes, in which case we may mask out the rest of the message. Better to be safe than leak the connection string.
|
||||
// Intentionally going to "i < message.Length - 1". If the quote isn't the second to last character, it is the last character, and we delete to the end of the string anyway.
|
||||
for (int i = startIndex + 1; i < message.Length - 1; i++)
|
||||
{
|
||||
if (message[startIndex] == message[i])
|
||||
{
|
||||
if (message[startIndex] == message[i + 1])
|
||||
{
|
||||
// we found a pair of quotes. Skip over the pair and continue.
|
||||
i++;
|
||||
continue;
|
||||
}
|
||||
else
|
||||
{
|
||||
// this is a single quote, and the end of the password.
|
||||
endIndex = i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
// The password is not wrapped in quotes.
|
||||
// The end is any whitespace, semi-colon, single, or double quote character.
|
||||
for (int i = startIndex + 1; i < message.Length; i++)
|
||||
{
|
||||
if (Char.IsWhiteSpace(message[i]) || ((IList<Char>)s_validPasswordEnding).Contains(message[i]))
|
||||
{
|
||||
endIndex = i - 1;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
message = message.Substring(0, startIndex) + mask + message.Substring(endIndex + 1);
|
||||
|
||||
// Bug 94478: We need to scrub the message before Assert, otherwise we will fall into
|
||||
// a recursive assert where the TeamFoundationServerException contains same message
|
||||
if (assertOnDetection)
|
||||
{
|
||||
Debug.Assert(false, String.Format(CultureInfo.InvariantCulture, "Message contains an unmasked secret. Message: {0}", message));
|
||||
}
|
||||
|
||||
// Trace raw that we have scrubbed a message.
|
||||
//FUTURE: We need a work item to add Tracing to the VSS Client assembly.
|
||||
//TraceLevel traceLevel = assertOnDetection ? TraceLevel.Error : TraceLevel.Info;
|
||||
//TeamFoundationTracingService.TraceRaw(99230, traceLevel, s_area, s_layer, "An unmasked password was detected in a message. MESSAGE: {0}. STACK TRACE: {1}", message, Environment.StackTrace);
|
||||
}
|
||||
catch (Exception /*exception*/)
|
||||
{
|
||||
// With an exception here the message may still contain an unmasked password.
|
||||
// We also do not want to interupt the current thread with this exception, because it may be constucting a message
|
||||
// for a different exception. Trace this exception and continue on using a generic exception message.
|
||||
//TeamFoundationTracingService.TraceExceptionRaw(99231, s_area, s_layer, exception);
|
||||
}
|
||||
finally
|
||||
{
|
||||
// Iterate to the next password (if it exists)
|
||||
startIndex += mask.Length;
|
||||
}
|
||||
} while (startIndex < message.Length);
|
||||
|
||||
return message;
|
||||
}
|
||||
|
||||
private const string c_passwordToken = "Password=";
|
||||
private const string c_passwordTokenSpaced = "-Password ";
|
||||
private const string c_pwdToken = "Pwd=";
|
||||
private const string c_pwdTokenSpaced = "-Pwd ";
|
||||
private const string c_accountKeyToken = "AccountKey=";
|
||||
private const string c_authBearerToken = "Bearer ";
|
||||
/// <remarks>
|
||||
/// {"typ":" // eyJ0eXAiOi
|
||||
/// </remarks>
|
||||
private const string c_jwtTypToken = "eyJ0eXAiOi";
|
||||
/// <remarks>
|
||||
/// {"alg":" // eyJhbGciOi
|
||||
/// </remarks>
|
||||
private const string c_jwtAlgToken = "eyJhbGciOi";
|
||||
/// <remarks>
|
||||
/// {"x5t":" // eyJ4NXQiOi
|
||||
/// </remarks>
|
||||
private const string c_jwtX5tToken = "eyJ4NXQiOi";
|
||||
/// <remarks>
|
||||
/// {"kid":" // eyJraWQiOi
|
||||
/// </remarks>
|
||||
private const string c_jwtKidToken = "eyJraWQiOi";
|
||||
|
||||
|
||||
|
||||
private static readonly char[] s_validPasswordEnding = new char[] { ';', '\'', '"' };
|
||||
}
|
||||
}
|
||||
49
src/Sdk/Common/Common/Utility/SecureCompare.cs
Normal file
49
src/Sdk/Common/Common/Utility/SecureCompare.cs
Normal file
@@ -0,0 +1,49 @@
|
||||
using System;
|
||||
using System.ComponentModel;
|
||||
|
||||
namespace GitHub.Services.Common
|
||||
{
|
||||
public static class SecureCompare
|
||||
{
|
||||
/// <summary>
|
||||
/// Compare two byte arrays for byte-by-byte equality.
|
||||
/// If both arrays are the same length, the running time of this routine will not vary with the number of equal bytes between the two.
|
||||
/// </summary>
|
||||
/// <param name="lhs">A byte array (non-null)</param>
|
||||
/// <param name="rhs">A byte array (non-null)</param>
|
||||
/// <remarks>
|
||||
/// Checking secret values using built-in equality operators is insecure.
|
||||
/// Operations like `==` on strings will stop the comparison when the first unmatched character is encountered.
|
||||
/// When checking secret values from an untrusted source that we use for authentication, we must be careful
|
||||
/// not to stop the comparison early for incorrect values.
|
||||
/// If we do, an attacker can send a large volume of requests and use statistical methods to infer the secret value byte-by-byte.
|
||||
///
|
||||
/// This method is intended to be used with arrays of the same length -- for example, two hashes from the same SHA algorithm.
|
||||
/// Comparing strings of unequal length can leak length information to an attacker.
|
||||
/// </remarks>
|
||||
public static bool TimeInvariantEquals(byte[] lhs, byte[] rhs)
|
||||
{
|
||||
if (lhs.Length != rhs.Length)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
// Must use bitwise operations
|
||||
// Conditional branching or short-circuiting Boolean operators would change the running time depending on the result
|
||||
int result = 0;
|
||||
for (int i = 0; i < lhs.Length; i++)
|
||||
{
|
||||
result |= lhs[i] ^ rhs[i];
|
||||
}
|
||||
|
||||
return result == 0;
|
||||
}
|
||||
|
||||
// Hide the `Equals` method inherited from `object`
|
||||
[EditorBrowsable(EditorBrowsableState.Never)]
|
||||
public new static bool Equals(object lhs, object rhs)
|
||||
{
|
||||
throw new NotImplementedException($"This is not the secure equals method! Use `{nameof(SecureCompare.TimeInvariantEquals)}` instead.");
|
||||
}
|
||||
}
|
||||
}
|
||||
188
src/Sdk/Common/Common/Utility/StreamParser.cs
Normal file
188
src/Sdk/Common/Common/Utility/StreamParser.cs
Normal file
@@ -0,0 +1,188 @@
|
||||
using System;
|
||||
using System.ComponentModel;
|
||||
using System.IO;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace GitHub.Services.Common
|
||||
{
|
||||
/// <summary>
|
||||
/// Simple helper class used to break up a stream into smaller streams
|
||||
/// </summary>
|
||||
[EditorBrowsable(EditorBrowsableState.Never)]
|
||||
public class StreamParser
|
||||
{
|
||||
public StreamParser(Stream fileStream, int chunkSize)
|
||||
{
|
||||
m_stream = fileStream;
|
||||
m_chunkSize = chunkSize;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns total length of file.
|
||||
/// </summary>
|
||||
public long Length
|
||||
{
|
||||
get
|
||||
{
|
||||
return m_stream.Length;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// returns the next substream
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
public SubStream GetNextStream()
|
||||
{
|
||||
return new SubStream(m_stream, m_chunkSize);
|
||||
}
|
||||
|
||||
Stream m_stream;
|
||||
int m_chunkSize;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Streams a subsection of a larger stream
|
||||
/// </summary>
|
||||
[EditorBrowsable(EditorBrowsableState.Never)]
|
||||
public class SubStream : Stream
|
||||
{
|
||||
public SubStream(Stream stream, int maxStreamSize)
|
||||
{
|
||||
m_startingPosition = stream.Position;
|
||||
long remainingStream = stream.Length - m_startingPosition;
|
||||
m_length = Math.Min(maxStreamSize, remainingStream);
|
||||
m_stream = stream;
|
||||
}
|
||||
|
||||
public override bool CanRead
|
||||
{
|
||||
get
|
||||
{
|
||||
return m_stream.CanRead && m_stream.Position <= this.EndingPostionOnOuterStream;
|
||||
}
|
||||
}
|
||||
|
||||
public override bool CanSeek
|
||||
{
|
||||
get { return m_stream.CanSeek; }
|
||||
}
|
||||
|
||||
public override bool CanWrite
|
||||
{
|
||||
get { return false; }
|
||||
}
|
||||
|
||||
public override void Flush()
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
|
||||
public override long Length
|
||||
{
|
||||
get
|
||||
{
|
||||
return m_length;
|
||||
}
|
||||
}
|
||||
|
||||
public override long Position
|
||||
{
|
||||
get
|
||||
{
|
||||
return m_stream.Position - m_startingPosition;
|
||||
}
|
||||
set
|
||||
{
|
||||
if (value >= m_length)
|
||||
{
|
||||
throw new EndOfStreamException();
|
||||
}
|
||||
|
||||
m_stream.Position = m_startingPosition + value;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Postion in larger stream where this substream starts
|
||||
/// </summary>
|
||||
public long StartingPostionOnOuterStream
|
||||
{
|
||||
get
|
||||
{
|
||||
return m_startingPosition;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Postion in larger stream where this substream ends
|
||||
/// </summary>
|
||||
public long EndingPostionOnOuterStream
|
||||
{
|
||||
get
|
||||
{
|
||||
return m_startingPosition + m_length - 1;
|
||||
}
|
||||
}
|
||||
|
||||
public override Task<int> ReadAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken)
|
||||
{
|
||||
// check that the read is only in our substream
|
||||
count = EnsureLessThanOrEqualToRemainingBytes(count);
|
||||
|
||||
return m_stream.ReadAsync(buffer, offset, count, cancellationToken);
|
||||
}
|
||||
|
||||
public override int Read(byte[] buffer, int offset, int count)
|
||||
{
|
||||
// check that the read is only in our substream
|
||||
count = EnsureLessThanOrEqualToRemainingBytes(count);
|
||||
|
||||
return m_stream.Read(buffer, offset, count);
|
||||
}
|
||||
|
||||
public override long Seek(long offset, SeekOrigin origin)
|
||||
{
|
||||
if (origin == SeekOrigin.Begin && 0 <= offset && offset < m_length)
|
||||
{
|
||||
return m_stream.Seek(offset + m_startingPosition, origin);
|
||||
}
|
||||
else if (origin == SeekOrigin.End && 0 >= offset && offset > -m_length)
|
||||
{
|
||||
return m_stream.Seek(offset - ((m_stream.Length-1) - this.EndingPostionOnOuterStream), origin);
|
||||
}
|
||||
else if (origin == SeekOrigin.Current && (offset + m_stream.Position) >= this.StartingPostionOnOuterStream && (offset + m_stream.Position) < this.EndingPostionOnOuterStream)
|
||||
{
|
||||
return m_stream.Seek(offset, origin);
|
||||
}
|
||||
|
||||
throw new ArgumentException();
|
||||
}
|
||||
|
||||
public override void SetLength(long value)
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
|
||||
public override void Write(byte[] buffer, int offset, int count)
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
|
||||
private int EnsureLessThanOrEqualToRemainingBytes(int numBytes)
|
||||
{
|
||||
long remainingBytesInStream = m_length - this.Position;
|
||||
if (numBytes > remainingBytesInStream)
|
||||
{
|
||||
numBytes = Convert.ToInt32(remainingBytesInStream);
|
||||
}
|
||||
return numBytes;
|
||||
}
|
||||
|
||||
private long m_length;
|
||||
private long m_startingPosition;
|
||||
private Stream m_stream;
|
||||
}
|
||||
|
||||
}
|
||||
292
src/Sdk/Common/Common/Utility/TypeExtensionMethods.cs
Normal file
292
src/Sdk/Common/Common/Utility/TypeExtensionMethods.cs
Normal file
@@ -0,0 +1,292 @@
|
||||
using System;
|
||||
using System.Collections;
|
||||
using System.Collections.Generic;
|
||||
using System.Globalization;
|
||||
using System.Linq;
|
||||
using System.Reflection;
|
||||
|
||||
namespace GitHub.Services.Common
|
||||
{
|
||||
public static class TypeExtensionMethods
|
||||
{
|
||||
/// <summary>
|
||||
/// Determins if a value is assignable to the requested type. It goes
|
||||
/// the extra step beyond IsAssignableFrom in that it also checks for
|
||||
/// IConvertible and attempts to convert the value.
|
||||
/// </summary>
|
||||
/// <param name="type"></param>
|
||||
/// <param name="value"></param>
|
||||
/// <returns></returns>
|
||||
public static bool IsAssignableOrConvertibleFrom(this Type type, object value)
|
||||
{
|
||||
if (value == null)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!type.GetTypeInfo().IsAssignableFrom(value.GetType().GetTypeInfo()))
|
||||
{
|
||||
if (value is IConvertible)
|
||||
{
|
||||
// Try and convert to the requested type, if successful
|
||||
// assign value to the result so we don't have to do again.
|
||||
try
|
||||
{
|
||||
ConvertUtility.ChangeType(value, type, CultureInfo.CurrentCulture);
|
||||
return true;
|
||||
}
|
||||
catch (FormatException)
|
||||
{
|
||||
}
|
||||
catch (InvalidCastException)
|
||||
{
|
||||
}
|
||||
catch (OverflowException)
|
||||
{
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Determines if the type is of the type t.
|
||||
/// </summary>
|
||||
/// <param name="type">The type to check.</param>
|
||||
/// <param name="t">The type to compare to.</param>
|
||||
/// <returns>True if of the same type, otherwise false.</returns>
|
||||
public static bool IsOfType(this Type type, Type t)
|
||||
{
|
||||
if (t.GetTypeInfo().IsAssignableFrom(type.GetTypeInfo()))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
else if (type.GetTypeInfo().IsGenericType &&
|
||||
type.GetGenericTypeDefinition() == t)
|
||||
{
|
||||
//generic type
|
||||
return true;
|
||||
}
|
||||
else if (type.GetTypeInfo().ImplementedInterfaces.Any(
|
||||
i => i.GetTypeInfo().IsGenericType &&
|
||||
i.GetGenericTypeDefinition() == t))
|
||||
{
|
||||
//implements generic type
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Determines if the type is a Dictionary.
|
||||
/// </summary>
|
||||
/// <param name="type">The type to check.</param>
|
||||
/// <returns>True if a dictionary, otherwise false.</returns>
|
||||
public static bool IsDictionary(this Type type)
|
||||
{
|
||||
if (typeof(IDictionary).GetTypeInfo().IsAssignableFrom(type.GetTypeInfo()))
|
||||
{
|
||||
//non-generic dictionary
|
||||
return true;
|
||||
}
|
||||
else if (type.GetTypeInfo().IsGenericType &&
|
||||
type.GetGenericTypeDefinition() == typeof(IDictionary<,>))
|
||||
{
|
||||
//generic dictionary interface
|
||||
return true;
|
||||
}
|
||||
else if (type.GetTypeInfo().ImplementedInterfaces.Any(
|
||||
i => i.GetTypeInfo().IsGenericType &&
|
||||
i.GetGenericTypeDefinition() == typeof(IDictionary<,>)))
|
||||
{
|
||||
//implements generic dictionary
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Determines if the type is a List.
|
||||
/// </summary>
|
||||
/// <param name="type">The type to check.</param>
|
||||
/// <returns>True if a list, otherwise false.</returns>
|
||||
public static bool IsList(this Type type)
|
||||
{
|
||||
if (typeof(IList).GetTypeInfo().IsAssignableFrom(type.GetTypeInfo()))
|
||||
{
|
||||
//non-generic list
|
||||
return true;
|
||||
}
|
||||
else if (type.GetTypeInfo().IsGenericType &&
|
||||
type.GetGenericTypeDefinition() == typeof(IList<>))
|
||||
{
|
||||
//generic list interface
|
||||
return true;
|
||||
}
|
||||
else if (type.GetTypeInfo().ImplementedInterfaces.Any(
|
||||
i => i.GetTypeInfo().IsGenericType &&
|
||||
i.GetGenericTypeDefinition() == typeof(IList<>)))
|
||||
{
|
||||
//implements generic list
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get's the type of the field/property specified.
|
||||
/// </summary>
|
||||
/// <param name="type">The type to get the field/property from.</param>
|
||||
/// <param name="name">The name of the field/property.</param>
|
||||
/// <returns>The type of the field/property or null if no match found.</returns>
|
||||
public static Type GetMemberType(this Type type, string name)
|
||||
{
|
||||
TypeInfo typeInfo = type.GetTypeInfo();
|
||||
PropertyInfo propertyInfo = GetPublicInstancePropertyInfo(type, name);
|
||||
if (propertyInfo != null)
|
||||
{
|
||||
return propertyInfo.PropertyType;
|
||||
}
|
||||
else
|
||||
{
|
||||
FieldInfo fieldInfo = GetPublicInstanceFieldInfo(type, name);
|
||||
if (fieldInfo != null)
|
||||
{
|
||||
return fieldInfo.FieldType;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get's the value of the field/property specified.
|
||||
/// </summary>
|
||||
/// <param name="type">The type to get the field/property from.</param>
|
||||
/// <param name="name">The name of the field/property.</param>
|
||||
/// <param name="obj">The object to get the value from.</param>
|
||||
/// <returns>The value of the field/property or null if no match found.</returns>
|
||||
public static object GetMemberValue(this Type type, string name, object obj)
|
||||
{
|
||||
PropertyInfo propertyInfo = GetPublicInstancePropertyInfo(type, name);
|
||||
if (propertyInfo != null)
|
||||
{
|
||||
return propertyInfo.GetValue(obj);
|
||||
}
|
||||
else
|
||||
{
|
||||
FieldInfo fieldInfo = GetPublicInstanceFieldInfo(type, name);
|
||||
if (fieldInfo != null)
|
||||
{
|
||||
return fieldInfo.GetValue(obj);
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Set's the value of the field/property specified.
|
||||
/// </summary>
|
||||
/// <param name="type">The type to get the field/property from.</param>
|
||||
/// <param name="name">The name of the field/property.</param>
|
||||
/// <param name="obj">The object to set the value to.</param>
|
||||
/// <param name="value">The value to set.</param>
|
||||
public static void SetMemberValue(this Type type, string name, object obj, object value)
|
||||
{
|
||||
PropertyInfo propertyInfo = GetPublicInstancePropertyInfo(type, name);
|
||||
if (propertyInfo != null)
|
||||
{
|
||||
if (!propertyInfo.SetMethod.IsPublic)
|
||||
{
|
||||
// this is here to match original behaviour before we switched to PCL version of code.
|
||||
throw new ArgumentException("Property set method not public.");
|
||||
}
|
||||
propertyInfo.SetValue(obj, value);
|
||||
}
|
||||
else
|
||||
{
|
||||
FieldInfo fieldInfo = GetPublicInstanceFieldInfo(type, name);
|
||||
if (fieldInfo != null)
|
||||
{
|
||||
fieldInfo.SetValue(obj, value);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Portable compliant way to get a constructor with specified arguments. This will return a constructor that is public or private as long as the arguments match. NULL will be returned if there is no match.
|
||||
/// Note that it will pick the first one it finds that matches, which is not necesarily the best match.
|
||||
/// </summary>
|
||||
/// <param name="type">The Type that has the constructor</param>
|
||||
/// <param name="parameterTypes">The type of the arguments for the constructor.</param>
|
||||
/// <returns></returns>
|
||||
public static ConstructorInfo GetFirstMatchingConstructor(this Type type, params Type[] parameterTypes)
|
||||
{
|
||||
return type.GetTypeInfo().DeclaredConstructors.GetFirstMatchingConstructor(parameterTypes);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Portable compliant way to get a constructor with specified arguments from a prefiltered list. This will return a constructor that is public or private as long as the arguments match. NULL will be returned if there is no match.
|
||||
/// Note that it will pick the first one it finds that matches, which is not necesarily the best match.
|
||||
/// </summary>
|
||||
/// <param name="constructors">Prefiltered list of constructors</param>
|
||||
/// <param name="parameterTypes">The type of the arguments for the constructor.</param>
|
||||
/// <returns></returns>
|
||||
public static ConstructorInfo GetFirstMatchingConstructor(this IEnumerable<ConstructorInfo> constructors, params Type[] parameterTypes)
|
||||
{
|
||||
foreach (ConstructorInfo constructorInfo in constructors)
|
||||
{
|
||||
ParameterInfo[] parameters = constructorInfo.GetParameters();
|
||||
if (parameters.Length == parameterTypes.Length)
|
||||
{
|
||||
int i;
|
||||
bool matches = true;
|
||||
for (i = 0; i < parameterTypes.Length; i++)
|
||||
{
|
||||
if (parameters[i].ParameterType != parameterTypes[i] && !parameters[i].ParameterType.GetTypeInfo().IsAssignableFrom(parameterTypes[i].GetTypeInfo()))
|
||||
{
|
||||
matches = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (matches)
|
||||
{
|
||||
return constructorInfo;
|
||||
}
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private static PropertyInfo GetPublicInstancePropertyInfo(Type type, string name)
|
||||
{
|
||||
Type typeToCheck = type;
|
||||
PropertyInfo propertyInfo = null;
|
||||
while (propertyInfo == null && typeToCheck != null)
|
||||
{
|
||||
TypeInfo typeInfo = typeToCheck.GetTypeInfo();
|
||||
propertyInfo = typeInfo.DeclaredProperties.FirstOrDefault(p => p.Name.Equals(name, StringComparison.OrdinalIgnoreCase) && p.GetMethod.Attributes.HasFlag(MethodAttributes.Public) && !p.GetMethod.Attributes.HasFlag(MethodAttributes.Static));
|
||||
typeToCheck = typeInfo.BaseType;
|
||||
}
|
||||
return propertyInfo;
|
||||
}
|
||||
|
||||
private static FieldInfo GetPublicInstanceFieldInfo(Type type, string name)
|
||||
{
|
||||
Type typeToCheck = type;
|
||||
FieldInfo fieldInfo = null;
|
||||
while (fieldInfo == null && typeToCheck != null)
|
||||
{
|
||||
TypeInfo typeInfo = typeToCheck.GetTypeInfo();
|
||||
fieldInfo = typeInfo.DeclaredFields.FirstOrDefault(f => f.Name.Equals(name, StringComparison.OrdinalIgnoreCase) && f.IsPublic && !f.IsStatic);
|
||||
typeToCheck = typeInfo.BaseType;
|
||||
}
|
||||
return fieldInfo;
|
||||
}
|
||||
}
|
||||
}
|
||||
160
src/Sdk/Common/Common/Utility/UriExtensions.cs
Normal file
160
src/Sdk/Common/Common/Utility/UriExtensions.cs
Normal file
@@ -0,0 +1,160 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.Specialized;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
|
||||
namespace GitHub.Services.Common
|
||||
{
|
||||
public static class UriExtensions
|
||||
{
|
||||
public static Uri AppendQuery(this Uri uri, String name, String value)
|
||||
{
|
||||
ArgumentUtility.CheckForNull(uri, "uri");
|
||||
ArgumentUtility.CheckStringForNullOrEmpty(name, "name");
|
||||
ArgumentUtility.CheckStringForNullOrEmpty(value, "value");
|
||||
|
||||
StringBuilder stringBuilder = new StringBuilder(uri.Query.TrimStart('?'));
|
||||
|
||||
AppendSingleQueryValue(stringBuilder, name, value);
|
||||
|
||||
UriBuilder uriBuilder = new UriBuilder(uri);
|
||||
|
||||
uriBuilder.Query = stringBuilder.ToString();
|
||||
|
||||
return uriBuilder.Uri;
|
||||
}
|
||||
|
||||
public static Uri AppendQuery(this Uri uri, IEnumerable<KeyValuePair<String, String>> queryValues)
|
||||
{
|
||||
ArgumentUtility.CheckForNull(uri, "uri");
|
||||
ArgumentUtility.CheckForNull(queryValues, "queryValues");
|
||||
|
||||
StringBuilder stringBuilder = new StringBuilder(uri.Query.TrimStart('?'));
|
||||
|
||||
foreach (KeyValuePair<String, String> kvp in queryValues)
|
||||
{
|
||||
AppendSingleQueryValue(stringBuilder, kvp.Key, kvp.Value);
|
||||
}
|
||||
|
||||
UriBuilder uriBuilder = new UriBuilder(uri);
|
||||
uriBuilder.Query = stringBuilder.ToString();
|
||||
return uriBuilder.Uri;
|
||||
}
|
||||
|
||||
public static Uri AppendQuery(this Uri uri, NameValueCollection queryValues)
|
||||
{
|
||||
ArgumentUtility.CheckForNull(uri, "uri");
|
||||
ArgumentUtility.CheckForNull(queryValues, "queryValues");
|
||||
|
||||
StringBuilder stringBuilder = new StringBuilder(uri.Query.TrimStart('?'));
|
||||
|
||||
foreach (String name in queryValues)
|
||||
{
|
||||
AppendSingleQueryValue(stringBuilder, name, queryValues[name]);
|
||||
}
|
||||
|
||||
UriBuilder uriBuilder = new UriBuilder(uri);
|
||||
|
||||
uriBuilder.Query = stringBuilder.ToString();
|
||||
|
||||
return uriBuilder.Uri;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Performs an Add similar to the NameValuCollection 'Add' method where the value gets added as an item in a comma delimited list if the key is already present.
|
||||
/// </summary>
|
||||
/// <typeparam name="T"></typeparam>
|
||||
/// <param name="collection"></param>
|
||||
/// <param name="name"></param>
|
||||
/// <param name="value"></param>
|
||||
/// <param name="convert"></param>
|
||||
public static void Add<T>(this IList<KeyValuePair<String, String>> collection, String key, T value, Func<T, String> convert = null)
|
||||
{
|
||||
collection.AddMultiple<T>(key, new List<T> { value }, convert);
|
||||
}
|
||||
|
||||
public static void AddMultiple<T>(this IList<KeyValuePair<String, String>> collection, String key, IEnumerable<T> values, Func<T, String> convert)
|
||||
{
|
||||
ArgumentUtility.CheckForNull(collection, "collection");
|
||||
ArgumentUtility.CheckStringForNullOrEmpty(key, "name");
|
||||
|
||||
if (convert == null) convert = (val) => val.ToString();
|
||||
|
||||
if (values != null && values.Any())
|
||||
{
|
||||
StringBuilder newValue = new StringBuilder();
|
||||
KeyValuePair<String, String> matchingKvp = collection.FirstOrDefault(kvp => kvp.Key.Equals(key));
|
||||
if (matchingKvp.Key == key)
|
||||
{
|
||||
collection.Remove(matchingKvp);
|
||||
newValue.Append(matchingKvp.Value);
|
||||
}
|
||||
|
||||
foreach (var value in values)
|
||||
{
|
||||
if (newValue.Length > 0)
|
||||
{
|
||||
newValue.Append(",");
|
||||
}
|
||||
newValue.Append(convert(value));
|
||||
}
|
||||
|
||||
collection.Add(new KeyValuePair<String, String>(key, newValue.ToString()));
|
||||
}
|
||||
}
|
||||
|
||||
public static void Add(this IList<KeyValuePair<String, String>> collection, String key, String value)
|
||||
{
|
||||
collection.AddMultiple(key, new[] { value });
|
||||
}
|
||||
|
||||
public static void AddMultiple(this IList<KeyValuePair<String, String>> collection, String key, IEnumerable<String> values)
|
||||
{
|
||||
collection.AddMultiple(key, values, (val) => val);
|
||||
}
|
||||
|
||||
public static void AddMultiple<T>(this NameValueCollection collection, String name, IEnumerable<T> values, Func<T, String> convert)
|
||||
{
|
||||
ArgumentUtility.CheckForNull(collection, "collection");
|
||||
ArgumentUtility.CheckStringForNullOrEmpty(name, "name");
|
||||
|
||||
if (convert == null) convert = (val) => val.ToString();
|
||||
|
||||
if (values != null)
|
||||
{
|
||||
foreach (var value in values)
|
||||
{
|
||||
collection.Add(name, convert(value));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static void AddMultiple(this NameValueCollection collection, String name, IEnumerable<String> values)
|
||||
{
|
||||
ArgumentUtility.CheckForNull(collection, "collection");
|
||||
collection.AddMultiple(name, values, (val) => val);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get the absolute path of the given Uri, if it is absolute.
|
||||
/// </summary>
|
||||
/// <returns>If the URI is absolute, the string form of it is returned; otherwise,
|
||||
/// the URI's string representation.</returns>
|
||||
public static string AbsoluteUri(this Uri uri)
|
||||
{
|
||||
return uri.IsAbsoluteUri ? uri.AbsoluteUri : uri.ToString();
|
||||
}
|
||||
|
||||
private static void AppendSingleQueryValue(StringBuilder builder, String name, String value)
|
||||
{
|
||||
if (builder.Length > 0)
|
||||
{
|
||||
builder.Append("&");
|
||||
}
|
||||
builder.Append(Uri.EscapeDataString(name));
|
||||
builder.Append("=");
|
||||
builder.Append(Uri.EscapeDataString(value));
|
||||
}
|
||||
}
|
||||
}
|
||||
2204
src/Sdk/Common/Common/Utility/UriUtility.cs
Normal file
2204
src/Sdk/Common/Common/Utility/UriUtility.cs
Normal file
File diff suppressed because it is too large
Load Diff
272
src/Sdk/Common/Common/Utility/VssStringComparer.cs
Normal file
272
src/Sdk/Common/Common/Utility/VssStringComparer.cs
Normal file
@@ -0,0 +1,272 @@
|
||||
using System;
|
||||
using System.Diagnostics;
|
||||
|
||||
namespace GitHub.Services.Common
|
||||
{
|
||||
|
||||
// NOTE: Since the recommendations are for Ordinal and OrdinalIgnoreCase, no need to explain those, but
|
||||
// please explain any instances using non-Ordinal comparisons (CurrentCulture, InvariantCulture)
|
||||
// so that developers following you can understand the choices and verify they are correct.
|
||||
|
||||
// NOTE: please try to keep the semantic-named properties in alphabetical order to ease merges
|
||||
|
||||
// NOTE: do NOT add xml doc comments - everything in here should be a very thin wrapper around String
|
||||
// or StringComparer. The usage of the methods and properties in this class should be intuitively
|
||||
// obvious, so please don't add xml doc comments to this class since it should be wholly internal
|
||||
// by the time we ship.
|
||||
|
||||
// NOTE: Current guidelines from the CLR team (Dave Fetterman) is to stick with the same operation for both
|
||||
// Compare and Equals for a given semantic inner class. This has the nice side effect that you don't
|
||||
// get different behavior between calling Equals or calling Compare == 0. This may seem odd given the
|
||||
// recommendations about using CurrentCulture for UI operations and Compare being used for sorting
|
||||
// items for user display in many cases, but we need to have the type of string data determine the
|
||||
// string comparison enum to use instead of the consumer of the comparison operation so that we're
|
||||
// consistent in how we treat a given semantic.
|
||||
|
||||
// VssStringComparer should act like StringComparer with a few additional methods for usefulness (Contains,
|
||||
// StartsWith, EndsWith, etc.) so that it can be a "one-stop shop" for string comparisons.
|
||||
public class VssStringComparer : StringComparer
|
||||
{
|
||||
private StringComparison m_stringComparison;
|
||||
private StringComparer m_stringComparer;
|
||||
|
||||
protected VssStringComparer(StringComparison stringComparison)
|
||||
: base()
|
||||
{
|
||||
m_stringComparison = stringComparison;
|
||||
}
|
||||
|
||||
// pass-through implementations based on our current StringComparison setting
|
||||
public override int Compare(string x, string y) { return String.Compare(x, y, m_stringComparison); }
|
||||
public override bool Equals(string x, string y) { return String.Equals(x, y, m_stringComparison); }
|
||||
public override int GetHashCode(string x) { return MatchingStringComparer.GetHashCode(x); }
|
||||
|
||||
[System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Naming", "CA1704:IdentifiersShouldBeSpelledCorrectly", MessageId = "y")]
|
||||
[System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Naming", "CA1704:IdentifiersShouldBeSpelledCorrectly", MessageId = "x")]
|
||||
public int Compare(string x, int indexX, string y, int indexY, int length) { return String.Compare(x, indexX, y, indexY, length, m_stringComparison); }
|
||||
|
||||
// add new useful methods here
|
||||
public bool Contains(string main, string pattern)
|
||||
{
|
||||
ArgumentUtility.CheckForNull(main, "main");
|
||||
ArgumentUtility.CheckForNull(pattern, "pattern");
|
||||
|
||||
return main.IndexOf(pattern, m_stringComparison) >= 0;
|
||||
}
|
||||
|
||||
public int IndexOf(string main, string pattern)
|
||||
{
|
||||
ArgumentUtility.CheckForNull(main, "main");
|
||||
ArgumentUtility.CheckForNull(pattern, "pattern");
|
||||
|
||||
return main.IndexOf(pattern, m_stringComparison);
|
||||
}
|
||||
|
||||
public bool StartsWith(string main, string pattern)
|
||||
{
|
||||
ArgumentUtility.CheckForNull(main, "main");
|
||||
ArgumentUtility.CheckForNull(pattern, "pattern");
|
||||
|
||||
return main.StartsWith(pattern, m_stringComparison);
|
||||
}
|
||||
|
||||
public bool EndsWith(string main, string pattern)
|
||||
{
|
||||
ArgumentUtility.CheckForNull(main, "main");
|
||||
ArgumentUtility.CheckForNull(pattern, "pattern");
|
||||
|
||||
return main.EndsWith(pattern, m_stringComparison);
|
||||
}
|
||||
|
||||
private StringComparer MatchingStringComparer
|
||||
{
|
||||
get
|
||||
{
|
||||
if (m_stringComparer == null)
|
||||
{
|
||||
switch (m_stringComparison)
|
||||
{
|
||||
case StringComparison.CurrentCulture:
|
||||
m_stringComparer = StringComparer.CurrentCulture;
|
||||
break;
|
||||
|
||||
case StringComparison.CurrentCultureIgnoreCase:
|
||||
m_stringComparer = StringComparer.CurrentCultureIgnoreCase;
|
||||
break;
|
||||
|
||||
case StringComparison.Ordinal:
|
||||
m_stringComparer = StringComparer.Ordinal;
|
||||
break;
|
||||
|
||||
case StringComparison.OrdinalIgnoreCase:
|
||||
m_stringComparer = StringComparer.OrdinalIgnoreCase;
|
||||
break;
|
||||
|
||||
default:
|
||||
Debug.Assert(false, "Unknown StringComparison value");
|
||||
m_stringComparer = StringComparer.Ordinal;
|
||||
break;
|
||||
}
|
||||
}
|
||||
return m_stringComparer;
|
||||
}
|
||||
}
|
||||
|
||||
protected static VssStringComparer s_ordinal = new VssStringComparer(StringComparison.Ordinal);
|
||||
protected static VssStringComparer s_ordinalIgnoreCase = new VssStringComparer(StringComparison.OrdinalIgnoreCase);
|
||||
protected static VssStringComparer s_currentCulture = new VssStringComparer(StringComparison.CurrentCulture);
|
||||
protected static VssStringComparer s_currentCultureIgnoreCase = new VssStringComparer(StringComparison.CurrentCultureIgnoreCase);
|
||||
private static VssStringComparer s_dataSourceIgnoreProtocol = new DataSourceIgnoreProtocolComparer();
|
||||
|
||||
|
||||
public static VssStringComparer ActiveDirectoryEntityIdComparer { get { return s_ordinalIgnoreCase; } }
|
||||
public static VssStringComparer ArtifactType { get { return s_ordinalIgnoreCase; } }
|
||||
public static VssStringComparer ArtifactTool { get { return s_ordinalIgnoreCase; } }
|
||||
public static VssStringComparer AssemblyName { get { return s_ordinalIgnoreCase; } }
|
||||
public static VssStringComparer ContentType { get { return s_ordinalIgnoreCase; } }
|
||||
public static VssStringComparer DomainName { get { return s_ordinalIgnoreCase; } }
|
||||
public static VssStringComparer DomainNameUI { get { return s_currentCultureIgnoreCase; } }
|
||||
public static VssStringComparer DatabaseCategory { get { return s_ordinalIgnoreCase; } }
|
||||
public static VssStringComparer DatabaseName { get { return s_ordinalIgnoreCase; } }
|
||||
public static VssStringComparer DataSource { get { return s_ordinalIgnoreCase; } }
|
||||
public static VssStringComparer DataSourceIgnoreProtocol { get { return s_dataSourceIgnoreProtocol; } }
|
||||
public static VssStringComparer DirectoryName { get { return s_ordinalIgnoreCase; } }
|
||||
public static VssStringComparer DirectoryEntityIdentifierConstants { get { return s_ordinalIgnoreCase; } }
|
||||
public static VssStringComparer DirectoryEntityPropertyComparer { get { return s_ordinalIgnoreCase; } }
|
||||
public static VssStringComparer DirectoryEntityTypeComparer { get { return s_ordinalIgnoreCase; } }
|
||||
public static VssStringComparer DirectoryEntryNameComparer { get { return s_currentCultureIgnoreCase; } }
|
||||
public static VssStringComparer DirectoryKeyStringComparer { get { return s_ordinalIgnoreCase; } }
|
||||
public static VssStringComparer EncodingName { get { return s_ordinalIgnoreCase; } }
|
||||
public static VssStringComparer EnvVar { get { return s_ordinalIgnoreCase; } }
|
||||
public static VssStringComparer ExceptionSource { get { return s_ordinalIgnoreCase; } }
|
||||
public static VssStringComparer FilePath { get { return s_ordinalIgnoreCase; } }
|
||||
public static VssStringComparer FilePathUI { get { return s_currentCultureIgnoreCase; } }
|
||||
public static VssStringComparer Guid { get { return s_ordinalIgnoreCase; } }
|
||||
public static VssStringComparer Hostname { get { return s_ordinalIgnoreCase; } }
|
||||
public static VssStringComparer HostnameUI { get { return s_currentCultureIgnoreCase; } }
|
||||
public static VssStringComparer HttpRequestMethod { get { return s_ordinalIgnoreCase; } }
|
||||
public static VssStringComparer IdentityDescriptor { get { return s_ordinalIgnoreCase; } }
|
||||
public static VssStringComparer IdentityDomain { get { return s_ordinalIgnoreCase; } }
|
||||
public static VssStringComparer IdentityOriginId { get { return s_ordinalIgnoreCase; } }
|
||||
public static VssStringComparer IdentityType { get { return s_ordinalIgnoreCase; } }
|
||||
public static VssStringComparer LinkName { get { return s_ordinalIgnoreCase; } }
|
||||
public static VssStringComparer MachineName { get { return s_ordinalIgnoreCase; } }
|
||||
public static VssStringComparer MailAddress { get { return s_ordinalIgnoreCase; } }
|
||||
public static VssStringComparer PropertyName { get { return s_ordinalIgnoreCase; } }
|
||||
public static VssStringComparer RegistrationAttributeName { get { return s_ordinalIgnoreCase; } }
|
||||
public static VssStringComparer ReservedGroupName { get { return s_ordinalIgnoreCase; } }
|
||||
public static VssStringComparer WMDSchemaClassName { get { return s_ordinalIgnoreCase; } }
|
||||
public static VssStringComparer SamAccountName { get { return s_ordinalIgnoreCase; } }
|
||||
public static VssStringComparer AccountName { get { return s_currentCultureIgnoreCase; } }
|
||||
public static VssStringComparer SocialType { get { return s_currentCultureIgnoreCase; } }
|
||||
public static VssStringComparer ServerUrl { get { return s_ordinalIgnoreCase; } }
|
||||
public static VssStringComparer ServerUrlUI { get { return s_currentCultureIgnoreCase; } }
|
||||
public static VssStringComparer ServiceInterface { get { return s_ordinalIgnoreCase; } }
|
||||
public static VssStringComparer ServicingOperation { get { return s_ordinalIgnoreCase; } }
|
||||
public static VssStringComparer ToolId { get { return s_ordinalIgnoreCase; } }
|
||||
public static VssStringComparer Url { get { return s_ordinal; } }
|
||||
public static VssStringComparer UrlPath { get { return s_ordinalIgnoreCase; } }
|
||||
public static VssStringComparer UriScheme { get { return s_ordinalIgnoreCase; } }
|
||||
public static VssStringComparer UriAuthority { get { return s_ordinalIgnoreCase; } }
|
||||
public static VssStringComparer UserId { get { return s_ordinalIgnoreCase; } }
|
||||
public static VssStringComparer UserName { get { return s_ordinalIgnoreCase; } }
|
||||
public static VssStringComparer UserNameUI { get { return s_currentCultureIgnoreCase; } }
|
||||
public static VssStringComparer XmlAttributeName { get { return s_ordinalIgnoreCase; } }
|
||||
public static VssStringComparer XmlNodeName { get { return s_ordinalIgnoreCase; } }
|
||||
public static VssStringComparer XmlElement { get { return s_ordinal; } }
|
||||
public static VssStringComparer XmlAttributeValue { get { return s_ordinalIgnoreCase; } }
|
||||
|
||||
//Framework comparers.
|
||||
public static VssStringComparer RegistryPath { get { return s_ordinalIgnoreCase; } }
|
||||
public static VssStringComparer ServiceType { get { return s_currentCultureIgnoreCase; } }
|
||||
public static VssStringComparer AccessMappingMoniker { get { return s_ordinalIgnoreCase; } }
|
||||
public static VssStringComparer CatalogNodePath { get { return s_ordinal; } }
|
||||
public static VssStringComparer CatalogServiceReference { get { return s_ordinalIgnoreCase; } }
|
||||
public static VssStringComparer CatalogNodeDependency { get { return s_ordinalIgnoreCase; } }
|
||||
public static VssStringComparer ServicingTokenName { get { return s_ordinalIgnoreCase; } }
|
||||
public static VssStringComparer IdentityPropertyName { get { return s_ordinalIgnoreCase; } }
|
||||
public static VssStringComparer Collation { get { return s_ordinalIgnoreCase; } }
|
||||
public static VssStringComparer FeatureAvailabilityName { get { return s_ordinalIgnoreCase; } }
|
||||
public static VssStringComparer TagName { get { return s_currentCultureIgnoreCase; } }
|
||||
|
||||
//Framework Hosting comparers.
|
||||
public static VssStringComparer HostingAccountPropertyName { get { return s_ordinalIgnoreCase; } }
|
||||
public static VssStringComparer MessageBusName { get { return s_ordinalIgnoreCase; } }
|
||||
public static VssStringComparer MessageBusSubscriptionName { get { return s_ordinalIgnoreCase; } }
|
||||
|
||||
public static VssStringComparer SID { get { return s_ordinalIgnoreCase; } }
|
||||
public static VssStringComparer FieldName { get { return s_ordinal; } }
|
||||
public static VssStringComparer FieldNameUI { get { return s_currentCultureIgnoreCase; } }
|
||||
public static VssStringComparer FieldType { get { return s_ordinal; } }
|
||||
public static VssStringComparer EventType { get { return s_ordinal; } }
|
||||
public static VssStringComparer EventTypeIgnoreCase { get { return s_ordinalIgnoreCase; } }
|
||||
public static VssStringComparer RegistrationEntryName { get { return s_ordinalIgnoreCase; } }
|
||||
public static VssStringComparer ServerName { get { return s_ordinalIgnoreCase; } }
|
||||
public static VssStringComparer GroupName { get { return s_currentCultureIgnoreCase; } }
|
||||
public static VssStringComparer RegistrationUtilities { get { return s_ordinal; } }
|
||||
public static VssStringComparer RegistrationUtilitiesCaseInsensitive { get { return s_ordinalIgnoreCase; } }
|
||||
public static VssStringComparer IdentityName { get { return s_ordinalIgnoreCase; } }
|
||||
public static VssStringComparer IdentityNameOrdinal { get { return s_ordinal; } }
|
||||
public static VssStringComparer PlugInId { get { return s_ordinalIgnoreCase; } }
|
||||
public static VssStringComparer ExtensionName { get { return s_ordinalIgnoreCase; } }
|
||||
public static VssStringComparer ExtensionType { get { return s_ordinalIgnoreCase; } }
|
||||
public static VssStringComparer DomainUrl { get { return s_ordinalIgnoreCase; } }
|
||||
public static VssStringComparer AccountInfoAccount { get { return s_ordinalIgnoreCase; } }
|
||||
public static VssStringComparer AccountInfoPassword { get { return s_ordinal; } }
|
||||
public static VssStringComparer AttributesDescriptor { get { return s_ordinalIgnoreCase; } }
|
||||
|
||||
// Converters comparer
|
||||
public static VssStringComparer VSSServerPath { get { return s_ordinalIgnoreCase; } }
|
||||
|
||||
// Item rename in VSS is case sensitive.
|
||||
public static VssStringComparer VSSItemName { get { return s_ordinal; } }
|
||||
// Web Access Comparers
|
||||
public static VssStringComparer HtmlElementName { get { return s_ordinalIgnoreCase; } }
|
||||
public static VssStringComparer HtmlAttributeName { get { return s_ordinalIgnoreCase; } }
|
||||
public static VssStringComparer HtmlAttributeValue { get { return s_ordinalIgnoreCase; } }
|
||||
|
||||
public static VssStringComparer StringFieldConditionEquality { get { return s_ordinalIgnoreCase; } }
|
||||
public static VssStringComparer StringFieldConditionOrdinal { get { return s_ordinal; } }
|
||||
|
||||
// Service Endpoint Comparer
|
||||
public static VssStringComparer ServiceEndpointTypeCompararer { get { return s_ordinalIgnoreCase; } }
|
||||
|
||||
private class DataSourceIgnoreProtocolComparer : VssStringComparer
|
||||
{
|
||||
public DataSourceIgnoreProtocolComparer()
|
||||
: base(StringComparison.OrdinalIgnoreCase)
|
||||
{
|
||||
}
|
||||
|
||||
public override int Compare(string x, string y)
|
||||
{
|
||||
return base.Compare(RemoveProtocolPrefix(x), RemoveProtocolPrefix(y));
|
||||
}
|
||||
|
||||
public override bool Equals(string x, string y)
|
||||
{
|
||||
return base.Equals(RemoveProtocolPrefix(x), RemoveProtocolPrefix(y));
|
||||
}
|
||||
|
||||
private static string RemoveProtocolPrefix(string x)
|
||||
{
|
||||
if (x != null)
|
||||
{
|
||||
if (x.StartsWith(c_tcpPrefix, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
x = x.Substring(c_tcpPrefix.Length);
|
||||
}
|
||||
else if (x.StartsWith(c_npPrefix, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
x = x.Substring(c_npPrefix.Length);
|
||||
}
|
||||
}
|
||||
|
||||
return x;
|
||||
}
|
||||
|
||||
private const string c_tcpPrefix = "tcp:";
|
||||
private const string c_npPrefix = "np:";
|
||||
}
|
||||
}
|
||||
}
|
||||
1486
src/Sdk/Common/Common/Utility/XmlUtility.cs
Normal file
1486
src/Sdk/Common/Common/Utility/XmlUtility.cs
Normal file
File diff suppressed because it is too large
Load Diff
518
src/Sdk/Common/Common/VssCommonConstants.cs
Normal file
518
src/Sdk/Common/Common/VssCommonConstants.cs
Normal file
@@ -0,0 +1,518 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.ComponentModel;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
|
||||
namespace GitHub.Services.Common
|
||||
{
|
||||
[GenerateSpecificConstants]
|
||||
public static class IdentityConstants
|
||||
{
|
||||
static IdentityConstants()
|
||||
{
|
||||
// For the normalization of incoming IdentityType strings.
|
||||
// This is an optimization; it is not required that any particular IdentityType values
|
||||
// appear in this list, but it helps performance to have common values here
|
||||
var identityTypeMap = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase)
|
||||
{
|
||||
{ IdentityConstants.WindowsType, IdentityConstants.WindowsType },
|
||||
{ IdentityConstants.TeamFoundationType, IdentityConstants.TeamFoundationType },
|
||||
{ IdentityConstants.ClaimsType, IdentityConstants.ClaimsType },
|
||||
{ IdentityConstants.BindPendingIdentityType, IdentityConstants.BindPendingIdentityType },
|
||||
{ IdentityConstants.UnauthenticatedIdentityType, IdentityConstants.UnauthenticatedIdentityType },
|
||||
{ IdentityConstants.ServiceIdentityType, IdentityConstants.ServiceIdentityType },
|
||||
{ IdentityConstants.AggregateIdentityType, IdentityConstants.AggregateIdentityType },
|
||||
{ IdentityConstants.ServerTestIdentity, IdentityConstants.ServerTestIdentity },
|
||||
{ IdentityConstants.ImportedIdentityType, IdentityConstants.ImportedIdentityType },
|
||||
{ IdentityConstants.GroupScopeType, IdentityConstants.GroupScopeType },
|
||||
{ IdentityConstants.CspPartnerIdentityType, IdentityConstants.CspPartnerIdentityType },
|
||||
{ IdentityConstants.System_ServicePrincipal, IdentityConstants.System_ServicePrincipal },
|
||||
{ IdentityConstants.System_License, IdentityConstants.System_License },
|
||||
{ IdentityConstants.System_Scope, IdentityConstants.System_Scope },
|
||||
{ IdentityConstants.PermissionLevelDefinitionType, IdentityConstants.PermissionLevelDefinitionType}
|
||||
};
|
||||
|
||||
IdentityTypeMap = identityTypeMap;
|
||||
}
|
||||
|
||||
public const string WindowsType = "System.Security.Principal.WindowsIdentity"; // hard coding to make PCL compliant. typeof(WindowsIdentity).FullName
|
||||
public const string TeamFoundationType = "GitHub.Identity";
|
||||
public const string ClaimsType = "Microsoft.IdentityModel.Claims.ClaimsIdentity";
|
||||
// In WIF 4.5, Microsoft.IdentityModel.Claims.ClaimsIdentity was moved to System.Security.Claims namespace
|
||||
[EditorBrowsable(EditorBrowsableState.Never)]
|
||||
public const string Wif45ClaimsIdentityType = "System.Security.Claims.ClaimsIdentity";
|
||||
public const string AlternateLoginType = "GitHub.Services.Cloud.AlternateLoginIdentity";
|
||||
public const string BindPendingIdentityType = "GitHub.BindPendingIdentity";
|
||||
public const string ServerTestIdentity = "GitHub.Services.Identity.ServerTestIdentity";
|
||||
public const string UnauthenticatedIdentityType = "GitHub.UnauthenticatedIdentity";
|
||||
public const string ServiceIdentityType = "GitHub.ServiceIdentity";
|
||||
public const string AggregateIdentityType = "GitHub.AggregateIdentity";
|
||||
public const string ImportedIdentityType = "GitHub.ImportedIdentity";
|
||||
public const string UnknownIdentityType = "GitHub.Services.Identity.UnknownIdentity";
|
||||
public const string CspPartnerIdentityType = "GitHub.Claims.CspPartnerIdentity";
|
||||
public const string PermissionLevelDefinitionType = "GitHub.Services.PermissionLevel.PermissionLevelIdentity";
|
||||
|
||||
// this is used to represent scopes in the new Graph Rest Api
|
||||
public const string GroupScopeType = "GitHub.Services.Graph.GraphScope";
|
||||
|
||||
// These are used with the System Subject Store
|
||||
public const string SystemPrefix = "System:";
|
||||
public const string System_ServicePrincipal = SystemPrefix + "ServicePrincipal";
|
||||
public const string System_WellKnownGroup = SystemPrefix + "WellKnownGroup";
|
||||
public const string System_License = SystemPrefix + "License";
|
||||
public const string System_Scope = SystemPrefix + "Scope";
|
||||
public const string System_CspPartner = SystemPrefix + "CspPartner";
|
||||
public const string System_PublicAccess = SystemPrefix + "PublicAccess";
|
||||
|
||||
// This is used to convey an ACE via an IdentityDescriptor
|
||||
public const string System_AccessControl = SystemPrefix + "AccessControl";
|
||||
|
||||
public const int MaxIdLength = 256;
|
||||
public const int MaxTypeLength = 128;
|
||||
public const byte UnknownIdentityTypeId = byte.MaxValue;
|
||||
|
||||
// Social type for identity
|
||||
public const byte UnknownSocialTypeId = byte.MaxValue;
|
||||
|
||||
/// <summary>
|
||||
/// Special value for the unique user ID for active (non-deleted) users.
|
||||
/// </summary>
|
||||
public const int ActiveUniqueId = 0;
|
||||
|
||||
/// <summary>
|
||||
/// Value of attribute that denotes whether user or group.
|
||||
/// </summary>
|
||||
public const string SchemaClassGroup = "Group";
|
||||
public const string SchemaClassUser = "User";
|
||||
|
||||
public const string BindPendingSidPrefix = "upn:";
|
||||
[GenerateConstant]
|
||||
public const string MsaDomain = "Windows Live ID";
|
||||
[GenerateConstant]
|
||||
public const string GitHubDomain = "github.com";
|
||||
public const string DomainQualifiedAccountNameFormat = "{0}\\{1}";
|
||||
public const string MsaSidSuffix = "@Live.com";
|
||||
public const string AadOidPrefix = "oid:";
|
||||
public const string FrameworkIdentityIdentifierDelimiter = ":";
|
||||
public const string IdentityDescriptorPartsSeparator = ";";
|
||||
public const string IdentityMinimumResourceVersion = "IdentityMinimumResourceVersion";
|
||||
public const int DefaultMinimumResourceVersion = -1;
|
||||
public const char DomainAccountNameSeparator = '\\';
|
||||
public const bool DefaultUseAccountNameAsDirectoryAlias = true;
|
||||
|
||||
/// <summary>
|
||||
/// Values used in switch_hint query parameter to force sign in with personal or work account
|
||||
/// </summary>
|
||||
public const string SwitchHintQueryKey = "switch_hint";
|
||||
public const char SwitchToPersonal = 'P';
|
||||
public const char SwitchToWork = 'W';
|
||||
|
||||
public const string AllowNonServiceIdentitiesInDeploymentAdminsGroup =
|
||||
nameof(AllowNonServiceIdentitiesInDeploymentAdminsGroup);
|
||||
|
||||
/// <summary>
|
||||
/// The DB layer only supports byte, even though the data layer contracts suggests a
|
||||
/// 32-bit integer. Note: changing this constant implies that every new identity object
|
||||
/// that is created, going forward will have this resource version set. Existing identites
|
||||
/// will need to be updated to the current resource version level manually.
|
||||
///
|
||||
/// This is created for rolling out of a feature based on identity not service host.
|
||||
/// This value must be greater than 0. Otherwise, IMS won't update tbl_identityextension for
|
||||
/// identity extended properties.
|
||||
/// </summary>
|
||||
public const byte DefaultResourceVersion = 2;
|
||||
|
||||
// Identity ResourceVersions
|
||||
[Obsolete]
|
||||
public const byte ScopeManifestIssuance = 2;
|
||||
[Obsolete]
|
||||
public const byte ScopeManifestEnforcementWithInitialGrace = 3;
|
||||
[Obsolete]
|
||||
public const byte ScopeManifestEnforcementWithoutInitialGrace = 4;
|
||||
|
||||
/// <summary>
|
||||
/// The Global scope, [SERVER], represents the highest group Scope ID in the given request context.
|
||||
/// For example, [SERVER] at a Deployment context would represent the deployment Scope ID. When
|
||||
/// using the global scope in a search, a search for [SERVER]\Team Foundation Administrators
|
||||
/// at the deployment level would return the deployment administrators group, while the same call
|
||||
/// at the Application host level would return the Account Administrators group. The search will
|
||||
/// not recurse down into sub-scopes.
|
||||
///
|
||||
/// [SERVER] is a deprecated concept, introduced before TFS 2010. We recommend using either the
|
||||
/// collection name in square brackets (i.e. [DefaultCollection] or the scope ID in square brackets
|
||||
/// (i.e. [SCOPE_GUID]) instead.
|
||||
/// </summary>
|
||||
public const string GlobalScope = "[SERVER]";
|
||||
|
||||
public static readonly Guid LinkedId = new Guid("FFFFFFFF-FFFF-FFFF-FFFF-FFFFFFFFFFFF");
|
||||
|
||||
public static class EtwIdentityProviderName
|
||||
{
|
||||
public const string Aad = nameof(Aad);
|
||||
public const string Msa = nameof(Msa);
|
||||
public const string Vsts = nameof(Vsts);
|
||||
}
|
||||
|
||||
public static class EtwIdentityCategory
|
||||
{
|
||||
public const string AuthenticatedIdentity = nameof(AuthenticatedIdentity);
|
||||
public const string UnauthenticatedIdentity = nameof(UnauthenticatedIdentity);
|
||||
public const string ServiceIdentity = nameof(ServiceIdentity);
|
||||
public const string UnexpectedIdentityType = nameof(UnexpectedIdentityType);
|
||||
}
|
||||
|
||||
public static readonly IReadOnlyDictionary<String, String> IdentityTypeMap;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Common attributes tags used in the collection of properties of TeamFoundationIdentity.
|
||||
/// </summary>
|
||||
public static class IdentityAttributeTags
|
||||
{
|
||||
public const string WildCard = "*";
|
||||
|
||||
public const string AccountName = "Account";
|
||||
public const string Alias = "Alias";
|
||||
public const string CrossProject = "CrossProject";
|
||||
public const string Description = "Description";
|
||||
public const string Disambiguation = "Disambiguation";
|
||||
public const string DistinguishedName = "DN";
|
||||
public const string Domain = "Domain";
|
||||
public const string GlobalScope = "GlobalScope";
|
||||
public const string MailAddress = "Mail";
|
||||
public const string RestrictedVisible = "RestrictedVisible";
|
||||
public const string SchemaClassName = "SchemaClassName";
|
||||
public const string ScopeName = "ScopeName";
|
||||
public const string SecurityGroup = "SecurityGroup";
|
||||
public const string SpecialType = "SpecialType";
|
||||
public const string ScopeId = "ScopeId";
|
||||
public const string ScopeType = "ScopeType";
|
||||
public const string LocalScopeId = "LocalScopeId";
|
||||
public const string SecuringHostId = "SecuringHostId";
|
||||
public const string VirtualPlugin = "VirtualPlugin";
|
||||
public const string ProviderDisplayName = "ProviderDisplayName";
|
||||
public const string IsGroupDeleted = "IsGroupDeleted";
|
||||
|
||||
public const string Cuid = "CUID";
|
||||
public const string CuidState = "CUIDState";
|
||||
public const string Puid = "PUID";
|
||||
public const string Oid = "http://schemas.microsoft.com/identity/claims/objectidentifier";
|
||||
public const string ConsumerPuid = "ConsumerPUID";
|
||||
public const string ComplianceValidated = "ComplianceValidated";
|
||||
public const string AuthenticationCredentialValidFrom = "AuthenticationCredentialValidFrom";
|
||||
public const string MetadataUpdateDate = "MetadataUpdateDate";
|
||||
public const string DirectoryAlias = "DirectoryAlias";
|
||||
public const string CacheMaxAge = "CacheMaxAge";
|
||||
// temporary used in the ServiceIdentity and CspIdentity
|
||||
public const string ServiceStorageKey = "http://schemas.microsoft.com/ws/2008/06/identity/claims/primarysid";
|
||||
public const string ProvData = "prov_data";
|
||||
|
||||
public const string AadRefreshToken = "vss:AadRefreshToken";
|
||||
public const string AadRefreshTokenUpdated = "GitHub.Aad.AadRefreshTokenUpdateDate";
|
||||
public const string AadUserPrincipalName = "AadUserPrincipalName";
|
||||
public const string AcsIdentityProvider = "http://schemas.microsoft.com/accesscontrolservice/2010/07/claims/identityprovider";
|
||||
public const string AadIdentityProvider = "http://schemas.microsoft.com/identity/claims/identityprovider";
|
||||
public const string IdentityProviderClaim = "http://schemas.microsoft.com/teamfoundationserver/2010/12/claims/identityprovider";
|
||||
public const string NameIdentifierClaim = "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/nameidentifier";
|
||||
public const string TenantIdentifierClaim = "http://schemas.microsoft.com/identity/claims/tenantid";
|
||||
public const string AadTenantDisambiguationClaim = "tenant_disambiguate";
|
||||
public const string AadMsaPassthroughClaim = "msapt";
|
||||
public const string AppidClaim = "appid";
|
||||
|
||||
public const string IdentityTypeClaim = "IdentityTypeClaim";
|
||||
public const string IsClientClaim = "IsClient";
|
||||
|
||||
// tbl_IdentityExtension properties. No longer stored in PropertyService
|
||||
public const string ConfirmedNotificationAddress = "ConfirmedNotificationAddress";
|
||||
public const string CustomNotificationAddresses = "CustomNotificationAddresses";
|
||||
public const string IsDeletedInOrigin = "IsDeletedInOrigin";
|
||||
|
||||
// Extended properties, currently used only for Group images
|
||||
public const string ImageId = "GitHub.Identity.Image.Id";
|
||||
public const string ImageData = @"GitHub.Identity.Image.Data";
|
||||
public const string ImageType = @"GitHub.Identity.Image.Type";
|
||||
public const string ImageUploadDate = @"GitHub.Identity.Image.UploadDate";
|
||||
public const string CandidateImageData = @"GitHub.Identity.CandidateImage.Data";
|
||||
public const string CandidateImageUploadDate = @"GitHub.Identity.CandidateImage.UploadDate";
|
||||
|
||||
// Extended Properties used On Prem
|
||||
public const string LastAccessedTime = "LastAccessedTime";
|
||||
|
||||
// Extended Property used by Profile to get the MasterId of an identity.
|
||||
// DO NOT USE without consulting with and getting permission from the
|
||||
// Identity team. This is a bad pattern that we are currently supporting
|
||||
// for compat with Profile, and the whole concept of MasterIds may be
|
||||
// changing with our Sharding work.
|
||||
public const string UserId = "UserId";
|
||||
|
||||
// Obsolete extended properties, which should be removed with the next major version (whichever version follows Dev15/TFS 2017)
|
||||
[Obsolete] public const string EmailConfirmationSendDates = "EmailConfirmationSendDates";
|
||||
[Obsolete] public const string MsdnLicense = "MSDNLicense";
|
||||
[Obsolete] public const string BasicAuthPwdKey = "GitHub.Identity.BasicAuthPwd";
|
||||
[Obsolete] public const string BasicAuthSaltKey = "GitHub.Identity.BasicAuthSalt";
|
||||
[Obsolete] public const string BasicAuthAlgorithm = "Microsoft.TeaFoundation.Identity.BasicAuthAlgorithm";
|
||||
[Obsolete] public const string BasicAuthFailures = "Microsoft.TeaFoundation.Identity.BasicAuthFailures";
|
||||
[Obsolete] public const string BasicAuthDisabled = "Microsoft.TeaFoundation.Identity.BasicAuthDisabled";
|
||||
[Obsolete] public const string BasicAuthPasswordChanges = "GitHub.Identity.BasicAuthSettingsChanges";
|
||||
|
||||
|
||||
[SuppressMessage("Microsoft.Security", "CA2104:DoNotDeclareReadOnlyMutableReferenceTypes", Justification = "When the target .NET framework is revisioned to 4, change return to ISet<String>")]
|
||||
public static readonly HashSet<string> ReadOnlyProperties = new HashSet<string>(
|
||||
new[]
|
||||
{
|
||||
AccountName,
|
||||
Alias,
|
||||
ComplianceValidated,
|
||||
CrossProject,
|
||||
Description,
|
||||
Disambiguation,
|
||||
DistinguishedName,
|
||||
Domain,
|
||||
GlobalScope,
|
||||
MailAddress,
|
||||
RestrictedVisible,
|
||||
SchemaClassName,
|
||||
ScopeName,
|
||||
SecurityGroup,
|
||||
SpecialType,
|
||||
ScopeId,
|
||||
ScopeType,
|
||||
LocalScopeId,
|
||||
SecuringHostId,
|
||||
Cuid,
|
||||
CuidState,
|
||||
Puid,
|
||||
VirtualPlugin,
|
||||
Oid,
|
||||
AcsIdentityProvider,
|
||||
AadIdentityProvider,
|
||||
AadTenantDisambiguationClaim,
|
||||
AadMsaPassthroughClaim,
|
||||
IdentityProviderClaim,
|
||||
NameIdentifierClaim,
|
||||
IsClientClaim,
|
||||
UserId,
|
||||
CacheMaxAge,
|
||||
IsGroupDeleted,
|
||||
},
|
||||
StringComparer.OrdinalIgnoreCase
|
||||
);
|
||||
|
||||
[SuppressMessage("Microsoft.Security", "CA2104:DoNotDeclareReadOnlyMutableReferenceTypes", Justification = "When the target .NET framework is revisioned to 4, change return to ISet<String>")]
|
||||
public static readonly HashSet<string> GroupReadOnlyProperties = new HashSet<string>(
|
||||
new[]
|
||||
{
|
||||
Alias,
|
||||
ComplianceValidated,
|
||||
CrossProject,
|
||||
Disambiguation,
|
||||
DistinguishedName,
|
||||
Domain,
|
||||
GlobalScope,
|
||||
MailAddress,
|
||||
RestrictedVisible,
|
||||
SchemaClassName,
|
||||
ScopeName,
|
||||
SecurityGroup,
|
||||
SpecialType,
|
||||
ScopeId,
|
||||
ScopeType,
|
||||
LocalScopeId,
|
||||
SecuringHostId,
|
||||
Cuid,
|
||||
CuidState,
|
||||
Puid,
|
||||
VirtualPlugin,
|
||||
Oid,
|
||||
AcsIdentityProvider,
|
||||
AadIdentityProvider,
|
||||
AadTenantDisambiguationClaim,
|
||||
AadMsaPassthroughClaim,
|
||||
IdentityProviderClaim,
|
||||
NameIdentifierClaim,
|
||||
IsClientClaim,
|
||||
UserId,
|
||||
CacheMaxAge,
|
||||
IsGroupDeleted,
|
||||
},
|
||||
StringComparer.OrdinalIgnoreCase
|
||||
);
|
||||
|
||||
[Obsolete]
|
||||
public static readonly ISet<string> WhiteListedProperties = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
|
||||
}
|
||||
|
||||
[EditorBrowsable(EditorBrowsableState.Never)]
|
||||
public class LocationSecurityConstants
|
||||
{
|
||||
public static readonly Guid NamespaceId = new Guid("2725D2BC-7520-4AF4-B0E3-8D876494731F");
|
||||
public static readonly Char PathSeparator = '/';
|
||||
public static readonly string NamespaceRootToken = PathSeparator.ToString();
|
||||
public static readonly string ServiceDefinitionsToken = String.Concat(NamespaceRootToken, "ServiceDefinitions");
|
||||
public static readonly string AccessMappingsToken = String.Concat(NamespaceRootToken, "AccessMappings");
|
||||
|
||||
// Read for ServiceDefinitions and AccessMappings
|
||||
public const Int32 Read = 1;
|
||||
// Create/Update/Delete for ServiceDefinitions and AccessMappings
|
||||
public const Int32 Write = 2;
|
||||
public const Int32 AllPermissions = Read | Write;
|
||||
}
|
||||
|
||||
[EditorBrowsable(EditorBrowsableState.Never)]
|
||||
public class GraphSecurityConstants
|
||||
{
|
||||
public static readonly Guid NamespaceId = new Guid("C2EE56C9-E8FA-4CDD-9D48-2C44F697A58E");
|
||||
public static readonly string RefsToken = "Refs";
|
||||
public static readonly string SubjectsToken = "Subjects";
|
||||
|
||||
public const int ReadByPublicIdentifier = 1;
|
||||
public const int ReadByPersonalIdentifier = 2;
|
||||
}
|
||||
|
||||
public enum WinHttpErrorCode
|
||||
{
|
||||
WINHTTP_ERROR_BASE = 12000,
|
||||
WINHTTP_ERROR_LAST = WINHTTP_ERROR_BASE + 188,
|
||||
|
||||
ERROR_WINHTTP_OUT_OF_HANDLES = WINHTTP_ERROR_BASE + 1,
|
||||
ERROR_WINHTTP_TIMEOUT = WINHTTP_ERROR_BASE + 2,
|
||||
ERROR_WINHTTP_INTERNAL_ERROR = WINHTTP_ERROR_BASE + 4,
|
||||
ERROR_WINHTTP_INVALID_URL = WINHTTP_ERROR_BASE + 5,
|
||||
ERROR_WINHTTP_UNRECOGNIZED_SCHEME = WINHTTP_ERROR_BASE + 6,
|
||||
ERROR_WINHTTP_NAME_NOT_RESOLVED = WINHTTP_ERROR_BASE + 7,
|
||||
ERROR_WINHTTP_INVALID_OPTION = WINHTTP_ERROR_BASE + 9,
|
||||
ERROR_WINHTTP_OPTION_NOT_SETTABLE = WINHTTP_ERROR_BASE + 11,
|
||||
ERROR_WINHTTP_SHUTDOWN = WINHTTP_ERROR_BASE + 12,
|
||||
ERROR_WINHTTP_LOGIN_FAILURE = WINHTTP_ERROR_BASE + 15,
|
||||
ERROR_WINHTTP_OPERATION_CANCELLED = WINHTTP_ERROR_BASE + 17,
|
||||
ERROR_WINHTTP_INCORRECT_HANDLE_TYPE = WINHTTP_ERROR_BASE + 18,
|
||||
ERROR_WINHTTP_INCORRECT_HANDLE_STATE = WINHTTP_ERROR_BASE + 19,
|
||||
ERROR_WINHTTP_CANNOT_CONNECT = WINHTTP_ERROR_BASE + 29,
|
||||
ERROR_WINHTTP_CONNECTION_ERROR = WINHTTP_ERROR_BASE + 30,
|
||||
ERROR_WINHTTP_RESEND_REQUEST = WINHTTP_ERROR_BASE + 32,
|
||||
ERROR_WINHTTP_SECURE_CERT_DATE_INVALID = WINHTTP_ERROR_BASE + 37,
|
||||
ERROR_WINHTTP_SECURE_CERT_CN_INVALID = WINHTTP_ERROR_BASE + 38,
|
||||
ERROR_WINHTTP_CLIENT_AUTH_CERT_NEEDED = WINHTTP_ERROR_BASE + 44,
|
||||
ERROR_WINHTTP_SECURE_INVALID_CA = WINHTTP_ERROR_BASE + 45,
|
||||
ERROR_WINHTTP_SECURE_CERT_REV_FAILED = WINHTTP_ERROR_BASE + 57,
|
||||
ERROR_WINHTTP_CANNOT_CALL_BEFORE_OPEN = WINHTTP_ERROR_BASE + 100,
|
||||
ERROR_WINHTTP_CANNOT_CALL_BEFORE_SEND = WINHTTP_ERROR_BASE + 101,
|
||||
ERROR_WINHTTP_CANNOT_CALL_AFTER_SEND = WINHTTP_ERROR_BASE + 102,
|
||||
ERROR_WINHTTP_CANNOT_CALL_AFTER_OPEN = WINHTTP_ERROR_BASE + 103,
|
||||
ERROR_WINHTTP_HEADER_NOT_FOUND = WINHTTP_ERROR_BASE + 150,
|
||||
ERROR_WINHTTP_INVALID_SERVER_RESPONSE = WINHTTP_ERROR_BASE + 152,
|
||||
ERROR_WINHTTP_INVALID_HEADER = WINHTTP_ERROR_BASE + 153,
|
||||
ERROR_WINHTTP_INVALID_QUERY_REQUEST = WINHTTP_ERROR_BASE + 154,
|
||||
ERROR_WINHTTP_HEADER_ALREADY_EXISTS = WINHTTP_ERROR_BASE + 155,
|
||||
ERROR_WINHTTP_REDIRECT_FAILED = WINHTTP_ERROR_BASE + 156,
|
||||
ERROR_WINHTTP_SECURE_CHANNEL_ERROR = WINHTTP_ERROR_BASE + 157,
|
||||
ERROR_WINHTTP_BAD_AUTO_PROXY_SCRIPT = WINHTTP_ERROR_BASE + 166,
|
||||
ERROR_WINHTTP_UNABLE_TO_DOWNLOAD_SCRIPT = WINHTTP_ERROR_BASE + 167,
|
||||
ERROR_WINHTTP_SECURE_INVALID_CERT = WINHTTP_ERROR_BASE + 169,
|
||||
ERROR_WINHTTP_SECURE_CERT_REVOKED = WINHTTP_ERROR_BASE + 170,
|
||||
ERROR_WINHTTP_NOT_INITIALIZED = WINHTTP_ERROR_BASE + 172,
|
||||
ERROR_WINHTTP_SECURE_FAILURE = WINHTTP_ERROR_BASE + 175,
|
||||
ERROR_WINHTTP_UNHANDLED_SCRIPT_TYPE = WINHTTP_ERROR_BASE + 176,
|
||||
ERROR_WINHTTP_SCRIPT_EXECUTION_ERROR = WINHTTP_ERROR_BASE + 177,
|
||||
ERROR_WINHTTP_AUTO_PROXY_SERVICE_ERROR = WINHTTP_ERROR_BASE + 178,
|
||||
ERROR_WINHTTP_SECURE_CERT_WRONG_USAGE = WINHTTP_ERROR_BASE + 179,
|
||||
ERROR_WINHTTP_AUTODETECTION_FAILED = WINHTTP_ERROR_BASE + 180,
|
||||
ERROR_WINHTTP_HEADER_COUNT_EXCEEDED = WINHTTP_ERROR_BASE + 181,
|
||||
ERROR_WINHTTP_HEADER_SIZE_OVERFLOW = WINHTTP_ERROR_BASE + 182,
|
||||
ERROR_WINHTTP_CHUNKED_ENCODING_HEADER_SIZE_OVERFLOW = WINHTTP_ERROR_BASE + 183,
|
||||
ERROR_WINHTTP_RESPONSE_DRAIN_OVERFLOW = WINHTTP_ERROR_BASE + 184,
|
||||
ERROR_WINHTTP_CLIENT_CERT_NO_PRIVATE_KEY = WINHTTP_ERROR_BASE + 185,
|
||||
ERROR_WINHTTP_CLIENT_CERT_NO_ACCESS_PRIVATE_KEY = WINHTTP_ERROR_BASE + 186,
|
||||
ERROR_WINHTTP_CLIENT_AUTH_CERT_NEEDED_PROXY = WINHTTP_ERROR_BASE + 187,
|
||||
ERROR_WINHTTP_SECURE_FAILURE_PROXY = WINHTTP_ERROR_BASE + 188
|
||||
}
|
||||
|
||||
public enum CurlErrorCode
|
||||
{
|
||||
CURLE_OK = 0,
|
||||
CURLE_UNSUPPORTED_PROTOCOL = 1,
|
||||
CURLE_FAILED_INIT = 2,
|
||||
CURLE_URL_MALFORMAT = 3,
|
||||
CURLE_NOT_BUILT_IN = 4,
|
||||
CURLE_COULDNT_RESOLVE_PROXY = 5,
|
||||
CURLE_COULDNT_RESOLVE_HOST = 6,
|
||||
CURLE_COULDNT_CONNECT = 7,
|
||||
CURLE_FTP_WEIRD_SERVER_REPLY = 8,
|
||||
CURLE_REMOTE_ACCESS_DENIED = 9,
|
||||
CURLE_FTP_ACCEPT_FAILED = 10,
|
||||
CURLE_FTP_WEIRD_PASS_REPLY = 11,
|
||||
CURLE_FTP_ACCEPT_TIMEOUT = 12,
|
||||
CURLE_FTP_WEIRD_PASV_REPLY = 13,
|
||||
CURLE_FTP_WEIRD_227_FORMAT = 14,
|
||||
CURLE_FTP_CANT_GET_HOST = 15,
|
||||
CURLE_HTTP2 = 16,
|
||||
CURLE_FTP_COULDNT_SET_TYPE = 17,
|
||||
CURLE_PARTIAL_FILE = 18,
|
||||
CURLE_FTP_COULDNT_RETR_FILE = 19,
|
||||
CURLE_QUOTE_ERROR = 21,
|
||||
CURLE_HTTP_RETURNED_ERROR = 22,
|
||||
CURLE_WRITE_ERROR = 23,
|
||||
CURLE_UPLOAD_FAILED = 25,
|
||||
CURLE_READ_ERROR = 26,
|
||||
CURLE_OUT_OF_MEMORY = 27,
|
||||
CURLE_OPERATION_TIMEDOUT = 28,
|
||||
CURLE_FTP_PORT_FAILED = 30,
|
||||
CURLE_FTP_COULDNT_USE_REST = 31,
|
||||
CURLE_RANGE_ERROR = 33,
|
||||
CURLE_HTTP_POST_ERROR = 34,
|
||||
CURLE_SSL_CONNECT_ERROR = 35,
|
||||
CURLE_BAD_DOWNLOAD_RESUME = 36,
|
||||
CURLE_FILE_COULDNT_READ_FILE = 37,
|
||||
CURLE_LDAP_CANNOT_BIND = 38,
|
||||
CURLE_LDAP_SEARCH_FAILED = 39,
|
||||
CURLE_FUNCTION_NOT_FOUND = 41,
|
||||
CURLE_ABORTED_BY_CALLBACK = 42,
|
||||
CURLE_BAD_FUNCTION_ARGUMENT = 43,
|
||||
CURLE_INTERFACE_FAILED = 45,
|
||||
CURLE_TOO_MANY_REDIRECTS = 47,
|
||||
CURLE_UNKNOWN_OPTION = 48,
|
||||
CURLE_TELNET_OPTION_SYNTAX = 49,
|
||||
CURLE_PEER_FAILED_VERIFICATION = 51,
|
||||
CURLE_GOT_NOTHING = 52,
|
||||
CURLE_SSL_ENGINE_NOTFOUND = 53,
|
||||
CURLE_SSL_ENGINE_SETFAILED = 54,
|
||||
CURLE_SEND_ERROR = 55,
|
||||
CURLE_RECV_ERROR = 56,
|
||||
CURLE_SSL_CERTPROBLEM = 58,
|
||||
CURLE_SSL_CIPHER = 59,
|
||||
CURLE_SSL_CACERT = 60,
|
||||
CURLE_BAD_CONTENT_ENCODING = 61,
|
||||
CURLE_LDAP_INVALID_URL = 62,
|
||||
CURLE_FILESIZE_EXCEEDED = 63,
|
||||
CURLE_USE_SSL_FAILED = 64,
|
||||
CURLE_SEND_FAIL_REWIND = 65,
|
||||
CURLE_SSL_ENGINE_INITFAILED = 66,
|
||||
CURLE_LOGIN_DENIED = 67,
|
||||
CURLE_TFTP_NOTFOUND = 68,
|
||||
CURLE_TFTP_PERM = 69,
|
||||
CURLE_REMOTE_DISK_FULL = 70,
|
||||
CURLE_TFTP_ILLEGAL = 71,
|
||||
CURLE_TFTP_UNKNOWNID = 72,
|
||||
CURLE_REMOTE_FILE_EXISTS = 73,
|
||||
CURLE_TFTP_NOSUCHUSER = 74,
|
||||
CURLE_CONV_FAILED = 75,
|
||||
CURLE_CONV_REQD = 76,
|
||||
CURLE_SSL_CACERT_BADFILE = 77,
|
||||
CURLE_REMOTE_FILE_NOT_FOUND = 78,
|
||||
CURLE_SSH = 79,
|
||||
CURLE_SSL_SHUTDOWN_FAILED = 80,
|
||||
CURLE_AGAIN = 81,
|
||||
CURLE_SSL_CRL_BADFILE = 82,
|
||||
CURLE_SSL_ISSUER_ERROR = 83,
|
||||
CURLE_FTP_PRET_FAILED = 84,
|
||||
CURLE_RTSP_CSEQ_ERROR = 85,
|
||||
CURLE_RTSP_SESSION_ERROR = 86,
|
||||
CURLE_FTP_BAD_FILE_LIST = 87,
|
||||
CURLE_CHUNK_FAILED = 88,
|
||||
CURLE_NO_CONNECTION_AVAILABLE = 89,
|
||||
CURLE_SSL_PINNEDPUBKEYNOTMATCH = 90,
|
||||
CURLE_SSL_INVALIDCERTSTATUS = 91,
|
||||
CURLE_HTTP2_STREAM = 92,
|
||||
CURLE_RECURSIVE_API_CALL = 93
|
||||
}
|
||||
}
|
||||
265
src/Sdk/Common/Common/VssException.cs
Normal file
265
src/Sdk/Common/Common/VssException.cs
Normal file
@@ -0,0 +1,265 @@
|
||||
// Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.Linq;
|
||||
using System.Reflection;
|
||||
using System.Runtime.Serialization;
|
||||
using System.Security;
|
||||
|
||||
namespace GitHub.Services.Common
|
||||
{
|
||||
/// <summary>
|
||||
/// Base class for all custom exceptions thrown from Vss and Tfs code.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// All Exceptions in the VSS space -- any exception that flows across
|
||||
/// a REST API boudary -- should derive from VssServiceException. This is likely
|
||||
/// almost ALL new exceptions. Legacy TFS exceptions that do not flow through rest
|
||||
/// derive from TeamFoundationServerException or TeamFoundationServiceException
|
||||
/// </remarks>
|
||||
[Serializable]
|
||||
[SuppressMessage("Microsoft.Usage", "CA2240:ImplementISerializableCorrectly")]
|
||||
[ExceptionMapping("0.0", "3.0", "VssException", "GitHub.Services.Common.VssException, GitHub.Services.Common, Version=14.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a")]
|
||||
public abstract class VssException : ApplicationException
|
||||
{
|
||||
/// <summary>
|
||||
/// No-arg constructor that sumply defers to the base class.
|
||||
/// </summary>
|
||||
public VssException() : base()
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initializes an exception with the specified error message.
|
||||
/// </summary>
|
||||
/// <param name="errorCode">Application-defined error code for this exception</param>
|
||||
public VssException(int errorCode) : this(errorCode, false)
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initializes an exception with the specified error message.
|
||||
/// </summary>
|
||||
/// <param name="errorCode">Application-defined error code for this exception</param>
|
||||
/// <param name="logException">Indicate whether this exception should be logged</param>
|
||||
public VssException(int errorCode, bool logException)
|
||||
{
|
||||
ErrorCode = errorCode;
|
||||
LogException = logException;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initializes an exception with the specified error message.
|
||||
/// </summary>
|
||||
/// <param name="message">A human readable message that describes the error</param>
|
||||
public VssException(string message) : base(SecretUtility.ScrubSecrets(message))
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initializes an exception with the specified error message and an inner exception that caused this exception to be raised.
|
||||
/// </summary>
|
||||
/// <param name="message">A human readable message that describes the error</param>
|
||||
/// <param name="innerException"></param>
|
||||
public VssException(string message, Exception innerException) : base(SecretUtility.ScrubSecrets(message), innerException)
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initializes an exception with the specified error message and an inner exception that caused this exception to be raised.
|
||||
/// </summary>
|
||||
/// <param name="message">A human readable message that describes the error</param>
|
||||
/// <param name="errorCode">Application defined error code</param>
|
||||
/// <param name="innerException"></param>
|
||||
public VssException(string message, int errorCode, Exception innerException) : this(message, innerException)
|
||||
{
|
||||
ErrorCode = errorCode;
|
||||
LogException = false;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initializes an exception with the specified error message and an inner exception that caused this exception to be raised.
|
||||
/// </summary>
|
||||
/// <param name="message">A human readable message that describes the error</param>
|
||||
/// <param name="errorCode">Application defined error code</param>
|
||||
public VssException(string message, int errorCode) : this(message, errorCode, false)
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initializes an exception with the specified error message and an inner exception that caused this exception to be raised.
|
||||
/// </summary>
|
||||
/// <param name="message">A human readable message that describes the error</param>
|
||||
/// <param name="errorCode">Application defined error code</param>
|
||||
/// <param name="logException">Indicate whether this exception should be logged</param>
|
||||
public VssException(string message, int errorCode, bool logException) : this(message)
|
||||
{
|
||||
ErrorCode = errorCode;
|
||||
LogException = logException;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initializes an exception with the specified error message and an inner exception that caused this exception to be raised.
|
||||
/// </summary>
|
||||
/// <param name="message">A human readable message that describes the error</param>
|
||||
/// <param name="errorCode">Application defined error code</param>
|
||||
/// <param name="logException"></param>
|
||||
/// <param name="innerException"></param>
|
||||
public VssException(string message, int errorCode, bool logException, Exception innerException) : this(message, innerException)
|
||||
{
|
||||
ErrorCode = errorCode;
|
||||
LogException = logException;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initializes an exception from serialized data
|
||||
/// </summary>
|
||||
/// <param name="info">object holding the serialized data</param>
|
||||
/// <param name="context">context info about the source or destination</param>
|
||||
protected VssException(SerializationInfo info, StreamingContext context)
|
||||
: base(info, context)
|
||||
{
|
||||
LogException = (bool)info.GetValue("m_logException", typeof(bool));
|
||||
ReportException = (bool)info.GetValue("m_reportException", typeof(bool));
|
||||
ErrorCode = (int)info.GetValue("m_errorCode", typeof(int));
|
||||
EventId = (int)info.GetValue("m_eventId", typeof(int));
|
||||
}
|
||||
|
||||
[SecurityCritical]
|
||||
public override void GetObjectData(SerializationInfo info, StreamingContext context)
|
||||
{
|
||||
base.GetObjectData(info, context);
|
||||
info.AddValue("m_logException", LogException);
|
||||
info.AddValue("m_reportException", ReportException);
|
||||
info.AddValue("m_errorCode", ErrorCode);
|
||||
info.AddValue("m_eventId", EventId);
|
||||
}
|
||||
|
||||
/// <summary>Indicate whether this exception instance should be logged</summary>
|
||||
/// <value>True (false) if the exception should (should not) be logged</value>
|
||||
public bool LogException
|
||||
{
|
||||
get
|
||||
{
|
||||
return m_logException;
|
||||
}
|
||||
set
|
||||
{
|
||||
m_logException = value;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>A user-defined error code.</summary>
|
||||
public int ErrorCode
|
||||
{
|
||||
get
|
||||
{
|
||||
return m_errorCode;
|
||||
}
|
||||
set
|
||||
{
|
||||
m_errorCode = value;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>The event ID to report if the exception is marked for the event log</summary>
|
||||
/// <value>The event ID used in the entry added to the event log</value>
|
||||
public int EventId
|
||||
{
|
||||
get
|
||||
{
|
||||
return m_eventId;
|
||||
}
|
||||
set
|
||||
{
|
||||
m_eventId = value;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Indicate whether the exception should be reported through Dr. Watson</summary>
|
||||
/// <value>True (false) if the exception should (should not) be reported</value>
|
||||
public bool ReportException
|
||||
{
|
||||
get
|
||||
{
|
||||
return m_reportException;
|
||||
}
|
||||
set
|
||||
{
|
||||
m_reportException = value;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the default serialized type name and type key for the given exception type.
|
||||
/// </summary>
|
||||
internal static void GetTypeNameAndKeyForExceptionType(Type exceptionType, Version restApiVersion, out String typeName, out String typeKey)
|
||||
{
|
||||
typeName = null;
|
||||
typeKey = exceptionType.Name;
|
||||
if (restApiVersion != null)
|
||||
{
|
||||
IEnumerable<ExceptionMappingAttribute> exceptionAttributes = exceptionType.GetTypeInfo().GetCustomAttributes<ExceptionMappingAttribute>().Where(ea => ea.MinApiVersion <= restApiVersion && ea.ExclusiveMaxApiVersion > restApiVersion);
|
||||
if (exceptionAttributes.Any())
|
||||
{
|
||||
ExceptionMappingAttribute exceptionAttribute = exceptionAttributes.First();
|
||||
typeName = exceptionAttribute.TypeName;
|
||||
typeKey = exceptionAttribute.TypeKey;
|
||||
}
|
||||
else if (restApiVersion < s_backCompatExclusiveMaxVersion) //if restApiVersion < 3 we send the assembly qualified name with the current binary version switched out to 14
|
||||
{
|
||||
typeName = GetBackCompatAssemblyQualifiedName(exceptionType);
|
||||
}
|
||||
}
|
||||
|
||||
if (typeName == null)
|
||||
{
|
||||
|
||||
AssemblyName asmName = exceptionType.GetTypeInfo().Assembly.GetName();
|
||||
if (asmName != null)
|
||||
{
|
||||
//going forward we send "FullName" and simple assembly name which includes no version.
|
||||
typeName = exceptionType.FullName + ", " + asmName.Name;
|
||||
}
|
||||
else
|
||||
{
|
||||
String assemblyString = exceptionType.GetTypeInfo().Assembly.FullName;
|
||||
assemblyString = assemblyString.Substring(0, assemblyString.IndexOf(','));
|
||||
typeName = exceptionType.FullName + ", " + assemblyString;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
internal static String GetBackCompatAssemblyQualifiedName(Type type)
|
||||
{
|
||||
AssemblyName current = type.GetTypeInfo().Assembly.GetName();
|
||||
if (current != null)
|
||||
{
|
||||
AssemblyName old = current;
|
||||
old.Version = new Version(c_backCompatVersion, 0, 0, 0);
|
||||
return Assembly.CreateQualifiedName(old.ToString(), type.FullName);
|
||||
}
|
||||
else
|
||||
{
|
||||
//this is probably not necessary...
|
||||
return type.AssemblyQualifiedName.Replace(c_currentAssemblyMajorVersionString, c_backCompatVersionString);
|
||||
}
|
||||
}
|
||||
|
||||
private const String c_currentAssemblyMajorVersionString = "Version=" + GeneratedVersionInfo.AssemblyMajorVersion;
|
||||
private const String c_backCompatVersionString = "Version=14";
|
||||
private const int c_backCompatVersion = 14;
|
||||
|
||||
private static Version s_backCompatExclusiveMaxVersion = new Version(3, 0);
|
||||
private bool m_logException;
|
||||
private bool m_reportException;
|
||||
private int m_errorCode;
|
||||
|
||||
private int m_eventId = DefaultExceptionEventId;
|
||||
|
||||
//From EventLog.cs in Framework.
|
||||
public const int DefaultExceptionEventId = 3000;
|
||||
}
|
||||
}
|
||||
629
src/Sdk/Common/Common/VssHttpMessageHandler.cs
Normal file
629
src/Sdk/Common/Common/VssHttpMessageHandler.cs
Normal file
@@ -0,0 +1,629 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Net;
|
||||
using System.Net.Http;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using GitHub.Services.Common.Diagnostics;
|
||||
using GitHub.Services.Common.Internal;
|
||||
|
||||
namespace GitHub.Services.Common
|
||||
{
|
||||
/// <summary>
|
||||
/// Provides authentication for Visual Studio Services.
|
||||
/// </summary>
|
||||
public class VssHttpMessageHandler : HttpMessageHandler
|
||||
{
|
||||
/// <summary>
|
||||
/// Initializes a new <c>VssHttpMessageHandler</c> instance with default credentials and request
|
||||
/// settings.
|
||||
/// </summary>
|
||||
public VssHttpMessageHandler()
|
||||
: this(new VssCredentials(), new VssHttpRequestSettings())
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new <c>VssHttpMessageHandler</c> instance with the specified credentials and request
|
||||
/// settings.
|
||||
/// </summary>
|
||||
/// <param name="credentials">The credentials which should be used</param>
|
||||
/// <param name="settings">The request settings which should be used</param>
|
||||
public VssHttpMessageHandler(
|
||||
VssCredentials credentials,
|
||||
VssHttpRequestSettings settings)
|
||||
: this(credentials, settings, new HttpClientHandler())
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new <c>VssHttpMessageHandler</c> instance with the specified credentials and request
|
||||
/// settings.
|
||||
/// </summary>
|
||||
/// <param name="credentials">The credentials which should be used</param>
|
||||
/// <param name="settings">The request settings which should be used</param>
|
||||
/// <param name="innerHandler"></param>
|
||||
public VssHttpMessageHandler(
|
||||
VssCredentials credentials,
|
||||
VssHttpRequestSettings settings,
|
||||
HttpMessageHandler innerHandler)
|
||||
{
|
||||
this.Credentials = credentials;
|
||||
this.Settings = settings;
|
||||
this.ExpectContinue = settings.ExpectContinue;
|
||||
|
||||
m_credentialWrapper = new CredentialWrapper();
|
||||
m_messageInvoker = new HttpMessageInvoker(innerHandler);
|
||||
|
||||
// If we were given a pipeline make sure we find the inner-most handler to apply our settings as this
|
||||
// will be the actual outgoing transport.
|
||||
{
|
||||
HttpMessageHandler transportHandler = innerHandler;
|
||||
DelegatingHandler delegatingHandler = transportHandler as DelegatingHandler;
|
||||
while (delegatingHandler != null)
|
||||
{
|
||||
transportHandler = delegatingHandler.InnerHandler;
|
||||
delegatingHandler = transportHandler as DelegatingHandler;
|
||||
}
|
||||
|
||||
m_transportHandler = transportHandler;
|
||||
}
|
||||
|
||||
ApplySettings(m_transportHandler, m_credentialWrapper, this.Settings);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the credentials associated with this handler.
|
||||
/// </summary>
|
||||
public VssCredentials Credentials
|
||||
{
|
||||
get;
|
||||
private set;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the settings associated with this handler.
|
||||
/// </summary>
|
||||
public VssHttpRequestSettings Settings
|
||||
{
|
||||
get;
|
||||
private set;
|
||||
}
|
||||
|
||||
private Boolean ExpectContinue
|
||||
{
|
||||
get;
|
||||
set;
|
||||
}
|
||||
|
||||
protected override void Dispose(Boolean disposing)
|
||||
{
|
||||
base.Dispose(disposing);
|
||||
|
||||
if (disposing)
|
||||
{
|
||||
if (m_messageInvoker != null)
|
||||
{
|
||||
m_messageInvoker.Dispose();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
internal static readonly String PropertyName = "MS.VS.MessageHandler";
|
||||
|
||||
/// <summary>
|
||||
/// Handles the authentication hand-shake for a Visual Studio service.
|
||||
/// </summary>
|
||||
/// <param name="request">The HTTP request message</param>
|
||||
/// <param name="cancellationToken">The cancellation token used for cooperative cancellation</param>
|
||||
/// <returns>A new <c>Task<HttpResponseMessage></c> which wraps the response from the remote service</returns>
|
||||
protected override async Task<HttpResponseMessage> SendAsync(
|
||||
HttpRequestMessage request,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
VssTraceActivity traceActivity = VssTraceActivity.Current;
|
||||
|
||||
var traceInfo = VssHttpMessageHandlerTraceInfo.GetTraceInfo(request);
|
||||
traceInfo?.TraceHandlerStartTime();
|
||||
|
||||
if (!m_appliedClientCertificatesToTransportHandler &&
|
||||
request.RequestUri.Scheme == "https")
|
||||
{
|
||||
HttpClientHandler httpClientHandler = m_transportHandler as HttpClientHandler;
|
||||
if (httpClientHandler != null &&
|
||||
this.Settings.ClientCertificateManager != null &&
|
||||
this.Settings.ClientCertificateManager.ClientCertificates != null &&
|
||||
this.Settings.ClientCertificateManager.ClientCertificates.Count > 0)
|
||||
{
|
||||
httpClientHandler.ClientCertificates.AddRange(this.Settings.ClientCertificateManager.ClientCertificates);
|
||||
}
|
||||
m_appliedClientCertificatesToTransportHandler = true;
|
||||
}
|
||||
|
||||
if (!m_appliedServerCertificateValidationCallbackToTransportHandler &&
|
||||
request.RequestUri.Scheme == "https")
|
||||
{
|
||||
HttpClientHandler httpClientHandler = m_transportHandler as HttpClientHandler;
|
||||
if (httpClientHandler != null &&
|
||||
this.Settings.ServerCertificateValidationCallback != null)
|
||||
{
|
||||
httpClientHandler.ServerCertificateCustomValidationCallback = this.Settings.ServerCertificateValidationCallback;
|
||||
}
|
||||
m_appliedServerCertificateValidationCallbackToTransportHandler = true;
|
||||
}
|
||||
|
||||
// The .NET Core 2.1 runtime switched its HTTP default from HTTP 1.1 to HTTP 2.
|
||||
// This causes problems with some versions of the Curl handler on Linux.
|
||||
// See GitHub issue https://github.com/dotnet/corefx/issues/32376
|
||||
if (Settings.UseHttp11)
|
||||
{
|
||||
request.Version = HttpVersion.Version11;
|
||||
}
|
||||
|
||||
IssuedToken token = null;
|
||||
IssuedTokenProvider provider;
|
||||
if (this.Credentials.TryGetTokenProvider(request.RequestUri, out provider))
|
||||
{
|
||||
token = provider.CurrentToken;
|
||||
}
|
||||
|
||||
// Add ourselves to the message so the underlying token issuers may use it if necessary
|
||||
request.Properties[VssHttpMessageHandler.PropertyName] = this;
|
||||
|
||||
Boolean succeeded = false;
|
||||
Boolean lastResponseDemandedProxyAuth = false;
|
||||
Int32 retries = m_maxAuthRetries;
|
||||
HttpResponseMessage response = null;
|
||||
HttpResponseMessageWrapper responseWrapper;
|
||||
CancellationTokenSource tokenSource = null;
|
||||
|
||||
try
|
||||
{
|
||||
tokenSource = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken);
|
||||
|
||||
if (this.Settings.SendTimeout > TimeSpan.Zero)
|
||||
{
|
||||
tokenSource.CancelAfter(this.Settings.SendTimeout);
|
||||
}
|
||||
|
||||
do
|
||||
{
|
||||
if (response != null)
|
||||
{
|
||||
response.Dispose();
|
||||
}
|
||||
|
||||
ApplyHeaders(request);
|
||||
|
||||
// In the case of a Windows token, only apply it to the web proxy if it
|
||||
// returned a 407 Proxy Authentication Required. If we didn't get this
|
||||
// status code back, then the proxy (if there is one) is clearly working fine,
|
||||
// so we shouldn't mess with its credentials.
|
||||
ApplyToken(request, token, applyICredentialsToWebProxy: lastResponseDemandedProxyAuth);
|
||||
lastResponseDemandedProxyAuth = false;
|
||||
|
||||
// The WinHttpHandler will chunk any content that does not have a computed length which is
|
||||
// not what we want. By loading into a buffer up-front we bypass this behavior and there is
|
||||
// no difference in the normal HttpClientHandler behavior here since this is what they were
|
||||
// already doing.
|
||||
await BufferRequestContentAsync(request, tokenSource.Token).ConfigureAwait(false);
|
||||
|
||||
traceInfo?.TraceBufferedRequestTime();
|
||||
|
||||
// ConfigureAwait(false) enables the continuation to be run outside any captured
|
||||
// SyncronizationContext (such as ASP.NET's) which keeps things from deadlocking...
|
||||
response = await m_messageInvoker.SendAsync(request, tokenSource.Token).ConfigureAwait(false);
|
||||
|
||||
traceInfo?.TraceRequestSendTime();
|
||||
|
||||
// Now buffer the response content if configured to do so. In general we will be buffering
|
||||
// the response content in this location, except in the few cases where the caller has
|
||||
// specified HttpCompletionOption.ResponseHeadersRead.
|
||||
// Trace content type in case of error
|
||||
await BufferResponseContentAsync(request, response, () => $"[ContentType: {response.Content.GetType().Name}]", tokenSource.Token).ConfigureAwait(false);
|
||||
|
||||
traceInfo?.TraceResponseContentTime();
|
||||
|
||||
responseWrapper = new HttpResponseMessageWrapper(response);
|
||||
|
||||
if (!this.Credentials.IsAuthenticationChallenge(responseWrapper))
|
||||
{
|
||||
// Validate the token after it has been successfully authenticated with the server.
|
||||
if (provider != null)
|
||||
{
|
||||
provider.ValidateToken(token, responseWrapper);
|
||||
}
|
||||
|
||||
// Make sure that once we can authenticate with the service that we turn off the
|
||||
// Expect100Continue behavior to increase performance.
|
||||
this.ExpectContinue = false;
|
||||
succeeded = true;
|
||||
break;
|
||||
}
|
||||
else
|
||||
{
|
||||
// In the case of a Windows token, only apply it to the web proxy if it
|
||||
// returned a 407 Proxy Authentication Required. If we didn't get this
|
||||
// status code back, then the proxy (if there is one) is clearly working fine,
|
||||
// so we shouldn't mess with its credentials.
|
||||
lastResponseDemandedProxyAuth = responseWrapper.StatusCode == HttpStatusCode.ProxyAuthenticationRequired;
|
||||
|
||||
// Invalidate the token and ensure that we have the correct token provider for the challenge
|
||||
// which we just received
|
||||
VssHttpEventSource.Log.AuthenticationFailed(traceActivity, response);
|
||||
|
||||
if (provider != null)
|
||||
{
|
||||
provider.InvalidateToken(token);
|
||||
}
|
||||
|
||||
// Ensure we have an appropriate token provider for the current challenge
|
||||
provider = this.Credentials.CreateTokenProvider(request.RequestUri, responseWrapper, token);
|
||||
|
||||
// Make sure we don't invoke the provider in an invalid state
|
||||
if (provider == null)
|
||||
{
|
||||
VssHttpEventSource.Log.IssuedTokenProviderNotFound(traceActivity);
|
||||
break;
|
||||
}
|
||||
else if (provider.GetTokenIsInteractive && this.Credentials.PromptType == CredentialPromptType.DoNotPrompt)
|
||||
{
|
||||
VssHttpEventSource.Log.IssuedTokenProviderPromptRequired(traceActivity, provider);
|
||||
break;
|
||||
}
|
||||
|
||||
// If the user has already tried once but still unauthorized, stop retrying. The main scenario for this condition
|
||||
// is a user typed in a valid credentials for a hosted account but the associated identity does not have
|
||||
// access. We do not want to continually prompt 3 times without telling them the failure reason. In the
|
||||
// next release we should rethink about presenting user the failure and options between retries.
|
||||
IEnumerable<String> headerValues;
|
||||
Boolean hasAuthenticateError =
|
||||
response.Headers.TryGetValues(HttpHeaders.VssAuthenticateError, out headerValues) &&
|
||||
!String.IsNullOrEmpty(headerValues.FirstOrDefault());
|
||||
|
||||
if (retries == 0 || (retries < m_maxAuthRetries && hasAuthenticateError))
|
||||
{
|
||||
break;
|
||||
}
|
||||
|
||||
// Now invoke the provider and await the result
|
||||
token = await provider.GetTokenAsync(token, tokenSource.Token).ConfigureAwait(false);
|
||||
|
||||
// I always see 0 here, but the method above could take more time so keep for now
|
||||
traceInfo?.TraceGetTokenTime();
|
||||
|
||||
// If we just received a token, lets ask the server for the VSID
|
||||
request.Headers.Add(HttpHeaders.VssUserData, String.Empty);
|
||||
|
||||
retries--;
|
||||
}
|
||||
}
|
||||
while (retries >= 0);
|
||||
|
||||
if (traceInfo != null)
|
||||
{
|
||||
traceInfo.TokenRetries = m_maxAuthRetries - retries;
|
||||
}
|
||||
|
||||
// We're out of retries and the response was an auth challenge -- then the request was unauthorized
|
||||
// and we will throw a strongly-typed exception with a friendly error message.
|
||||
if (!succeeded && response != null && this.Credentials.IsAuthenticationChallenge(responseWrapper))
|
||||
{
|
||||
String message = null;
|
||||
IEnumerable<String> serviceError;
|
||||
|
||||
if (response.Headers.TryGetValues(HttpHeaders.TfsServiceError, out serviceError))
|
||||
{
|
||||
message = UriUtility.UrlDecode(serviceError.FirstOrDefault());
|
||||
}
|
||||
else
|
||||
{
|
||||
message = CommonResources.VssUnauthorized(request.RequestUri.GetLeftPart(UriPartial.Authority));
|
||||
}
|
||||
|
||||
// Make sure we do not leak the response object when raising an exception
|
||||
if (response != null)
|
||||
{
|
||||
response.Dispose();
|
||||
}
|
||||
|
||||
VssHttpEventSource.Log.HttpRequestUnauthorized(traceActivity, request, message);
|
||||
VssUnauthorizedException unauthorizedException = new VssUnauthorizedException(message);
|
||||
|
||||
if (provider != null)
|
||||
{
|
||||
unauthorizedException.Data.Add(CredentialsType, provider.CredentialType);
|
||||
}
|
||||
|
||||
throw unauthorizedException;
|
||||
}
|
||||
|
||||
return response;
|
||||
}
|
||||
catch (OperationCanceledException ex)
|
||||
{
|
||||
if (cancellationToken.IsCancellationRequested)
|
||||
{
|
||||
VssHttpEventSource.Log.HttpRequestCancelled(traceActivity, request);
|
||||
throw;
|
||||
}
|
||||
else
|
||||
{
|
||||
VssHttpEventSource.Log.HttpRequestTimedOut(traceActivity, request, this.Settings.SendTimeout);
|
||||
throw new TimeoutException(CommonResources.HttpRequestTimeout(this.Settings.SendTimeout), ex);
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
// We always dispose of the token source since otherwise we leak resources if there is a timer pending
|
||||
if (tokenSource != null)
|
||||
{
|
||||
tokenSource.Dispose();
|
||||
}
|
||||
|
||||
traceInfo?.TraceTrailingTime();
|
||||
}
|
||||
}
|
||||
|
||||
private static async Task BufferRequestContentAsync(
|
||||
HttpRequestMessage request,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
if (request.Content != null &&
|
||||
request.Headers.TransferEncodingChunked != true)
|
||||
{
|
||||
Int64? contentLength = request.Content.Headers.ContentLength;
|
||||
if (contentLength == null)
|
||||
{
|
||||
await request.Content.LoadIntoBufferAsync().EnforceCancellation(cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
// Explicitly turn off chunked encoding since we have computed the request content size
|
||||
request.Headers.TransferEncodingChunked = false;
|
||||
}
|
||||
}
|
||||
|
||||
protected virtual async Task BufferResponseContentAsync(
|
||||
HttpRequestMessage request,
|
||||
HttpResponseMessage response,
|
||||
Func<string> makeErrorMessage,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
// Determine whether or not we should go ahead and buffer the output under our timeout scope. If
|
||||
// we do not perform this action here there is a potential network stack hang since we override
|
||||
// the HttpClient.SendTimeout value and the cancellation token for monitoring request timeout does
|
||||
// not survive beyond this scope.
|
||||
if (response == null || response.StatusCode == HttpStatusCode.NoContent || response.Content == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// Do not try to buffer with a size of 0. This forces all calls to effectively use the behavior of
|
||||
// HttpCompletionOption.ResponseHeadersRead if that is desired.
|
||||
if (this.Settings.MaxContentBufferSize == 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// Read the completion option provided by the caller. If we don't find the property then we
|
||||
// assume it is OK to buffer by default.
|
||||
HttpCompletionOption completionOption;
|
||||
if (!request.Properties.TryGetValue(VssHttpRequestSettings.HttpCompletionOptionPropertyName, out completionOption))
|
||||
{
|
||||
completionOption = HttpCompletionOption.ResponseContentRead;
|
||||
}
|
||||
|
||||
// If the caller specified that response content should be read then we need to go ahead and
|
||||
// buffer it all up to the maximum buffer size specified by the settings. Anything larger than
|
||||
// the maximum will trigger an error in the underlying stack.
|
||||
if (completionOption == HttpCompletionOption.ResponseContentRead)
|
||||
{
|
||||
await response.Content.LoadIntoBufferAsync(this.Settings.MaxContentBufferSize).EnforceCancellation(cancellationToken, makeErrorMessage).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
|
||||
private void ApplyHeaders(HttpRequestMessage request)
|
||||
{
|
||||
if (this.Settings.ApplyTo(request))
|
||||
{
|
||||
VssTraceActivity activity = request.GetActivity();
|
||||
if (activity != null &&
|
||||
activity != VssTraceActivity.Empty &&
|
||||
!request.Headers.Contains(HttpHeaders.TfsSessionHeader))
|
||||
{
|
||||
request.Headers.Add(HttpHeaders.TfsSessionHeader, activity.Id.ToString("D"));
|
||||
}
|
||||
|
||||
request.Headers.ExpectContinue = this.ExpectContinue;
|
||||
}
|
||||
}
|
||||
|
||||
private void ApplyToken(
|
||||
HttpRequestMessage request,
|
||||
IssuedToken token,
|
||||
bool applyICredentialsToWebProxy = false)
|
||||
{
|
||||
if (token == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
ICredentials credentialsToken = token as ICredentials;
|
||||
if (credentialsToken != null)
|
||||
{
|
||||
if (applyICredentialsToWebProxy)
|
||||
{
|
||||
HttpClientHandler httpClientHandler = m_transportHandler as HttpClientHandler;
|
||||
|
||||
if (httpClientHandler != null &&
|
||||
httpClientHandler.Proxy != null)
|
||||
{
|
||||
httpClientHandler.Proxy.Credentials = credentialsToken;
|
||||
}
|
||||
}
|
||||
|
||||
m_credentialWrapper.InnerCredentials = credentialsToken;
|
||||
}
|
||||
else
|
||||
{
|
||||
token.ApplyTo(new HttpRequestMessageWrapper(request));
|
||||
}
|
||||
}
|
||||
|
||||
private static void ApplySettings(
|
||||
HttpMessageHandler handler,
|
||||
ICredentials defaultCredentials,
|
||||
VssHttpRequestSettings settings)
|
||||
{
|
||||
HttpClientHandler httpClientHandler = handler as HttpClientHandler;
|
||||
if (httpClientHandler != null)
|
||||
{
|
||||
httpClientHandler.AllowAutoRedirect = settings.AllowAutoRedirect;
|
||||
httpClientHandler.ClientCertificateOptions = ClientCertificateOption.Manual;
|
||||
//Setting httpClientHandler.UseDefaultCredentials to false in .Net Core, clears httpClientHandler.Credentials if
|
||||
//credentials is already set to defaultcredentials. Therefore httpClientHandler.Credentials must be
|
||||
//set after httpClientHandler.UseDefaultCredentials.
|
||||
httpClientHandler.UseDefaultCredentials = false;
|
||||
httpClientHandler.Credentials = defaultCredentials;
|
||||
httpClientHandler.PreAuthenticate = false;
|
||||
httpClientHandler.Proxy = DefaultWebProxy;
|
||||
httpClientHandler.UseCookies = false;
|
||||
httpClientHandler.UseProxy = true;
|
||||
|
||||
if (settings.CompressionEnabled)
|
||||
{
|
||||
httpClientHandler.AutomaticDecompression = DecompressionMethods.GZip;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// setting this to WebRequest.DefaultWebProxy in NETSTANDARD is causing a System.PlatformNotSupportedException
|
||||
//.in System.Net.SystemWebProxy.IsBypassed. Comment in IsBypassed method indicates ".NET Core and .NET Native
|
||||
// code will handle this exception and call into WinInet/WinHttp as appropriate to use the system proxy."
|
||||
// This needs to be investigated further.
|
||||
private static IWebProxy s_defaultWebProxy = null;
|
||||
|
||||
/// <summary>
|
||||
/// Allows you to set a proxy to be used by all VssHttpMessageHandler requests without affecting the global WebRequest.DefaultWebProxy. If not set it returns the WebRequest.DefaultWebProxy.
|
||||
/// </summary>
|
||||
public static IWebProxy DefaultWebProxy
|
||||
{
|
||||
get
|
||||
{
|
||||
var toReturn = WebProxyWrapper.Wrap(s_defaultWebProxy);
|
||||
|
||||
if (null != toReturn &&
|
||||
toReturn.Credentials == null)
|
||||
{
|
||||
toReturn.Credentials = CredentialCache.DefaultCredentials;
|
||||
}
|
||||
|
||||
return toReturn;
|
||||
}
|
||||
set
|
||||
{
|
||||
s_defaultWebProxy = value;
|
||||
}
|
||||
}
|
||||
|
||||
internal const String CredentialsType = nameof(CredentialsType);
|
||||
|
||||
private const Int32 m_maxAuthRetries = 3;
|
||||
private HttpMessageInvoker m_messageInvoker;
|
||||
private CredentialWrapper m_credentialWrapper;
|
||||
private bool m_appliedClientCertificatesToTransportHandler;
|
||||
private bool m_appliedServerCertificateValidationCallbackToTransportHandler;
|
||||
private readonly HttpMessageHandler m_transportHandler;
|
||||
|
||||
//.Net Core does not attempt NTLM schema on Linux, unless ICredentials is a CredentialCache instance
|
||||
//This workaround may not be needed after this corefx fix is consumed: https://github.com/dotnet/corefx/pull/7923
|
||||
private sealed class CredentialWrapper : CredentialCache, ICredentials
|
||||
{
|
||||
public ICredentials InnerCredentials
|
||||
{
|
||||
get;
|
||||
set;
|
||||
}
|
||||
|
||||
NetworkCredential ICredentials.GetCredential(
|
||||
Uri uri,
|
||||
String authType)
|
||||
{
|
||||
return InnerCredentials != null ? InnerCredentials.GetCredential(uri, authType) : null;
|
||||
}
|
||||
}
|
||||
|
||||
private sealed class WebProxyWrapper : IWebProxy
|
||||
{
|
||||
private WebProxyWrapper(IWebProxy toWrap)
|
||||
{
|
||||
m_wrapped = toWrap;
|
||||
m_credentials = null;
|
||||
}
|
||||
|
||||
public static WebProxyWrapper Wrap(IWebProxy toWrap)
|
||||
{
|
||||
if (null == toWrap)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
return new WebProxyWrapper(toWrap);
|
||||
}
|
||||
|
||||
public ICredentials Credentials
|
||||
{
|
||||
get
|
||||
{
|
||||
ICredentials credentials = m_credentials;
|
||||
|
||||
if (null == credentials)
|
||||
{
|
||||
// This means to fall back to the Credentials from the wrapped
|
||||
// IWebProxy.
|
||||
credentials = m_wrapped.Credentials;
|
||||
}
|
||||
else if (Object.ReferenceEquals(credentials, m_nullCredentials))
|
||||
{
|
||||
// This sentinel value means we have explicitly had our credentials
|
||||
// set to null.
|
||||
credentials = null;
|
||||
}
|
||||
|
||||
return credentials;
|
||||
}
|
||||
|
||||
set
|
||||
{
|
||||
if (null == value)
|
||||
{
|
||||
// Use this as a sentinel value to distinguish the case when someone has
|
||||
// explicitly set our credentials to null. We don't want to fall back to
|
||||
// m_wrapped.Credentials when we have credentials that are explicitly null.
|
||||
m_credentials = m_nullCredentials;
|
||||
}
|
||||
else
|
||||
{
|
||||
m_credentials = value;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public Uri GetProxy(Uri destination)
|
||||
{
|
||||
return m_wrapped.GetProxy(destination);
|
||||
}
|
||||
|
||||
public bool IsBypassed(Uri host)
|
||||
{
|
||||
return m_wrapped.IsBypassed(host);
|
||||
}
|
||||
|
||||
private readonly IWebProxy m_wrapped;
|
||||
private ICredentials m_credentials;
|
||||
|
||||
private static readonly ICredentials m_nullCredentials = new CredentialWrapper();
|
||||
}
|
||||
}
|
||||
}
|
||||
109
src/Sdk/Common/Common/VssHttpMessageHandlerTraceInfo.cs
Normal file
109
src/Sdk/Common/Common/VssHttpMessageHandlerTraceInfo.cs
Normal file
@@ -0,0 +1,109 @@
|
||||
using System;
|
||||
using System.Net.Http;
|
||||
|
||||
namespace GitHub.Services.Common
|
||||
{
|
||||
/// <summary>
|
||||
/// This class is used by the message handler, if injected as a request property, to trace additional
|
||||
/// timing details for outgoing requests. This information is added to the HttpOutgoingRequest logs
|
||||
/// </summary>
|
||||
public class VssHttpMessageHandlerTraceInfo
|
||||
{
|
||||
DateTime _lastTime;
|
||||
|
||||
static readonly String TfsTraceInfoKey = "TFS_TraceInfo";
|
||||
|
||||
public int TokenRetries { get; internal set; }
|
||||
|
||||
public TimeSpan HandlerStartTime { get; private set; }
|
||||
public TimeSpan BufferedRequestTime { get; private set; }
|
||||
public TimeSpan RequestSendTime { get; private set; }
|
||||
public TimeSpan ResponseContentTime { get; private set; }
|
||||
public TimeSpan GetTokenTime { get; private set; }
|
||||
public TimeSpan TrailingTime { get; private set; }
|
||||
|
||||
public VssHttpMessageHandlerTraceInfo()
|
||||
{
|
||||
_lastTime = DateTime.UtcNow;
|
||||
}
|
||||
|
||||
internal void TraceHandlerStartTime()
|
||||
{
|
||||
var previous = _lastTime;
|
||||
_lastTime = DateTime.UtcNow;
|
||||
HandlerStartTime += (_lastTime - previous);
|
||||
}
|
||||
|
||||
internal void TraceBufferedRequestTime()
|
||||
{
|
||||
var previous = _lastTime;
|
||||
_lastTime = DateTime.UtcNow;
|
||||
BufferedRequestTime += (_lastTime - previous);
|
||||
}
|
||||
|
||||
internal void TraceRequestSendTime()
|
||||
{
|
||||
var previous = _lastTime;
|
||||
_lastTime = DateTime.UtcNow;
|
||||
RequestSendTime += (_lastTime - previous);
|
||||
}
|
||||
|
||||
internal void TraceResponseContentTime()
|
||||
{
|
||||
var previous = _lastTime;
|
||||
_lastTime = DateTime.UtcNow;
|
||||
ResponseContentTime += (_lastTime - previous);
|
||||
}
|
||||
|
||||
internal void TraceGetTokenTime()
|
||||
{
|
||||
var previous = _lastTime;
|
||||
_lastTime = DateTime.UtcNow;
|
||||
GetTokenTime += (_lastTime - previous);
|
||||
}
|
||||
|
||||
internal void TraceTrailingTime()
|
||||
{
|
||||
var previous = _lastTime;
|
||||
_lastTime = DateTime.UtcNow;
|
||||
TrailingTime += (_lastTime - previous);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Set the provided traceInfo as a property on a request message (if not already set)
|
||||
/// </summary>
|
||||
/// <param name="message"></param>
|
||||
/// <param name="traceInfo"></param>
|
||||
public static void SetTraceInfo(HttpRequestMessage message, VssHttpMessageHandlerTraceInfo traceInfo)
|
||||
{
|
||||
object existingTraceInfo;
|
||||
if (!message.Properties.TryGetValue(TfsTraceInfoKey, out existingTraceInfo))
|
||||
{
|
||||
message.Properties.Add(TfsTraceInfoKey, traceInfo);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get VssHttpMessageHandlerTraceInfo from request message, or return null if none found
|
||||
/// </summary>
|
||||
/// <param name="message"></param>
|
||||
/// <returns></returns>
|
||||
public static VssHttpMessageHandlerTraceInfo GetTraceInfo(HttpRequestMessage message)
|
||||
{
|
||||
VssHttpMessageHandlerTraceInfo traceInfo = null;
|
||||
|
||||
if (message.Properties.TryGetValue(TfsTraceInfoKey, out object traceInfoObject))
|
||||
{
|
||||
traceInfo = traceInfoObject as VssHttpMessageHandlerTraceInfo;
|
||||
}
|
||||
|
||||
return traceInfo;
|
||||
}
|
||||
|
||||
public override string ToString()
|
||||
{
|
||||
return $"R:{TokenRetries}, HS:{HandlerStartTime.Ticks}, BR:{BufferedRequestTime.Ticks}, RS:{RequestSendTime.Ticks}, RC:{ResponseContentTime.Ticks}, GT:{GetTokenTime.Ticks}, TT={TrailingTime.Ticks}";
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user