├── .editorconfig ├── .gitattributes ├── .github ├── dependabot.yml ├── release.yml └── workflows │ ├── build.yml │ ├── changelog.config │ ├── changelog.yml │ ├── dotnet-file.yml │ ├── includes.yml │ ├── os-matrix.json │ ├── pages.yml │ └── triage.yml ├── .gitignore ├── .netconfig ├── CredentialManager.sln ├── Directory.Build.rsp ├── Directory.Solution.targets ├── Gemfile ├── _config.yml ├── assets ├── css │ └── style.scss └── images │ ├── gcm.png │ └── gcm.svg ├── changelog.md ├── license.txt ├── readme.md └── src ├── Analyzers ├── Analyzers.csproj └── Devlooped.CredentialManager.targets ├── Core ├── .editorconfig ├── AssemblyUtils.cs ├── Authentication │ └── OAuth │ │ ├── HttpListenerExtensions.cs │ │ ├── IOAuth2WebBrowser.cs │ │ ├── Json │ │ ├── DeviceAuthorizationEndpointResponseJson.cs │ │ ├── ErrorResponseJson.cs │ │ └── TokenEndpointResponseJson.cs │ │ ├── OAuth2AuthorizationCodeResult.cs │ │ ├── OAuth2Client.cs │ │ ├── OAuth2Constants.cs │ │ ├── OAuth2CryptographicGenerator.cs │ │ ├── OAuth2DeviceCodeResult.cs │ │ ├── OAuth2Exception.cs │ │ ├── OAuth2ServerEndpoints.cs │ │ ├── OAuth2SystemWebBrowser.cs │ │ └── OAuth2TokenResult.cs ├── Base64UrlConvert.cs ├── BrowserUtils.cs ├── ChildProcess.cs ├── CommandContext.cs ├── Constants.cs ├── ConvertUtils.cs ├── Core.csproj ├── Credential.cs ├── CredentialCacheStore.cs ├── CredentialStore.cs ├── CurlCookie.cs ├── DictionaryExtensions.cs ├── DisposableObject.cs ├── EncodingEx.cs ├── EnsureArgument.cs ├── EnvironmentBase.cs ├── FileCredential.cs ├── FileSystem.cs ├── Git.cs ├── GitConfiguration.cs ├── GitConfigurationEntry.cs ├── GitConfigurationKeyComparer.cs ├── GitStreamReader.cs ├── GitVersion.cs ├── Gpg.cs ├── HttpClientFactory.cs ├── HttpRequestExtensions.cs ├── ICredentialStore.cs ├── ISessionManager.cs ├── ISystemPrompts.cs ├── ITerminal.cs ├── ITrace2Writer.cs ├── IniFile.cs ├── Interop │ ├── InteropException.cs │ ├── InteropUtils.cs │ ├── Linux │ │ ├── LinuxFileSystem.cs │ │ ├── LinuxSessionManager.cs │ │ ├── LinuxTerminal.cs │ │ ├── Native │ │ │ ├── Glib.cs │ │ │ ├── Gobject.cs │ │ │ ├── Libsecret.cs │ │ │ └── termios_Linux.cs │ │ ├── SecretServiceCollection.cs │ │ └── SecretServiceCredential.cs │ ├── MacOS │ │ ├── MacOSEnvironment.cs │ │ ├── MacOSFileSystem.cs │ │ ├── MacOSKeychain.cs │ │ ├── MacOSKeychainCredential.cs │ │ ├── MacOSSessionManager.cs │ │ ├── MacOSTerminal.cs │ │ └── Native │ │ │ ├── CoreFoundation.cs │ │ │ ├── LibC.cs │ │ │ ├── LibSystem.cs │ │ │ ├── SecurityFramework.cs │ │ │ └── termios_MacOS.cs │ ├── Posix │ │ ├── GpgPassCredentialStore.cs │ │ ├── Native │ │ │ ├── Fcntl.cs │ │ │ ├── Signal.cs │ │ │ ├── Stat.cs │ │ │ ├── Stdio.cs │ │ │ ├── Stdlib.cs │ │ │ ├── Termios.cs │ │ │ └── Unistd.cs │ │ ├── PosixEnvironment.cs │ │ ├── PosixFileDescriptor.cs │ │ ├── PosixFileSystem.Custom.cs │ │ ├── PosixFileSystem.cs │ │ ├── PosixSessionManager.cs │ │ └── PosixTerminal.cs │ ├── U8StringConverter.cs │ ├── U8StringMarshaler.cs │ └── Windows │ │ ├── DpapiCredentialStore.cs │ │ ├── Native │ │ ├── Advapi32.cs │ │ ├── CredUi.cs │ │ ├── Kernel32.cs │ │ ├── Ole32.cs │ │ ├── Shell32.cs │ │ ├── User32.cs │ │ └── Win32Error.cs │ │ ├── WindowsCredential.cs │ │ ├── WindowsCredentialManager.cs │ │ ├── WindowsEnvironment.cs │ │ ├── WindowsFileSystem.cs │ │ ├── WindowsProcessManager.cs │ │ ├── WindowsSessionManager.cs │ │ ├── WindowsSettings.cs │ │ ├── WindowsSystemPrompts.cs │ │ └── WindowsTerminal.cs ├── NameValueCollectionExtensions.cs ├── NullCredentialStore.cs ├── PlaintextCredentialStore.cs ├── PlatformUtils.cs ├── ProcessManager.cs ├── Settings.cs ├── StandardStreams.cs ├── StreamExtensions.cs ├── StringExtensions.cs ├── Trace.cs ├── Trace2.cs ├── Trace2CollectorWriter.cs ├── Trace2Exception.cs ├── Trace2FileWriter.cs ├── Trace2Message.cs ├── Trace2StreamWriter.cs ├── TraceUtils.cs ├── UriExtensions.cs └── WslUtils.cs ├── CredentialManager ├── CredentialManager.cs ├── CredentialManager.csproj ├── exclude.txt └── readme.md ├── Directory.Build.props ├── Directory.Build.targets ├── Directory.props ├── Directory.targets ├── SponsorLink ├── Analyzer │ ├── Analyzer.csproj │ ├── GraceApiAnalyzer.cs │ ├── Properties │ │ └── launchSettings.json │ ├── StatusReportingAnalyzer.cs │ ├── StatusReportingGenerator.cs │ └── buildTransitive │ │ └── SponsorableLib.targets ├── Directory.Build.props ├── Directory.Build.targets ├── Library │ ├── Library.csproj │ ├── MyClass.cs │ ├── Resources.resx │ └── readme.md ├── SponsorLink.Analyzer.Tests.targets ├── SponsorLink.Analyzer.targets ├── SponsorLink │ ├── AnalyzerOptionsExtensions.cs │ ├── AppDomainDictionary.cs │ ├── DiagnosticsManager.cs │ ├── ManifestStatus.cs │ ├── Resources.es-AR.resx │ ├── Resources.es.resx │ ├── Resources.resx │ ├── SponsorLink.cs │ ├── SponsorLink.csproj │ ├── SponsorLinkAnalyzer.cs │ ├── SponsorStatus.cs │ ├── SponsorableLib.targets │ ├── Tracing.cs │ ├── buildTransitive │ │ └── Devlooped.Sponsors.targets │ └── sponsorable.md ├── SponsorLinkAnalyzer.sln ├── Tests │ ├── .netconfig │ ├── AnalyzerTests.cs │ ├── Attributes.cs │ ├── Extensions.cs │ ├── JsonOptions.cs │ ├── Resources.resx │ ├── Sample.cs │ ├── SponsorLinkTests.cs │ ├── SponsorableManifest.cs │ ├── Tests.csproj │ └── keys │ │ ├── kzu.key │ │ ├── kzu.key.jwk │ │ ├── kzu.key.txt │ │ ├── kzu.pub │ │ ├── kzu.pub.jwk │ │ ├── kzu.pub.txt │ │ └── sponsorlink.jwt ├── jwk.ps1 └── readme.md ├── Tests ├── Attributes.cs ├── EndToEnd.cs └── Tests.csproj └── icon.png /.gitattributes: -------------------------------------------------------------------------------- 1 | # normalize by default 2 | * text=auto encoding=UTF-8 3 | *.sh text eol=lf 4 | 5 | # These are windows specific files which we may as well ensure are 6 | # always crlf on checkout 7 | *.bat text eol=crlf 8 | *.cmd text eol=crlf 9 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | # Please see the documentation for all configuration options: 2 | # https://help.github.com/github/administering-a-repository/configuration-options-for-dependency-updates 3 | 4 | version: 2 5 | updates: 6 | - package-ecosystem: nuget 7 | directory: / 8 | schedule: 9 | interval: daily 10 | groups: 11 | Azure: 12 | patterns: 13 | - "Azure*" 14 | - "Microsoft.Azure*" 15 | Identity: 16 | patterns: 17 | - "System.IdentityModel*" 18 | - "Microsoft.IdentityModel*" 19 | System: 20 | patterns: 21 | - "System*" 22 | exclude-patterns: 23 | - "System.IdentityModel*" 24 | Extensions: 25 | patterns: 26 | - "Microsoft.Extensions*" 27 | Web: 28 | patterns: 29 | - "Microsoft.AspNetCore*" 30 | Tests: 31 | patterns: 32 | - "Microsoft.NET.Test*" 33 | - "xunit*" 34 | - "coverlet*" 35 | ThisAssembly: 36 | patterns: 37 | - "ThisAssembly*" 38 | ProtoBuf: 39 | patterns: 40 | - "protobuf-*" 41 | -------------------------------------------------------------------------------- /.github/release.yml: -------------------------------------------------------------------------------- 1 | changelog: 2 | exclude: 3 | labels: 4 | - bydesign 5 | - dependencies 6 | - duplicate 7 | - question 8 | - invalid 9 | - wontfix 10 | - need info 11 | - techdebt 12 | authors: 13 | - devlooped-bot 14 | - dependabot 15 | - github-actions 16 | categories: 17 | - title: ✨ Implemented enhancements 18 | labels: 19 | - enhancement 20 | - title: 🐛 Fixed bugs 21 | labels: 22 | - bug 23 | - title: 📝 Documentation updates 24 | labels: 25 | - docs 26 | - documentation 27 | - title: 🔨 Other 28 | labels: 29 | - '*' 30 | exclude: 31 | labels: 32 | - dependencies 33 | -------------------------------------------------------------------------------- /.github/workflows/changelog.config: -------------------------------------------------------------------------------- 1 | usernames-as-github-logins=true 2 | issues_wo_labels=true 3 | pr_wo_labels=true 4 | exclude-labels=bydesign,dependencies,duplicate,discussion,question,invalid,wontfix,need info,docs 5 | enhancement-label=:sparkles: Implemented enhancements: 6 | bugs-label=:bug: Fixed bugs: 7 | issues-label=:hammer: Other: 8 | pr-label=:twisted_rightwards_arrows: Merged: 9 | unreleased=false 10 | -------------------------------------------------------------------------------- /.github/workflows/changelog.yml: -------------------------------------------------------------------------------- 1 | name: changelog 2 | on: 3 | workflow_dispatch: 4 | release: 5 | types: [released] 6 | 7 | jobs: 8 | changelog: 9 | runs-on: ubuntu-latest 10 | steps: 11 | - name: 🤖 defaults 12 | uses: devlooped/actions-bot@v1 13 | with: 14 | name: ${{ secrets.BOT_NAME }} 15 | email: ${{ secrets.BOT_EMAIL }} 16 | gh_token: ${{ secrets.GH_TOKEN }} 17 | github_token: ${{ secrets.GITHUB_TOKEN }} 18 | 19 | - name: 🤘 checkout 20 | uses: actions/checkout@v4 21 | with: 22 | fetch-depth: 0 23 | ref: main 24 | token: ${{ env.GH_TOKEN }} 25 | 26 | - name: ⚙ ruby 27 | uses: ruby/setup-ruby@v1 28 | with: 29 | ruby-version: 3.0.3 30 | 31 | - name: ⚙ changelog 32 | run: | 33 | gem install github_changelog_generator 34 | github_changelog_generator --user ${GITHUB_REPOSITORY%/*} --project ${GITHUB_REPOSITORY##*/} --token $GH_TOKEN --o changelog.md --config-file .github/workflows/changelog.config 35 | 36 | - name: 🚀 changelog 37 | run: | 38 | git add changelog.md 39 | (git commit -m "🖉 Update changelog with ${GITHUB_REF#refs/*/}" && git push) || echo "Done" -------------------------------------------------------------------------------- /.github/workflows/dotnet-file.yml: -------------------------------------------------------------------------------- 1 | # Synchronizes .netconfig-configured files with dotnet-file 2 | name: dotnet-file 3 | on: 4 | workflow_dispatch: 5 | schedule: 6 | - cron: "0 0 * * *" 7 | push: 8 | branches: [ 'dotnet-file' ] 9 | 10 | env: 11 | DOTNET_NOLOGO: true 12 | 13 | jobs: 14 | run: 15 | uses: devlooped/oss/.github/workflows/dotnet-file-core.yml@main 16 | secrets: inherit -------------------------------------------------------------------------------- /.github/workflows/includes.yml: -------------------------------------------------------------------------------- 1 | name: +Mᐁ includes 2 | on: 3 | workflow_dispatch: 4 | push: 5 | branches: 6 | - 'main' 7 | paths: 8 | - '**.md' 9 | - '!changelog.md' 10 | 11 | jobs: 12 | includes: 13 | runs-on: ubuntu-latest 14 | permissions: 15 | contents: write 16 | pull-requests: write 17 | steps: 18 | - name: 🤖 defaults 19 | uses: devlooped/actions-bot@v1 20 | with: 21 | name: ${{ secrets.BOT_NAME }} 22 | email: ${{ secrets.BOT_EMAIL }} 23 | gh_token: ${{ secrets.GH_TOKEN }} 24 | github_token: ${{ secrets.GITHUB_TOKEN }} 25 | 26 | - name: 🤘 checkout 27 | uses: actions/checkout@v4 28 | with: 29 | token: ${{ env.GH_TOKEN }} 30 | 31 | - name: +Mᐁ includes 32 | uses: devlooped/actions-includes@v1 33 | 34 | - name: ✍ pull request 35 | uses: peter-evans/create-pull-request@v6 36 | with: 37 | add-paths: '**.md' 38 | base: main 39 | branch: markdown-includes 40 | delete-branch: true 41 | labels: docs 42 | author: ${{ env.BOT_AUTHOR }} 43 | committer: ${{ env.BOT_AUTHOR }} 44 | commit-message: +Mᐁ includes 45 | title: +Mᐁ includes 46 | body: +Mᐁ includes 47 | token: ${{ env.GH_TOKEN }} 48 | -------------------------------------------------------------------------------- /.github/workflows/os-matrix.json: -------------------------------------------------------------------------------- 1 | [ 'ubuntu-latest', 'windows-latest', 'macOS-latest' ] -------------------------------------------------------------------------------- /.github/workflows/pages.yml: -------------------------------------------------------------------------------- 1 | # Workflow to cross-post a jekyll site (or GitHub Pages) 2 | # to another org/repo. 3 | # Required secrets in repository consuming this workflow: 4 | # - PAGES_ORGANIZATION: the target organization to publish 5 | # pages to. 6 | # - PAGES_ACCESS_TOKEN: a token that is valid in the target 7 | # org/repo for pushing the resulting site 8 | # - PAGES_REPOSITORY: optional repository name under the 9 | # target organization. Defaults to source repo name. 10 | 11 | name: pages 12 | on: 13 | workflow_dispatch: 14 | push: 15 | branches: 16 | - main 17 | - pages 18 | - docs 19 | 20 | env: 21 | PAGES_ORGANIZATION: ${{ secrets.PAGES_ORGANIZATION }} 22 | PAGES_REPOSITORY: ${{ secrets.PAGES_REPOSITORY }} 23 | 24 | jobs: 25 | gh-pages: 26 | runs-on: ubuntu-latest 27 | env: 28 | PAGES_ORGANIZATION: ${{ secrets.PAGES_ORGANIZATION }} 29 | PAGES_REPOSITORY: ${{ secrets.PAGES_REPOSITORY }} 30 | PAGES_ACCESS_TOKEN: ${{ secrets.PAGES_ACCESS_TOKEN }} 31 | steps: 32 | - name: ✅ organization 33 | if: env.PAGES_ORGANIZATION == '' 34 | run: | 35 | echo "::error title=PAGES_ORGANIZATION secret is required." 36 | exit 1 37 | 38 | - name: ✅ token 39 | if: env.PAGES_ACCESS_TOKEN == '' 40 | run: | 41 | echo "::error title=PAGES_ACCESS_TOKEN secret is required." 42 | exit 1 43 | 44 | - name: 🤘 checkout 45 | uses: actions/checkout@v2 46 | 47 | - name: ⚙ jekyll 48 | run: | 49 | sudo gem install bundler 50 | sudo bundle install 51 | 52 | - name: 🖉 default repo 53 | if: env.PAGES_REPOSITORY == '' 54 | run: echo "PAGES_REPOSITORY=${GITHUB_REPOSITORY#*/}" >> $GITHUB_ENV 55 | 56 | - name: 🙏 build 57 | run: bundle exec jekyll build -b ${{ env.PAGES_REPOSITORY }} 58 | env: 59 | JEKYLL_GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 60 | 61 | - name: ✓ commit 62 | run: | 63 | cd _site 64 | git init 65 | git add -A 66 | git config --local user.email "bot@clarius.org" 67 | git config --local user.name "bot@clarius.org" 68 | git commit -m "Publish pages from ${GITHUB_REPOSITORY}@${GITHUB_SHA:0:9}" 69 | 70 | - name: 🚀 push 71 | uses: ad-m/github-push-action@v0.6.0 72 | with: 73 | github_token: ${{ env.PAGES_ACCESS_TOKEN }} 74 | repository: ${{ env.PAGES_ORGANIZATION }}/${{ env.PAGES_REPOSITORY }} 75 | branch: gh-pages 76 | force: true 77 | directory: ./_site -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | bin 2 | obj 3 | artifacts 4 | pack 5 | TestResults 6 | results 7 | BenchmarkDotNet.Artifacts 8 | /app 9 | .vs 10 | .vscode 11 | .genaiscript 12 | .idea 13 | local.settings.json 14 | 15 | *.suo 16 | *.sdf 17 | *.userprefs 18 | *.user 19 | *.nupkg 20 | *.metaproj 21 | *.tmp 22 | *.log 23 | *.cache 24 | *.binlog 25 | *.zip 26 | __azurite*.* 27 | __*__ 28 | 29 | .nuget 30 | *.lock.json 31 | *.nuget.props 32 | *.nuget.targets 33 | 34 | node_modules 35 | _site 36 | .jekyll-metadata 37 | .jekyll-cache 38 | .sass-cache 39 | Gemfile.lock 40 | package-lock.json 41 | -------------------------------------------------------------------------------- /CredentialManager.sln: -------------------------------------------------------------------------------- 1 |  2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | # Visual Studio Version 17 4 | VisualStudioVersion = 17.1.32421.90 5 | MinimumVisualStudioVersion = 10.0.40219.1 6 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "CredentialManager", "src\CredentialManager\CredentialManager.csproj", "{141A5D74-1821-41CB-831A-0CDCD469E4AE}" 7 | EndProject 8 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Analyzers", "src\Analyzers\Analyzers.csproj", "{47DE5EE9-22A7-4D23-A50E-0724A900D290}" 9 | EndProject 10 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Core", "src\Core\Core.csproj", "{D0CE060F-5C92-494F-A4D4-441F7CDB1340}" 11 | EndProject 12 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Tests", "src\Tests\Tests.csproj", "{160A2CD4-2C93-4C6E-BD87-5F462A932C58}" 13 | EndProject 14 | Global 15 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 16 | Debug|Any CPU = Debug|Any CPU 17 | Release|Any CPU = Release|Any CPU 18 | EndGlobalSection 19 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 20 | {141A5D74-1821-41CB-831A-0CDCD469E4AE}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 21 | {141A5D74-1821-41CB-831A-0CDCD469E4AE}.Debug|Any CPU.Build.0 = Debug|Any CPU 22 | {141A5D74-1821-41CB-831A-0CDCD469E4AE}.Release|Any CPU.ActiveCfg = Release|Any CPU 23 | {141A5D74-1821-41CB-831A-0CDCD469E4AE}.Release|Any CPU.Build.0 = Release|Any CPU 24 | {47DE5EE9-22A7-4D23-A50E-0724A900D290}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 25 | {47DE5EE9-22A7-4D23-A50E-0724A900D290}.Debug|Any CPU.Build.0 = Debug|Any CPU 26 | {47DE5EE9-22A7-4D23-A50E-0724A900D290}.Release|Any CPU.ActiveCfg = Release|Any CPU 27 | {47DE5EE9-22A7-4D23-A50E-0724A900D290}.Release|Any CPU.Build.0 = Release|Any CPU 28 | {D0CE060F-5C92-494F-A4D4-441F7CDB1340}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 29 | {D0CE060F-5C92-494F-A4D4-441F7CDB1340}.Debug|Any CPU.Build.0 = Debug|Any CPU 30 | {D0CE060F-5C92-494F-A4D4-441F7CDB1340}.Release|Any CPU.ActiveCfg = Release|Any CPU 31 | {D0CE060F-5C92-494F-A4D4-441F7CDB1340}.Release|Any CPU.Build.0 = Release|Any CPU 32 | {160A2CD4-2C93-4C6E-BD87-5F462A932C58}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 33 | {160A2CD4-2C93-4C6E-BD87-5F462A932C58}.Debug|Any CPU.Build.0 = Debug|Any CPU 34 | {160A2CD4-2C93-4C6E-BD87-5F462A932C58}.Release|Any CPU.ActiveCfg = Release|Any CPU 35 | {160A2CD4-2C93-4C6E-BD87-5F462A932C58}.Release|Any CPU.Build.0 = Release|Any CPU 36 | EndGlobalSection 37 | GlobalSection(SolutionProperties) = preSolution 38 | HideSolutionNode = FALSE 39 | EndGlobalSection 40 | GlobalSection(ExtensibilityGlobals) = postSolution 41 | SolutionGuid = {9553CCFA-953D-400C-BE44-25CA2B8E0F31} 42 | EndGlobalSection 43 | EndGlobal 44 | -------------------------------------------------------------------------------- /Directory.Build.rsp: -------------------------------------------------------------------------------- 1 | # See https://docs.microsoft.com/en-us/visualstudio/msbuild/msbuild-response-files 2 | -nr:false 3 | -m:1 4 | -v:m 5 | -clp:Summary;ForceNoAlign 6 | -------------------------------------------------------------------------------- /Directory.Solution.targets: -------------------------------------------------------------------------------- 1 | 2 | 3 | $([System.IO.Path]::GetFullPath($(MSBuildThisFileDirectory)\src\NuGetize.targets)) 4 | windows 5 | osx 6 | linux 7 | 8 | 9 | 10 | 11 | 12 | CustomAfterMicrosoftCommonTargets=$(CustomAfterMicrosoftCommonTargets); 13 | OSPlatform=$(OSPlatform); 14 | %(AdditionalProperties) 15 | 16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source 'https://rubygems.org' 2 | 3 | gem 'github-pages', '~> 231', group: :jekyll_plugins 4 | -------------------------------------------------------------------------------- /_config.yml: -------------------------------------------------------------------------------- 1 | theme: jekyll-theme-slate 2 | 3 | exclude: [ 'src/', '*.sln', 'Gemfile*', '*.rsp' ] -------------------------------------------------------------------------------- /assets/css/style.scss: -------------------------------------------------------------------------------- 1 | --- 2 | --- 3 | 4 | @import "jekyll-theme-slate"; 5 | 6 | .inner { 7 | max-width: 960px; 8 | } 9 | 10 | pre, code { 11 | background-color: unset; 12 | font-size: unset; 13 | } 14 | 15 | code { 16 | font-size: 0.80em; 17 | } 18 | 19 | h1 > img { 20 | border: unset; 21 | box-shadow: unset; 22 | vertical-align: middle; 23 | -moz-box-shadow: unset; 24 | -o-box-shadow: unset; 25 | -ms-box-shadow: unset; 26 | } 27 | -------------------------------------------------------------------------------- /assets/images/gcm.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/devlooped/CredentialManager/752b34b21d807de3348b16d2be507a5f7e8213b9/assets/images/gcm.png -------------------------------------------------------------------------------- /license.txt: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) Daniel Cazzulino and Contributors 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | 23 | -------------------------------------------------------------------------------- /src/Analyzers/Analyzers.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Devlooped.CredentialManager 5 | Devlooped.CredentialManager.Analyzers 6 | netstandard2.0 7 | analyzers/dotnet 8 | false 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | $(MSBuildThisFileDirectory)..\SponsorLink\SponsorLink.Analyzer.targets 24 | 25 | Devlooped.CredentialManager 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | -------------------------------------------------------------------------------- /src/Analyzers/Devlooped.CredentialManager.targets: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /src/Core/.editorconfig: -------------------------------------------------------------------------------- 1 | [**] 2 | generated_code = true -------------------------------------------------------------------------------- /src/Core/AssemblyUtils.cs: -------------------------------------------------------------------------------- 1 | using System.Reflection; 2 | 3 | namespace GitCredentialManager; 4 | 5 | public static class AssemblyUtils 6 | { 7 | public static bool TryGetAssemblyVersion(out string version) 8 | { 9 | try 10 | { 11 | var assembly = Assembly.GetEntryAssembly() ?? Assembly.GetExecutingAssembly(); 12 | var assemblyVersionAttribute = assembly.GetCustomAttribute(); 13 | version = assemblyVersionAttribute is null 14 | ? assembly.GetName().Version.ToString() 15 | : assemblyVersionAttribute.InformationalVersion; 16 | return true; 17 | } 18 | catch 19 | { 20 | version = null; 21 | return false; 22 | } 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/Core/Authentication/OAuth/HttpListenerExtensions.cs: -------------------------------------------------------------------------------- 1 | using System.Net; 2 | using System.Text; 3 | using System.Threading.Tasks; 4 | 5 | namespace GitCredentialManager.Authentication.OAuth 6 | { 7 | public static class HttpListenerExtensions 8 | { 9 | public static async Task WriteResponseAsync(this HttpListenerResponse response, string responseText) 10 | { 11 | byte[] responseData = Encoding.UTF8.GetBytes(responseText); 12 | response.ContentLength64 = responseData.Length; 13 | await response.OutputStream.WriteAsync(responseData, 0, responseData.Length); 14 | } 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/Core/Authentication/OAuth/IOAuth2WebBrowser.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Net; 3 | using System.Threading; 4 | using System.Threading.Tasks; 5 | 6 | namespace GitCredentialManager.Authentication.OAuth 7 | { 8 | public interface IOAuth2WebBrowser 9 | { 10 | Uri UpdateRedirectUri(Uri uri); 11 | 12 | Task GetAuthenticationCodeAsync(Uri authorizationUri, Uri redirectUri, CancellationToken ct); 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /src/Core/Authentication/OAuth/Json/DeviceAuthorizationEndpointResponseJson.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Text.Json.Serialization; 3 | 4 | namespace GitCredentialManager.Authentication.OAuth.Json 5 | { 6 | public class DeviceAuthorizationEndpointResponseJson 7 | { 8 | [JsonRequired] 9 | [JsonPropertyName("device_code")] 10 | public string DeviceCode { get; set; } 11 | 12 | [JsonRequired] 13 | [JsonPropertyName("user_code")] 14 | public string UserCode { get; set; } 15 | 16 | [JsonRequired] 17 | [JsonPropertyName("verification_uri")] 18 | public Uri VerificationUri { get; set; } 19 | 20 | [JsonPropertyName("expires_in")] 21 | public long ExpiresIn { get; set; } 22 | 23 | [JsonPropertyName("interval")] 24 | public long PollingInterval { get; set; } 25 | 26 | public OAuth2DeviceCodeResult ToResult() 27 | { 28 | return new OAuth2DeviceCodeResult(DeviceCode, UserCode, VerificationUri, TimeSpan.FromSeconds(PollingInterval)) 29 | { 30 | ExpiresIn = TimeSpan.FromSeconds(ExpiresIn) 31 | }; 32 | } 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/Core/Authentication/OAuth/Json/ErrorResponseJson.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Text; 3 | using System.Text.Json; 4 | using System.Text.Json.Serialization; 5 | 6 | namespace GitCredentialManager.Authentication.OAuth.Json 7 | { 8 | public class ErrorResponseJson 9 | { 10 | [JsonRequired] 11 | [JsonPropertyName("error")] 12 | public string Error { get; set; } 13 | 14 | [JsonPropertyName("error_description")] 15 | public string Description { get; set; } 16 | 17 | [JsonPropertyName("error_uri")] 18 | public Uri Uri { get; set; } 19 | 20 | public OAuth2Exception ToException(Exception innerException = null) 21 | { 22 | var message = new StringBuilder(Error); 23 | 24 | if (!string.IsNullOrEmpty(Description)) 25 | { 26 | message.AppendFormat(": {0}", Description); 27 | } 28 | 29 | if (Uri != null) 30 | { 31 | message.AppendFormat(" [{0}]", Uri); 32 | } 33 | 34 | return new OAuth2Exception(message.ToString(), innerException) {HelpLink = Uri?.ToString()}; 35 | } 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/Core/Authentication/OAuth/Json/TokenEndpointResponseJson.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Text.Json; 3 | using System.Text.Json.Serialization; 4 | 5 | namespace GitCredentialManager.Authentication.OAuth.Json 6 | { 7 | public class TokenEndpointResponseJson 8 | { 9 | [JsonRequired] 10 | [JsonPropertyName("access_token")] 11 | public string AccessToken { get; set; } 12 | 13 | [JsonRequired] 14 | [JsonPropertyName("token_type")] 15 | public string TokenType { get; set; } 16 | 17 | [JsonPropertyName("expires_in")] 18 | public long? ExpiresIn { get; set; } 19 | 20 | [JsonPropertyName("refresh_token")] 21 | public string RefreshToken { get; set; } 22 | 23 | [JsonPropertyName("scope")] 24 | public virtual string Scope { get; set; } 25 | 26 | public OAuth2TokenResult ToResult() 27 | { 28 | return new OAuth2TokenResult(AccessToken, TokenType) 29 | { 30 | ExpiresIn = ExpiresIn.HasValue ? TimeSpan.FromSeconds(ExpiresIn.Value) : null, 31 | RefreshToken = RefreshToken, 32 | Scopes = Scope?.Split(' ') 33 | }; 34 | } 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/Core/Authentication/OAuth/OAuth2AuthorizationCodeResult.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace GitCredentialManager.Authentication.OAuth 4 | { 5 | public class OAuth2AuthorizationCodeResult 6 | { 7 | public OAuth2AuthorizationCodeResult(string code, Uri redirectUri = null, string codeVerifier = null) 8 | { 9 | Code = code; 10 | RedirectUri = redirectUri; 11 | CodeVerifier = codeVerifier; 12 | } 13 | 14 | public string Code { get; } 15 | public Uri RedirectUri { get; } 16 | public string CodeVerifier { get; } 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/Core/Authentication/OAuth/OAuth2Constants.cs: -------------------------------------------------------------------------------- 1 | 2 | namespace GitCredentialManager.Authentication.OAuth 3 | { 4 | public static class OAuth2Constants 5 | { 6 | public const string ClientIdParameter = "client_id"; 7 | public const string ClientSecretParameter = "client_secret"; 8 | public const string RedirectUriParameter = "redirect_uri"; 9 | public const string ScopeParameter = "scope"; 10 | public const string Trace2Category = "oauth2"; 11 | 12 | public static class AuthorizationEndpoint 13 | { 14 | public const string StateParameter = "state"; 15 | public const string AuthorizationCodeResponseType = "code"; 16 | public const string ResponseTypeParameter = "response_type"; 17 | public const string PkceChallengeParameter = "code_challenge"; 18 | public const string PkceChallengeMethodParameter = "code_challenge_method"; 19 | public const string PkceChallengeMethodPlain = "plain"; 20 | public const string PkceChallengeMethodS256 = "S256"; 21 | } 22 | 23 | public static class AuthorizationGrantResponse 24 | { 25 | public const string AuthorizationCodeParameter = "code"; 26 | public const string ErrorCodeParameter = "error"; 27 | public const string ErrorDescriptionParameter = "error_description"; 28 | public const string ErrorUriParameter = "error_uri"; 29 | public const string StateParameter = "state"; 30 | } 31 | 32 | public static class TokenEndpoint 33 | { 34 | public const string GrantTypeParameter = "grant_type"; 35 | public const string AuthorizationCodeGrantType = "authorization_code"; 36 | public const string RefreshTokenGrantType = "refresh_token"; 37 | public const string PkceVerifierParameter = "code_verifier"; 38 | public const string AuthorizationCodeParameter = "code"; 39 | public const string RefreshTokenParameter = "refresh_token"; 40 | } 41 | 42 | public static class DeviceAuthorization 43 | { 44 | public const string GrantTypeParameter = "grant_type"; 45 | public const string DeviceCodeParameter = "device_code"; 46 | public const string DeviceCodeGrantType = "urn:ietf:params:oauth:grant-type:device_code"; 47 | 48 | public static class Errors 49 | { 50 | public const string AuthorizationPending = "authorization_pending"; 51 | public const string SlowDown = "slow_down"; 52 | } 53 | } 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /src/Core/Authentication/OAuth/OAuth2DeviceCodeResult.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace GitCredentialManager.Authentication.OAuth 4 | { 5 | public class OAuth2DeviceCodeResult 6 | { 7 | public OAuth2DeviceCodeResult(string deviceCode, string userCode, Uri verificationUri, TimeSpan? interval) 8 | { 9 | DeviceCode = deviceCode; 10 | UserCode = userCode; 11 | VerificationUri = verificationUri; 12 | PollingInterval = interval ?? TimeSpan.FromSeconds(5); 13 | } 14 | 15 | public string DeviceCode { get; } 16 | 17 | public string UserCode { get; } 18 | 19 | public Uri VerificationUri { get; } 20 | 21 | public TimeSpan PollingInterval { get; } 22 | 23 | public TimeSpan? ExpiresIn { get; internal set; } 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/Core/Authentication/OAuth/OAuth2Exception.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace GitCredentialManager.Authentication.OAuth 4 | { 5 | public class OAuth2Exception : Exception 6 | { 7 | public OAuth2Exception(string message) : base(message) { } 8 | 9 | public OAuth2Exception(string message, Exception innerException) : base(message, innerException) { } 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /src/Core/Authentication/OAuth/OAuth2ServerEndpoints.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace GitCredentialManager.Authentication.OAuth 4 | { 5 | /// 6 | /// Represents the various OAuth2 endpoints for an . 7 | /// 8 | public class OAuth2ServerEndpoints 9 | { 10 | private Uri _deviceAuthorizationEndpoint; 11 | 12 | public OAuth2ServerEndpoints(Uri authorizationEndpoint, Uri tokenEndpoint) 13 | { 14 | EnsureArgument.AbsoluteUri(authorizationEndpoint, nameof(authorizationEndpoint)); 15 | EnsureArgument.AbsoluteUri(tokenEndpoint, nameof(tokenEndpoint)); 16 | 17 | AuthorizationEndpoint = authorizationEndpoint; 18 | TokenEndpoint = tokenEndpoint; 19 | } 20 | 21 | public Uri AuthorizationEndpoint { get; } 22 | 23 | public Uri TokenEndpoint { get; } 24 | 25 | public Uri DeviceAuthorizationEndpoint 26 | { 27 | get => _deviceAuthorizationEndpoint; 28 | set 29 | { 30 | if (value != null) 31 | { 32 | EnsureArgument.AbsoluteUri(value, nameof(value)); 33 | } 34 | 35 | _deviceAuthorizationEndpoint = value; 36 | } 37 | } 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/Core/Authentication/OAuth/OAuth2TokenResult.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace GitCredentialManager.Authentication.OAuth 4 | { 5 | public class OAuth2TokenResult 6 | { 7 | public OAuth2TokenResult(string accessToken, string tokenType) 8 | { 9 | AccessToken = accessToken; 10 | TokenType = tokenType; 11 | } 12 | 13 | public string AccessToken { get; } 14 | 15 | public string TokenType { get; } 16 | 17 | public string RefreshToken { get; set; } 18 | 19 | public TimeSpan? ExpiresIn { get; set; } 20 | 21 | public string[] Scopes { get; set; } 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/Core/Base64UrlConvert.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace GitCredentialManager 4 | { 5 | public static class Base64UrlConvert 6 | { 7 | public static string Encode(byte[] data, bool includePadding = true) 8 | { 9 | const char base64PadCharacter = '='; 10 | const char base64Character62 = '+'; 11 | const char base64Character63 = '/'; 12 | const char base64UrlCharacter62 = '-'; 13 | const char base64UrlCharacter63 = '_'; 14 | 15 | // The base64url format is the same as regular base64 format except: 16 | // 1. character 62 is "-" (minus) not "+" (plus) 17 | // 2. character 63 is "_" (underscore) not "/" (slash) 18 | string base64Url = Convert.ToBase64String(data) 19 | .Replace(base64Character62, base64UrlCharacter62) 20 | .Replace(base64Character63, base64UrlCharacter63); 21 | 22 | return includePadding ? base64Url : base64Url.TrimEnd(base64PadCharacter); 23 | } 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/Core/ChildProcess.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Diagnostics; 3 | using System.IO; 4 | using System.Threading.Tasks; 5 | 6 | namespace GitCredentialManager; 7 | 8 | public class ChildProcess : DisposableObject 9 | { 10 | private readonly ITrace2 _trace2; 11 | 12 | private DateTimeOffset _startTime; 13 | private DateTimeOffset _exitTime => Process.ExitTime; 14 | private ProcessStartInfo _startInfo => Process.StartInfo; 15 | 16 | private int _id => Process.Id; 17 | 18 | public ProcessStartInfo StartInfo => Process.StartInfo; 19 | public Process Process { get; } 20 | public StreamWriter StandardInput => Process.StandardInput; 21 | public StreamReader StandardOutput => Process.StandardOutput; 22 | public StreamReader StandardError => Process.StandardError; 23 | public int ExitCode => Process.ExitCode; 24 | 25 | public static ChildProcess Start(ITrace2 trace2, ProcessStartInfo startInfo, Trace2ProcessClass processClass) 26 | { 27 | var childProc = new ChildProcess(trace2, startInfo); 28 | childProc.Start(processClass); 29 | return childProc; 30 | } 31 | 32 | public ChildProcess(ITrace2 trace2, ProcessStartInfo startInfo) 33 | { 34 | _trace2 = trace2; 35 | Process = new Process() { StartInfo = startInfo }; 36 | Process.Exited += ProcessOnExited; 37 | } 38 | 39 | public bool Start(Trace2ProcessClass processClass) 40 | { 41 | ThrowIfDisposed(); 42 | // Record the time just before the process starts, since: 43 | // (1) There is no event related to Start as there is with Exit. 44 | // (2) Using Process.StartTime causes a race condition that leads 45 | // to an exception if the process finishes executing before the 46 | // variable is passed to Trace2. 47 | _startTime = DateTimeOffset.UtcNow; 48 | _trace2.WriteChildStart( 49 | _startTime, 50 | processClass, 51 | _startInfo.UseShellExecute, 52 | _startInfo.FileName, 53 | _startInfo.Arguments); 54 | return Process.Start(); 55 | } 56 | 57 | public void WaitForExit() => Process.WaitForExit(); 58 | 59 | public void Kill() => Process.Kill(); 60 | 61 | protected override void ReleaseManagedResources() 62 | { 63 | Process.Exited -= ProcessOnExited; 64 | Process.Dispose(); 65 | base.ReleaseUnmanagedResources(); 66 | } 67 | 68 | private void ProcessOnExited(object sender, EventArgs e) 69 | { 70 | if (sender is Process) 71 | { 72 | double elapsedTime = (_exitTime - _startTime).TotalSeconds; 73 | _trace2.WriteChildExit( 74 | elapsedTime, 75 | _id, 76 | Process.ExitCode); 77 | } 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /src/Core/ConvertUtils.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace GitCredentialManager 4 | { 5 | public static class ConvertUtils 6 | { 7 | public static bool TryToInt32(object value, out int i) 8 | { 9 | return TryConvert(Convert.ToInt32, value, out i); 10 | } 11 | 12 | public static bool TryConvert(Func convert, object value, out T @out) 13 | { 14 | try 15 | { 16 | @out = convert(value); 17 | return true; 18 | } 19 | catch 20 | { 21 | @out = default(T); 22 | return false; 23 | } 24 | } 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/Core/Core.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | netstandard2.0 5 | true 6 | false 7 | annotations 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /src/Core/Credential.cs: -------------------------------------------------------------------------------- 1 | 2 | namespace GitCredentialManager 3 | { 4 | /// 5 | /// Represents a credential. 6 | /// 7 | public interface ICredential 8 | { 9 | /// 10 | /// Account associated with this credential. 11 | /// 12 | string Account { get; } 13 | 14 | /// 15 | /// Password. 16 | /// 17 | string Password { get; } 18 | } 19 | 20 | /// 21 | /// Represents a credential (username/password pair) that Git can use to authenticate to a remote repository. 22 | /// 23 | public class GitCredential : ICredential 24 | { 25 | public GitCredential(string userName, string password) 26 | { 27 | Account = userName; 28 | Password = password; 29 | } 30 | 31 | public string Account { get; } 32 | 33 | public string Password { get; } 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/Core/CredentialCacheStore.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | 4 | namespace GitCredentialManager 5 | { 6 | public class CredentialCacheStore : ICredentialStore 7 | { 8 | readonly IGit _git; 9 | readonly string _options; 10 | 11 | public CredentialCacheStore(IGit git, string options) 12 | { 13 | _git = git; 14 | if (string.IsNullOrEmpty(options)) 15 | { 16 | _options = string.Empty; 17 | } 18 | else 19 | { 20 | _options = options; 21 | } 22 | } 23 | 24 | #region ICredentialStore 25 | 26 | public IList GetAccounts(string service) 27 | { 28 | // Listing accounts is not supported by the credential-cache store so we just attempt to retrieve 29 | // the username from first credential for the given service and return an empty list if it fails. 30 | var input = MakeGitCredentialsEntry(service, null); 31 | 32 | var result = _git.InvokeHelperAsync( 33 | $"credential-cache get {_options}", 34 | input 35 | ).GetAwaiter().GetResult(); 36 | 37 | if (result.TryGetValue("username", out string value)) 38 | { 39 | return new List { value }; 40 | } 41 | 42 | return Array.Empty(); 43 | } 44 | 45 | public ICredential Get(string service, string account) 46 | { 47 | var input = MakeGitCredentialsEntry(service, account); 48 | 49 | var result = _git.InvokeHelperAsync( 50 | $"credential-cache get {_options}", 51 | input 52 | ).GetAwaiter().GetResult(); 53 | 54 | if (result.ContainsKey("username") && result.ContainsKey("password")) 55 | { 56 | return new GitCredential(result["username"], result["password"]); 57 | } 58 | 59 | return null; 60 | } 61 | 62 | public void AddOrUpdate(string service, string account, string secret) 63 | { 64 | var input = MakeGitCredentialsEntry(service, account); 65 | input["password"] = secret; 66 | 67 | // per https://git-scm.com/docs/gitcredentials : 68 | // For a store or erase operation, the helper’s output is ignored. 69 | _git.InvokeHelperAsync( 70 | $"credential-cache store {_options}", 71 | input 72 | ).GetAwaiter().GetResult(); 73 | } 74 | 75 | public bool Remove(string service, string account) 76 | { 77 | var input = MakeGitCredentialsEntry(service, account); 78 | 79 | // per https://git-scm.com/docs/gitcredentials : 80 | // For a store or erase operation, the helper’s output is ignored. 81 | _git.InvokeHelperAsync( 82 | $"credential-cache erase {_options}", 83 | input 84 | ).GetAwaiter().GetResult(); 85 | 86 | // the credential cache doesn't tell us whether anything was erased 87 | // but we're optimistic sorts 88 | return true; 89 | } 90 | 91 | #endregion 92 | 93 | private Dictionary MakeGitCredentialsEntry(string service, string account) 94 | { 95 | var result = new Dictionary(); 96 | 97 | result["url"] = service; 98 | if (!string.IsNullOrEmpty(account)) 99 | { 100 | result["username"] = account; 101 | } 102 | 103 | return result; 104 | } 105 | } 106 | } 107 | -------------------------------------------------------------------------------- /src/Core/CurlCookie.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Net; 4 | using GitCredentialManager; 5 | 6 | namespace GitCredentialManager 7 | { 8 | public class CurlCookieParser 9 | { 10 | private readonly ITrace _trace; 11 | 12 | public CurlCookieParser(ITrace trace) 13 | { 14 | _trace = trace; 15 | } 16 | 17 | public IList Parse(string content) 18 | { 19 | if (string.IsNullOrWhiteSpace(content)) 20 | { 21 | return Array.Empty(); 22 | } 23 | 24 | const string HttpOnlyPrefix = "#HttpOnly_"; 25 | 26 | var cookies = new List(); 27 | 28 | // Parse the cookie file content 29 | var lines = content.Split(new[] { '\r', '\n' }, StringSplitOptions.RemoveEmptyEntries); 30 | foreach (var line in lines) 31 | { 32 | var parts = line.Split(new[] { '\t' }, StringSplitOptions.None); 33 | if (parts.Length >= 7 && (!parts[0].StartsWith("#") || parts[0].StartsWith(HttpOnlyPrefix))) 34 | { 35 | var domain = parts[0].StartsWith(HttpOnlyPrefix) ? parts[0].Substring(HttpOnlyPrefix.Length) : parts[0]; 36 | var includeSubdomains = StringComparer.OrdinalIgnoreCase.Equals(parts[1], "TRUE"); 37 | if (!includeSubdomains) 38 | { 39 | domain = domain.TrimStart('.'); 40 | } 41 | var path = string.IsNullOrWhiteSpace(parts[2]) ? "/" : parts[2]; 42 | var secureOnly = parts[3].Equals("TRUE", StringComparison.OrdinalIgnoreCase); 43 | var expires = ParseExpires(parts[4]); 44 | var name = parts[5]; 45 | var value = parts[6]; 46 | 47 | cookies.Add(new Cookie() 48 | { 49 | Domain = domain, 50 | Path = path, 51 | Expires = expires, 52 | HttpOnly = true, 53 | Secure = secureOnly, 54 | Name = name, 55 | Value = value, 56 | }); 57 | } 58 | else 59 | { 60 | _trace.WriteLine($"Invalid cookie line: {line}"); 61 | } 62 | } 63 | 64 | return cookies; 65 | } 66 | 67 | private static DateTime ParseExpires(string expires) 68 | { 69 | #if NETFRAMEWORK || NETSTANDARD2_0 70 | DateTime epoch = new DateTime(1970, 01, 01, 0, 0, 0, DateTimeKind.Utc); 71 | #else 72 | DateTime epoch = DateTime.UnixEpoch; 73 | #endif 74 | 75 | if (long.TryParse(expires, out long i)) 76 | { 77 | return epoch.AddSeconds(i); 78 | } 79 | 80 | return epoch; 81 | } 82 | } 83 | } -------------------------------------------------------------------------------- /src/Core/DictionaryExtensions.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using System.Linq; 3 | using System.Text; 4 | using System.Web; 5 | 6 | namespace GitCredentialManager 7 | { 8 | public static class DictionaryExtensions 9 | { 10 | /// 11 | /// Get the value of a dictionary entry as 'booleany' (either 'truthy' or 'falsey'). 12 | /// 13 | /// Dictionary. 14 | /// Dictionary entry key. 15 | /// Default value if the key is not present, or was neither 'truthy' or 'falsey'. 16 | /// Dictionary entry value. 17 | /// 18 | /// 'Truthy' and 'fasley' is defined by the implementation of and . 19 | /// 20 | public static bool GetBooleanyOrDefault(this IReadOnlyDictionary dict, string key, bool defaultValue) 21 | { 22 | if (dict.TryGetValue(key, out string value)) 23 | { 24 | return value.ToBooleanyOrDefault(defaultValue); 25 | } 26 | 27 | return defaultValue; 28 | } 29 | 30 | public static string ToQueryString(this IDictionary dict) 31 | { 32 | var sb = new StringBuilder(); 33 | int i = 0; 34 | 35 | foreach (var kvp in dict) 36 | { 37 | string key = HttpUtility.UrlEncode(kvp.Key); 38 | string value = HttpUtility.UrlEncode(kvp.Value); 39 | 40 | if (i > 0) 41 | { 42 | sb.Append('&'); 43 | } 44 | 45 | sb.AppendFormat("{0}={1}", key, value); 46 | 47 | i++; 48 | } 49 | 50 | return sb.ToString(); 51 | } 52 | 53 | public static void Append(this IDictionary> dict, TKey key, TValue value) 54 | { 55 | if (!dict.TryGetValue(key, out var values)) 56 | { 57 | values = new List(); 58 | dict[key] = values; 59 | } 60 | 61 | values.Add(value); 62 | } 63 | 64 | public static IEnumerable GetValues(this IDictionary> dict, TKey key) 65 | { 66 | return dict.TryGetValue(key, out var values) ? values : Enumerable.Empty(); 67 | } 68 | 69 | public static IEnumerable GetValues(this IDictionary> dict, TKey key) 70 | { 71 | return dict.TryGetValue(key, out var values) ? values : Enumerable.Empty(); 72 | } 73 | 74 | public static IDictionary> ToDictionary(this IEnumerable> grouping) 75 | { 76 | return grouping.ToDictionary(x => x.Key, x => (IEnumerable) x); 77 | } 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /src/Core/DisposableObject.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace GitCredentialManager 4 | { 5 | /// 6 | /// An object that implements the interface and the disposable pattern. 7 | /// 8 | public abstract class DisposableObject : IDisposable 9 | { 10 | private bool _isDisposed; 11 | 12 | /// 13 | /// Throw an exception if the object has been disposed. 14 | /// 15 | /// Thrown if the object has been disposed. 16 | protected void ThrowIfDisposed() 17 | { 18 | if (_isDisposed) 19 | { 20 | throw new ObjectDisposedException(GetType().Name); 21 | } 22 | } 23 | 24 | /// 25 | /// Called when unmanaged resources should be released and memory freed. 26 | /// 27 | protected virtual void ReleaseUnmanagedResources() { } 28 | 29 | /// 30 | /// Called when managed resources should be released. 31 | /// 32 | protected virtual void ReleaseManagedResources() { } 33 | 34 | /// 35 | /// Called when the application is being terminated. Clean up and release any resources. 36 | /// 37 | /// True if the instance is being disposed, false if being finalized. 38 | private void Dispose(bool disposing) 39 | { 40 | if (_isDisposed) 41 | { 42 | return; 43 | } 44 | 45 | ReleaseUnmanagedResources(); 46 | 47 | if (disposing) 48 | { 49 | ReleaseManagedResources(); 50 | } 51 | 52 | _isDisposed = true; 53 | } 54 | 55 | public void Dispose() 56 | { 57 | Dispose(true); 58 | GC.SuppressFinalize(this); 59 | } 60 | 61 | ~DisposableObject() 62 | { 63 | Dispose(false); 64 | } 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /src/Core/EncodingEx.cs: -------------------------------------------------------------------------------- 1 | using System.Text; 2 | 3 | namespace GitCredentialManager; 4 | 5 | public static class EncodingEx 6 | { 7 | public static readonly Encoding UTF8NoBom = new UTF8Encoding(encoderShouldEmitUTF8Identifier: false); 8 | } 9 | -------------------------------------------------------------------------------- /src/Core/EnsureArgument.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace GitCredentialManager 4 | { 5 | public static class EnsureArgument 6 | { 7 | public static void NotNull(T arg, string name) 8 | { 9 | if (arg is null) 10 | { 11 | throw new ArgumentNullException(name); 12 | } 13 | } 14 | 15 | public static void NotNullOrEmpty(string arg, string name) 16 | { 17 | NotNull(arg, name); 18 | 19 | if (string.IsNullOrEmpty(arg)) 20 | { 21 | throw new ArgumentException("Argument cannot be empty.", name); 22 | } 23 | } 24 | 25 | public static void NotNullOrWhiteSpace(string arg, string name) 26 | { 27 | NotNull(arg, name); 28 | 29 | if (string.IsNullOrWhiteSpace(arg)) 30 | { 31 | throw new ArgumentException("Argument cannot be empty or white space.", name); 32 | } 33 | } 34 | 35 | public static void AbsoluteUri(Uri arg, string name) 36 | { 37 | NotNull(arg, name); 38 | 39 | if (!arg.IsAbsoluteUri) 40 | { 41 | throw new ArgumentException("Argument must be an absolute URI.", name); 42 | } 43 | } 44 | 45 | public static void PositiveOrZero(int arg, string name) 46 | { 47 | if (arg < 0) 48 | { 49 | throw new ArgumentOutOfRangeException(name, "Argument must be positive or zero (non-negative)."); 50 | } 51 | } 52 | 53 | public static void Positive(int arg, string name) 54 | { 55 | if (arg <= 0) 56 | { 57 | throw new ArgumentOutOfRangeException(name, "Argument must be positive."); 58 | } 59 | } 60 | 61 | public static void NegativeOrZero(int arg, string name) 62 | { 63 | if (arg > 0) 64 | { 65 | throw new ArgumentOutOfRangeException(name, "Argument must be negative or zero (non-positive)."); 66 | } 67 | } 68 | 69 | public static void Negative(int arg, string name) 70 | { 71 | if (arg >= 0) 72 | { 73 | throw new ArgumentOutOfRangeException(name, "Argument must be negative."); 74 | } 75 | } 76 | 77 | public static void InRange(int arg, string name, int lower, int upper, bool lowerInclusive = true, bool upperInclusive = true) 78 | { 79 | if (lowerInclusive && arg < lower) 80 | { 81 | throw new ArgumentOutOfRangeException(name, $"Argument must be greater than or equal to {lower}."); 82 | } 83 | 84 | if (!lowerInclusive && arg <= lower) 85 | { 86 | throw new ArgumentOutOfRangeException(name, $"Argument must be strictly greater than {lower}."); 87 | } 88 | 89 | if (upperInclusive && arg > upper) 90 | { 91 | throw new ArgumentOutOfRangeException(name, $"Argument must be less than or equal to {upper}."); 92 | } 93 | 94 | if (!upperInclusive && arg >= upper) 95 | { 96 | throw new ArgumentOutOfRangeException(name, $"Argument must be strictly less than {upper}."); 97 | } 98 | } 99 | } 100 | } 101 | -------------------------------------------------------------------------------- /src/Core/FileCredential.cs: -------------------------------------------------------------------------------- 1 | namespace GitCredentialManager 2 | { 3 | public class FileCredential : ICredential 4 | { 5 | public FileCredential(string fullPath, string service, string account, string password) 6 | { 7 | FullPath = fullPath; 8 | Service = service; 9 | Account = account; 10 | Password = password; 11 | } 12 | 13 | public string FullPath { get; } 14 | 15 | public string Service { get; } 16 | 17 | public string Account { get; } 18 | 19 | public string Password { get; } 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/Core/GitConfigurationEntry.cs: -------------------------------------------------------------------------------- 1 | namespace GitCredentialManager 2 | { 3 | public class GitConfigurationEntry 4 | { 5 | public GitConfigurationEntry(string key, string value) 6 | { 7 | Key = key; 8 | Value = value; 9 | } 10 | 11 | public string Key { get; } 12 | public string Value { get; } 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /src/Core/GitConfigurationKeyComparer.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace GitCredentialManager 4 | { 5 | /// 6 | /// Represents string comparison of Git configuration entry key names. 7 | /// 8 | /// 9 | /// Git configuration entries have the form "section[.scope].property", where the 10 | /// scope part is optional. 11 | /// 12 | /// The section and property components are NOT case sensitive. 13 | /// The scope component if present IS case sensitive. 14 | /// 15 | public class GitConfigurationKeyComparer : StringComparer 16 | { 17 | public static readonly GitConfigurationKeyComparer Instance = new GitConfigurationKeyComparer(); 18 | 19 | public static readonly StringComparer SectionComparer = OrdinalIgnoreCase; 20 | public static readonly StringComparer ScopeComparer = Ordinal; 21 | public static readonly StringComparer PropertyComparer = OrdinalIgnoreCase; 22 | 23 | private GitConfigurationKeyComparer() { } 24 | 25 | public override int Compare(string x, string y) 26 | { 27 | TrySplit(x, out string xSection, out string xScope, out string xProperty); 28 | TrySplit(y, out string ySection, out string yScope, out string yProperty); 29 | 30 | int cmpSection = OrdinalIgnoreCase.Compare(xSection, ySection); 31 | if (cmpSection != 0) return cmpSection; 32 | 33 | int cmpProperty = OrdinalIgnoreCase.Compare(xProperty, yProperty); 34 | if (cmpProperty != 0) return cmpProperty; 35 | 36 | return Ordinal.Compare(xScope, yScope); 37 | } 38 | 39 | public override bool Equals(string x, string y) 40 | { 41 | if (ReferenceEquals(x, y)) return true; 42 | if (x is null || y is null) return false; 43 | 44 | TrySplit(x, out string xSection, out string xScope, out string xProperty); 45 | TrySplit(y, out string ySection, out string yScope, out string yProperty); 46 | 47 | // Section and property names are not case sensitive, but the inner 'scope' IS case sensitive! 48 | return OrdinalIgnoreCase.Equals(xSection, ySection) && 49 | OrdinalIgnoreCase.Equals(xProperty, yProperty) && 50 | Ordinal.Equals(xScope, yScope); 51 | } 52 | 53 | public override int GetHashCode(string obj) 54 | { 55 | TrySplit(obj, out string section, out string scope, out string property); 56 | 57 | int code = OrdinalIgnoreCase.GetHashCode(section) ^ 58 | OrdinalIgnoreCase.GetHashCode(property); 59 | 60 | return scope is null 61 | ? code 62 | : code ^ Ordinal.GetHashCode(scope); 63 | } 64 | 65 | public static bool TrySplit(string str, out string section, out string scope, out string property) 66 | { 67 | section = null; 68 | scope = null; 69 | property = null; 70 | 71 | if (string.IsNullOrWhiteSpace(str)) 72 | { 73 | return false; 74 | } 75 | 76 | section = str.TruncateFromIndexOf('.'); 77 | property = str.TrimUntilLastIndexOf('.'); 78 | int scopeLength = str.Length - (section.Length + property.Length + 2); 79 | scope = scopeLength > 0 ? str.Substring(section.Length + 1, scopeLength) : null; 80 | 81 | return true; 82 | } 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /src/Core/GitStreamReader.cs: -------------------------------------------------------------------------------- 1 | using System.IO; 2 | using System.Text; 3 | using System.Threading; 4 | using System.Threading.Tasks; 5 | 6 | namespace GitCredentialManager; 7 | 8 | /// 9 | /// StreamReader that does NOT consider a lone carriage-return as a new-line character, 10 | /// only a line-feed or carriage-return immediately followed by a line-feed. 11 | /// 12 | /// The only major operating system that uses a lone carriage-return as a new-line character 13 | /// is the classic Macintosh OS (before OS X), which is not supported by Git. 14 | /// 15 | public class GitStreamReader : StreamReader 16 | { 17 | public GitStreamReader(Stream stream, Encoding encoding) : base(stream, encoding) { } 18 | 19 | public override string ReadLine() 20 | { 21 | #if NETFRAMEWORK 22 | return ReadLineAsync().ConfigureAwait(false).GetAwaiter().GetResult(); 23 | #elif NETSTANDARD2_0 24 | return ReadLineAsync().ConfigureAwait(false).GetAwaiter().GetResult(); 25 | #else 26 | return ReadLineAsync(CancellationToken.None).ConfigureAwait(false).GetAwaiter().GetResult(); 27 | #endif 28 | } 29 | 30 | #if NETFRAMEWORK 31 | public override async Task ReadLineAsync() 32 | #elif NETSTANDARD2_0 33 | public override async Task ReadLineAsync() 34 | #else 35 | public override async ValueTask ReadLineAsync(CancellationToken cancellationToken) 36 | #endif 37 | { 38 | int nr; 39 | var sb = new StringBuilder(); 40 | var buffer = new char[1]; 41 | bool lastWasCR = false; 42 | 43 | while ((nr = await base.ReadAsync(buffer, 0, 1).ConfigureAwait(false)) > 0) 44 | { 45 | char c = buffer[0]; 46 | 47 | // Only treat a line-feed as a new-line character. 48 | // Carriage-returns alone are NOT considered new-line characters. 49 | if (c == '\n') 50 | { 51 | if (lastWasCR) 52 | { 53 | // If the last character was a carriage-return we should remove it from the string builder 54 | // since together with this line-feed it is considered a new-line character. 55 | sb.Length--; 56 | } 57 | 58 | // We have a new-line character, so we should stop reading. 59 | break; 60 | } 61 | 62 | lastWasCR = c == '\r'; 63 | 64 | sb.Append(c); 65 | } 66 | 67 | if (sb.Length == 0 && nr == 0) 68 | { 69 | return null; 70 | } 71 | 72 | return sb.ToString(); 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /src/Core/HttpRequestExtensions.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Globalization; 3 | using System.Net.Http; 4 | using System.Net.Http.Headers; 5 | using System.Text; 6 | 7 | namespace GitCredentialManager 8 | { 9 | public static class HttpRequestExtensions 10 | { 11 | /// 12 | /// Add a basic authentication header to the request, with the given username and password. 13 | /// 14 | /// 15 | /// The header value is formed by computing the base64 string from the UTF-8 string "{}:{}". 16 | /// 17 | public static void AddBasicAuthenticationHeader(this HttpRequestMessage request, string userName, string password) 18 | { 19 | string basicAuthValue = string.Format(CultureInfo.InvariantCulture, "{0}:{1}", userName, password); 20 | byte[] authBytes = Encoding.UTF8.GetBytes(basicAuthValue); 21 | string base64String = Convert.ToBase64String(authBytes); 22 | request.Headers.Authorization = new AuthenticationHeaderValue(Constants.Http.WwwAuthenticateBasicScheme, base64String); 23 | } 24 | 25 | /// 26 | /// Add a bearer authentication header to the request, with the given bearer token. 27 | /// 28 | public static void AddBearerAuthenticationHeader(this HttpRequestMessage request, string bearerToken) 29 | { 30 | request.Headers.Authorization = new AuthenticationHeaderValue(Constants.Http.WwwAuthenticateBearerScheme, bearerToken); 31 | } 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/Core/ICredentialStore.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | 3 | namespace GitCredentialManager 4 | { 5 | /// 6 | /// Represents a secure storage location for s. 7 | /// 8 | public interface ICredentialStore 9 | { 10 | /// 11 | /// Get all accounts from the store for the given service. 12 | /// 13 | /// Name of the service to match against. Use null to match all values. 14 | /// All accounts that match the query. 15 | IList GetAccounts(string service); 16 | 17 | /// 18 | /// Get the first credential from the store that matches the given query. 19 | /// 20 | /// Name of the service to match against. Use null to match all values. 21 | /// Account name to match against. Use null to match all values. 22 | /// First matching credential or null if none are found. 23 | ICredential Get(string service, string account); 24 | 25 | /// 26 | /// Add or update credential in the store with the specified key. 27 | /// 28 | /// Name of the service this credential is for. Use null to match all values. 29 | /// Account associated with this credential. Use null to match all values. 30 | /// Secret value to store. 31 | void AddOrUpdate(string service, string account, string secret); 32 | 33 | /// 34 | /// Delete credential from the store that matches the given query. 35 | /// 36 | /// Name of the service to match against. Use null to match all values. 37 | /// Account name to match against. Use null to match all values. 38 | /// True if the credential was deleted, false otherwise. 39 | bool Remove(string service, string account); 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /src/Core/ISessionManager.cs: -------------------------------------------------------------------------------- 1 | namespace GitCredentialManager 2 | { 3 | public interface ISessionManager 4 | { 5 | /// 6 | /// Determine if the current session has access to a desktop/can display UI. 7 | /// 8 | /// True if the session can display UI, false otherwise. 9 | bool IsDesktopSession { get; } 10 | 11 | /// 12 | /// Determine if the current session has access to a web browser. 13 | /// 14 | /// True if the session can display a web browser, false otherwise. 15 | bool IsWebBrowserAvailable { get; } 16 | } 17 | 18 | public abstract class SessionManager : ISessionManager 19 | { 20 | protected IEnvironment Environment { get; } 21 | protected IFileSystem FileSystem { get; } 22 | 23 | protected SessionManager(IEnvironment env, IFileSystem fs) 24 | { 25 | EnsureArgument.NotNull(env, nameof(env)); 26 | EnsureArgument.NotNull(fs, nameof(fs)); 27 | 28 | Environment = env; 29 | FileSystem = fs; 30 | } 31 | 32 | public abstract bool IsDesktopSession { get; } 33 | 34 | public virtual bool IsWebBrowserAvailable => IsDesktopSession; 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/Core/ISystemPrompts.cs: -------------------------------------------------------------------------------- 1 | 2 | namespace GitCredentialManager 3 | { 4 | /// 5 | /// Represents native system UI prompts. 6 | /// 7 | public interface ISystemPrompts 8 | { 9 | /// 10 | /// The parent window handle or ID. Used for correctly positioning and parenting system dialogs. 11 | /// 12 | /// This value is platform specific. 13 | object ParentWindowId { get; set; } 14 | 15 | /// 16 | /// Show a basic credential prompt using native system UI. 17 | /// 18 | /// The name or URL of the resource to collect credentials for. 19 | /// Optional pre-filled username. 20 | /// The captured basic credential. 21 | /// True if the user completes the dialog, false otherwise. 22 | bool ShowCredentialPrompt(string resource, string userName, out ICredential credential); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/Core/ITerminal.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using GitCredentialManager.Interop; 3 | 4 | namespace GitCredentialManager 5 | { 6 | /// 7 | /// Represents a terminal (TTY) interface. 8 | /// 9 | public interface ITerminal 10 | { 11 | /// 12 | /// Write a message to the terminal screen. 13 | /// 14 | /// Format message to print to the terminal. 15 | /// Format argument values. 16 | /// Throw if an error occurs interacting with the native terminal device. 17 | void WriteLine(string format, params object[] args); 18 | 19 | /// 20 | /// Prompt for user input. 21 | /// 22 | /// Prompt message. 23 | /// User input. 24 | /// Throw if an error occurs interacting with the native terminal device. 25 | string Prompt(string prompt); 26 | 27 | /// 28 | /// Prompt for secret user input. 29 | /// 30 | /// 31 | /// Typed user input is masked or hidden. 32 | /// 33 | /// Prompt message. 34 | /// Secret user input. 35 | /// Throw if an error occurs interacting with the native terminal device. 36 | string PromptSecret(string prompt); 37 | } 38 | 39 | public static class TerminalExtensions 40 | { 41 | /// 42 | /// Write a blank line to the terminal screen. 43 | /// 44 | public static void WriteLine(this ITerminal terminal) 45 | { 46 | terminal.WriteLine(Environment.NewLine); 47 | } 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /src/Core/ITrace2Writer.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Text; 3 | 4 | namespace GitCredentialManager; 5 | 6 | /// 7 | /// The different format targets supported in the TRACE2 tracing 8 | /// system. 9 | /// 10 | public enum Trace2FormatTarget 11 | { 12 | Event, 13 | Normal, 14 | Performance 15 | } 16 | 17 | public interface ITrace2Writer : IDisposable 18 | { 19 | bool Failed { get; } 20 | 21 | void Write(Trace2Message message); 22 | } 23 | 24 | public class Trace2Writer : DisposableObject, ITrace2Writer 25 | { 26 | private readonly Trace2FormatTarget _formatTarget; 27 | 28 | public bool Failed { get; protected set; } 29 | 30 | protected Trace2Writer(Trace2FormatTarget formatTarget) 31 | { 32 | _formatTarget = formatTarget; 33 | } 34 | 35 | protected string Format(Trace2Message message) 36 | { 37 | EnsureArgument.NotNull(message, nameof(message)); 38 | var sb = new StringBuilder(); 39 | 40 | switch (_formatTarget) 41 | { 42 | case Trace2FormatTarget.Event: 43 | sb.Append(message.ToJson()); 44 | break; 45 | case Trace2FormatTarget.Normal: 46 | sb.Append(message.ToNormalString()); 47 | break; 48 | case Trace2FormatTarget.Performance: 49 | sb.Append(message.ToPerformanceString()); 50 | break; 51 | default: 52 | Console.WriteLine($"warning: unrecognized format target '{_formatTarget}', disabling TRACE2 tracing."); 53 | Failed = true; 54 | break; 55 | } 56 | 57 | sb.Append('\n'); 58 | return sb.ToString(); 59 | } 60 | 61 | public virtual void Write(Trace2Message message) 62 | { } 63 | } 64 | -------------------------------------------------------------------------------- /src/Core/Interop/InteropException.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.ComponentModel; 3 | using System.Diagnostics; 4 | 5 | namespace GitCredentialManager.Interop 6 | { 7 | /// 8 | /// An unexpected error occurred in interop-code. 9 | /// 10 | [DebuggerDisplay("{DebuggerDisplay}")] 11 | public class InteropException : Exception 12 | { 13 | public InteropException() 14 | : base() { } 15 | 16 | public InteropException(string message, int errorCode) 17 | : base(message) 18 | { 19 | ErrorCode = errorCode; 20 | } 21 | 22 | public InteropException(string message, int errorCode, Exception innerException) 23 | : base(message, innerException) 24 | { 25 | ErrorCode = errorCode; 26 | } 27 | 28 | public InteropException(string message, Win32Exception w32Exception) 29 | : base(message, w32Exception) 30 | { 31 | ErrorCode = w32Exception.NativeErrorCode; 32 | } 33 | 34 | /// 35 | /// Native error code. 36 | /// 37 | public int ErrorCode { get; } 38 | 39 | private string DebuggerDisplay => $"{Message} [0x{ErrorCode:x}]"; 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /src/Core/Interop/InteropUtils.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Linq; 3 | using System.Runtime.InteropServices; 4 | 5 | namespace GitCredentialManager.Interop 6 | { 7 | internal static class InteropUtils 8 | { 9 | public static byte[] ToByteArray(IntPtr ptr, long count) 10 | { 11 | var destination = new byte[count]; 12 | Marshal.Copy(ptr, destination, 0, destination.Length); 13 | return destination; 14 | } 15 | 16 | public static bool AreEqual(byte[] bytes, IntPtr ptr, uint length) 17 | { 18 | if (bytes.Length == 0 && (ptr == IntPtr.Zero || length == 0)) 19 | { 20 | return true; 21 | } 22 | 23 | if (bytes.Length != length) 24 | { 25 | return false; 26 | } 27 | 28 | byte[] ptrBytes = ToByteArray(ptr, length); 29 | return bytes.SequenceEqual(ptrBytes); 30 | } 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/Core/Interop/Linux/LinuxFileSystem.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.IO; 3 | using GitCredentialManager.Interop.Posix; 4 | 5 | namespace GitCredentialManager.Interop.Linux 6 | { 7 | public class LinuxFileSystem : PosixFileSystem 8 | { 9 | public override bool IsSamePath(string a, string b) 10 | { 11 | if (string.IsNullOrWhiteSpace(a) || string.IsNullOrWhiteSpace(b)) 12 | { 13 | return false; 14 | } 15 | 16 | // Normalize paths 17 | a = Path.GetFullPath(a); 18 | b = Path.GetFullPath(b); 19 | 20 | // Resolve symbolic links 21 | a = ResolveSymbolicLinks(a); 22 | b = ResolveSymbolicLinks(b); 23 | 24 | return StringComparer.Ordinal.Equals(a, b); 25 | } 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/Core/Interop/Linux/LinuxSessionManager.cs: -------------------------------------------------------------------------------- 1 | using GitCredentialManager.Interop.Posix; 2 | 3 | namespace GitCredentialManager.Interop.Linux; 4 | 5 | public class LinuxSessionManager : PosixSessionManager 6 | { 7 | private bool? _isWebBrowserAvailable; 8 | 9 | public LinuxSessionManager(IEnvironment env, IFileSystem fs) : base(env, fs) 10 | { 11 | PlatformUtils.EnsureLinux(); 12 | } 13 | 14 | public override bool IsWebBrowserAvailable 15 | { 16 | get 17 | { 18 | return _isWebBrowserAvailable ??= GetWebBrowserAvailable(); 19 | } 20 | } 21 | 22 | private bool GetWebBrowserAvailable() 23 | { 24 | // If this is a Windows Subsystem for Linux distribution we may 25 | // be able to launch the web browser of the host Windows OS. 26 | if (WslUtils.IsWslDistribution(Environment, FileSystem, out _)) 27 | { 28 | // We need a shell execute handler to be able to launch to browser 29 | if (!BrowserUtils.TryGetLinuxShellExecuteHandler(Environment, out _)) 30 | { 31 | return false; 32 | } 33 | 34 | // 35 | // If we are in Windows logon session 0 then the user can never interact, 36 | // even in the WinSta0 window station. This is typical when SSH-ing into a 37 | // Windows 10+ machine using the default OpenSSH Server configuration, 38 | // which runs in the 'services' session 0. 39 | // 40 | // If we're in any other session, and in the WinSta0 window station then 41 | // the user can possibly interact. However, since it's hard to determine 42 | // the window station from PowerShell cmdlets (we'd need to write P/Invoke 43 | // code and that's just messy and too many levels of indirection quite 44 | // frankly!) we just assume any non session 0 is interactive. 45 | // 46 | // This assumption doesn't hold true if the user has changed the user that 47 | // the OpenSSH Server service runs as (not a built-in NT service) *AND* 48 | // they've SSH-ed into the Windows host (and then started a WSL shell). 49 | // This feels like a very small subset of users... 50 | // 51 | if (WslUtils.GetWindowsSessionId(FileSystem) == 0) 52 | { 53 | return false; 54 | } 55 | 56 | // If we are not in session 0, or we cannot get the Windows session ID, 57 | // assume that we *CAN* launch the browser so that users are never blocked. 58 | return true; 59 | } 60 | 61 | // We require an interactive desktop session to be able to launch a browser 62 | return IsDesktopSession; 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /src/Core/Interop/Linux/LinuxTerminal.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using GitCredentialManager.Interop.Linux.Native; 3 | using GitCredentialManager.Interop.Posix; 4 | using GitCredentialManager.Interop.Posix.Native; 5 | 6 | namespace GitCredentialManager.Interop.Linux 7 | { 8 | public class LinuxTerminal : PosixTerminal 9 | { 10 | public LinuxTerminal(ITrace trace, ITrace2 trace2) 11 | : base(trace, trace2) { } 12 | 13 | protected override IDisposable CreateTtyContext(int fd, bool echo) 14 | { 15 | return new TtyContext(Trace, Trace2, fd, echo); 16 | } 17 | 18 | private class TtyContext : IDisposable 19 | { 20 | private readonly ITrace _trace; 21 | private readonly int _fd; 22 | 23 | private termios_Linux _originalTerm; 24 | private bool _isDisposed; 25 | 26 | public TtyContext(ITrace trace, ITrace2 trace2, int fd, bool echo) 27 | { 28 | EnsureArgument.NotNull(trace, nameof(trace)); 29 | EnsureArgument.PositiveOrZero(fd, nameof(fd)); 30 | 31 | _trace = trace; 32 | _fd = fd; 33 | 34 | int error = 0; 35 | 36 | // Capture current terminal settings so we can restore them later 37 | if ((error = Termios_Linux.tcgetattr(_fd, out termios_Linux t)) != 0) 38 | { 39 | throw new Trace2InteropException(trace2, "Failed to get initial terminal settings", error); 40 | } 41 | 42 | _originalTerm = t; 43 | 44 | // Set desired echo state 45 | _trace.WriteLine($"Setting terminal echo state to '{echo}'"); 46 | if (echo) 47 | t.c_lflag |= LocalFlags.ECHO; 48 | else 49 | t.c_lflag &= ~LocalFlags.ECHO; 50 | 51 | if ((error = Termios_Linux.tcsetattr(_fd, SetActionFlags.TCSAFLUSH, ref t)) != 0) 52 | { 53 | throw new Trace2InteropException(trace2, "Failed to set terminal settings", error); 54 | } 55 | } 56 | 57 | public void Dispose() 58 | { 59 | if (_isDisposed) 60 | { 61 | return; 62 | } 63 | 64 | int error = 0; 65 | 66 | // Restore original terminal settings 67 | if ((error = Termios_Linux.tcsetattr(_fd, SetActionFlags.TCSAFLUSH, ref _originalTerm)) != 0) 68 | { 69 | _trace.WriteLine($"Failed to get restore terminal settings (error: {error:x}"); 70 | } 71 | 72 | _isDisposed = true; 73 | } 74 | } 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /src/Core/Interop/Linux/Native/Glib.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Runtime.InteropServices; 3 | 4 | namespace GitCredentialManager.Interop.Linux.Native 5 | { 6 | public static class Glib 7 | { 8 | private const string LibraryName = "libglib-2.0.so.0"; 9 | 10 | public struct GHashTable { /* transparent */ } 11 | 12 | [StructLayout(LayoutKind.Sequential)] 13 | public struct GList 14 | { 15 | public IntPtr data; 16 | public IntPtr next; 17 | public IntPtr prev; 18 | } 19 | 20 | [StructLayout(LayoutKind.Sequential)] 21 | public struct GError 22 | { 23 | public int domain; 24 | public int code; 25 | public IntPtr message; 26 | } 27 | 28 | [UnmanagedFunctionPointer(CallingConvention.Cdecl)] 29 | public delegate uint GHashFunc(IntPtr key); 30 | 31 | [UnmanagedFunctionPointer(CallingConvention.Cdecl)] 32 | public delegate bool GEqualFunc(IntPtr a, IntPtr b); 33 | 34 | [UnmanagedFunctionPointer(CallingConvention.Cdecl)] 35 | public delegate void GDestroyNotify(IntPtr data); 36 | 37 | [DllImport(LibraryName, CharSet = CharSet.Ansi, CallingConvention = CallingConvention.Cdecl)] 38 | public static extern uint g_str_hash(IntPtr key); 39 | 40 | [DllImport(LibraryName, CharSet = CharSet.Ansi, CallingConvention = CallingConvention.Cdecl)] 41 | public static extern bool g_str_equal(IntPtr a, IntPtr b); 42 | 43 | [DllImport(LibraryName, CharSet = CharSet.Ansi, CallingConvention = CallingConvention.Cdecl)] 44 | public static extern unsafe GHashTable* g_hash_table_new(GHashFunc hash_func, GEqualFunc key_equal_func); 45 | 46 | [DllImport(LibraryName, CharSet = CharSet.Ansi, CallingConvention = CallingConvention.Cdecl)] 47 | public static extern unsafe GHashTable* g_hash_table_new_full( 48 | GHashFunc hash_func, 49 | GEqualFunc key_equal_func, 50 | GDestroyNotify key_destroy_func, 51 | GDestroyNotify value_destroy_func); 52 | 53 | [DllImport(LibraryName, CharSet = CharSet.Ansi, CallingConvention = CallingConvention.Cdecl)] 54 | public static extern unsafe void g_hash_table_destroy(GHashTable* hash_table); 55 | 56 | [DllImport(LibraryName, CharSet = CharSet.Ansi, CallingConvention = CallingConvention.Cdecl)] 57 | public static extern unsafe bool g_hash_table_insert(GHashTable* hash_table, IntPtr key, IntPtr value); 58 | 59 | [DllImport(LibraryName, CharSet = CharSet.Ansi, CallingConvention = CallingConvention.Cdecl)] 60 | public static extern unsafe IntPtr g_hash_table_lookup(GHashTable* hash_table, IntPtr key); 61 | 62 | [DllImport(LibraryName, CharSet = CharSet.Ansi, CallingConvention = CallingConvention.Cdecl)] 63 | public static extern unsafe void g_list_free_full(GList* list, GDestroyNotify free_func); 64 | 65 | [DllImport(LibraryName, CharSet = CharSet.Ansi, CallingConvention = CallingConvention.Cdecl)] 66 | public static extern unsafe void g_hash_table_unref(GHashTable* hash_table); 67 | 68 | [DllImport(LibraryName, CharSet = CharSet.Ansi, CallingConvention = CallingConvention.Cdecl)] 69 | public static extern unsafe void g_error_free(GError* error); 70 | 71 | [DllImport(LibraryName, CharSet = CharSet.Ansi, CallingConvention = CallingConvention.Cdecl)] 72 | public static extern void g_free(IntPtr mem); 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /src/Core/Interop/Linux/Native/Gobject.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Runtime.InteropServices; 3 | 4 | namespace GitCredentialManager.Interop.Linux.Native 5 | { 6 | public static class Gobject 7 | { 8 | private const string LibraryName = "libgobject-2.0.so.0"; 9 | 10 | [DllImport(LibraryName, CharSet = CharSet.Ansi, CallingConvention = CallingConvention.Cdecl)] 11 | public static extern void g_object_ref(IntPtr @object); 12 | 13 | [DllImport(LibraryName, CharSet = CharSet.Ansi, CallingConvention = CallingConvention.Cdecl)] 14 | public static extern void g_object_unref(IntPtr @object); 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/Core/Interop/Linux/Native/termios_Linux.cs: -------------------------------------------------------------------------------- 1 | using System.Runtime.InteropServices; 2 | using GitCredentialManager.Interop.Posix.Native; 3 | 4 | namespace GitCredentialManager.Interop.Linux.Native 5 | { 6 | public static class Termios_Linux 7 | { 8 | [DllImport("libc", CallingConvention = CallingConvention.Cdecl, CharSet = CharSet.Ansi)] 9 | public static extern int tcgetattr(int fd, out termios_Linux termios); 10 | 11 | [DllImport("libc", CallingConvention = CallingConvention.Cdecl, CharSet = CharSet.Ansi)] 12 | public static extern int tcsetattr(int fd, SetActionFlags optActions, ref termios_Linux termios); 13 | } 14 | 15 | [StructLayout(LayoutKind.Explicit)] 16 | public struct termios_Linux 17 | { 18 | // Linux has an array of 32 elements 19 | private const int NCCS = 32; 20 | 21 | // Linux uses unsigned 32-bit sized flags 22 | [FieldOffset(0)] public InputFlags c_iflag; 23 | [FieldOffset(4)] public OutputFlags c_oflag; 24 | [FieldOffset(8)] public ControlFlags c_cflag; 25 | [FieldOffset(12)] public LocalFlags c_lflag; 26 | 27 | [MarshalAs(UnmanagedType.ByValArray, SizeConst = NCCS)] 28 | [FieldOffset(16)] public byte[] c_cc; 29 | 30 | [FieldOffset(16 + NCCS)] public uint c_ispeed; 31 | [FieldOffset(16 + NCCS + 4)] public uint c_ospeed; 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/Core/Interop/Linux/SecretServiceCredential.cs: -------------------------------------------------------------------------------- 1 | using System.Diagnostics; 2 | 3 | namespace GitCredentialManager.Interop.Linux 4 | { 5 | [DebuggerDisplay("{DebuggerDisplay}")] 6 | public class SecretServiceCredential : ICredential 7 | { 8 | internal SecretServiceCredential(string service, string account, string password) 9 | { 10 | Service = service; 11 | Account = account; 12 | Password = password; 13 | } 14 | 15 | public string Service { get; } 16 | 17 | public string Account { get; } 18 | 19 | public string Password { get; } 20 | 21 | private string DebuggerDisplay => $"[Service: {Service}, Account: {Account}]"; 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/Core/Interop/MacOS/MacOSEnvironment.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Diagnostics; 4 | using System.IO; 5 | using System.Threading; 6 | using GitCredentialManager.Interop.Posix; 7 | 8 | namespace GitCredentialManager.Interop.MacOS 9 | { 10 | public class MacOSEnvironment : PosixEnvironment 11 | { 12 | private ICollection _pathsToIgnore; 13 | 14 | public MacOSEnvironment(IFileSystem fileSystem) 15 | : base(fileSystem) { } 16 | 17 | internal MacOSEnvironment(IFileSystem fileSystem, IReadOnlyDictionary variables) 18 | : base(fileSystem, variables) { } 19 | 20 | public override bool TryLocateExecutable(string program, out string path) 21 | { 22 | if (_pathsToIgnore is null) 23 | { 24 | _pathsToIgnore = new List(); 25 | if (Variables.TryGetValue("HOMEBREW_PREFIX", out string homebrewPrefix)) 26 | { 27 | string homebrewGit = Path.Combine(homebrewPrefix, "Homebrew/Library/Homebrew/shims/shared/git"); 28 | _pathsToIgnore.Add(homebrewGit); 29 | } 30 | } 31 | return TryLocateExecutable(program, _pathsToIgnore, out path); 32 | } 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/Core/Interop/MacOS/MacOSFileSystem.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.IO; 3 | using GitCredentialManager.Interop.Posix; 4 | 5 | namespace GitCredentialManager.Interop.MacOS 6 | { 7 | public class MacOSFileSystem : PosixFileSystem 8 | { 9 | public override bool IsSamePath(string a, string b) 10 | { 11 | if (string.IsNullOrWhiteSpace(a) || string.IsNullOrWhiteSpace(b)) 12 | { 13 | return false; 14 | } 15 | 16 | // Normalize paths 17 | a = Path.GetFullPath(a); 18 | b = Path.GetFullPath(b); 19 | 20 | // Resolve symbolic links 21 | a = ResolveSymbolicLinks(a); 22 | b = ResolveSymbolicLinks(b); 23 | 24 | // TODO: determine if file system is case-sensitive 25 | // By default HFS+/APFS is NOT case-sensitive... 26 | return StringComparer.OrdinalIgnoreCase.Equals(a, b); 27 | } 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/Core/Interop/MacOS/MacOSKeychainCredential.cs: -------------------------------------------------------------------------------- 1 | using System.Diagnostics; 2 | 3 | namespace GitCredentialManager.Interop.MacOS 4 | { 5 | [DebuggerDisplay("{DebuggerDisplay}")] 6 | public class MacOSKeychainCredential : ICredential 7 | { 8 | internal MacOSKeychainCredential(string service, string account, string password, string label) 9 | { 10 | Service = service; 11 | Account = account; 12 | Password = password; 13 | Label = label; 14 | } 15 | 16 | public string Service { get; } 17 | 18 | public string Account { get; } 19 | 20 | public string Label { get; } 21 | 22 | public string Password { get; } 23 | 24 | private string DebuggerDisplay => $"{Label} [Service: {Service}, Account: {Account}]"; 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/Core/Interop/MacOS/MacOSSessionManager.cs: -------------------------------------------------------------------------------- 1 | using GitCredentialManager.Interop.MacOS.Native; 2 | using GitCredentialManager.Interop.Posix; 3 | 4 | namespace GitCredentialManager.Interop.MacOS 5 | { 6 | public class MacOSSessionManager : PosixSessionManager 7 | { 8 | public MacOSSessionManager(IEnvironment env, IFileSystem fs) : base(env, fs) 9 | { 10 | PlatformUtils.EnsureMacOS(); 11 | } 12 | 13 | public override bool IsDesktopSession 14 | { 15 | get 16 | { 17 | // Get information about the current session 18 | int error = SecurityFramework.SessionGetInfo(SecurityFramework.CallerSecuritySession, out int id, out var sessionFlags); 19 | 20 | // Check if the session supports Quartz 21 | if (error == 0 && (sessionFlags & SessionAttributeBits.SessionHasGraphicAccess) != 0) 22 | { 23 | return true; 24 | } 25 | 26 | // Fall-through and check if X11 is available on macOS 27 | return base.IsDesktopSession; 28 | } 29 | } 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/Core/Interop/MacOS/MacOSTerminal.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using GitCredentialManager.Interop.MacOS.Native; 3 | using GitCredentialManager.Interop.Posix; 4 | using GitCredentialManager.Interop.Posix.Native; 5 | 6 | namespace GitCredentialManager.Interop.MacOS 7 | { 8 | public class MacOSTerminal : PosixTerminal 9 | { 10 | public MacOSTerminal(ITrace trace, ITrace2 trace2) 11 | : base(trace, trace2) { } 12 | 13 | protected override IDisposable CreateTtyContext(int fd, bool echo) 14 | { 15 | return new TtyContext(Trace, Trace2, fd, echo); 16 | } 17 | 18 | private class TtyContext : IDisposable 19 | { 20 | private readonly ITrace _trace; 21 | private readonly int _fd; 22 | 23 | private termios_MacOS _originalTerm; 24 | private bool _isDisposed; 25 | 26 | public TtyContext(ITrace trace, ITrace2 trace2, int fd, bool echo) 27 | { 28 | EnsureArgument.NotNull(trace, nameof(trace)); 29 | EnsureArgument.PositiveOrZero(fd, nameof(fd)); 30 | 31 | _trace = trace; 32 | _fd = fd; 33 | 34 | int error = 0; 35 | 36 | // Capture current terminal settings so we can restore them later 37 | if ((error = Termios_MacOS.tcgetattr(_fd, out termios_MacOS t)) != 0) 38 | { 39 | throw new Trace2InteropException(trace2, "Failed to get initial terminal settings", error); 40 | } 41 | 42 | _originalTerm = t; 43 | 44 | // Set desired echo state 45 | _trace.WriteLine($"Setting terminal echo state to '{echo}'"); 46 | if (echo) 47 | t.c_lflag |= LocalFlags.ECHO; 48 | else 49 | t.c_lflag &= ~LocalFlags.ECHO; 50 | 51 | if ((error = Termios_MacOS.tcsetattr(_fd, SetActionFlags.TCSAFLUSH, ref t)) != 0) 52 | { 53 | throw new Trace2InteropException(trace2, "Failed to set terminal settings", error); 54 | } 55 | } 56 | 57 | public void Dispose() 58 | { 59 | if (_isDisposed) 60 | { 61 | return; 62 | } 63 | 64 | int error = 0; 65 | 66 | // Restore original terminal settings 67 | if ((error = Termios_MacOS.tcsetattr(_fd, SetActionFlags.TCSAFLUSH, ref _originalTerm)) != 0) 68 | { 69 | _trace.WriteLine($"Failed to get restore terminal settings (error: {error:x}"); 70 | } 71 | 72 | _isDisposed = true; 73 | } 74 | } 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /src/Core/Interop/MacOS/Native/LibC.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Runtime.InteropServices; 3 | 4 | namespace GitCredentialManager.Interop.MacOS.Native 5 | { 6 | public static class LibC 7 | { 8 | private const string LibCLib = "libc"; 9 | 10 | [DllImport(LibCLib, CharSet = CharSet.Ansi, CallingConvention = CallingConvention.Cdecl)] 11 | public static extern int _NSGetExecutablePath(IntPtr buf, out int bufsize); 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /src/Core/Interop/MacOS/Native/LibSystem.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Runtime.InteropServices; 3 | 4 | namespace GitCredentialManager.Interop.MacOS.Native 5 | { 6 | public static class LibSystem 7 | { 8 | private const string LibSystemLib = "/System/Library/Frameworks/System.framework/System"; 9 | 10 | [DllImport(LibSystemLib, CharSet = CharSet.Ansi, CallingConvention = CallingConvention.Cdecl)] 11 | public static extern IntPtr dlopen(string name, int flags); 12 | 13 | [DllImport(LibSystemLib, CharSet = CharSet.Ansi, CallingConvention = CallingConvention.Cdecl)] 14 | public static extern IntPtr dlsym(IntPtr handle, string symbol); 15 | 16 | public static IntPtr GetGlobal(IntPtr handle, string symbol) 17 | { 18 | IntPtr ptr = dlsym(handle, symbol); 19 | return Marshal.PtrToStructure(ptr); 20 | } 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/Core/Interop/MacOS/Native/termios_MacOS.cs: -------------------------------------------------------------------------------- 1 | using System.Runtime.InteropServices; 2 | using GitCredentialManager.Interop.Posix.Native; 3 | 4 | namespace GitCredentialManager.Interop.MacOS.Native 5 | { 6 | public static class Termios_MacOS 7 | { 8 | [DllImport("libc", CallingConvention = CallingConvention.Cdecl, CharSet = CharSet.Ansi)] 9 | public static extern int tcgetattr(int fd, out termios_MacOS termios); 10 | 11 | [DllImport("libc", CallingConvention = CallingConvention.Cdecl, CharSet = CharSet.Ansi)] 12 | public static extern int tcsetattr(int fd, SetActionFlags optActions, ref termios_MacOS termios); 13 | } 14 | 15 | [StructLayout(LayoutKind.Explicit)] 16 | public struct termios_MacOS 17 | { 18 | // macOS has an array of 20 elements 19 | private const int NCCS = 20; 20 | 21 | // macOS uses unsigned 64-bit sized flags 22 | [FieldOffset(0)] public InputFlags c_iflag; 23 | [FieldOffset(8)] public OutputFlags c_oflag; 24 | [FieldOffset(16)] public ControlFlags c_cflag; 25 | [FieldOffset(24)] public LocalFlags c_lflag; 26 | 27 | [MarshalAs(UnmanagedType.ByValArray, SizeConst = NCCS)] 28 | [FieldOffset(32)] public byte[] c_cc; 29 | 30 | [FieldOffset(32 + NCCS)] public ulong c_ispeed; 31 | [FieldOffset(32 + NCCS + 8)] public ulong c_ospeed; 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/Core/Interop/Posix/GpgPassCredentialStore.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.IO; 4 | using System.Text; 5 | 6 | namespace GitCredentialManager.Interop.Posix 7 | { 8 | public class GpgPassCredentialStore : PlaintextCredentialStore 9 | { 10 | public const string PasswordStoreDirEnvar = "PASSWORD_STORE_DIR"; 11 | 12 | private readonly IGpg _gpg; 13 | 14 | public GpgPassCredentialStore(IFileSystem fileSystem, IGpg gpg, string storeRoot, string @namespace = null) 15 | : base(fileSystem, storeRoot, @namespace) 16 | { 17 | PlatformUtils.EnsurePosix(); 18 | EnsureArgument.NotNull(gpg, nameof(gpg)); 19 | _gpg = gpg; 20 | } 21 | 22 | protected override string CredentialFileExtension => ".gpg"; 23 | 24 | private string GetGpgId() 25 | { 26 | string gpgIdPath = Path.Combine(StoreRoot, ".gpg-id"); 27 | if (!FileSystem.FileExists(gpgIdPath)) 28 | { 29 | throw new Exception($"Cannot find GPG ID in '{gpgIdPath}'; password store has not been initialized"); 30 | } 31 | 32 | using (var stream = FileSystem.OpenFileStream(gpgIdPath, FileMode.Open, FileAccess.Read, FileShare.Read)) 33 | using (var reader = new StreamReader(stream)) 34 | { 35 | return reader.ReadLine(); 36 | } 37 | } 38 | 39 | protected override bool TryDeserializeCredential(string path, out FileCredential credential) 40 | { 41 | string text = _gpg.DecryptFile(path); 42 | 43 | int line1Idx = text.IndexOf(Environment.NewLine, StringComparison.OrdinalIgnoreCase); 44 | if (line1Idx > 0) 45 | { 46 | // Password is the first line 47 | string password = text.Substring(0, line1Idx); 48 | 49 | // All subsequent lines are metadata/attributes 50 | string attrText = text.Substring(line1Idx + Environment.NewLine.Length); 51 | using var attrReader = new StringReader(attrText); 52 | IDictionary attrs = attrReader.ReadDictionary(StringComparer.OrdinalIgnoreCase); 53 | 54 | // Account is optional 55 | attrs.TryGetValue("account", out string account); 56 | 57 | // Service is required 58 | if (attrs.TryGetValue("service", out string service)) 59 | { 60 | credential = new FileCredential(path, service, account, password); 61 | return true; 62 | } 63 | } 64 | 65 | credential = null; 66 | return false; 67 | } 68 | 69 | protected override void SerializeCredential(FileCredential credential) 70 | { 71 | string gpgId = GetGpgId(); 72 | 73 | var sb = new StringBuilder(credential.Password); 74 | sb.AppendFormat("{1}service={0}{1}", credential.Service, Environment.NewLine); 75 | sb.AppendFormat("account={0}{1}", credential.Account, Environment.NewLine); 76 | string fileContents = sb.ToString(); 77 | 78 | // Ensure the parent directory exists 79 | string parentDir = Path.GetDirectoryName(credential.FullPath); 80 | if (!FileSystem.DirectoryExists(parentDir)) 81 | { 82 | FileSystem.CreateDirectory(parentDir); 83 | } 84 | 85 | // Delete any existing file 86 | if (FileSystem.FileExists(credential.FullPath)) 87 | { 88 | FileSystem.DeleteFile(credential.FullPath); 89 | } 90 | 91 | // Encrypt! 92 | _gpg.EncryptFile(credential.FullPath, gpgId, fileContents); 93 | } 94 | } 95 | } 96 | -------------------------------------------------------------------------------- /src/Core/Interop/Posix/Native/Fcntl.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Runtime.InteropServices; 3 | 4 | namespace GitCredentialManager.Interop.Posix.Native 5 | { 6 | public static class Fcntl 7 | { 8 | [DllImport("libc", CallingConvention = CallingConvention.Cdecl, CharSet = CharSet.Ansi)] 9 | public static extern int open(string pathname, OpenFlags flags); 10 | } 11 | 12 | [Flags] 13 | public enum OpenFlags 14 | { 15 | O_RDONLY = 0, 16 | O_WRONLY = 1, 17 | O_RDWR = 2, 18 | O_CREAT = 64, 19 | O_EXCL = 128, 20 | O_NOCTTY = 256, 21 | O_TRUNC = 512, 22 | O_APPEND = 1024, 23 | O_NONBLOCK = 2048, 24 | O_SYNC = 4096, 25 | O_NOFOLLOW = 131072, 26 | O_DIRECTORY = 65536, 27 | O_DIRECT = 16384, 28 | O_ASYNC = 8192, 29 | O_LARGEFILE = 32768, 30 | O_CLOEXEC = 524288, 31 | O_PATH = 2097152, 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/Core/Interop/Posix/Native/Signal.cs: -------------------------------------------------------------------------------- 1 | using System.Runtime.InteropServices; 2 | 3 | namespace GitCredentialManager.Interop.Posix.Native 4 | { 5 | public static class Signal 6 | { 7 | /// 8 | /// Interrupt. 9 | /// 10 | public const int SIGINT = 2; 11 | 12 | /// 13 | /// Quit. 14 | /// 15 | public const int SIGQUIT = 3; 16 | 17 | /// 18 | /// Abort. 19 | /// 20 | public const int SIGABRT = 6; 21 | 22 | /// 23 | /// Kill (cannot be caught or ignored). 24 | /// 25 | public const int SIGKILL = 9; 26 | 27 | /// 28 | /// Software termination signal from kill. 29 | /// 30 | public const int SIGTERM = 15; 31 | 32 | [DllImport("libc", CallingConvention = CallingConvention.Cdecl, CharSet = CharSet.Ansi)] 33 | public static extern void kill(int pid, int sig); 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/Core/Interop/Posix/Native/Stat.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Runtime.InteropServices; 3 | 4 | namespace GitCredentialManager.Interop.Posix.Native 5 | { 6 | public static class Stat 7 | { 8 | [DllImport("libc", CallingConvention = CallingConvention.Cdecl, CharSet = CharSet.Ansi)] 9 | public static extern int chmod(string path, NativeFileMode mode); 10 | } 11 | [Flags] 12 | public enum NativeFileMode 13 | { 14 | NONE = 0, 15 | 16 | // Default permissions (RW for owner, RW for group, RW for other) 17 | DEFAULT = S_IWOTH | S_IROTH | S_IWGRP | S_IRGRP | S_IWUSR | S_IRUSR, 18 | 19 | // All file access permissions (RWX for owner, group, and other) 20 | ACCESSPERMS = S_IRWXO | S_IRWXU | S_IRWXG, 21 | 22 | // Read for owner (0000400) 23 | S_IRUSR = 0x100, 24 | // Write for owner (0000200) 25 | S_IWUSR = 0x080, 26 | // Execute for owner (0000100) 27 | S_IXUSR = 0x040, 28 | // Access permissions for owner 29 | S_IRWXU = S_IRUSR | S_IWUSR | S_IXUSR, 30 | 31 | // Read for group (0000040) 32 | S_IRGRP = 0x020, 33 | // Write for group (0000020) 34 | S_IWGRP = 0x010, 35 | // Execute for group (0000010) 36 | S_IXGRP = 0x008, 37 | // Access permissions for group 38 | S_IRWXG = S_IRGRP | S_IWGRP | S_IXGRP, 39 | 40 | // Read for other (0000004) 41 | S_IROTH = 0x004, 42 | // Write for other (0000002) 43 | S_IWOTH = 0x002, 44 | // Execute for other (0000001) 45 | S_IXOTH = 0x001, 46 | // Access permissions for other 47 | S_IRWXO = S_IROTH | S_IWOTH | S_IXOTH, 48 | 49 | // Set user ID on execution (0004000) 50 | S_ISUID = 0x800, 51 | // Set group ID on execution (0002000) 52 | S_ISGID = 0x400, 53 | // Sticky bit (0001000) 54 | S_ISVTX = 0x200, 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /src/Core/Interop/Posix/Native/Stdio.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Runtime.InteropServices; 3 | using System.Text; 4 | 5 | namespace GitCredentialManager.Interop.Posix.Native 6 | { 7 | public static class Stdio 8 | { 9 | public const int EOF = -1; 10 | 11 | [DllImport("libc", CallingConvention = CallingConvention.Cdecl, CharSet = CharSet.Ansi)] 12 | public static extern IntPtr fgets(StringBuilder sb, int size, IntPtr stream); 13 | 14 | [DllImport("libc", CallingConvention = CallingConvention.Cdecl, CharSet = CharSet.Ansi)] 15 | public static extern int fputc(int c, IntPtr stream); 16 | 17 | [DllImport("libc", CallingConvention = CallingConvention.Cdecl, CharSet = CharSet.Ansi)] 18 | public static extern int fprintf(IntPtr stream, string format, string message); 19 | 20 | [DllImport("libc", CallingConvention = CallingConvention.Cdecl, CharSet = CharSet.Ansi)] 21 | public static extern int fgetc(IntPtr stream); 22 | 23 | [DllImport("libc", CallingConvention = CallingConvention.Cdecl, CharSet = CharSet.Ansi)] 24 | public static extern int fputs(string str, IntPtr stream); 25 | 26 | [DllImport("libc", CallingConvention = CallingConvention.Cdecl, CharSet = CharSet.Ansi)] 27 | public static extern void setbuf(IntPtr stream, int size); 28 | 29 | [DllImport("libc", CallingConvention = CallingConvention.Cdecl, CharSet = CharSet.Ansi)] 30 | public static extern IntPtr fdopen(int fd, string mode); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/Core/Interop/Posix/Native/Stdlib.cs: -------------------------------------------------------------------------------- 1 | using System.Runtime.InteropServices; 2 | 3 | namespace GitCredentialManager.Interop.Posix.Native 4 | { 5 | public static class Stdlib 6 | { 7 | [DllImport("libc", CallingConvention = CallingConvention.Cdecl, CharSet = CharSet.Ansi)] 8 | public static extern unsafe byte* realpath(byte* path, byte* resolved_path); 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /src/Core/Interop/Posix/Native/Unistd.cs: -------------------------------------------------------------------------------- 1 | using System.Runtime.InteropServices; 2 | 3 | namespace GitCredentialManager.Interop.Posix.Native 4 | { 5 | public static class Unistd 6 | { 7 | [DllImport("libc", CallingConvention = CallingConvention.Cdecl, CharSet = CharSet.Ansi)] 8 | public static extern int read(int fd, byte[] buf, int count); 9 | 10 | [DllImport("libc", CallingConvention = CallingConvention.Cdecl, CharSet = CharSet.Ansi)] 11 | public static extern int write(int fd, byte[] buf, int size); 12 | 13 | [DllImport("libc", CallingConvention = CallingConvention.Cdecl, CharSet = CharSet.Ansi)] 14 | public static extern int close(int fd); 15 | 16 | [DllImport("libc", CallingConvention = CallingConvention.Cdecl, CharSet = CharSet.Ansi)] 17 | public static extern int getpid(); 18 | 19 | [DllImport("libc", CallingConvention = CallingConvention.Cdecl, CharSet = CharSet.Ansi)] 20 | public static extern int getppid(); 21 | 22 | [DllImport("libc", CallingConvention = CallingConvention.Cdecl, CharSet = CharSet.Ansi)] 23 | public static extern int geteuid(); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/Core/Interop/Posix/PosixEnvironment.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | 5 | namespace GitCredentialManager.Interop.Posix 6 | { 7 | public class PosixEnvironment : EnvironmentBase 8 | { 9 | public PosixEnvironment(IFileSystem fileSystem) 10 | : base(fileSystem) { } 11 | 12 | internal PosixEnvironment(IFileSystem fileSystem, IReadOnlyDictionary variables) 13 | : base(fileSystem, variables) { } 14 | 15 | #region EnvironmentBase 16 | 17 | public override void AddDirectoryToPath(string directoryPath, EnvironmentVariableTarget target) 18 | { 19 | throw new NotImplementedException(); 20 | } 21 | 22 | public override void RemoveDirectoryFromPath(string directoryPath, EnvironmentVariableTarget target) 23 | { 24 | throw new NotImplementedException(); 25 | } 26 | 27 | protected override string[] SplitPathVariable(string value) 28 | { 29 | return value.Split(':'); 30 | } 31 | 32 | #endregion 33 | 34 | protected override IReadOnlyDictionary GetCurrentVariables() 35 | { 36 | var dict = new Dictionary(); 37 | var variables = Environment.GetEnvironmentVariables(); 38 | 39 | foreach (var key in variables.Keys) 40 | { 41 | if (key is string name && variables[key] is string value) 42 | { 43 | dict[name] = value; 44 | } 45 | } 46 | 47 | return dict; 48 | } 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /src/Core/Interop/Posix/PosixFileDescriptor.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Text; 3 | using GitCredentialManager.Interop.Posix.Native; 4 | 5 | namespace GitCredentialManager.Interop.Posix 6 | { 7 | /// 8 | /// Represents a thin wrapper over a POSIX file descriptor. 9 | /// 10 | public class PosixFileDescriptor : DisposableObject 11 | { 12 | private readonly int _fd; 13 | 14 | private PosixFileDescriptor() 15 | { 16 | PlatformUtils.EnsurePosix(); 17 | } 18 | 19 | public PosixFileDescriptor(string filename, OpenFlags mode) : this() 20 | { 21 | _fd = Fcntl.open(filename, mode); 22 | } 23 | 24 | /// 25 | /// True if the file descriptor is invalid (-1), false otherwise. 26 | /// 27 | public bool IsInvalid => _fd == -1; 28 | 29 | public static implicit operator int(PosixFileDescriptor fd) 30 | { 31 | return fd._fd; 32 | } 33 | 34 | /// 35 | /// Read number of bytes into the buffer from the file. 36 | /// 37 | /// Buffer into which to read bytes will be placed. 38 | /// Maximum number of bytes to read. 39 | /// Number of bytes actually read. A value of -1 indicates failure. 40 | public int Read(byte[] buf, int count) 41 | { 42 | ThrowIfDisposed(); 43 | ThrowIfInvalid(); 44 | return Unistd.read(_fd, buf, count); 45 | } 46 | 47 | /// 48 | /// Write number of bytes from the buffer to the file. 49 | /// 50 | /// Buffer into which to read bytes will be placed. 51 | /// Number of bytes from buffer to write. 52 | /// Number of bytes actually written. A value of -1 indicates failure. 53 | public int Write(byte[] buf, int size) 54 | { 55 | ThrowIfDisposed(); 56 | ThrowIfInvalid(); 57 | return Unistd.write(_fd, buf, size); 58 | } 59 | 60 | /// 61 | /// Write as a UTF8 encoded string to the file. 62 | /// 63 | /// String value to write to the file. 64 | /// Number of UTF8 bytes written. A value of -1 indicates failure. 65 | public int Write(string str) 66 | { 67 | byte[] buf = Encoding.UTF8.GetBytes(str); 68 | return Write(buf, buf.Length); 69 | } 70 | 71 | protected override void ReleaseUnmanagedResources() 72 | { 73 | if (!IsInvalid) 74 | { 75 | Unistd.close(_fd); 76 | } 77 | 78 | base.ReleaseUnmanagedResources(); 79 | } 80 | 81 | private void ThrowIfInvalid() 82 | { 83 | if (IsInvalid) 84 | { 85 | throw new InvalidOperationException("File descriptor is invalid"); 86 | } 87 | } 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /src/Core/Interop/Posix/PosixFileSystem.Custom.cs: -------------------------------------------------------------------------------- 1 | namespace GitCredentialManager.Interop.Posix; 2 | 3 | // NOTE: delete the existing TryResolve* in the sync'ed file, make the class partial, 4 | // and this implementation will fix the build when upgrading. Note that links will 5 | // actually not be supported. 6 | partial class PosixFileSystem 7 | { 8 | #if NETSTANDARD2_0 9 | private static bool TryResolveFileLink(string path, out string target) 10 | { 11 | target = null; 12 | return false; 13 | } 14 | 15 | private static bool TryResolveDirectoryLink(string path, out string target) 16 | { 17 | target = null; 18 | return false; 19 | } 20 | #elif !NETFRAMEWORK 21 | private static bool TryResolveFileLink(string path, out string target) 22 | { 23 | FileSystemInfo fsi = Polyfills.FileResolveLinkTarget(path, true); 24 | target = fsi?.FullName; 25 | return fsi != null; 26 | } 27 | 28 | private static bool TryResolveDirectoryLink(string path, out string target) 29 | { 30 | FileSystemInfo fsi = Polyfills.DirectoryResolveLinkTarget(path, true); 31 | target = fsi?.FullName; 32 | return fsi != null; 33 | } 34 | #endif 35 | } 36 | -------------------------------------------------------------------------------- /src/Core/Interop/Posix/PosixFileSystem.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.IO; 3 | 4 | namespace GitCredentialManager.Interop.Posix 5 | { 6 | public abstract partial class PosixFileSystem : FileSystem 7 | { 8 | /// 9 | /// Recursively resolve a symbolic link. 10 | /// 11 | /// Path to resolve. 12 | /// Resolved symlink, or original path if not a link. 13 | /// Path is not absolute. 14 | protected internal static string ResolveSymbolicLinks(string path) 15 | { 16 | #if NETFRAMEWORK 17 | // Support for symlinks only exists in .NET 6+. 18 | // Since we're still targeting .NET Framework on Windows it 19 | // doesn't matter if we don't resolve symlinks for POSIX here 20 | // (unless we're running on Mono.. but why do that?) 21 | return path; 22 | #else 23 | if (!Path.IsPathRooted(path)) 24 | { 25 | throw new ArgumentException("Path must be absolute", nameof(path)); 26 | } 27 | 28 | // If the file or directory doesn't actually exist we cannot resolve 29 | // any symlinks, so just return the original input path. 30 | if (!File.Exists(path) && !Directory.Exists(path)) 31 | { 32 | return path; 33 | } 34 | 35 | // If the file is a symlink then resolve it! 36 | string realPath = TryResolveFileLink(path, out string resolvedFile) 37 | ? resolvedFile 38 | : path; 39 | 40 | // Work backwards from the file name resolving directories symlinks 41 | string partialPath = Path.GetFileName(realPath); 42 | string dirPath = Path.GetDirectoryName(realPath); 43 | while (dirPath != null) 44 | { 45 | // Try to resolve directory symlinks 46 | if (TryResolveDirectoryLink(dirPath, out string resolvedDir)) 47 | { 48 | dirPath = resolvedDir; 49 | } 50 | 51 | string dirName = Path.GetFileName(dirPath); 52 | partialPath = Path.Combine(dirName, partialPath); 53 | dirPath = Path.GetDirectoryName(dirPath); 54 | } 55 | 56 | return Path.Combine("/", partialPath); 57 | #endif 58 | } 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /src/Core/Interop/Posix/PosixSessionManager.cs: -------------------------------------------------------------------------------- 1 | namespace GitCredentialManager.Interop.Posix 2 | { 3 | public abstract class PosixSessionManager : SessionManager 4 | { 5 | protected PosixSessionManager(IEnvironment env, IFileSystem fs) : base(env, fs) 6 | { 7 | PlatformUtils.EnsurePosix(); 8 | } 9 | 10 | // Check if we have an X11 or Wayland display environment available 11 | public override bool IsDesktopSession => 12 | !string.IsNullOrWhiteSpace(System.Environment.GetEnvironmentVariable("DISPLAY")) || 13 | !string.IsNullOrWhiteSpace(System.Environment.GetEnvironmentVariable("WAYLAND_DISPLAY")); 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /src/Core/Interop/U8StringConverter.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Runtime.InteropServices; 3 | using System.Text; 4 | 5 | namespace GitCredentialManager.Interop 6 | { 7 | /// 8 | /// Conversion utilities to convert between .NET strings (UTF-16) and byte arrays (UTF-8). 9 | /// 10 | public static class U8StringConverter 11 | { 12 | private const byte NULL = (byte) '\0'; 13 | 14 | // Throw only on invalid bytes when converting from managed to native. 15 | // We shouldn't have invalid managed strings so this would be an error condition. 16 | // We continue to accept potentially malformed native strings however, because they may come from Git which 17 | // doesn't technically care about Unicode encoding format compliance. 18 | private static readonly Encoding NativeEncoding = new UTF8Encoding(false, throwOnInvalidBytes: true); 19 | private static readonly Encoding ManagedEncoding = new UTF8Encoding(false, throwOnInvalidBytes: false); 20 | 21 | public static unsafe IntPtr ToNative(string str) 22 | { 23 | if (str == null) 24 | { 25 | return IntPtr.Zero; 26 | } 27 | 28 | int length = NativeEncoding.GetByteCount(str); 29 | 30 | // +1 for the null terminator byte 31 | var buffer = (byte*)Marshal.AllocHGlobal(length + 1).ToPointer(); 32 | 33 | if (length > 0) 34 | { 35 | fixed (char* pValue = str) 36 | { 37 | NativeEncoding.GetBytes(pValue, str.Length, buffer, length); 38 | } 39 | } 40 | buffer[length] = NULL; 41 | 42 | return new IntPtr(buffer); 43 | } 44 | 45 | public static unsafe string ToManaged(byte* buf) 46 | { 47 | byte* end = buf; 48 | 49 | if (buf == null) 50 | { 51 | return null; 52 | } 53 | 54 | if (*buf == NULL) 55 | { 56 | return string.Empty; 57 | } 58 | 59 | while (*end != NULL) 60 | { 61 | end++; 62 | } 63 | 64 | return new string((sbyte*)buf, 0, (int)(end - buf), ManagedEncoding); 65 | } 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /src/Core/Interop/U8StringMarshaler.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Runtime.InteropServices; 3 | 4 | namespace GitCredentialManager.Interop 5 | { 6 | /// 7 | /// Marshaler for converting between .NET strings (UTF-16) and byte arrays (UTF-8). 8 | /// Uses internally. 9 | /// 10 | public class U8StringMarshaler : ICustomMarshaler 11 | { 12 | // We need to clean up strings that we marshal to native, but should not clean up strings that 13 | // we marshal to managed. 14 | private static readonly U8StringMarshaler NativeInstance = new U8StringMarshaler(true); 15 | private static readonly U8StringMarshaler ManagedInstance = new U8StringMarshaler(false); 16 | 17 | private readonly bool _cleanup; 18 | 19 | public const string NativeCookie = "U8StringMarshaler.Native"; 20 | public const string ManagedCookie = "U8StringMarshaler.Managed"; 21 | 22 | public static ICustomMarshaler GetInstance(string cookie) 23 | { 24 | switch (cookie) 25 | { 26 | case NativeCookie: 27 | return NativeInstance; 28 | case ManagedCookie: 29 | return ManagedInstance; 30 | default: 31 | throw new ArgumentException("Invalid marshaler cookie"); 32 | } 33 | } 34 | 35 | private U8StringMarshaler(bool cleanup) 36 | { 37 | _cleanup = cleanup; 38 | } 39 | 40 | public int GetNativeDataSize() 41 | { 42 | return -1; 43 | } 44 | 45 | public IntPtr MarshalManagedToNative(object value) 46 | { 47 | switch (value) 48 | { 49 | case null: 50 | return IntPtr.Zero; 51 | case string str: 52 | return U8StringConverter.ToNative(str); 53 | default: 54 | throw new MarshalDirectiveException("Cannot marshal a non-string"); 55 | } 56 | } 57 | 58 | public unsafe object MarshalNativeToManaged(IntPtr ptr) 59 | { 60 | return U8StringConverter.ToManaged((byte*) ptr); 61 | } 62 | 63 | public void CleanUpManagedData(object value) 64 | { 65 | } 66 | 67 | public virtual void CleanUpNativeData(IntPtr ptr) 68 | { 69 | if (ptr != IntPtr.Zero && _cleanup) 70 | { 71 | Marshal.FreeHGlobal(ptr); 72 | } 73 | } 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /src/Core/Interop/Windows/DpapiCredentialStore.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.IO; 4 | using System.Security.Cryptography; 5 | using System.Text; 6 | 7 | namespace GitCredentialManager.Interop.Windows 8 | { 9 | public class DpapiCredentialStore : PlaintextCredentialStore 10 | { 11 | public DpapiCredentialStore(IFileSystem fileSystem, string storeRoot, string @namespace = null) 12 | : base(fileSystem, storeRoot, @namespace) 13 | { 14 | PlatformUtils.EnsureWindows(); 15 | } 16 | 17 | protected override bool TryDeserializeCredential(string path, out FileCredential credential) 18 | { 19 | string text; 20 | using (var stream = FileSystem.OpenFileStream(path, FileMode.Open, FileAccess.Read, FileShare.Read)) 21 | using (var reader = new StreamReader(stream)) 22 | { 23 | text = reader.ReadToEnd(); 24 | } 25 | 26 | int line1Idx = text.IndexOf(Environment.NewLine, StringComparison.OrdinalIgnoreCase); 27 | if (line1Idx > 0) 28 | { 29 | // The first line is a base64 encoded set of bytes that need to be decrypted by DPAPI 30 | string cryptoBase64 = text.Substring(0, line1Idx); 31 | byte[] cryptoBytes = Convert.FromBase64String(cryptoBase64); 32 | byte[] plainBytes = ProtectedData.Unprotect( 33 | cryptoBytes, null, DataProtectionScope.CurrentUser); 34 | string password = Encoding.UTF8.GetString(plainBytes); 35 | 36 | // All subsequent lines are metadata/attributes 37 | string attrText = text.Substring(line1Idx + Environment.NewLine.Length); 38 | using var attrReader = new StringReader(attrText); 39 | IDictionary attrs = attrReader.ReadDictionary(StringComparer.OrdinalIgnoreCase); 40 | 41 | // Account is optional 42 | attrs.TryGetValue("account", out string account); 43 | 44 | // Service is required 45 | if (attrs.TryGetValue("service", out string service)) 46 | { 47 | credential = new FileCredential(path, service, account, password); 48 | return true; 49 | } 50 | } 51 | 52 | credential = null; 53 | return false; 54 | } 55 | 56 | protected override void SerializeCredential(FileCredential credential) 57 | { 58 | // Ensure the parent directory exists 59 | string parentDir = Path.GetDirectoryName(credential.FullPath); 60 | if (!FileSystem.DirectoryExists(parentDir)) 61 | { 62 | FileSystem.CreateDirectory(parentDir); 63 | } 64 | 65 | // Use DPAPI to encrypt the password value, and then store the base64 encoding of the resulting bytes 66 | byte[] plainBytes = Encoding.UTF8.GetBytes(credential.Password); 67 | byte[] cryptoBytes = ProtectedData.Protect( 68 | plainBytes, null, DataProtectionScope.CurrentUser); 69 | string cryptoBase64 = Convert.ToBase64String(cryptoBytes); 70 | 71 | using (var stream = FileSystem.OpenFileStream(credential.FullPath, FileMode.Create, FileAccess.Write, FileShare.None)) 72 | using (var writer = new StreamWriter(stream)) 73 | { 74 | writer.WriteLine(cryptoBase64); 75 | writer.WriteLine("service={0}", credential.Service); 76 | writer.WriteLine("account={0}", credential.Account); 77 | writer.Flush(); 78 | } 79 | } 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /src/Core/Interop/Windows/Native/Ole32.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Runtime.InteropServices; 3 | 4 | namespace GitCredentialManager.Interop.Windows.Native 5 | { 6 | public static class Ole32 7 | { 8 | private const string LibraryName = "ole32.dll"; 9 | 10 | public const uint RPC_E_TOO_LATE = 0x80010119; 11 | 12 | [DllImport(LibraryName)] 13 | public static extern int CoInitializeSecurity( 14 | IntPtr pVoid, 15 | int cAuthSvc, 16 | IntPtr asAuthSvc, 17 | IntPtr pReserved1, 18 | RpcAuthnLevel level, 19 | RpcImpLevel impers, 20 | IntPtr pAuthList, 21 | EoAuthnCap dwCapabilities, 22 | IntPtr pReserved3); 23 | 24 | public enum RpcAuthnLevel 25 | { 26 | Default = 0, 27 | None = 1, 28 | Connect = 2, 29 | Call = 3, 30 | Pkt = 4, 31 | PktIntegrity = 5, 32 | PktPrivacy = 6 33 | } 34 | 35 | public enum RpcImpLevel 36 | { 37 | Default = 0, 38 | Anonymous = 1, 39 | Identify = 2, 40 | Impersonate = 3, 41 | Delegate = 4 42 | } 43 | 44 | public enum EoAuthnCap 45 | { 46 | None = 0x00, 47 | MutualAuth = 0x01, 48 | StaticCloaking = 0x20, 49 | DynamicCloaking = 0x40, 50 | AnyAuthority = 0x80, 51 | MakeFullSIC = 0x100, 52 | Default = 0x800, 53 | SecureRefs = 0x02, 54 | AccessControl = 0x04, 55 | AppID = 0x08, 56 | Dynamic = 0x10, 57 | RequireFullSIC = 0x200, 58 | AutoImpersonate = 0x400, 59 | NoCustomMarshal = 0x2000, 60 | DisableAAA = 0x1000 61 | } 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /src/Core/Interop/Windows/Native/Shell32.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Runtime.InteropServices; 3 | 4 | namespace GitCredentialManager.Interop.Windows.Native 5 | { 6 | public static class Shell32 7 | { 8 | private const string LibraryName = "shell32.dll"; 9 | 10 | /// 11 | /// Parses a Unicode command line string and returns an array of pointers 12 | /// to the command line arguments, along with a count of such arguments, 13 | /// in a way that is similar to the standard C run-time argv and argc values. 14 | /// 15 | /// 16 | /// Pointer to a null-terminated Unicode string that contains the full command line. 17 | /// If this parameter is an empty string the function returns the path to the current executable file. 18 | /// 19 | /// 20 | /// Pointer to an int that receives the number of array elements returned, similar to argc. 21 | /// 22 | /// A pointer to an array of LPWSTR values, similar to argv. 23 | [DllImport("Shell32.dll", CallingConvention = CallingConvention.StdCall, CharSet = CharSet.Unicode, SetLastError = true)] 24 | public static extern IntPtr CommandLineToArgvW(IntPtr lpCmdLine, out int pNumArgs); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/Core/Interop/Windows/Native/User32.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Runtime.InteropServices; 3 | 4 | namespace GitCredentialManager.Interop.Windows.Native 5 | { 6 | public static class User32 7 | { 8 | private const string LibraryName = "user32.dll"; 9 | 10 | public const int UOI_FLAGS = 1; 11 | public const int WSF_VISIBLE = 0x0001; 12 | 13 | [DllImport(LibraryName, CallingConvention = CallingConvention.StdCall, SetLastError = true)] 14 | public static extern IntPtr GetProcessWindowStation(); 15 | 16 | [DllImport(LibraryName, EntryPoint = "GetUserObjectInformation", CallingConvention = CallingConvention.StdCall, SetLastError = true)] 17 | public static extern unsafe bool GetUserObjectInformation(IntPtr hObj, int nIndex, void* pvBuffer, uint nLength, ref uint lpnLengthNeeded); 18 | 19 | [DllImport(LibraryName, EntryPoint="SetWindowLong", SetLastError = true)] 20 | private static extern IntPtr SetWindowLongPtr32(IntPtr hWnd, int nIndex, IntPtr value); 21 | 22 | [DllImport(LibraryName, EntryPoint="SetWindowLongPtr", SetLastError = true)] 23 | private static extern IntPtr SetWindowLongPtr64(IntPtr hWnd, int nIndex, IntPtr value); 24 | 25 | public static IntPtr SetWindowLongPtr(IntPtr hWnd, int nIndex, IntPtr value) 26 | { 27 | if (IntPtr.Size == 8) 28 | return SetWindowLongPtr64(hWnd, nIndex, value); 29 | else 30 | return SetWindowLongPtr32(hWnd, nIndex, value); 31 | } 32 | 33 | [DllImport(LibraryName, SetLastError = true)] 34 | public static extern bool GetWindowRect(IntPtr hwnd, out RECT lpRect); 35 | 36 | [DllImport(LibraryName, SetLastError = true)] 37 | public static extern bool GetClientRect(IntPtr hwnd, out RECT lpRect); 38 | 39 | /// 40 | /// Retrieves the handle to the ancestor of the specified window. 41 | /// 42 | /// 43 | /// A handle to the window whose ancestor is to be retrieved. 44 | /// If this parameter is the desktop window, the function returns NULL. 45 | /// 46 | /// The ancestor to be retrieved. 47 | /// The return value is the handle to the ancestor window. 48 | [DllImport("user32.dll", SetLastError = true)] 49 | public static extern IntPtr GetAncestor(IntPtr hwnd, GetAncestorFlags flags); 50 | } 51 | 52 | public enum GetAncestorFlags 53 | { 54 | /// 55 | /// Retrieves the parent window. This does not include the owner, as it does with the GetParent function. 56 | /// 57 | GetParent = 1, 58 | 59 | /// 60 | /// Retrieves the root window by walking the chain of parent windows. 61 | /// 62 | GetRoot = 2, 63 | 64 | /// 65 | /// Retrieves the owned root window by walking the chain of parent and owner windows returned by GetParent. 66 | /// 67 | GetRootOwner = 3 68 | } 69 | 70 | [StructLayout(LayoutKind.Sequential)] 71 | public struct USEROBJECTFLAGS 72 | { 73 | public int fInherit; 74 | public int fReserved; 75 | public int dwFlags; 76 | } 77 | 78 | public enum WindowLongParam 79 | { 80 | GWL_WNDPROC = -4, 81 | GWL_HINSTANCE = -6, 82 | GWL_HWNDPARENT = -8, 83 | GWL_ID = -12, 84 | GWL_STYLE = -16, 85 | GWL_EXSTYLE = -20, 86 | GWL_USERDATA = -21 87 | } 88 | 89 | [StructLayout(LayoutKind.Sequential)] 90 | public struct RECT 91 | { 92 | public int left; 93 | public int top; 94 | public int right; 95 | public int bottom; 96 | } 97 | } 98 | -------------------------------------------------------------------------------- /src/Core/Interop/Windows/WindowsCredential.cs: -------------------------------------------------------------------------------- 1 | 2 | namespace GitCredentialManager.Interop.Windows 3 | { 4 | public class WindowsCredential : ICredential 5 | { 6 | public WindowsCredential(string service, string userName, string password, string targetName) 7 | { 8 | Service = service; 9 | UserName = userName; 10 | Password = password; 11 | TargetName = targetName; 12 | } 13 | 14 | public string Service { get; } 15 | 16 | public string UserName { get; } 17 | 18 | public string Password { get; } 19 | 20 | public string TargetName { get; } 21 | 22 | string ICredential.Account => UserName; 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/Core/Interop/Windows/WindowsFileSystem.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.IO; 3 | 4 | namespace GitCredentialManager.Interop.Windows 5 | { 6 | public class WindowsFileSystem : FileSystem 7 | { 8 | public override bool IsSamePath(string a, string b) 9 | { 10 | if (string.IsNullOrWhiteSpace(a) || string.IsNullOrWhiteSpace(b)) 11 | { 12 | return false; 13 | } 14 | 15 | a = Path.GetFullPath(a); 16 | b = Path.GetFullPath(b); 17 | 18 | // Note: we do not resolve or handle symlinks on Windows 19 | // because they require administrator permissions to even create! 20 | 21 | return StringComparer.OrdinalIgnoreCase.Equals(a, b); 22 | } 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/Core/Interop/Windows/WindowsProcessManager.cs: -------------------------------------------------------------------------------- 1 | namespace GitCredentialManager.Interop.Windows; 2 | 3 | public class WindowsProcessManager : ProcessManager 4 | { 5 | public WindowsProcessManager(ITrace2 trace2) : base(trace2) 6 | { 7 | PlatformUtils.EnsureWindows(); 8 | } 9 | 10 | public override ChildProcess CreateProcess(string path, string args, bool useShellExecute, string workingDirectory) 11 | { 12 | // If we're asked to start a WSL executable we must launch via the wsl.exe command tool 13 | if (!useShellExecute && WslUtils.IsWslPath(path)) 14 | { 15 | string wslPath = WslUtils.ConvertToDistroPath(path, out string distro); 16 | return WslUtils.CreateWslProcess(distro, $"{wslPath} {args}", Trace2, workingDirectory); 17 | } 18 | 19 | return base.CreateProcess(path, args, useShellExecute, workingDirectory); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/Core/Interop/Windows/WindowsSessionManager.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using GitCredentialManager.Interop.Windows.Native; 3 | 4 | namespace GitCredentialManager.Interop.Windows 5 | { 6 | public class WindowsSessionManager : SessionManager 7 | { 8 | public WindowsSessionManager(IEnvironment env, IFileSystem fs) : base(env, fs) 9 | { 10 | PlatformUtils.EnsureWindows(); 11 | } 12 | 13 | public override unsafe bool IsDesktopSession 14 | { 15 | get 16 | { 17 | // Environment.UserInteractive is hard-coded to return true for POSIX and Windows platforms on .NET Core 2.x and 3.x. 18 | // In .NET 5 the implementation on Windows has been 'fixed', but still POSIX versions always return true. 19 | // 20 | // This code is lifted from the .NET 5 targeting dotnet/runtime implementation for Windows: 21 | // https://github.com/dotnet/runtime/blob/cf654f08fb0078a96a4e414a0d2eab5e6c069387/src/libraries/System.Private.CoreLib/src/System/Environment.Windows.cs#L125-L145 22 | 23 | // Per documentation of GetProcessWindowStation, this handle should not be closed 24 | IntPtr handle = User32.GetProcessWindowStation(); 25 | if (handle != IntPtr.Zero) 26 | { 27 | USEROBJECTFLAGS flags = default; 28 | uint dummy = 0; 29 | if (User32.GetUserObjectInformation(handle, User32.UOI_FLAGS, &flags, 30 | (uint) sizeof(USEROBJECTFLAGS), ref dummy)) 31 | { 32 | return (flags.dwFlags & User32.WSF_VISIBLE) != 0; 33 | } 34 | } 35 | 36 | // If we can't determine, return true optimistically 37 | // This will include cases like Windows Nano which do not expose WindowStations 38 | return true; 39 | } 40 | } 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/Core/Interop/Windows/WindowsSettings.cs: -------------------------------------------------------------------------------- 1 | 2 | namespace GitCredentialManager.Interop.Windows 3 | { 4 | /// 5 | /// Reads settings from Git configuration, environment variables, and defaults from the Windows Registry. 6 | /// 7 | public class WindowsSettings : Settings 8 | { 9 | private readonly ITrace _trace; 10 | 11 | public WindowsSettings(IEnvironment environment, IGit git, ITrace trace) 12 | : base(environment, git) 13 | { 14 | EnsureArgument.NotNull(trace, nameof(trace)); 15 | _trace = trace; 16 | 17 | PlatformUtils.EnsureWindows(); 18 | } 19 | 20 | protected override bool TryGetExternalDefault(string section, string scope, string property, out string value) 21 | { 22 | value = null; 23 | 24 | #if NETFRAMEWORK 25 | // Check for machine (HKLM) registry keys that match the Git configuration name. 26 | // These can be set by system administrators via Group Policy, so make useful defaults. 27 | using (Microsoft.Win32.RegistryKey configKey = Microsoft.Win32.Registry.LocalMachine.OpenSubKey(Constants.WindowsRegistry.HKConfigurationPath)) 28 | { 29 | if (configKey is null) 30 | { 31 | // No configuration key exists 32 | return false; 33 | } 34 | 35 | string name = string.IsNullOrWhiteSpace(scope) 36 | ? $"{section}.{property}" 37 | : $"{section}.{scope}.{property}"; 38 | 39 | object registryValue = configKey.GetValue(name); 40 | if (registryValue is null) 41 | { 42 | // No property exists 43 | return false; 44 | } 45 | 46 | value = registryValue.ToString(); 47 | _trace.WriteLine($"Default setting found in registry: {name}={value}"); 48 | 49 | return true; 50 | } 51 | #else 52 | return base.TryGetExternalDefault(section, scope, property, out value); 53 | #endif 54 | } 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /src/Core/NameValueCollectionExtensions.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Collections.Specialized; 4 | 5 | namespace GitCredentialManager 6 | { 7 | public static class NameValueCollectionExtensions 8 | { 9 | public static IDictionary ToDictionary(this NameValueCollection collection, IEqualityComparer comparer = null) 10 | { 11 | var dict = new Dictionary(comparer ?? StringComparer.Ordinal); 12 | 13 | foreach (string key in collection.AllKeys) 14 | { 15 | dict[key] = collection[key]; 16 | } 17 | 18 | return dict; 19 | } 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/Core/NullCredentialStore.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | 4 | namespace GitCredentialManager; 5 | 6 | /// 7 | /// Credential store that does nothing. This is useful when you want to disable internal credential storage 8 | /// and only use another helper configured in Git to store credentials. 9 | /// 10 | public class NullCredentialStore : ICredentialStore 11 | { 12 | public IList GetAccounts(string service) => Array.Empty(); 13 | 14 | public ICredential Get(string service, string account) => null; 15 | 16 | public void AddOrUpdate(string service, string account, string secret) { } 17 | 18 | public bool Remove(string service, string account) => false; 19 | } 20 | -------------------------------------------------------------------------------- /src/Core/ProcessManager.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Diagnostics; 3 | 4 | namespace GitCredentialManager; 5 | 6 | public interface IProcessManager 7 | { 8 | /// 9 | /// Create a process ready to start. 10 | /// 11 | /// Absolute file path of executable or command to start. 12 | /// Command line arguments to pass to executable. 13 | /// 14 | /// True to resolve using the OS shell, false to use as an absolute file path. 15 | /// 16 | /// Working directory for the new process. 17 | /// object ready to start. 18 | ChildProcess CreateProcess(string path, string args, bool useShellExecute, string workingDirectory); 19 | 20 | /// 21 | /// Create a process ready to start. 22 | /// 23 | /// Process start info. 24 | /// object ready to start. 25 | ChildProcess CreateProcess(ProcessStartInfo psi); 26 | } 27 | 28 | public class ProcessManager : IProcessManager 29 | { 30 | private const string SidEnvar = "GIT_TRACE2_PARENT_SID"; 31 | 32 | protected readonly ITrace2 Trace2; 33 | 34 | public static string Sid { get; internal set; } 35 | 36 | public static int Depth { get; internal set; } 37 | 38 | public ProcessManager(ITrace2 trace2) 39 | { 40 | EnsureArgument.NotNull(trace2, nameof(trace2)); 41 | 42 | Trace2 = trace2; 43 | } 44 | 45 | public virtual ChildProcess CreateProcess(string path, string args, bool useShellExecute, string workingDirectory) 46 | { 47 | var psi = new ProcessStartInfo(path, args) 48 | { 49 | RedirectStandardInput = true, 50 | RedirectStandardOutput = true, 51 | RedirectStandardError = false, // Do not redirect stderr as tracing might be enabled 52 | UseShellExecute = useShellExecute, 53 | WorkingDirectory = workingDirectory ?? string.Empty 54 | }; 55 | 56 | return CreateProcess(psi); 57 | } 58 | 59 | public virtual ChildProcess CreateProcess(ProcessStartInfo psi) 60 | { 61 | return new ChildProcess(Trace2, psi); 62 | } 63 | 64 | /// 65 | /// Create a TRACE2 "session id" (sid) for this process. 66 | /// 67 | public static void CreateSid() 68 | { 69 | Sid = Environment.GetEnvironmentVariable(SidEnvar); 70 | 71 | if (!string.IsNullOrEmpty(Sid)) 72 | { 73 | // Use trim to ensure no accidental leading or trailing slashes 74 | Sid = $"{Sid.Trim('/')}/{Guid.NewGuid():D}"; 75 | // Only check for process depth if there is a parent. 76 | // If there is not a parent, depth defaults to 0. 77 | Depth = GetProcessDepth(); 78 | } 79 | else 80 | { 81 | // We are the root process; create our own 'root' SID 82 | Sid = Guid.NewGuid().ToString("D"); 83 | } 84 | 85 | Environment.SetEnvironmentVariable(SidEnvar, Sid); 86 | } 87 | 88 | /// 89 | /// Get "depth" of current process relative to top-level GCM process. 90 | /// 91 | /// Depth of current process. 92 | internal static int GetProcessDepth() 93 | { 94 | char processSeparator = '/'; 95 | 96 | int count = 0; 97 | // Use AsSpan() for slight performance bump over traditional foreach loop. 98 | foreach (var c in Sid.AsSpan()) 99 | { 100 | if (c == processSeparator) 101 | count++; 102 | } 103 | 104 | return count; 105 | } 106 | } 107 | -------------------------------------------------------------------------------- /src/Core/StandardStreams.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.IO; 3 | using System.Text; 4 | 5 | namespace GitCredentialManager 6 | { 7 | /// 8 | /// Represents the standard I/O streams (input, output, error) of a process. 9 | /// 10 | public interface IStandardStreams 11 | { 12 | /// 13 | /// The standard input text stream from the calling process, typically Git. 14 | /// 15 | TextReader In { get; } 16 | 17 | /// 18 | /// The standard output text stream connected back to the calling process, typically Git. 19 | /// 20 | TextWriter Out { get; } 21 | 22 | /// 23 | /// The standard error text stream connected back to the calling process, typically Git. 24 | /// 25 | TextWriter Error { get; } 26 | } 27 | 28 | public class StandardStreams : IStandardStreams 29 | { 30 | private const string LineFeed = "\n"; 31 | 32 | private TextReader _stdIn; 33 | private TextWriter _stdOut; 34 | private TextWriter _stdErr; 35 | 36 | public TextReader In 37 | { 38 | get 39 | { 40 | if (_stdIn == null) 41 | { 42 | _stdIn = new GitStreamReader(Console.OpenStandardInput(), EncodingEx.UTF8NoBom); 43 | } 44 | 45 | return _stdIn; 46 | } 47 | } 48 | 49 | public TextWriter Out 50 | { 51 | get 52 | { 53 | if (_stdOut == null) 54 | { 55 | _stdOut = new StreamWriter(Console.OpenStandardOutput(), EncodingEx.UTF8NoBom) 56 | { 57 | AutoFlush = true, 58 | NewLine = LineFeed, 59 | }; 60 | } 61 | 62 | return _stdOut; 63 | } 64 | } 65 | 66 | public TextWriter Error 67 | { 68 | get 69 | { 70 | if (_stdErr == null) 71 | { 72 | _stdErr = new StreamWriter(Console.OpenStandardError(), EncodingEx.UTF8NoBom) 73 | { 74 | AutoFlush = true, 75 | NewLine = LineFeed, 76 | }; 77 | } 78 | 79 | return _stdErr; 80 | } 81 | } 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /src/Core/Trace2Exception.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.ComponentModel; 3 | using System.IO; 4 | using GitCredentialManager.Authentication.OAuth; 5 | using GitCredentialManager.Interop; 6 | 7 | namespace GitCredentialManager; 8 | 9 | public class Trace2Exception : Exception 10 | { 11 | public Trace2Exception(ITrace2 trace2, string message) : base(message) 12 | { 13 | trace2.WriteError(message); 14 | } 15 | 16 | public Trace2Exception(ITrace2 trace2, string message, string messageFormat) : base(message) 17 | { 18 | trace2.WriteError(message, messageFormat); 19 | } 20 | } 21 | 22 | public class Trace2InvalidOperationException : InvalidOperationException 23 | { 24 | public Trace2InvalidOperationException(ITrace2 trace2, string message) : base(message) 25 | { 26 | trace2.WriteError(message); 27 | } 28 | } 29 | 30 | public class Trace2OAuth2Exception : OAuth2Exception 31 | { 32 | public Trace2OAuth2Exception(ITrace2 trace2, string message) : base(message) 33 | { 34 | trace2.WriteError(message); 35 | } 36 | 37 | public Trace2OAuth2Exception(ITrace2 trace2, string message, string messageFormat) : base(message) 38 | { 39 | trace2.WriteError(message, messageFormat); 40 | } 41 | } 42 | 43 | public class Trace2InteropException : InteropException 44 | { 45 | public Trace2InteropException(ITrace2 trace2, string message, int errorCode) : base(message, errorCode) 46 | { 47 | trace2.WriteError($"message: {message} error code: {errorCode}"); 48 | } 49 | 50 | public Trace2InteropException(ITrace2 trace2, string message, Win32Exception ex) : base(message, ex) 51 | { 52 | trace2.WriteError(message); 53 | } 54 | } 55 | 56 | public class Trace2GitException : GitException 57 | { 58 | public Trace2GitException(ITrace2 trace2, string message, int errorCode, string gitMessage) : 59 | base(message, gitMessage, errorCode) 60 | { 61 | var format = $"message: '{message}' error code: '{errorCode}' git message: '{{0}}'"; 62 | var traceMessage = string.Format(format, gitMessage); 63 | 64 | trace2.WriteError(traceMessage, format); 65 | } 66 | } 67 | 68 | public class Trace2FileNotFoundException : FileNotFoundException 69 | { 70 | public Trace2FileNotFoundException(ITrace2 trace2, string message, string messageFormat, string fileName) : 71 | base(message, fileName) 72 | { 73 | trace2.WriteError(message, messageFormat); 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /src/Core/Trace2FileWriter.cs: -------------------------------------------------------------------------------- 1 | using System.IO; 2 | using System; 3 | 4 | namespace GitCredentialManager; 5 | 6 | public class Trace2FileWriter : Trace2Writer 7 | { 8 | private readonly string _path; 9 | 10 | public Trace2FileWriter(Trace2FormatTarget formatTarget, string path) : base(formatTarget) 11 | { 12 | _path = path; 13 | } 14 | 15 | public override void Write(Trace2Message message) 16 | { 17 | try 18 | { 19 | File.AppendAllText(_path, Format(message)); 20 | } 21 | catch (DirectoryNotFoundException) 22 | { 23 | // Do nothing, as this either means we don't have the 24 | // parent directories above the file, or this trace2 25 | // target points to a directory. 26 | } 27 | catch (UnauthorizedAccessException) 28 | { 29 | // Do nothing, as this either means the file is not 30 | // accessible with current permissions, or we are on 31 | // Windows and the file is currently open for writing 32 | // by another process (likely Git itself.) 33 | } 34 | catch (IOException) 35 | { 36 | // Do nothing, as this likely means that the file is currently 37 | // open by another process (on Windows). 38 | } 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/Core/Trace2StreamWriter.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.IO; 3 | 4 | namespace GitCredentialManager; 5 | 6 | public class Trace2StreamWriter : Trace2Writer 7 | { 8 | private readonly TextWriter _writer; 9 | 10 | public Trace2StreamWriter(Trace2FormatTarget formatTarget, TextWriter writer) 11 | : base(formatTarget) 12 | { 13 | _writer = writer; 14 | } 15 | 16 | public override void Write(Trace2Message message) 17 | { 18 | try 19 | { 20 | _writer.Write(Format(message)); 21 | _writer.Flush(); 22 | } 23 | catch 24 | { 25 | Failed = true; 26 | } 27 | } 28 | 29 | protected override void ReleaseManagedResources() 30 | { 31 | _writer.Dispose(); 32 | base.ReleaseManagedResources(); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/Core/TraceUtils.cs: -------------------------------------------------------------------------------- 1 | namespace GitCredentialManager; 2 | 3 | public static class TraceUtils 4 | { 5 | public static string FormatSource(string source, int sourceColumnMaxWidth) 6 | { 7 | int idx = 0; 8 | int maxlen = sourceColumnMaxWidth - 3; 9 | int srclen = source.Length; 10 | 11 | while (idx >= 0 && (srclen - idx) > maxlen) 12 | { 13 | idx = source.IndexOf('\\', idx + 1); 14 | } 15 | 16 | // If we cannot find a path separator which allows the path to be long enough, just truncate the file name 17 | if (idx < 0) 18 | { 19 | idx = srclen - maxlen; 20 | } 21 | 22 | return "..." + source.Substring(idx); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/CredentialManager/CredentialManager.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | netstandard2.0 5 | Devlooped.CredentialManager 6 | GitCredentialManager 7 | Devlooped.CredentialManager 8 | cross-platform xplat credentials manager 9 | https://clarius.org/CredentialManager 10 | 11 | Packages the Git Credential Manager cross-platform implementation for Windows, macOS and Linux for use as a generic credential store. 12 | 13 | Usage: 14 | 15 | var store = CredentialManager.Create("myapp"); 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 36 | 37 | 38 | 39 | 40 | 41 | $(ILRepackArgs) /out:"@(IntermediateAssembly -> '%(FullPath)')" 42 | $(ILRepackArgs) "@(IntermediateAssembly -> '%(FullPath)')" 43 | $(ILRepackArgs) @(MergedAssemblies -> '"%(FullPath)"', ' ') 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 59 | 60 | 61 | 62 | -------------------------------------------------------------------------------- /src/CredentialManager/exclude.txt: -------------------------------------------------------------------------------- 1 | GitCredentialManager.ICredentialStore 2 | GitCredentialManager.ICredential -------------------------------------------------------------------------------- /src/CredentialManager/readme.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/Directory.props: -------------------------------------------------------------------------------- 1 | 2 | 3 | Devlooped.CredentialManager 4 | 5 | true 6 | 7 | -------------------------------------------------------------------------------- /src/Directory.targets: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | $(Description) 6 | 7 | > This project uses SponsorLink and may issue IDE-only warnings if no active sponsorship is detected. 8 | > Learn more at https://github.com/devlooped#sponsorlink. 9 | 10 | $(PackFolder.StartsWith('analyzers/')) 11 | 12 | 13 | 14 | 15 | 18 | 23 | 24 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 59 | 60 | 61 | 62 | -------------------------------------------------------------------------------- /src/SponsorLink/Analyzer/Analyzer.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | SponsorableLib.Analyzers 5 | netstandard2.0 6 | true 7 | analyzers/dotnet/roslyn4.0 8 | true 9 | false 10 | true 11 | $(MSBuildThisFileDirectory)..\SponsorLink.Analyzer.targets 12 | disable 13 | SponsorableLib 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | $([System.IO.File]::ReadAllText('$(MSBuildThisFileDirectory)..\Tests\keys\kzu.pub.jwk')) 36 | 37 | 38 | 39 | 40 | 41 | 42 | -------------------------------------------------------------------------------- /src/SponsorLink/Analyzer/GraceApiAnalyzer.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Immutable; 2 | using System.Linq; 3 | using Devlooped.Sponsors; 4 | using Microsoft.CodeAnalysis; 5 | using Microsoft.CodeAnalysis.CSharp; 6 | using Microsoft.CodeAnalysis.Diagnostics; 7 | using static Devlooped.Sponsors.SponsorLink; 8 | 9 | namespace Analyzer; 10 | 11 | /// 12 | /// Links the sponsor status for the current compilation. 13 | /// 14 | [DiagnosticAnalyzer(LanguageNames.CSharp, LanguageNames.VisualBasic)] 15 | public class GraceApiAnalyzer : DiagnosticAnalyzer 16 | { 17 | public override ImmutableArray SupportedDiagnostics => ImmutableArray.Create( 18 | new DiagnosticDescriptor( 19 | "SL010", "Grace API usage", "Reports info for APIs that are in grace period", "Sponsors", 20 | DiagnosticSeverity.Info, true, helpLinkUri: Funding.HelpUrl), 21 | new DiagnosticDescriptor( 22 | "SL011", "Report Sponsoring Status", "Fake to get it to call us", "Sponsors", 23 | DiagnosticSeverity.Warning, true) 24 | ); 25 | 26 | #pragma warning disable RS1026 // Enable concurrent execution 27 | public override void Initialize(AnalysisContext context) 28 | #pragma warning restore RS1026 // Enable concurrent execution 29 | { 30 | #if !DEBUG 31 | // Only enable concurrent execution in release builds, otherwise debugging is quite annoying. 32 | context.EnableConcurrentExecution(); 33 | #endif 34 | context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.None); 35 | // Report info grace and expiring diagnostics. 36 | context.RegisterSyntaxNodeAction(AnalyzeNode, SyntaxKind.InvocationExpression); 37 | context.RegisterSyntaxNodeAction(AnalyzeNode, SyntaxKind.SimpleMemberAccessExpression); 38 | } 39 | 40 | void AnalyzeNode(SyntaxNodeAnalysisContext context) 41 | { 42 | var status = Diagnostics.GetOrSetStatus(() => context.Options); 43 | if (status != SponsorStatus.Grace) 44 | return; 45 | 46 | ReportGraceSymbol(context, context.Node.GetLocation(), context.SemanticModel.GetSymbolInfo(context.Node).Symbol); 47 | } 48 | 49 | void ReportGraceSymbol(SyntaxNodeAnalysisContext context, Location location, ISymbol? symbol) 50 | { 51 | if (symbol != null && 52 | symbol.GetAttributes().Any(attr => 53 | attr.AttributeClass?.ToDisplayString() == "System.ComponentModel.CategoryAttribute" && 54 | attr.ConstructorArguments.Any(arg => arg.Value as string == "Sponsored"))) 55 | { 56 | context.ReportDiagnostic(Diagnostic.Create( 57 | SupportedDiagnostics[0], 58 | location)); 59 | } 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /src/SponsorLink/Analyzer/Properties/launchSettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "profiles": { 3 | "SponsorableLib": { 4 | "commandName": "DebugRoslynComponent", 5 | "targetProject": "..\\Tests\\Tests.csproj", 6 | "environmentVariables": { 7 | "SPONSORLINK_TRACE": "true" 8 | } 9 | } 10 | } 11 | } -------------------------------------------------------------------------------- /src/SponsorLink/Analyzer/StatusReportingAnalyzer.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Immutable; 3 | using System.IO; 4 | using System.Linq; 5 | using Devlooped.Sponsors; 6 | using Humanizer; 7 | using Microsoft.CodeAnalysis; 8 | using Microsoft.CodeAnalysis.Diagnostics; 9 | using Microsoft.CodeAnalysis.Text; 10 | using static Devlooped.Sponsors.SponsorLink; 11 | 12 | namespace Analyzer; 13 | 14 | [DiagnosticAnalyzer(LanguageNames.CSharp, LanguageNames.VisualBasic)] 15 | public class StatusReportingAnalyzer : DiagnosticAnalyzer 16 | { 17 | public override ImmutableArray SupportedDiagnostics => ImmutableArray.Create( 18 | new DiagnosticDescriptor( 19 | "SL001", "Report Sponsoring Status", "Reports sponsoring status determined by SponsorLink", "Sponsors", 20 | DiagnosticSeverity.Info, true), 21 | new DiagnosticDescriptor( 22 | "SL002", "Report Sponsoring Status", "Fake to get it to call us", "Sponsors", 23 | DiagnosticSeverity.Warning, true) 24 | ); 25 | 26 | public override void Initialize(AnalysisContext context) 27 | { 28 | context.EnableConcurrentExecution(); 29 | context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.None); 30 | 31 | context.RegisterCompilationAction(c => 32 | { 33 | var installed = c.Options.AdditionalFiles.Where(x => 34 | { 35 | var options = c.Options.AnalyzerConfigOptionsProvider.GetOptions(x); 36 | // In release builds, we'll have a single such item, since we IL-merge the analyzer. 37 | return options.TryGetValue("build_metadata.Analyzer.ItemType", out var itemType) && 38 | options.TryGetValue("build_metadata.Analyzer.NuGetPackageId", out var packageId) && 39 | itemType == "Analyzer" && 40 | packageId == "SponsorableLib"; 41 | }).Select(x => File.GetLastWriteTime(x.Path)).OrderByDescending(x => x).FirstOrDefault(); 42 | 43 | var status = Diagnostics.GetOrSetStatus(() => c.Options); 44 | 45 | var location = Location.None; 46 | if (c.Options.AnalyzerConfigOptionsProvider.GlobalOptions.TryGetValue("build_property.MSBuildProjectFullPath", out var value)) 47 | location = Location.Create(value, new TextSpan(), new LinePositionSpan()); 48 | 49 | c.ReportDiagnostic(Diagnostic.Create(SupportedDiagnostics[0], location, status.ToString())); 50 | 51 | if (installed != default) 52 | Tracing.Trace($"Status: {status}, Installed: {(DateTime.Now - installed).Humanize()} ago"); 53 | else 54 | Tracing.Trace($"Status: {status}, unknown install time"); 55 | }); 56 | } 57 | } -------------------------------------------------------------------------------- /src/SponsorLink/Analyzer/StatusReportingGenerator.cs: -------------------------------------------------------------------------------- 1 | using Devlooped.Sponsors; 2 | using Microsoft.CodeAnalysis; 3 | using static Devlooped.Sponsors.SponsorLink; 4 | 5 | namespace Analyzer; 6 | 7 | [Generator] 8 | public class StatusReportingGenerator : IIncrementalGenerator 9 | { 10 | public void Initialize(IncrementalGeneratorInitializationContext context) 11 | { 12 | context.RegisterSourceOutput( 13 | // this is required to ensure status is registered properly independently 14 | // of analyzer runs. 15 | context.GetStatusOptions(), 16 | (spc, source) => 17 | { 18 | var status = Diagnostics.GetOrSetStatus(source); 19 | spc.AddSource("StatusReporting.cs", 20 | $""" 21 | // Status: {status} 22 | // DesignTimeBuild: {source.GlobalOptions.IsDesignTimeBuild()} 23 | """); 24 | }); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/SponsorLink/Analyzer/buildTransitive/SponsorableLib.targets: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /src/SponsorLink/Directory.Build.props: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | false 5 | latest 6 | true 7 | annotations 8 | true 9 | 10 | false 11 | $([System.IO.Path]::GetFullPath('$(MSBuildThisFileDirectory)bin')) 12 | 13 | https://pkg.kzu.app/index.json;https://api.nuget.org/v3/index.json 14 | $(PackageOutputPath);$(RestoreSources) 15 | 16 | 18 | $([System.DateTime]::Parse("2024-03-15")) 19 | $([System.DateTime]::UtcNow.Subtract($(Epoc)).TotalDays) 20 | $([System.Math]::Truncate($(TotalDays))) 21 | $([System.Math]::Floor($([MSBuild]::Divide($([System.DateTime]::UtcNow.TimeOfDay.TotalSeconds), 10)))) 22 | 42.$(Days).$(Seconds) 23 | 24 | SponsorableLib 25 | 26 | 27 | 28 | -------------------------------------------------------------------------------- /src/SponsorLink/Directory.Build.targets: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /src/SponsorLink/Library/Library.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | SponsorableLib 5 | netstandard2.0 6 | true 7 | SponsorableLib 8 | Sample library incorporating SponsorLink checks 9 | true 10 | true 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /src/SponsorLink/Library/MyClass.cs: -------------------------------------------------------------------------------- 1 | namespace SponsorableLib; 2 | 3 | public class MyClass 4 | { 5 | } 6 | -------------------------------------------------------------------------------- /src/SponsorLink/Library/readme.md: -------------------------------------------------------------------------------- 1 | # Sponsorable Library 2 | 3 | Example of a library that is available for sponsorship and leverages 4 | [SponsorLink](https://github.com/devlooped/SponsorLink) to remind users 5 | in an IDE (VS/Rider). 6 | -------------------------------------------------------------------------------- /src/SponsorLink/SponsorLink.Analyzer.Tests.targets: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | true 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 24 | 25 | 30 | 31 | 37 | 38 | 39 | 40 | 41 | 42 | $([MSBuild]::ValueOrDefault('%(FullPath)', '').Replace('net6.0', 'netstandard2.0').Replace('net8.0', 'netstandard2.0').Replace('netcoreapp3.1', 'netstandard2.0')) 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | -------------------------------------------------------------------------------- /src/SponsorLink/SponsorLink/AnalyzerOptionsExtensions.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.CodeAnalysis.Diagnostics; 2 | 3 | static class AnalyzerOptionsExtensions 4 | { 5 | /// 6 | /// Gets whether the current build is a design-time build. 7 | /// 8 | public static bool IsDesignTimeBuild(this AnalyzerConfigOptionsProvider options) => 9 | options.GlobalOptions.TryGetValue("build_property.DesignTimeBuild", out var value) && 10 | bool.TryParse(value, out var isDesignTime) && isDesignTime; 11 | 12 | /// 13 | /// Gets whether the current build is a design-time build. 14 | /// 15 | public static bool IsDesignTimeBuild(this AnalyzerConfigOptions options) => 16 | options.TryGetValue("build_property.DesignTimeBuild", out var value) && 17 | bool.TryParse(value, out var isDesignTime) && isDesignTime; 18 | } -------------------------------------------------------------------------------- /src/SponsorLink/SponsorLink/AppDomainDictionary.cs: -------------------------------------------------------------------------------- 1 | // 2 | #nullable enable 3 | using System; 4 | 5 | namespace Devlooped.Sponsors; 6 | 7 | /// 8 | /// A helper class to store and retrieve values from the current 9 | /// as typed named values. 10 | /// 11 | /// 12 | /// This allows tools that run within the same app domain to share state, such as 13 | /// MSBuild tasks or Roslyn analyzers. 14 | /// 15 | static class AppDomainDictionary 16 | { 17 | /// 18 | /// Gets the value associated with the specified name, or creates a new one if it doesn't exist. 19 | /// 20 | public static TValue Get(string name) where TValue : notnull, new() 21 | { 22 | var data = AppDomain.CurrentDomain.GetData(name); 23 | if (data is TValue firstTry) 24 | return firstTry; 25 | 26 | lock (AppDomain.CurrentDomain) 27 | { 28 | if (AppDomain.CurrentDomain.GetData(name) is TValue secondTry) 29 | return secondTry; 30 | 31 | var newValue = new TValue(); 32 | AppDomain.CurrentDomain.SetData(name, newValue); 33 | return newValue; 34 | } 35 | } 36 | } -------------------------------------------------------------------------------- /src/SponsorLink/SponsorLink/ManifestStatus.cs: -------------------------------------------------------------------------------- 1 | // 2 | namespace Devlooped.Sponsors; 3 | 4 | /// 5 | /// The resulting status from validation. 6 | /// 7 | public enum ManifestStatus 8 | { 9 | /// 10 | /// The manifest couldn't be read at all. 11 | /// 12 | Unknown, 13 | /// 14 | /// The manifest was read and is valid (not expired and properly signed). 15 | /// 16 | Valid, 17 | /// 18 | /// The manifest was read but has expired. 19 | /// 20 | Expired, 21 | /// 22 | /// The manifest was read, but its signature is invalid. 23 | /// 24 | Invalid, 25 | } 26 | -------------------------------------------------------------------------------- /src/SponsorLink/SponsorLink/SponsorLinkAnalyzer.cs: -------------------------------------------------------------------------------- 1 | // 2 | #nullable enable 3 | using System.Collections.Immutable; 4 | using System.Linq; 5 | using Microsoft.CodeAnalysis; 6 | using Microsoft.CodeAnalysis.Diagnostics; 7 | using static Devlooped.Sponsors.SponsorLink; 8 | 9 | namespace Devlooped.Sponsors; 10 | 11 | /// 12 | /// Links the sponsor status for the current compilation. 13 | /// 14 | [DiagnosticAnalyzer(LanguageNames.CSharp, LanguageNames.VisualBasic)] 15 | public class SponsorLinkAnalyzer : DiagnosticAnalyzer 16 | { 17 | public override ImmutableArray SupportedDiagnostics { get; } = DiagnosticsManager.KnownDescriptors.Values.ToImmutableArray(); 18 | 19 | #pragma warning disable RS1026 // Enable concurrent execution 20 | public override void Initialize(AnalysisContext context) 21 | #pragma warning restore RS1026 // Enable concurrent execution 22 | { 23 | #if !DEBUG 24 | // Only enable concurrent execution in release builds, otherwise debugging is quite annoying. 25 | context.EnableConcurrentExecution(); 26 | #endif 27 | context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.None); 28 | 29 | #pragma warning disable RS1013 // Start action has no registered non-end actions 30 | // We do this so that the status is set at compilation start so we can use it 31 | // across all other analyzers. We report only on finish because multiple 32 | // analyzers can report the same diagnostic and we want to avoid duplicates. 33 | context.RegisterCompilationStartAction(ctx => 34 | { 35 | // Setting the status early allows other analyzers to potentially check for it. 36 | var status = Diagnostics.GetOrSetStatus(() => ctx.Options); 37 | 38 | // Never report any diagnostic unless we're in an editor. 39 | if (IsEditor) 40 | { 41 | // NOTE: for multiple projects with the same product name, we only report one diagnostic, 42 | // so it's expected to NOT get a diagnostic back. Also, we don't want to report 43 | // multiple diagnostics for each project in a solution that uses the same product. 44 | ctx.RegisterCompilationEndAction(ctx => 45 | { 46 | // We'd never report Info/hero link if users opted out of it. 47 | if (status.IsSponsor() && 48 | ctx.Options.AnalyzerConfigOptionsProvider.GlobalOptions.TryGetValue("build_property.SponsorLinkHero", out var slHero) && 49 | bool.TryParse(slHero, out var isHero) && isHero) 50 | return; 51 | 52 | // Only report if the package is directly referenced in the project for 53 | // any of the funding packages we monitor (i.e. we could have one or more 54 | // metapackages we also consider "direct references). 55 | // See SL_CollectDependencies in buildTransitive\Devlooped.Sponsors.targets 56 | foreach (var prop in Funding.PackageIds.Select(id => id.Replace('.', '_'))) 57 | { 58 | if (ctx.Options.AnalyzerConfigOptionsProvider.GlobalOptions.TryGetValue("build_property." + prop, out var package) && 59 | package?.Length > 0 && 60 | Diagnostics.TryGet() is { } diagnostic) 61 | { 62 | ctx.ReportDiagnostic(diagnostic); 63 | break; 64 | } 65 | } 66 | }); 67 | } 68 | }); 69 | #pragma warning restore RS1013 // Start action has no registered non-end actions 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /src/SponsorLink/SponsorLink/SponsorStatus.cs: -------------------------------------------------------------------------------- 1 | // 2 | namespace Devlooped.Sponsors; 3 | 4 | public static class SponsorStatusExtensions 5 | { 6 | /// 7 | /// Whether represents a sponsor (directly or indirectly). 8 | /// 9 | public static bool IsSponsor(this SponsorStatus status) 10 | => status == SponsorStatus.User || 11 | status == SponsorStatus.Team || 12 | status == SponsorStatus.Contributor || 13 | status == SponsorStatus.Organization; 14 | } 15 | 16 | /// 17 | /// The determined sponsoring status. 18 | /// 19 | public enum SponsorStatus 20 | { 21 | /// 22 | /// Sponsorship status is unknown. 23 | /// 24 | Unknown, 25 | /// 26 | /// Sponsorship status is unknown, but within the grace period. 27 | /// 28 | Grace, 29 | /// 30 | /// The sponsors manifest is expired but within the grace period. 31 | /// 32 | Expiring, 33 | /// 34 | /// The sponsors manifest is expired and outside the grace period. 35 | /// 36 | Expired, 37 | /// 38 | /// The user is personally sponsoring. 39 | /// 40 | User, 41 | /// 42 | /// The user is a team member. 43 | /// 44 | Team, 45 | /// 46 | /// The user is a contributor. 47 | /// 48 | Contributor, 49 | /// 50 | /// The user is a member of a contributing organization. 51 | /// 52 | Organization, 53 | /// 54 | /// The user is a OSS author. 55 | /// 56 | OpenSource, 57 | } 58 | -------------------------------------------------------------------------------- /src/SponsorLink/SponsorLink/SponsorableLib.targets: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | $([System.IO.Path]::GetFullPath($(MSBuildThisFileDirectory)sponsorable.md)) 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | $(WarningsNotAsErrors);LIB001;LIB002;LIB003;LIB004;LIB005 16 | 17 | $(BaseIntermediateOutputPath)autosync.stamp 18 | 19 | $(HOME) 20 | $(USERPROFILE) 21 | 22 | true 23 | $([System.IO.Path]::GetFullPath('$(UserProfileHome)/.sponsorlink')) 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 48 | 49 | 50 | 51 | 52 | %(GitRoot.FullPath) 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | -------------------------------------------------------------------------------- /src/SponsorLink/SponsorLink/Tracing.cs: -------------------------------------------------------------------------------- 1 | // 2 | #nullable enable 3 | using System; 4 | using System.Diagnostics; 5 | using System.IO; 6 | using System.Runtime.CompilerServices; 7 | using System.Text; 8 | 9 | namespace Devlooped.Sponsors; 10 | 11 | static class Tracing 12 | { 13 | public static void Trace([CallerMemberName] string? message = null, [CallerFilePath] string? filePath = null, [CallerLineNumber] int lineNumber = 0) 14 | { 15 | var trace = !string.IsNullOrEmpty(Environment.GetEnvironmentVariable("SPONSORLINK_TRACE")); 16 | #if DEBUG 17 | trace = true; 18 | #endif 19 | 20 | if (!trace) 21 | return; 22 | 23 | var line = new StringBuilder() 24 | .Append($"[{DateTime.Now:O}]") 25 | .Append($"[{Process.GetCurrentProcess().ProcessName}:{Process.GetCurrentProcess().Id}]") 26 | .Append($" {message} ") 27 | .AppendLine($" -> {filePath}({lineNumber})") 28 | .ToString(); 29 | 30 | var dir = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), ".sponsorlink"); 31 | Directory.CreateDirectory(dir); 32 | 33 | var tries = 0; 34 | // Best-effort only 35 | while (tries < 10) 36 | { 37 | try 38 | { 39 | File.AppendAllText(Path.Combine(dir, "trace.log"), line); 40 | Debugger.Log(0, "SponsorLink", line); 41 | return; 42 | } 43 | catch (IOException) 44 | { 45 | tries++; 46 | } 47 | } 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /src/SponsorLink/SponsorLink/sponsorable.md: -------------------------------------------------------------------------------- 1 | # Why Sponsor 2 | 3 | Well, why not? It's super cheap :) 4 | 5 | This could even be partially auto-generated from FUNDING.yml and what-not. -------------------------------------------------------------------------------- /src/SponsorLink/SponsorLinkAnalyzer.sln: -------------------------------------------------------------------------------- 1 |  2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | # Visual Studio Version 17 4 | VisualStudioVersion = 17.10.34928.147 5 | MinimumVisualStudioVersion = 10.0.40219.1 6 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Analyzer", "Analyzer\Analyzer.csproj", "{584984D6-926B-423D-9416-519613423BAE}" 7 | EndProject 8 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Library", "Library\Library.csproj", "{598CD398-A172-492C-8367-827D43276029}" 9 | EndProject 10 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Tests", "Tests\Tests.csproj", "{EA02494C-6ED4-47A0-8D43-20F50BE8554F}" 11 | EndProject 12 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "SponsorLink", "SponsorLink\SponsorLink.csproj", "{B91C7E99-3D2E-4FDF-B017-9123E810197F}" 13 | EndProject 14 | Global 15 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 16 | Debug|Any CPU = Debug|Any CPU 17 | Release|Any CPU = Release|Any CPU 18 | EndGlobalSection 19 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 20 | {584984D6-926B-423D-9416-519613423BAE}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 21 | {584984D6-926B-423D-9416-519613423BAE}.Debug|Any CPU.Build.0 = Debug|Any CPU 22 | {584984D6-926B-423D-9416-519613423BAE}.Release|Any CPU.ActiveCfg = Release|Any CPU 23 | {584984D6-926B-423D-9416-519613423BAE}.Release|Any CPU.Build.0 = Release|Any CPU 24 | {598CD398-A172-492C-8367-827D43276029}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 25 | {598CD398-A172-492C-8367-827D43276029}.Debug|Any CPU.Build.0 = Debug|Any CPU 26 | {598CD398-A172-492C-8367-827D43276029}.Release|Any CPU.ActiveCfg = Release|Any CPU 27 | {598CD398-A172-492C-8367-827D43276029}.Release|Any CPU.Build.0 = Release|Any CPU 28 | {EA02494C-6ED4-47A0-8D43-20F50BE8554F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 29 | {EA02494C-6ED4-47A0-8D43-20F50BE8554F}.Debug|Any CPU.Build.0 = Debug|Any CPU 30 | {EA02494C-6ED4-47A0-8D43-20F50BE8554F}.Release|Any CPU.ActiveCfg = Release|Any CPU 31 | {EA02494C-6ED4-47A0-8D43-20F50BE8554F}.Release|Any CPU.Build.0 = Release|Any CPU 32 | {B91C7E99-3D2E-4FDF-B017-9123E810197F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 33 | {B91C7E99-3D2E-4FDF-B017-9123E810197F}.Debug|Any CPU.Build.0 = Debug|Any CPU 34 | {B91C7E99-3D2E-4FDF-B017-9123E810197F}.Release|Any CPU.ActiveCfg = Release|Any CPU 35 | {B91C7E99-3D2E-4FDF-B017-9123E810197F}.Release|Any CPU.Build.0 = Release|Any CPU 36 | EndGlobalSection 37 | GlobalSection(SolutionProperties) = preSolution 38 | HideSolutionNode = FALSE 39 | EndGlobalSection 40 | GlobalSection(ExtensibilityGlobals) = postSolution 41 | SolutionGuid = {1DDA0EFF-BEF6-49BB-8AA8-D71FE1CD3E6F} 42 | EndGlobalSection 43 | EndGlobal 44 | -------------------------------------------------------------------------------- /src/SponsorLink/Tests/.netconfig: -------------------------------------------------------------------------------- 1 | [config] 2 | root = true 3 | [file "SponsorableManifest.cs"] 4 | url = https://github.com/devlooped/SponsorLink/blob/main/src/Core/SponsorableManifest.cs 5 | sha = 5a4cad3a084f53afe34a6b75e4f3a084a0f1bf9e 6 | etag = 9a07c856d06e0cde629fce3ec014f64f9adfd5ae5805a35acf623eba0ee045c1 7 | weak 8 | [file "JsonOptions.cs"] 9 | url = https://github.com/devlooped/SponsorLink/blob/main/src/Core/JsonOptions.cs 10 | sha = 80ea1bfe47049ef6c6ed4f424dcf7febb729cbba 11 | etag = 17799725ad9b24eb5998365962c30b9a487bddadca37c616e35b76b8c9eb161a 12 | weak 13 | [file "Extensions.cs"] 14 | url = https://github.com/devlooped/SponsorLink/blob/main/src/Core/Extensions.cs 15 | sha = c455f6fa1a4d404181d076d7f3362345c8ed7df2 16 | etag = 9e51b7e6540fae140490a5283b1e67ce071bd18a267bc2ae0b35c7248261aed1 17 | weak -------------------------------------------------------------------------------- /src/SponsorLink/Tests/Attributes.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.Extensions.Configuration; 2 | using Xunit; 3 | 4 | public class SecretsFactAttribute : FactAttribute 5 | { 6 | public SecretsFactAttribute(params string[] secrets) 7 | { 8 | var configuration = new ConfigurationBuilder() 9 | .AddUserSecrets() 10 | .Build(); 11 | 12 | var missing = new HashSet(); 13 | 14 | foreach (var secret in secrets) 15 | { 16 | if (string.IsNullOrEmpty(configuration[secret])) 17 | missing.Add(secret); 18 | } 19 | 20 | if (missing.Count > 0) 21 | Skip = "Missing user secrets: " + string.Join(',', missing); 22 | } 23 | } 24 | 25 | public class LocalFactAttribute : SecretsFactAttribute 26 | { 27 | public LocalFactAttribute(params string[] secrets) : base(secrets) 28 | { 29 | if (!string.IsNullOrEmpty(Environment.GetEnvironmentVariable("CI"))) 30 | Skip = "Non-CI test"; 31 | } 32 | } 33 | 34 | public class CIFactAttribute : FactAttribute 35 | { 36 | public CIFactAttribute() 37 | { 38 | if (string.IsNullOrEmpty(Environment.GetEnvironmentVariable("CI"))) 39 | Skip = "CI-only test"; 40 | } 41 | } 42 | 43 | public class LocalTheoryAttribute : TheoryAttribute 44 | { 45 | public LocalTheoryAttribute() 46 | { 47 | if (!string.IsNullOrEmpty(Environment.GetEnvironmentVariable("CI"))) 48 | Skip = "Non-CI test"; 49 | } 50 | } 51 | 52 | public class CITheoryAttribute : TheoryAttribute 53 | { 54 | public CITheoryAttribute() 55 | { 56 | if (string.IsNullOrEmpty(Environment.GetEnvironmentVariable("CI"))) 57 | Skip = "CI-only test"; 58 | } 59 | } -------------------------------------------------------------------------------- /src/SponsorLink/Tests/Extensions.cs: -------------------------------------------------------------------------------- 1 | using System.Diagnostics.CodeAnalysis; 2 | using System.Runtime.CompilerServices; 3 | using System.Security.Cryptography; 4 | using Microsoft.Extensions.Logging; 5 | using Microsoft.IdentityModel.Tokens; 6 | 7 | namespace Devlooped.Sponsors; 8 | 9 | static class Extensions 10 | { 11 | public static HashCode Add(this HashCode hash, params object[] items) 12 | { 13 | foreach (var item in items) 14 | hash.Add(item); 15 | 16 | return hash; 17 | } 18 | 19 | 20 | public static HashCode AddRange(this HashCode hash, IEnumerable items) 21 | { 22 | foreach (var item in items) 23 | hash.Add(item); 24 | 25 | return hash; 26 | } 27 | 28 | public static bool ThumbprintEquals(this SecurityKey key, RSA rsa) => key.ThumbprintEquals(new RsaSecurityKey(rsa)); 29 | 30 | public static bool ThumbprintEquals(this RSA rsa, SecurityKey key) => key.ThumbprintEquals(rsa); 31 | 32 | public static bool ThumbprintEquals(this SecurityKey first, SecurityKey second) 33 | { 34 | var expectedKey = JsonWebKeyConverter.ConvertFromSecurityKey(second); 35 | var actualKey = JsonWebKeyConverter.ConvertFromSecurityKey(first); 36 | return expectedKey.ComputeJwkThumbprint().AsSpan().SequenceEqual(actualKey.ComputeJwkThumbprint()); 37 | } 38 | 39 | public static Array Cast(this Array array, Type elementType) 40 | { 41 | //Convert the object list to the destination array type. 42 | var result = Array.CreateInstance(elementType, array.Length); 43 | Array.Copy(array, result, array.Length); 44 | return result; 45 | } 46 | 47 | public static void Assert(this ILogger logger, [DoesNotReturnIf(false)] bool condition, [CallerArgumentExpression(nameof(condition))] string? message = default, params object?[] args) 48 | { 49 | if (!condition) 50 | { 51 | //Debug.Assert(condition, message); 52 | logger.LogError(message, args); 53 | throw new InvalidOperationException(message); 54 | } 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /src/SponsorLink/Tests/JsonOptions.cs: -------------------------------------------------------------------------------- 1 | using System.Globalization; 2 | using System.Text.Json; 3 | using System.Text.Json.Serialization; 4 | using System.Text.Json.Serialization.Metadata; 5 | using Microsoft.IdentityModel.Tokens; 6 | 7 | namespace Devlooped.Sponsors; 8 | 9 | static partial class JsonOptions 10 | { 11 | public static JsonSerializerOptions Default { get; } = 12 | #if NET6_0_OR_GREATER 13 | new(JsonSerializerDefaults.Web) 14 | #else 15 | new() 16 | #endif 17 | { 18 | AllowTrailingCommas = true, 19 | PropertyNamingPolicy = JsonNamingPolicy.CamelCase, 20 | ReadCommentHandling = JsonCommentHandling.Skip, 21 | #if NET6_0_OR_GREATER 22 | DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingDefault | JsonIgnoreCondition.WhenWritingNull, 23 | #endif 24 | WriteIndented = true, 25 | Converters = 26 | { 27 | new JsonStringEnumConverter(allowIntegerValues: false), 28 | #if NET6_0_OR_GREATER 29 | new DateOnlyJsonConverter() 30 | #endif 31 | } 32 | }; 33 | 34 | public static JsonSerializerOptions JsonWebKey { get; } = new(JsonSerializerOptions.Default) 35 | { 36 | WriteIndented = true, 37 | DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingDefault | JsonIgnoreCondition.WhenWritingNull, 38 | TypeInfoResolver = new DefaultJsonTypeInfoResolver 39 | { 40 | Modifiers = 41 | { 42 | info => 43 | { 44 | if (info.Type != typeof(JsonWebKey)) 45 | return; 46 | 47 | foreach (var prop in info.Properties) 48 | { 49 | // Don't serialize empty lists, makes for more concise JWKs 50 | prop.ShouldSerialize = (obj, value) => 51 | value is not null && 52 | (value is not IList list || list.Count > 0); 53 | } 54 | } 55 | } 56 | } 57 | }; 58 | 59 | 60 | #if NET6_0_OR_GREATER 61 | public class DateOnlyJsonConverter : JsonConverter 62 | { 63 | public override DateOnly Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) 64 | => DateOnly.Parse(reader.GetString()?[..10] ?? "", CultureInfo.InvariantCulture); 65 | 66 | public override void Write(Utf8JsonWriter writer, DateOnly value, JsonSerializerOptions options) 67 | => writer.WriteStringValue(value.ToString("O", CultureInfo.InvariantCulture)); 68 | } 69 | #endif 70 | } 71 | -------------------------------------------------------------------------------- /src/SponsorLink/Tests/Sample.cs: -------------------------------------------------------------------------------- 1 | extern alias Analyzer; 2 | using System; 3 | using System.Globalization; 4 | using System.Runtime.CompilerServices; 5 | using System.Security.Cryptography; 6 | using Analyzer::Devlooped.Sponsors; 7 | using Microsoft.CodeAnalysis; 8 | using Xunit; 9 | using Xunit.Abstractions; 10 | 11 | namespace Tests; 12 | 13 | public class Sample(ITestOutputHelper output) 14 | { 15 | [Theory] 16 | [InlineData("es-AR", SponsorStatus.Unknown)] 17 | [InlineData("es-AR", SponsorStatus.Expiring)] 18 | [InlineData("es-AR", SponsorStatus.Expired)] 19 | [InlineData("es-AR", SponsorStatus.User)] 20 | [InlineData("es-AR", SponsorStatus.Contributor)] 21 | [InlineData("es", SponsorStatus.Unknown)] 22 | [InlineData("es", SponsorStatus.Expiring)] 23 | [InlineData("es", SponsorStatus.Expired)] 24 | [InlineData("es", SponsorStatus.User)] 25 | [InlineData("es", SponsorStatus.Contributor)] 26 | [InlineData("en", SponsorStatus.Unknown)] 27 | [InlineData("en", SponsorStatus.Expiring)] 28 | [InlineData("en", SponsorStatus.Expired)] 29 | [InlineData("en", SponsorStatus.User)] 30 | [InlineData("en", SponsorStatus.Contributor)] 31 | [InlineData("", SponsorStatus.Unknown)] 32 | [InlineData("", SponsorStatus.Expiring)] 33 | [InlineData("", SponsorStatus.Expired)] 34 | [InlineData("", SponsorStatus.User)] 35 | [InlineData("", SponsorStatus.Contributor)] 36 | public void Test(string culture, SponsorStatus kind) 37 | { 38 | Thread.CurrentThread.CurrentCulture = Thread.CurrentThread.CurrentUICulture = 39 | culture == "" ? CultureInfo.InvariantCulture : CultureInfo.GetCultureInfo(culture); 40 | 41 | var diag = GetDescriptor(["foo"], "bar", "FB", kind); 42 | 43 | output.WriteLine(diag.Title.ToString()); 44 | output.WriteLine(diag.MessageFormat.ToString()); 45 | output.WriteLine(diag.Description.ToString()); 46 | } 47 | 48 | [Fact] 49 | public void RenderSponsorables() 50 | { 51 | Assert.NotEmpty(SponsorLink.Sponsorables); 52 | 53 | foreach (var pair in SponsorLink.Sponsorables) 54 | { 55 | output.WriteLine($"{pair.Key} = {pair.Value}"); 56 | // Read the JWK 57 | var jsonWebKey = Microsoft.IdentityModel.Tokens.JsonWebKey.Create(pair.Value); 58 | 59 | Assert.NotNull(jsonWebKey); 60 | 61 | using var key = RSA.Create(new RSAParameters 62 | { 63 | Modulus = Microsoft.IdentityModel.Tokens.Base64UrlEncoder.DecodeBytes(jsonWebKey.N), 64 | Exponent = Microsoft.IdentityModel.Tokens.Base64UrlEncoder.DecodeBytes(jsonWebKey.E), 65 | }); 66 | } 67 | } 68 | 69 | DiagnosticDescriptor GetDescriptor(string[] sponsorable, string product, string prefix, SponsorStatus status) => status switch 70 | { 71 | SponsorStatus.Unknown => DiagnosticsManager.CreateUnknown(sponsorable, product, prefix), 72 | SponsorStatus.Expiring => DiagnosticsManager.CreateExpiring(sponsorable, prefix), 73 | SponsorStatus.Expired => DiagnosticsManager.CreateExpired(sponsorable, prefix), 74 | SponsorStatus.User => DiagnosticsManager.CreateSponsor(sponsorable, prefix), 75 | SponsorStatus.Contributor => DiagnosticsManager.CreateContributor(sponsorable, prefix), 76 | _ => throw new NotImplementedException(), 77 | }; 78 | } -------------------------------------------------------------------------------- /src/SponsorLink/Tests/keys/kzu.key: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/devlooped/CredentialManager/752b34b21d807de3348b16d2be507a5f7e8213b9/src/SponsorLink/Tests/keys/kzu.key -------------------------------------------------------------------------------- /src/SponsorLink/Tests/keys/kzu.key.jwk: -------------------------------------------------------------------------------- 1 | { 2 | "d": "OmDrKyd0ap7Az2V09C8E6aASK0nnXHGUdTIymmU1tGoxaWpkZ4gLGaYDp4L5fKc-AaqqD3PjvfJvEWfXLqJtEWfUl4gahWrgUkmuzPyAVFFioIzeGIvTGELsR6lTRke0IB2kvfvS7hRgX8Py8ohCAGHiidmoec2SyKkEg0aPdxrIKV8hx5ybC_D_4zRKn0GuVwATIeVZzPpTcyJX_sn4NHDOqut0Xg02iHhMKpF850BSoC97xGMlcSjLocFSTwI63msz6jWQ-6LRVXsfRr2mqakAvsPpqEQ3Ytk9Ud9xW0ctuAWyo6UXev5w2XEL8cSXm33-57fi3ekC_jGCqW0KfAU4Cr2UbuTC0cv8Vv0F4Xm5FizolmuSBFOvf55-eqsjmpwf9hftYAiIlFF-49-P0DpeJejSeoL06BE3e3_IVu3g3HNnSWVUOLJ5Uk5FQ-ieHhf-r2Tq5qZ8_-losHekQbCxCMY2isc-r6V6BMnVL_9kWPxpXwhjKrYxNFZEXUJ1", 3 | "dp": "HjCs_QF1Hn1SGS2OqZzYhGhNk4PTw9Hs97E7g3pb4liY0uECYOYp1RNoMyzvNBZVwlxhpeTTS299yPoXeYmseXfLtcjfIVi6mSWS_u27Hd0zfSdaPDOXyyK-mZfIV7Q76RTost0QY3LA0ciJbj3gJqpl38dhuNQ8h9Yqt-TFyb3CUaM3A_JUNKOTce8qnkLrasytPEuSroOBT8bgCWJIjw_mXWMGcoqRFWHw9Nyp9mIyvtPjUQ9ig3bGSP_-3LZf", 4 | "dq": "IP6EsAZ_6psFdlQrvnugYFs91fEP5QfBzNHmbfmsPRVX4QM5B3L6klQyJsLqvPfF1Xu17ZffLFkNBKuiphIcLPo0yZTJG9Y7S8gLuPAmrH-ndfxG-bQ8Yt0ZB1pA77ILIS8bUTKrMqAWS-VcaxcSCIyhSusLEWYYDi3PEzB375OUw4aXIk3ob8bePG7UqFSL6qmDPgkGLTxkY9m5dEiOshHygtVY-H_jjOIawliEPgmgAr2M-zlXiphovDyAT0PV", 5 | "e": "AQAB", 6 | "kty": "RSA", 7 | "n": "yP71VgOgHDtGbxdyN31mIFFITmGYEk2cwepKbyqKTbTYXF1OXaMoP5n3mfwqwzUQmEAsrclAigPcK4GIy5WWlc5YujIxKauJjsKe0FBxMnFp9o1UcBUHfgDJjaAKieQxb44717b1MwCcflEGnCGTXkntdr45y9Gi1D9-oBw5zIVZekgMP55XxmKvkJd1k-bYWSv-QFG2JJwRIGwr29Jr62juCsLB7Tg83ZGKCa22Y_7lQcezxRRD5OrGWhf3gTYArbrEzbYy653zbHfbOCJeVBe_bXDkR74yG3mmq_Ne0qhNk6wXuX-NrKEvdPxRSRBF7C465fcVY9PM6eTqEPQwKDiarHpU1NTwUetzb-YKry-h678RJWMhC7I9lzzWVobbC0YVKG7XpeVqBB4u7Q6cGo5Xkf19VldkIxQMu9sFeuHGDSoiCLqmRmwNn9GsMV77oZWr-OPrxEdZzL9BcI4fMJMz7YdiIu-qbIp_vqatbalfNasumf8RgtPOkR2vgc59", 8 | "p": "6JTf8Qb0iHRL6MIIs7MlkEKBNpnAu_Nie4HTqhxy2wfE4cBr6QZ98iJniXffDIjq_GxVpw9K-Bv2gTcNrlzOiBaLf3X2Itfice_Qd-luhNbnXVfiA5sg6dZ2wbBuue5ann5iJ_TIbxO4CLUiqQp0PCReUPzTQhzesHxM2-dBC9AYDl7P6p1FF53Hh_Knx9UywhoPvNtoCJy35-5rj0ghgPYz289dbOBccZnvabRueOr_wpHGMKaznqiDMrcFSZ07", 9 | "q": "3TvrN8R9imw6E6JkVQ4PtveE0vkvkSWHUpn9KwKFIJJiwL_HSS4z_8IYR1_0Q1OgK5-z-QcXhq9P7jTjz02I2uwWhP3RZQf99RZACfMaeIs8O2V-I89WdlJYOerzAelW4nYw7zyeVoT5c5osicGWfSmWslLRjA1yx7x1KA_MCU_KIEBlpe1RgEUYPET3OtvPKFIVQYoJfQC5PFlmrC-kgHZMSpdHjWgWi5gPn0fIBCKFsXcPrt2n_lKKGc4lFOen", 10 | "qi": "m-tgdFqO1Ax3C00oe7kdkYLHMD56wkGARdqPCqS5IGhFVKCOA8U6O_s5bSL4r0TzPE0KrJ4A5QJEwjbH4bXssPaaAlv1ZdWjn8YMQCYFolg_pgUWYYI5vNxG1gIsLGXPTfE8a6SObkJ2Q9VC5ZZp14r4lPvJhwFICIGSRBKcvS-gO_gqB3LKuG9TQBi-CE4DHDLJwsCbEBR8Ber45oTqvG7hphpOhBHsFZ8_6f3Reg_sK1BCz9HFCx8hhi8rBfUp" 11 | } -------------------------------------------------------------------------------- /src/SponsorLink/Tests/keys/kzu.key.txt: -------------------------------------------------------------------------------- 1 | MIIG4wIBAAKCAYEAyP71VgOgHDtGbxdyN31mIFFITmGYEk2cwepKbyqKTbTYXF1OXaMoP5n3mfwqwzUQmEAsrclAigPcK4GIy5WWlc5YujIxKauJjsKe0FBxMnFp9o1UcBUHfgDJjaAKieQxb44717b1MwCcflEGnCGTXkntdr45y9Gi1D9+oBw5zIVZekgMP55XxmKvkJd1k+bYWSv+QFG2JJwRIGwr29Jr62juCsLB7Tg83ZGKCa22Y/7lQcezxRRD5OrGWhf3gTYArbrEzbYy653zbHfbOCJeVBe/bXDkR74yG3mmq/Ne0qhNk6wXuX+NrKEvdPxRSRBF7C465fcVY9PM6eTqEPQwKDiarHpU1NTwUetzb+YKry+h678RJWMhC7I9lzzWVobbC0YVKG7XpeVqBB4u7Q6cGo5Xkf19VldkIxQMu9sFeuHGDSoiCLqmRmwNn9GsMV77oZWr+OPrxEdZzL9BcI4fMJMz7YdiIu+qbIp/vqatbalfNasumf8RgtPOkR2vgc59AgMBAAECggGAOmDrKyd0ap7Az2V09C8E6aASK0nnXHGUdTIymmU1tGoxaWpkZ4gLGaYDp4L5fKc+AaqqD3PjvfJvEWfXLqJtEWfUl4gahWrgUkmuzPyAVFFioIzeGIvTGELsR6lTRke0IB2kvfvS7hRgX8Py8ohCAGHiidmoec2SyKkEg0aPdxrIKV8hx5ybC/D/4zRKn0GuVwATIeVZzPpTcyJX/sn4NHDOqut0Xg02iHhMKpF850BSoC97xGMlcSjLocFSTwI63msz6jWQ+6LRVXsfRr2mqakAvsPpqEQ3Ytk9Ud9xW0ctuAWyo6UXev5w2XEL8cSXm33+57fi3ekC/jGCqW0KfAU4Cr2UbuTC0cv8Vv0F4Xm5FizolmuSBFOvf55+eqsjmpwf9hftYAiIlFF+49+P0DpeJejSeoL06BE3e3/IVu3g3HNnSWVUOLJ5Uk5FQ+ieHhf+r2Tq5qZ8/+losHekQbCxCMY2isc+r6V6BMnVL/9kWPxpXwhjKrYxNFZEXUJ1AoHBAOiU3/EG9Ih0S+jCCLOzJZBCgTaZwLvzYnuB06occtsHxOHAa+kGffIiZ4l33wyI6vxsVacPSvgb9oE3Da5czogWi3919iLX4nHv0HfpboTW511X4gObIOnWdsGwbrnuWp5+Yif0yG8TuAi1IqkKdDwkXlD800Ic3rB8TNvnQQvQGA5ez+qdRRedx4fyp8fVMsIaD7zbaAict+fua49IIYD2M9vPXWzgXHGZ72m0bnjq/8KRxjCms56ogzK3BUmdOwKBwQDdO+s3xH2KbDoTomRVDg+294TS+S+RJYdSmf0rAoUgkmLAv8dJLjP/whhHX/RDU6Arn7P5BxeGr0/uNOPPTYja7BaE/dFlB/31FkAJ8xp4izw7ZX4jz1Z2Ulg56vMB6VbidjDvPJ5WhPlzmiyJwZZ9KZayUtGMDXLHvHUoD8wJT8ogQGWl7VGARRg8RPc6288oUhVBigl9ALk8WWasL6SAdkxKl0eNaBaLmA+fR8gEIoWxdw+u3af+UooZziUU56cCgcAeMKz9AXUefVIZLY6pnNiEaE2Tg9PD0ez3sTuDelviWJjS4QJg5inVE2gzLO80FlXCXGGl5NNLb33I+hd5iax5d8u1yN8hWLqZJZL+7bsd3TN9J1o8M5fLIr6Zl8hXtDvpFOiy3RBjcsDRyIluPeAmqmXfx2G41DyH1iq35MXJvcJRozcD8lQ0o5Nx7yqeQutqzK08S5Kug4FPxuAJYkiPD+ZdYwZyipEVYfD03Kn2YjK+0+NRD2KDdsZI//7ctl8CgcAg/oSwBn/qmwV2VCu+e6BgWz3V8Q/lB8HM0eZt+aw9FVfhAzkHcvqSVDImwuq898XVe7Xtl98sWQ0Eq6KmEhws+jTJlMkb1jtLyAu48Casf6d1/Eb5tDxi3RkHWkDvsgshLxtRMqsyoBZL5VxrFxIIjKFK6wsRZhgOLc8TMHfvk5TDhpciTehvxt48btSoVIvqqYM+CQYtPGRj2bl0SI6yEfKC1Vj4f+OM4hrCWIQ+CaACvYz7OVeKmGi8PIBPQ9UCgcEAm+tgdFqO1Ax3C00oe7kdkYLHMD56wkGARdqPCqS5IGhFVKCOA8U6O/s5bSL4r0TzPE0KrJ4A5QJEwjbH4bXssPaaAlv1ZdWjn8YMQCYFolg/pgUWYYI5vNxG1gIsLGXPTfE8a6SObkJ2Q9VC5ZZp14r4lPvJhwFICIGSRBKcvS+gO/gqB3LKuG9TQBi+CE4DHDLJwsCbEBR8Ber45oTqvG7hphpOhBHsFZ8/6f3Reg/sK1BCz9HFCx8hhi8rBfUp -------------------------------------------------------------------------------- /src/SponsorLink/Tests/keys/kzu.pub: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/devlooped/CredentialManager/752b34b21d807de3348b16d2be507a5f7e8213b9/src/SponsorLink/Tests/keys/kzu.pub -------------------------------------------------------------------------------- /src/SponsorLink/Tests/keys/kzu.pub.jwk: -------------------------------------------------------------------------------- 1 | { 2 | "e": "AQAB", 3 | "kty": "RSA", 4 | "n": "yP71VgOgHDtGbxdyN31mIFFITmGYEk2cwepKbyqKTbTYXF1OXaMoP5n3mfwqwzUQmEAsrclAigPcK4GIy5WWlc5YujIxKauJjsKe0FBxMnFp9o1UcBUHfgDJjaAKieQxb44717b1MwCcflEGnCGTXkntdr45y9Gi1D9-oBw5zIVZekgMP55XxmKvkJd1k-bYWSv-QFG2JJwRIGwr29Jr62juCsLB7Tg83ZGKCa22Y_7lQcezxRRD5OrGWhf3gTYArbrEzbYy653zbHfbOCJeVBe_bXDkR74yG3mmq_Ne0qhNk6wXuX-NrKEvdPxRSRBF7C465fcVY9PM6eTqEPQwKDiarHpU1NTwUetzb-YKry-h678RJWMhC7I9lzzWVobbC0YVKG7XpeVqBB4u7Q6cGo5Xkf19VldkIxQMu9sFeuHGDSoiCLqmRmwNn9GsMV77oZWr-OPrxEdZzL9BcI4fMJMz7YdiIu-qbIp_vqatbalfNasumf8RgtPOkR2vgc59" 5 | } -------------------------------------------------------------------------------- /src/SponsorLink/Tests/keys/kzu.pub.txt: -------------------------------------------------------------------------------- 1 | MIIBigKCAYEAyP71VgOgHDtGbxdyN31mIFFITmGYEk2cwepKbyqKTbTYXF1OXaMoP5n3mfwqwzUQmEAsrclAigPcK4GIy5WWlc5YujIxKauJjsKe0FBxMnFp9o1UcBUHfgDJjaAKieQxb44717b1MwCcflEGnCGTXkntdr45y9Gi1D9+oBw5zIVZekgMP55XxmKvkJd1k+bYWSv+QFG2JJwRIGwr29Jr62juCsLB7Tg83ZGKCa22Y/7lQcezxRRD5OrGWhf3gTYArbrEzbYy653zbHfbOCJeVBe/bXDkR74yG3mmq/Ne0qhNk6wXuX+NrKEvdPxRSRBF7C465fcVY9PM6eTqEPQwKDiarHpU1NTwUetzb+YKry+h678RJWMhC7I9lzzWVobbC0YVKG7XpeVqBB4u7Q6cGo5Xkf19VldkIxQMu9sFeuHGDSoiCLqmRmwNn9GsMV77oZWr+OPrxEdZzL9BcI4fMJMz7YdiIu+qbIp/vqatbalfNasumf8RgtPOkR2vgc59AgMBAAE= -------------------------------------------------------------------------------- /src/SponsorLink/Tests/keys/sponsorlink.jwt: -------------------------------------------------------------------------------- 1 | eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJpYXQiOjE3MTk4NjgyMzAsImlzcyI6Imh0dHBzOi8vc3BvbnNvcmxpbmsuZGV2bG9vcGVkLmNvbS8iLCJhdWQiOlsiaHR0cHM6Ly9naXRodWIuY29tL3Nwb25zb3JzL2t6dSIsImh0dHBzOi8vZ2l0aHViLmNvbS9zcG9uc29ycy9kZXZsb29wZWQiXSwiY2xpZW50X2lkIjoiYTgyMzUwZmIyYmFlNDA3YjMwMjEiLCJzdWJfandrIjp7ImUiOiJBUUFCIiwia3R5IjoiUlNBIiwibiI6InlQNzFWZ09nSER0R2J4ZHlOMzFtSUZGSVRtR1lFazJjd2VwS2J5cUtUYlRZWEYxT1hhTW9QNW4zbWZ3cXd6VVFtRUFzcmNsQWlnUGNLNEdJeTVXV2xjNVl1akl4S2F1SmpzS2UwRkJ4TW5GcDlvMVVjQlVIZmdESmphQUtpZVF4YjQ0NzE3YjFNd0NjZmxFR25DR1RYa250ZHI0NXk5R2kxRDktb0J3NXpJVlpla2dNUDU1WHhtS3ZrSmQxay1iWVdTdi1RRkcySkp3UklHd3IyOUpyNjJqdUNzTEI3VGc4M1pHS0NhMjJZXzdsUWNlenhSUkQ1T3JHV2hmM2dUWUFyYnJFemJZeTY1M3piSGZiT0NKZVZCZV9iWERrUjc0eUczbW1xX05lMHFoTms2d1h1WC1OcktFdmRQeFJTUkJGN0M0NjVmY1ZZOVBNNmVUcUVQUXdLRGlhckhwVTFOVHdVZXR6Yi1ZS3J5LWg2NzhSSldNaEM3STlsenpXVm9iYkMwWVZLRzdYcGVWcUJCNHU3UTZjR281WGtmMTlWbGRrSXhRTXU5c0ZldUhHRFNvaUNMcW1SbXdObjlHc01WNzdvWldyLU9QcnhFZFp6TDlCY0k0Zk1KTXo3WWRpSXUtcWJJcF92cWF0YmFsZk5hc3VtZjhSZ3RQT2tSMnZnYzU5In19.er4apYbEjHVKlQ_aMXoRhHYeR8N-3uIrCk3HX8UuZO7mb0CaS94-422EI3z5O9vRvckcGkNVoiSIX0ykZqUMHTZxBae-QZc1u_rhdBOChoaxWqpUiPXLZ5-yi7mcRwqg2DOUb2eHTNfRjwJ-0tjL1R1TqZw9d8Bgku1zw2ZTuJl_WsBRHKHTD_s5KyCP5yhSOUumrsf3nXYrc20fJ7ql0FsL0MP66utJk7TFYHGhQV3cfcXYqFEpv-k6tqB9k3Syc0UnepmQT0Y3dtcBzQzCOzfKQ8bdaAXVHjfp4VvXBluHmh9lP6TeZmpvlmQDFvyk0kp1diTbo9pqmX_llNDWNxBdvaSZGa7RZMG_dE2WJGtQNu0C_sbEZDPZsKncxdtm-j-6Y7GRqx7uxe4Py8tAZ7SxjiPgD64jf9KF2OT6f6drVtzohVzYCs6-vhcXzC2sQvd_gQ-SoFNTa1MEcMgGbL-fFWUC7-7bQV1DlSg2YFwrxEIwbM-gHpLZHyyJLvYD -------------------------------------------------------------------------------- /src/SponsorLink/jwk.ps1: -------------------------------------------------------------------------------- 1 | curl https://raw.githubusercontent.com/devlooped/.github/main/sponsorlink.jwt --silent | jq -R 'split(".") | .[1] | @base64d | fromjson' | jq '.sub_jwk' -------------------------------------------------------------------------------- /src/SponsorLink/readme.md: -------------------------------------------------------------------------------- 1 | # SponsorLink .NET Analyzer Sample 2 | 3 | This is one opinionated implementation of [SponsorLink](https://devlooped.com/SponsorLink) 4 | for .NET projects leveraging Roslyn analyzers. 5 | 6 | It is intended for use by [devlooped](https://github.com/devlooped) projects, but can be 7 | used as a template for other sponsorables as well. Supporting arbitrary sponsoring scenarios 8 | is out of scope though, since we just use GitHub sponsors for now. 9 | 10 | ## Usage 11 | 12 | A project can include all the necessary files by using the [dotnet-file](https://github.com/devlooped/dotnet-file) 13 | tool and sync all files to a folder, such as: 14 | 15 | ```shell 16 | dotnet file add https://github.com/devlooped/SponsorLink/tree/main/samples/dotnet/ src/SponsorLink/ 17 | ``` 18 | 19 | Including the analyzer and targets in a project involves two steps. 20 | 21 | 1. Create an analyzer project and add the following property: 22 | 23 | ```xml 24 | 25 | ... 26 | $(MSBuildThisFileDirectory)..\SponsorLink\SponsorLink.Analyzer.targets 27 | 28 | ``` 29 | 30 | 2. Add a `buildTransitive\[PackageId].targets` file with the following import: 31 | 32 | ```xml 33 | 34 | 35 | 36 | ``` 37 | 38 | 3. Set the package id(s) that will be checked for funding in the analyzer, such as: 39 | 40 | ```xml 41 | 42 | SponsorableLib;SponsorableLib.Core 43 | 44 | ``` 45 | 46 | The default analyzer will report a diagnostic for sponsorship status only 47 | if the project being compiled as a direct package reference to one of the 48 | specified package ids. 49 | 50 | This property defaults to `$(PackageId)` if present. Otherwise, it defaults 51 | to `$(FundingProduct)`, which in turn defaults to `$(Product)` if not provided. 52 | 53 | As long as NuGetizer is used, the right packaging will be done automatically. -------------------------------------------------------------------------------- /src/Tests/Attributes.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Runtime.InteropServices; 5 | using System.Text; 6 | using System.Threading.Tasks; 7 | 8 | namespace Tests; 9 | 10 | public class OSFactAttribute : FactAttribute 11 | { 12 | public OSFactAttribute(params string[] onPlatforms) 13 | : this(onPlatforms.Select(OSPlatform.Create).ToArray()) { } 14 | 15 | protected OSFactAttribute(params OSPlatform[] onPlatforms) 16 | { 17 | var shouldSkip = onPlatforms.All(platform => !RuntimeInformation.IsOSPlatform(platform)); 18 | 19 | if (shouldSkip) 20 | Skip = $"Test runs only on platforms '{string.Join(", ", onPlatforms.Select(x => x.ToString()))}'"; 21 | } 22 | } 23 | 24 | public class WindowsFactAttribute() : OSFactAttribute(OSPlatform.Windows); 25 | 26 | public class LinuxFactAttribute() : OSFactAttribute(OSPlatform.Linux); 27 | 28 | public class macOSFactAttribute() : OSFactAttribute(OSPlatform.OSX); 29 | 30 | public class UnixFactAttribute : OSFactAttribute 31 | { 32 | public UnixFactAttribute() : base(OSPlatform.Linux, OSPlatform.OSX) 33 | { 34 | } 35 | } 36 | 37 | public class OSTheoryAttribute : TheoryAttribute 38 | { 39 | public OSTheoryAttribute(params string[] onPlatforms) 40 | : this(onPlatforms.Select(OSPlatform.Create).ToArray()) { } 41 | 42 | protected OSTheoryAttribute(params OSPlatform[] onPlatforms) 43 | { 44 | var shouldSkip = onPlatforms.All(platform => !RuntimeInformation.IsOSPlatform(platform)); 45 | 46 | if (shouldSkip) 47 | Skip = $"Test runs only on platforms '{string.Join(", ", onPlatforms.Select(x => x.ToString()))}'"; 48 | } 49 | } 50 | 51 | public class WindowsTheoryAttribute() : OSTheoryAttribute(OSPlatform.Windows); 52 | 53 | public class LinuxTheoryAttribute() : OSTheoryAttribute(OSPlatform.Linux); 54 | 55 | public class macOSTheoryAttribute() : OSTheoryAttribute(OSPlatform.OSX); 56 | 57 | public class UnixTheoryAttribute() : OSTheoryAttribute(OSPlatform.Linux, OSPlatform.OSX); 58 | 59 | public class LocalFactAttribute : OSFactAttribute 60 | { 61 | public LocalFactAttribute(params string[] onPlatforms) : base(onPlatforms) 62 | { 63 | if (!string.IsNullOrEmpty(Environment.GetEnvironmentVariable("CI"))) 64 | Skip = "Local-only test"; 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /src/Tests/EndToEnd.cs: -------------------------------------------------------------------------------- 1 | using System.Runtime.InteropServices; 2 | using GitCredentialManager; 3 | 4 | namespace Tests; 5 | 6 | /// 7 | /// These tests require the main CredentialManager package to be packed in order for the 8 | /// restore to work. We test end-to-end across all three OSes in CI. To test locally, 9 | /// pack the main package and then restore and run the tests. 10 | /// 11 | public class EndToEnd : IDisposable 12 | { 13 | public EndToEnd() => Environment.SetEnvironmentVariable("GCM_CREDENTIAL_STORE", null); 14 | 15 | public void Dispose() => Environment.SetEnvironmentVariable("GCM_CREDENTIAL_STORE", null); 16 | 17 | [WindowsFact] 18 | public void WindowsDPAPIStore() 19 | { 20 | Environment.SetEnvironmentVariable("GCM_CREDENTIAL_STORE", "dpapi"); 21 | Run(); 22 | } 23 | 24 | [OSFact(nameof(OSPlatform.Windows), nameof(OSPlatform.OSX))] 25 | public void DefaultStore() => Run(); 26 | 27 | [Fact] 28 | public void PlainTextStore() 29 | { 30 | Environment.SetEnvironmentVariable("GCM_CREDENTIAL_STORE", "plaintext"); 31 | Run(); 32 | } 33 | 34 | [UnixFact] 35 | public void GitCacheStore() 36 | { 37 | Environment.SetEnvironmentVariable("GCM_CREDENTIAL_STORE", "cache"); 38 | Run(); 39 | } 40 | 41 | [LocalFact(nameof(OSPlatform.Linux))] 42 | public void LinuxSecretService() 43 | { 44 | // To test this locally, you need: 45 | // sudo apt-get update 46 | // sudo apt install libsecret-1-0 libsecret-1-dev 47 | // sudo apt install gnome-keyring 48 | // dbus-launch --sh-syntax 49 | // export $(dbus-launch) 50 | // gnome-keyring-daemon -r -d 51 | 52 | // Then run the tests: dotnet test 53 | // This will require keyring unlocking interactively before tests proceed. 54 | 55 | Environment.SetEnvironmentVariable("GCM_CREDENTIAL_STORE", "secretservice"); 56 | Run(); 57 | } 58 | 59 | [WindowsFact] 60 | public void SavedOneNamespaceCannotRetrieveAnother() 61 | { 62 | var ns1 = Guid.NewGuid().ToString("N"); 63 | var ns2 = Guid.NewGuid().ToString("N"); 64 | 65 | var store1 = CredentialManager.Create(ns1); 66 | var store2 = CredentialManager.Create(ns2); 67 | 68 | var usr = Guid.NewGuid().ToString("N"); 69 | var pwd = Guid.NewGuid().ToString("N"); 70 | 71 | store1.AddOrUpdate("https://test.com", usr, pwd); 72 | 73 | Assert.Null(store2.Get("https://test.com", usr)); 74 | Assert.Empty(store2.GetAccounts("https://test.com")); 75 | } 76 | 77 | void Run() 78 | { 79 | var store = CredentialManager.Create(Guid.NewGuid().ToString("N")); 80 | 81 | var usr = Guid.NewGuid().ToString("N"); 82 | var pwd = Guid.NewGuid().ToString("N"); 83 | 84 | store.AddOrUpdate("https://test.com", usr, pwd); 85 | 86 | Assert.Equal(pwd, store.Get("https://test.com", usr).Password); 87 | } 88 | } -------------------------------------------------------------------------------- /src/Tests/Tests.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | net8.0 5 | enable 6 | enable 7 | 8 | false 9 | true 10 | https://api.nuget.org/v3/index.json;https://pkg.kzu.app/index.json 11 | $(PackageOutputPath);$(RestoreSources) 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | -------------------------------------------------------------------------------- /src/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/devlooped/CredentialManager/752b34b21d807de3348b16d2be507a5f7e8213b9/src/icon.png --------------------------------------------------------------------------------