Initial implementation of SSO (.com only currently).

This commit is contained in:
Steven Kirk 2017-11-14 15:31:42 +01:00
Родитель cefd4b9167
Коммит ea25f2ac6b
42 изменённых файлов: 671 добавлений и 131 удалений

3
.gitmodules поставляемый
Просмотреть файл

@ -13,3 +13,6 @@
[submodule "script"]
path = script
url = git@github.com:github/VisualStudioBuildScripts
[submodule "submodules/Rothko"]
path = submodules/Rothko
url = https://github.com/editor-tools/Rothko.git

Просмотреть файл

@ -106,6 +106,10 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "GitHub.InlineReviews.UnitTe
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "GitHub.Logging", "src\GitHub.Logging\GitHub.Logging.csproj", "{8D73575A-A89F-47CC-B153-B47DD06837F0}"
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Rothko", "Rothko", "{8B9A34C6-0D62-4D49-9606-1AB838923A74}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Rothko", "submodules\Rothko\src\Rothko.csproj", "{4A84E568-CA86-4510-8CD0-90D3EF9B65F9}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
@ -415,6 +419,16 @@ Global
{8D73575A-A89F-47CC-B153-B47DD06837F0}.Release|Any CPU.Build.0 = Release|Any CPU
{8D73575A-A89F-47CC-B153-B47DD06837F0}.ReleaseWithoutVsix|Any CPU.ActiveCfg = Release|Any CPU
{8D73575A-A89F-47CC-B153-B47DD06837F0}.ReleaseWithoutVsix|Any CPU.Build.0 = Release|Any CPU
{4A84E568-CA86-4510-8CD0-90D3EF9B65F9}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{4A84E568-CA86-4510-8CD0-90D3EF9B65F9}.Debug|Any CPU.Build.0 = Debug|Any CPU
{4A84E568-CA86-4510-8CD0-90D3EF9B65F9}.DebugCodeAnalysis|Any CPU.ActiveCfg = Debug|Any CPU
{4A84E568-CA86-4510-8CD0-90D3EF9B65F9}.DebugCodeAnalysis|Any CPU.Build.0 = Debug|Any CPU
{4A84E568-CA86-4510-8CD0-90D3EF9B65F9}.DebugWithoutVsix|Any CPU.ActiveCfg = Debug|Any CPU
{4A84E568-CA86-4510-8CD0-90D3EF9B65F9}.DebugWithoutVsix|Any CPU.Build.0 = Debug|Any CPU
{4A84E568-CA86-4510-8CD0-90D3EF9B65F9}.Release|Any CPU.ActiveCfg = Release|Any CPU
{4A84E568-CA86-4510-8CD0-90D3EF9B65F9}.Release|Any CPU.Build.0 = Release|Any CPU
{4A84E568-CA86-4510-8CD0-90D3EF9B65F9}.ReleaseWithoutVsix|Any CPU.ActiveCfg = Release|Any CPU
{4A84E568-CA86-4510-8CD0-90D3EF9B65F9}.ReleaseWithoutVsix|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
@ -438,5 +452,7 @@ Global
{7B835A7D-CF94-45E8-B191-96F5A4FE26A8} = {8A7DA2E7-262B-4581-807A-1C45CE79CDFD}
{110B206F-8554-4B51-BF86-94DAA32F5E26} = {8A7DA2E7-262B-4581-807A-1C45CE79CDFD}
{17EB676B-BB91-48B5-AA59-C67695C647C2} = {8A7DA2E7-262B-4581-807A-1C45CE79CDFD}
{8B9A34C6-0D62-4D49-9606-1AB838923A74} = {1E7F7253-A6AF-43C4-A955-37BEDDA01AB8}
{4A84E568-CA86-4510-8CD0-90D3EF9B65F9} = {8B9A34C6-0D62-4D49-9606-1AB838923A74}
EndGlobalSection
EndGlobal

2
script

@ -1 +1 @@
Subproject commit 02618c8047b0dcfd2d83c9a7e5a9c89ce9c97c98
Subproject commit 44d80cd7dca4d1ad08c7b55854b81c5ca7f86582

Просмотреть файл

@ -45,6 +45,10 @@
<assemblyIdentity name="System.Diagnostics.DiagnosticSource" publicKeyToken="cc7b13ffcd2ddd51" culture="neutral" />
<bindingRedirect oldVersion="0.0.0.0-4.0.1.0" newVersion="4.0.1.0" />
</dependentAssembly>
<dependentAssembly>
<assemblyIdentity name="Microsoft.VisualStudio.ComponentModelHost" publicKeyToken="b03f5f7f11d50a3a" culture="neutral" />
<bindingRedirect oldVersion="0.0.0.0-14.0.0.0" newVersion="14.0.0.0" />
</dependentAssembly>
</assemblyBinding>
</runtime>
</configuration>

Просмотреть файл

@ -48,10 +48,6 @@
<Reference Include="Moq">
<HintPath>..\..\packages\Moq.4.2.1312.1319\lib\net40\Moq.dll</HintPath>
</Reference>
<Reference Include="rothko, Version=0.0.2.0, Culture=neutral, PublicKeyToken=9f664c41f503810a, processorArchitecture=MSIL">
<HintPath>..\..\packages\Rothko.0.0.2-ghfvs\lib\net45\rothko.dll</HintPath>
<Private>True</Private>
</Reference>
<Reference Include="System" />
<Reference Include="System.ComponentModel.Composition" />
<Reference Include="System.Data" />
@ -157,6 +153,10 @@
</None>
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\..\submodules\Rothko\src\Rothko.csproj">
<Project>{4a84e568-ca86-4510-8cd0-90d3ef9b65f9}</Project>
<Name>Rothko</Name>
</ProjectReference>
<ProjectReference Include="..\GitHub.Api\GitHub.Api.csproj">
<Project>{b389adaf-62cc-486e-85b4-2d8b078df763}</Project>
<Name>GitHub.Api</Name>

Просмотреть файл

@ -3,7 +3,6 @@
<package id="Microsoft.VisualStudio.ComponentModelHost" version="14.0.25424" targetFramework="net461" />
<package id="Microsoft.VisualStudio.Shell.Immutable.10.0" version="10.0.30319" targetFramework="net461" />
<package id="Moq" version="4.2.1312.1319" targetFramework="net45" userInstalled="true" />
<package id="Rothko" version="0.0.2-ghfvs" targetFramework="net461" />
<package id="Rx-Core" version="2.2.5-custom" targetFramework="net45" userInstalled="true" />
<package id="Rx-Interfaces" version="2.2.5-custom" targetFramework="net45" userInstalled="true" />
<package id="Rx-Linq" version="2.2.5-custom" targetFramework="net45" userInstalled="true" />

Просмотреть файл

@ -3,10 +3,8 @@ using System.Globalization;
using System.Linq;
using System.Net;
using System.Net.NetworkInformation;
using System.Reflection;
using System.Security.Cryptography;
using System.Text;
using GitHub.Primitives;
namespace GitHub.Api
{
@ -33,6 +31,11 @@ namespace GitHub.Api
/// </summary>
public static string ClientSecret { get; private set; }
/// <summary>
/// Gets the scopes required by the application.
/// </summary>
public static string[] Scopes { get; } = new[] { "user", "repo", "gist", "write:public_key" };
/// <summary>
/// Gets a note that will be stored with the OAUTH token.
/// </summary>

Просмотреть файл

@ -64,6 +64,7 @@
<Compile Include="ApiClientConfiguration_User.cs" Condition="$(Buildtype) != 'Internal'" />
<Compile Include="IKeychain.cs" />
<Compile Include="ILoginManager.cs" />
<Compile Include="IOAuthCallbackListener.cs" />
<Compile Include="ITwoFactorChallengeHandler.cs" />
<Compile Include="LoginManager.cs" />
<Compile Include="KeychainCredentialStore.cs" />

Просмотреть файл

@ -1,4 +1,6 @@
using System.Runtime.InteropServices;
using System;
using System.Runtime.InteropServices;
using System.Threading;
using System.Threading.Tasks;
using GitHub.Primitives;
using GitHub.VisualStudio;
@ -25,6 +27,25 @@ namespace GitHub.Api
/// </exception>
Task<User> Login(HostAddress hostAddress, IGitHubClient client, string userName, string password);
/// <summary>
/// Attempts to log into a GitHub server via OAuth in the browser.
/// </summary>
/// <param name="hostAddress">The address of the server.</param>
/// <param name="client">An octokit client configured to access the server.</param>
/// <param name="oauthClient">An octokit OAuth client configured to access the server.</param>
/// <param name="openBrowser">A callback that should open a browser at the requested URL.</param>
/// <param name="cancel">A cancellation token used to cancel the operation.</param>
/// <returns>The logged in user.</returns>
/// <exception cref="AuthorizationException">
/// The login authorization failed.
/// </exception>
Task<User> LoginViaOAuth(
HostAddress hostAddress,
IGitHubClient client,
IOauthClient oauthClient,
Action<Uri> openBrowser,
CancellationToken cancel);
/// <summary>
/// Attempts to log into a GitHub server using existing credentials.
/// </summary>

Просмотреть файл

@ -0,0 +1,20 @@
using System;
using System.Threading;
using System.Threading.Tasks;
namespace GitHub.Api
{
/// <summary>
/// Listens for a callback from the OAuth endpoint signalling successful login.
/// </summary>
public interface IOAuthCallbackListener
{
/// <summary>
/// Listens for a callback with a `state` matching <paramref name="id"/>.
/// </summary>
/// <param name="id">The ID of the operation.</param>
/// <param name="cancel">A cancellation token.</param>
/// <returns>The temporary code included in the callback.</returns>
Task<string> Listen(string id, CancellationToken cancel);
}
}

Просмотреть файл

@ -1,5 +1,6 @@
using System;
using System.Net;
using System.Threading;
using System.Threading.Tasks;
using GitHub.Extensions;
using GitHub.Primitives;
@ -12,13 +13,13 @@ namespace GitHub.Api
/// </summary>
public class LoginManager : ILoginManager
{
readonly string[] scopes = { "user", "repo", "gist", "write:public_key" };
readonly IKeychain keychain;
readonly ITwoFactorChallengeHandler twoFactorChallengeHandler;
readonly string clientId;
readonly string clientSecret;
readonly string authorizationNote;
readonly string fingerprint;
IOAuthCallbackListener oauthListener;
/// <summary>
/// Initializes a new instance of the <see cref="LoginManager"/> class.
@ -32,6 +33,7 @@ namespace GitHub.Api
public LoginManager(
IKeychain keychain,
ITwoFactorChallengeHandler twoFactorChallengeHandler,
IOAuthCallbackListener oauthListener,
string clientId,
string clientSecret,
string authorizationNote = null,
@ -44,6 +46,7 @@ namespace GitHub.Api
this.keychain = keychain;
this.twoFactorChallengeHandler = twoFactorChallengeHandler;
this.oauthListener = oauthListener;
this.clientId = clientId;
this.clientSecret = clientSecret;
this.authorizationNote = authorizationNote;
@ -68,7 +71,7 @@ namespace GitHub.Api
var newAuth = new NewAuthorization
{
Scopes = scopes,
Scopes = ApiClientConfiguration.Scopes,
Note = authorizationNote,
Fingerprint = fingerprint,
};
@ -106,24 +109,33 @@ namespace GitHub.Api
} while (auth == null);
await keychain.Save(userName, auth.Token, hostAddress).ConfigureAwait(false);
return await ReadUserWithRetry(client);
}
var retry = 0;
/// <inheritdoc/>
public async Task<User> LoginViaOAuth(
HostAddress hostAddress,
IGitHubClient client,
IOauthClient oauthClient,
Action<Uri> openBrowser,
CancellationToken cancel)
{
Guard.ArgumentNotNull(hostAddress, nameof(hostAddress));
Guard.ArgumentNotNull(client, nameof(client));
Guard.ArgumentNotNull(oauthClient, nameof(oauthClient));
Guard.ArgumentNotNull(openBrowser, nameof(openBrowser));
while (true)
{
try
{
return await client.User.Current().ConfigureAwait(false);
}
catch (AuthorizationException)
{
if (retry++ == 3) throw;
}
var state = Guid.NewGuid().ToString();
var loginUrl = GetLoginUrl(oauthClient, state);
// It seems that attempting to use a token immediately sometimes fails, retry a few
// times with a delay of of 1s to allow the token to propagate.
await Task.Delay(1000);
}
openBrowser(loginUrl);
var code = await oauthListener.Listen(state, cancel);
var request = new OauthTokenRequest(clientId, clientSecret, code);
var token = await oauthClient.CreateAccessToken(request);
await keychain.Save("[oauth]", token.AccessToken, hostAddress).ConfigureAwait(false);
return await ReadUserWithRetry(client);
}
/// <inheritdoc/>
@ -258,5 +270,40 @@ namespace GitHub.Api
e is ForbiddenException ||
apiException?.StatusCode == (HttpStatusCode)422);
}
async Task<User> ReadUserWithRetry(IGitHubClient client)
{
var retry = 0;
while (true)
{
try
{
return await client.User.Current().ConfigureAwait(false);
}
catch (AuthorizationException)
{
if (retry++ == 3) throw;
}
// It seems that attempting to use a token immediately sometimes fails, retry a few
// times with a delay of of 1s to allow the token to propagate.
await Task.Delay(1000);
}
}
static Uri GetLoginUrl(IOauthClient client, string state)
{
var request = new OauthLoginRequest(ApiClientConfiguration.ClientId);
request.State = state;
foreach (var scope in ApiClientConfiguration.Scopes)
{
request.Scopes.Add(scope);
}
return client.GetGitHubLoginUrl(request);
}
}
}

Просмотреть файл

@ -14,7 +14,6 @@ namespace GitHub.Api
public HostAddress HostAddress { get; }
public UriString OriginalUrl { get; }
readonly IGitHubClient client;
readonly Lazy<IEnterpriseProbeTask> enterpriseProbe;
readonly Lazy<IWikiProbe> wikiProbe;
static readonly SemaphoreSlim sem = new SemaphoreSlim(1);
@ -32,11 +31,13 @@ namespace GitHub.Api
HostAddress = HostAddress.Create(repoUrl);
OriginalUrl = repoUrl;
client = githubClient;
Client = githubClient;
this.enterpriseProbe = enterpriseProbe;
this.wikiProbe = wikiProbe;
}
public IGitHubClient Client { get; }
public async Task<Repository> GetRepository()
{
// fast path to avoid locking when the cache has already been set
@ -51,7 +52,7 @@ namespace GitHub.Api
{
// this doesn't account for auth revoke on the server but its much faster
// than doing the API hit.
var authType = client.Connection.Credentials?.AuthenticationType ?? AuthenticationType.Anonymous;
var authType = Client.Connection.Credentials?.AuthenticationType ?? AuthenticationType.Anonymous;
return authType != AuthenticationType.Anonymous;
}
@ -67,7 +68,7 @@ namespace GitHub.Api
if (ownerLogin != null && repositoryName != null)
{
var repo = await client.Repository.Get(ownerLogin, repositoryName);
var repo = await Client.Repository.Get(ownerLogin, repositoryName);
if (repo != null)
{
hasWiki = await HasWikiInternal(repo);

Просмотреть файл

@ -29,11 +29,6 @@ namespace GitHub.Api
static readonly ILogger log = LogManager.ForContext<ApiClient>();
readonly IObservableGitHubClient gitHubClient;
// There are two sets of authorization scopes, old and new:
// The old scopes must be used by older versions of Enterprise that don't support the new scopes:
readonly string[] oldAuthorizationScopes = { "user", "repo", "gist" };
// These new scopes include write:public_key, which allows us to add public SSH keys to an account:
readonly string[] newAuthorizationScopes = { "user", "repo", "gist", "write:public_key" };
readonly static Lazy<string> lazyNote = new Lazy<string>(() => ProductName + " on " + GetMachineNameSafe());
readonly static Lazy<string> lazyFingerprint = new Lazy<string>(GetFingerprint);
@ -101,42 +96,6 @@ namespace GitHub.Api
return gitHubClient.User.Current();
}
public IObservable<ApplicationAuthorization> GetOrCreateApplicationAuthenticationCode(
Func<TwoFactorAuthorizationException, IObservable<TwoFactorChallengeResult>> twoFactorChallengeHander,
string authenticationCode = null,
bool useOldScopes = false,
bool useFingerPrint = true)
{
var newAuthorization = new NewAuthorization
{
Scopes = useOldScopes
? oldAuthorizationScopes
: newAuthorizationScopes,
Note = lazyNote.Value,
Fingerprint = useFingerPrint ? lazyFingerprint.Value : null
};
Func<TwoFactorAuthorizationException, IObservable<TwoFactorChallengeResult>> dispatchedHandler =
ex => Observable.Start(() => twoFactorChallengeHander(ex), RxApp.MainThreadScheduler).Merge();
var authorizationsClient = gitHubClient.Authorization;
return string.IsNullOrEmpty(authenticationCode)
? authorizationsClient.CreateAndDeleteExistingApplicationAuthorization(
ClientId,
ClientSecret,
newAuthorization,
dispatchedHandler,
true)
: authorizationsClient.CreateAndDeleteExistingApplicationAuthorization(
ClientId,
ClientSecret,
newAuthorization,
dispatchedHandler,
authenticationCode,
true);
}
public IObservable<Organization> GetOrganizations()
{
// Organization.GetAllForCurrent doesn't return all of the information we need (we

Просмотреть файл

@ -83,10 +83,6 @@
<HintPath>..\..\packages\Serilog.2.5.0\lib\net46\Serilog.dll</HintPath>
<Private>True</Private>
</Reference>
<Reference Include="rothko, Version=0.0.2.0, Culture=neutral, PublicKeyToken=9f664c41f503810a, processorArchitecture=MSIL">
<HintPath>..\..\packages\Rothko.0.0.2-ghfvs\lib\net45\rothko.dll</HintPath>
<Private>True</Private>
</Reference>
<Reference Include="SQLitePCL.raw, Version=0.7.3.0, Culture=neutral, PublicKeyToken=d89a3d1cc066b805, processorArchitecture=MSIL">
<HintPath>..\..\packages\SQLitePCL.raw_basic.0.7.3.0-vs2012\lib\net45\SQLitePCL.raw.dll</HintPath>
<Private>True</Private>
@ -120,6 +116,7 @@
<HintPath>..\..\packages\Rx-XAML.2.2.5-custom\lib\net45\System.Reactive.Windows.Threading.dll</HintPath>
<Private>True</Private>
</Reference>
<Reference Include="System.Web" />
<Reference Include="System.Xaml" />
<Reference Include="System.Xml.Linq" />
<Reference Include="System.Data.DataSetExtensions" />
@ -134,6 +131,7 @@
<Compile Include="Models\PullRequestReviewCommentModel.cs" />
<Compile Include="Models\PullRequestDetailArgument.cs" />
<Compile Include="Services\GlobalConnection.cs" />
<Compile Include="Services\OAuthCallbackListener.cs" />
<Compile Include="ViewModels\ViewModelBase.cs" />
<Compile Include="Caches\CacheIndex.cs" />
<Compile Include="Caches\CacheItem.cs" />
@ -261,6 +259,10 @@
<Project>{1ce2d235-8072-4649-ba5a-cfb1af8776e0}</Project>
<Name>ReactiveUI_Net45</Name>
</ProjectReference>
<ProjectReference Include="..\..\submodules\Rothko\src\Rothko.csproj">
<Project>{4a84e568-ca86-4510-8cd0-90d3ef9b65f9}</Project>
<Name>Rothko</Name>
</ProjectReference>
<ProjectReference Include="..\..\submodules\splat\Splat\Splat-Net45.csproj">
<Project>{252ce1c2-027a-4445-a3c2-e4d6c80a935a}</Project>
<Name>Splat-Net45</Name>

Просмотреть файл

@ -0,0 +1,154 @@
using System;
using System.Threading.Tasks;
namespace GitHub.App.Models
{
public interface IHttpListenerContext
{
//
// Summary:
// Gets the System.Net.HttpListenerRequest that represents a client's request for
// a resource.
//
// Returns:
// An System.Net.HttpListenerRequest object that represents the client request.
public HttpListenerRequest Request { get; }
//
// Summary:
// Gets the System.Net.HttpListenerResponse object that will be sent to the client
// in response to the client's request.
//
// Returns:
// An System.Net.HttpListenerResponse object used to send a response back to the
// client.
public HttpListenerResponse Response { get; }
//
// Summary:
// Gets an object used to obtain identity, authentication information, and security
// roles for the client whose request is represented by this System.Net.HttpListenerContext
// object.
//
// Returns:
// An System.Security.Principal.IPrincipal object that describes the client, or
// null if the System.Net.HttpListener that supplied this System.Net.HttpListenerContext
// does not require authentication.
public IPrincipal User { get; }
//
// Summary:
// Accept a WebSocket connection as an asynchronous operation.
//
// Parameters:
// subProtocol:
// The supported WebSocket sub-protocol.
//
// Returns:
// Returns System.Threading.Tasks.Task`1.The task object representing the asynchronous
// operation. The System.Threading.Tasks.Task`1.Result property on the task object
// returns an System.Net.WebSockets.HttpListenerWebSocketContext object.
//
// Exceptions:
// T:System.ArgumentException:
// subProtocol is an empty string-or- subProtocol contains illegal characters.
//
// T:System.Net.WebSockets.WebSocketException:
// An error occurred when sending the response to complete the WebSocket handshake.
public Task<HttpListenerWebSocketContext> AcceptWebSocketAsync(string subProtocol);
//
// Summary:
// Accept a WebSocket connection specifying the supported WebSocket sub-protocol
// and WebSocket keep-alive interval as an asynchronous operation.
//
// Parameters:
// subProtocol:
// The supported WebSocket sub-protocol.
//
// keepAliveInterval:
// The WebSocket protocol keep-alive interval in milliseconds.
//
// Returns:
// Returns System.Threading.Tasks.Task`1.The task object representing the asynchronous
// operation. The System.Threading.Tasks.Task`1.Result property on the task object
// returns an System.Net.WebSockets.HttpListenerWebSocketContext object.
//
// Exceptions:
// T:System.ArgumentException:
// subProtocol is an empty string-or- subProtocol contains illegal characters.
//
// T:System.ArgumentOutOfRangeException:
// keepAliveInterval is too small.
//
// T:System.Net.WebSockets.WebSocketException:
// An error occurred when sending the response to complete the WebSocket handshake.
public Task<HttpListenerWebSocketContext> AcceptWebSocketAsync(string subProtocol, TimeSpan keepAliveInterval);
//
// Summary:
// Accept a WebSocket connection specifying the supported WebSocket sub-protocol,
// receive buffer size, and WebSocket keep-alive interval as an asynchronous operation.
//
// Parameters:
// subProtocol:
// The supported WebSocket sub-protocol.
//
// receiveBufferSize:
// The receive buffer size in bytes.
//
// keepAliveInterval:
// The WebSocket protocol keep-alive interval in milliseconds.
//
// Returns:
// Returns System.Threading.Tasks.Task`1.The task object representing the asynchronous
// operation. The System.Threading.Tasks.Task`1.Result property on the task object
// returns an System.Net.WebSockets.HttpListenerWebSocketContext object.
//
// Exceptions:
// T:System.ArgumentException:
// subProtocol is an empty string-or- subProtocol contains illegal characters.
//
// T:System.ArgumentOutOfRangeException:
// keepAliveInterval is too small.-or- receiveBufferSize is less than 16 bytes-or-
// receiveBufferSize is greater than 64K bytes.
//
// T:System.Net.WebSockets.WebSocketException:
// An error occurred when sending the response to complete the WebSocket handshake.
public Task<HttpListenerWebSocketContext> AcceptWebSocketAsync(string subProtocol, int receiveBufferSize, TimeSpan keepAliveInterval);
//
// Summary:
// Accept a WebSocket connection specifying the supported WebSocket sub-protocol,
// receive buffer size, WebSocket keep-alive interval, and the internal buffer as
// an asynchronous operation.
//
// Parameters:
// subProtocol:
// The supported WebSocket sub-protocol.
//
// receiveBufferSize:
// The receive buffer size in bytes.
//
// keepAliveInterval:
// The WebSocket protocol keep-alive interval in milliseconds.
//
// internalBuffer:
// An internal buffer to use for this operation.
//
// Returns:
// Returns System.Threading.Tasks.Task`1.The task object representing the asynchronous
// operation. The System.Threading.Tasks.Task`1.Result property on the task object
// returns an System.Net.WebSockets.HttpListenerWebSocketContext object.
//
// Exceptions:
// T:System.ArgumentException:
// subProtocol is an empty string-or- subProtocol contains illegal characters.
//
// T:System.ArgumentOutOfRangeException:
// keepAliveInterval is too small.-or- receiveBufferSize is less than 16 bytes-or-
// receiveBufferSize is greater than 64K bytes.
//
// T:System.Net.WebSockets.WebSocketException:
// An error occurred when sending the response to complete the WebSocket handshake.
[EditorBrowsable(EditorBrowsableState.Never)]
public Task<HttpListenerWebSocketContext> AcceptWebSocketAsync(string subProtocol, int receiveBufferSize, TimeSpan keepAliveInterval, ArraySegment<byte> internalBuffer);
}
}

Просмотреть файл

@ -0,0 +1,75 @@
using System.ComponentModel.Composition;
using System.Net;
using System.Threading;
using System.Threading.Tasks;
using System.Web;
using GitHub.Api;
using GitHub.Extensions;
using Rothko;
using static System.FormattableString;
namespace GitHub.Services
{
/// <summary>
/// Listens for a callback from the OAuth endpoint on "http://localhost:42549".
/// </summary>
/// <remarks>
/// The GitHub for Visual Studio OAUTH application on GitHub is configured to call back to
/// http://localhost:42549 on successful login. This class listens on that port and returns
/// the temporary code received from the callback to the caller.
///
/// Note that as implemented this class can only listen for one OAUTH callback at a time. If
/// <see cref="Listen"/> is called when already listening for a callback, the original listen
/// operation will throw an <see cref="OperationCanceledException"/>.
/// </remarks>
[Export(typeof(IOAuthCallbackListener))]
[PartCreationPolicy(CreationPolicy.Shared)]
public class OAuthCallbackListener : IOAuthCallbackListener
{
const int CallbackPort = 42549;
readonly static string CallbackUrl = Invariant($"http://localhost:{CallbackPort}/");
readonly IHttpListener httpListener;
[ImportingConstructor]
public OAuthCallbackListener(IHttpListener httpListener)
{
Guard.ArgumentNotNull(httpListener, nameof(httpListener));
this.httpListener = httpListener;
httpListener.Prefixes.Add(CallbackUrl);
}
public async Task<string> Listen(string id, CancellationToken cancel)
{
var internalCancel = new CancellationTokenSource();
var combinedCancel = CancellationTokenSource.CreateLinkedTokenSource(cancel, internalCancel.Token);
if (httpListener.IsListening) httpListener.Stop();
httpListener.Start();
try
{
var complete = new TaskCompletionSource<HttpListenerContext>();
using (cancel.Register(httpListener.Stop))
{
var context = await httpListener.GetContextAsync();
var foo = context.Request;
var queryParts = HttpUtility.ParseQueryString(context.Request.Url.Query);
if (queryParts["state"] == id)
{
context.Response.Close();
return queryParts["code"];
}
}
throw new WebException("The login returned an unexpected response.");
}
finally
{
httpListener.Stop();
}
}
}
}

Просмотреть файл

@ -38,6 +38,7 @@ namespace GitHub.ViewModels
AuthenticationResults = Observable.Merge(
loginToGitHubViewModel.Login,
loginToGitHubViewModel.LoginViaOAuth,
EnterpriseLogin.Login);
}

Просмотреть файл

@ -3,6 +3,7 @@ using System.Diagnostics.CodeAnalysis;
using System.Reactive;
using System.Reactive.Linq;
using System.Reactive.Threading.Tasks;
using System.Threading;
using System.Threading.Tasks;
using GitHub.App;
using GitHub.Authentication;
@ -23,7 +24,9 @@ namespace GitHub.ViewModels
{
static readonly ILogger log = LogManager.ForContext<LoginTabViewModel>();
protected LoginTabViewModel(IConnectionManager connectionManager, IVisualStudioBrowser browser)
protected LoginTabViewModel(
IConnectionManager connectionManager,
IVisualStudioBrowser browser)
{
Guard.ArgumentNotNull(connectionManager, nameof(connectionManager));
Guard.ArgumentNotNull(browser, nameof(browser));
@ -59,6 +62,10 @@ namespace GitHub.ViewModels
}
});
LoginViaOAuth = ReactiveCommand.CreateAsyncTask(
this.WhenAnyValue(x => x.IsLoggingIn, x => !x),
LogInViaOAuth);
isLoggingIn = Login.IsExecuting.ToProperty(this, x => x.IsLoggingIn);
Reset = ReactiveCommand.CreateAsyncTask(_ => Clear());
@ -80,6 +87,7 @@ namespace GitHub.ViewModels
public IReactiveCommand<Unit> SignUp { get; }
public IReactiveCommand<AuthenticationResult> Login { get; }
public IReactiveCommand<AuthenticationResult> LoginViaOAuth { get; }
public IReactiveCommand<Unit> Reset { get; }
public IRecoveryCommand NavigateForgotPassword { get; }
@ -125,6 +133,12 @@ namespace GitHub.ViewModels
get { return canLogin.Value; }
}
protected ObservableAsPropertyHelper<bool> canSsoLogin;
public bool CanSsoLogin
{
get { return canSsoLogin.Value; }
}
UserError error;
public UserError Error
{
@ -133,6 +147,7 @@ namespace GitHub.ViewModels
}
protected abstract IObservable<AuthenticationResult> LogIn(object args);
protected abstract Task<AuthenticationResult> LogInViaOAuth(object args);
protected IObservable<AuthenticationResult> LogInToHost(HostAddress hostAddress)
{
@ -183,6 +198,18 @@ namespace GitHub.ViewModels
});
}
protected async Task LoginToHostViaOAuth(HostAddress address)
{
try
{
await ConnectionManager.LogInViaOAuth(address, CancellationToken.None);
}
catch (Exception e)
{
Error = new UserError(e.Message);
}
}
async Task Clear()
{
UsernameOrEmail = null;

Просмотреть файл

@ -19,7 +19,9 @@ namespace GitHub.ViewModels
public class LoginToGitHubForEnterpriseViewModel : LoginTabViewModel, ILoginToGitHubForEnterpriseViewModel
{
[ImportingConstructor]
public LoginToGitHubForEnterpriseViewModel(IConnectionManager connectionManager, IVisualStudioBrowser browser)
public LoginToGitHubForEnterpriseViewModel(
IConnectionManager connectionManager,
IVisualStudioBrowser browser)
: base(connectionManager, browser)
{
Guard.ArgumentNotNull(connectionManager, nameof(connectionManager));
@ -37,6 +39,10 @@ namespace GitHub.ViewModels
(x, y, z) => x.Value && y.Value && z.Value)
.ToProperty(this, x => x.CanLogin);
canSsoLogin = this.WhenAnyValue(
x => x.EnterpriseUrlValidator.ValidationResult.IsValid)
.ToProperty(this, x => x.CanLogin);
NavigateLearnMore = ReactiveCommand.CreateAsyncObservable(_ =>
{
browser.OpenUrl(GitHubUrls.LearnMore);
@ -50,6 +56,11 @@ namespace GitHub.ViewModels
return LogInToHost(HostAddress.Create(EnterpriseUrl));
}
protected override Task<AuthenticationResult> LogInViaOAuth(object args)
{
throw new NotImplementedException();
}
string enterpriseUrl;
public string EnterpriseUrl
{

Просмотреть файл

@ -2,6 +2,7 @@ using System;
using System.ComponentModel.Composition;
using System.Reactive;
using System.Reactive.Linq;
using System.Threading.Tasks;
using GitHub.Authentication;
using GitHub.Info;
using GitHub.Primitives;
@ -15,7 +16,9 @@ namespace GitHub.ViewModels
public class LoginToGitHubViewModel : LoginTabViewModel, ILoginToGitHubViewModel
{
[ImportingConstructor]
public LoginToGitHubViewModel(IConnectionManager connectionManager, IVisualStudioBrowser browser)
public LoginToGitHubViewModel(
IConnectionManager connectionManager,
IVisualStudioBrowser browser)
: base(connectionManager, browser)
{
BaseUri = HostAddress.GitHubDotComHostAddress.WebUri;
@ -36,5 +39,11 @@ namespace GitHub.ViewModels
{
return LogInToHost(HostAddress.GitHubDotComHostAddress);
}
protected override async Task<AuthenticationResult> LogInViaOAuth(object args)
{
await LoginToHostViaOAuth(HostAddress.GitHubDotComHostAddress);
return AuthenticationResult.Success;
}
}
}

Просмотреть файл

@ -27,11 +27,6 @@ namespace GitHub.Api
/// </summary>
/// <returns></returns>
IObservable<Repository> GetRepositoriesForOrganization(string organization);
IObservable<ApplicationAuthorization> GetOrCreateApplicationAuthenticationCode(
Func<TwoFactorAuthorizationException, IObservable<TwoFactorChallengeResult>> twoFactorChallengeHander,
string authenticationCode = null,
bool useOldScopes = false,
bool useFingerprint = true);
IObservable<string> GetGitIgnoreTemplates();
IObservable<LicenseMetadata> GetLicenses();

Просмотреть файл

@ -35,6 +35,11 @@ namespace GitHub.ViewModels
/// </summary>
IReactiveCommand<AuthenticationResult> Login { get; }
/// <summary>
/// Gets a command which, when invoked, performs an OAuth login.
/// </summary>
IReactiveCommand<AuthenticationResult> LoginViaOAuth { get; }
/// <summary>
/// Gets a command which, when invoked, direct the user to a
/// GitHub.com sign up flow

Просмотреть файл

@ -6,6 +6,7 @@ namespace GitHub.Api
{
public interface ISimpleApiClient
{
IGitHubClient Client { get; }
HostAddress HostAddress { get; }
UriString OriginalUrl { get; }
Task<Repository> GetRepository();

Просмотреть файл

@ -114,10 +114,6 @@
</Reference>
<Reference Include="PresentationCore" />
<Reference Include="PresentationFramework" />
<Reference Include="rothko, Version=0.0.2.0, Culture=neutral, PublicKeyToken=9f664c41f503810a, processorArchitecture=MSIL">
<HintPath>..\..\packages\Rothko.0.0.2-ghfvs\lib\net45\rothko.dll</HintPath>
<Private>True</Private>
</Reference>
<Reference Include="Serilog, Version=2.0.0.0, Culture=neutral, PublicKeyToken=24c2f752a8e58a10, processorArchitecture=MSIL">
<HintPath>..\..\packages\Serilog.2.5.0\lib\net46\Serilog.dll</HintPath>
<Private>True</Private>
@ -149,6 +145,7 @@
<Compile Include="Models\IPullRequestReviewCommentModel.cs" />
<Compile Include="Services\IGlobalConnection.cs" />
<Compile Include="Services\ILocalRepositories.cs" />
<Compile Include="Services\IVisualStudioBrowser.cs" />
<Compile Include="Settings\PkgCmdID.cs" />
<Compile Include="ViewModels\IHasErrorState.cs" />
<Compile Include="ViewModels\IHasLoading.cs" />
@ -249,7 +246,6 @@
<Compile Include="Primitives\UriString.cs" />
<Compile Include="Services\EnterpriseProbeTask.cs" />
<Compile Include="Services\ExportFactoryProvider.cs" />
<Compile Include="Services\IVisualStudioBrowser.cs" />
<Compile Include="Services\IEnterpriseProbeTask.cs" />
<Compile Include="Services\IGitHubServiceProvider.cs" />
<Compile Include="Services\IWikiProbe.cs" />
@ -264,6 +260,10 @@
<Project>{08dd4305-7787-4823-a53f-4d0f725a07f3}</Project>
<Name>Octokit</Name>
</ProjectReference>
<ProjectReference Include="..\..\submodules\Rothko\src\Rothko.csproj">
<Project>{4a84e568-ca86-4510-8cd0-90d3ef9b65f9}</Project>
<Name>Rothko</Name>
</ProjectReference>
<ProjectReference Include="..\GitHub.Extensions\GitHub.Extensions.csproj">
<Project>{6afe2e2d-6db0-4430-a2ea-f5f5388d2f78}</Project>
<Name>GitHub.Extensions</Name>

Просмотреть файл

@ -1,4 +1,5 @@
using System;
using System.Threading;
using System.Threading.Tasks;
using GitHub.Extensions;
using GitHub.Models;
@ -55,6 +56,21 @@ namespace GitHub.Services
/// </exception>
Task<IConnection> LogIn(HostAddress address, string username, string password);
/// <summary>
/// Attempts to log into a GitHub server via OAuth in the browser.
/// </summary>
/// <param name="address">The instance address.</param>
/// <param name="cancel">A cancellation token used to cancel the operation.</param>
/// <returns>
/// A connection if the login succeded. If the login fails, throws an exception. An
/// exception is also thrown if an existing connection with the same host address already
/// exists.
/// </returns>
/// <exception cref="InvalidOperationException">
/// A connection to the host already exists.
/// </exception>
Task<IConnection> LogInViaOAuth(HostAddress address, CancellationToken cancel);
/// <summary>
/// Logs out of a GitHub instance.
/// </summary>

Просмотреть файл

@ -15,7 +15,6 @@
<package id="Microsoft.VisualStudio.TextManager.Interop" version="7.10.6070" targetFramework="net461" />
<package id="Microsoft.VisualStudio.TextManager.Interop.8.0" version="8.0.50727" targetFramework="net461" />
<package id="Microsoft.VisualStudio.Threading" version="14.1.131" targetFramework="net461" />
<package id="Rothko" version="0.0.2-ghfvs" targetFramework="net461" />
<package id="Serilog" version="2.5.0" targetFramework="net461" />
<package id="SerilogAnalyzer" version="0.12.0.0" targetFramework="net461" />
<package id="SimpleJson" version="0.38.0" targetFramework="net461" />

Просмотреть файл

@ -359,10 +359,6 @@
</Reference>
<Reference Include="PresentationCore" />
<Reference Include="PresentationFramework" />
<Reference Include="rothko, Version=0.0.2.0, Culture=neutral, PublicKeyToken=9f664c41f503810a, processorArchitecture=MSIL">
<HintPath>..\..\packages\Rothko.0.0.2-ghfvs\lib\net45\rothko.dll</HintPath>
<Private>True</Private>
</Reference>
<Reference Include="Serilog, Version=2.0.0.0, Culture=neutral, PublicKeyToken=24c2f752a8e58a10, processorArchitecture=MSIL">
<HintPath>..\..\packages\Serilog.2.5.0\lib\net46\Serilog.dll</HintPath>
<Private>True</Private>

Просмотреть файл

@ -31,7 +31,6 @@
<package id="Microsoft.VisualStudio.Utilities" version="14.3.25407" targetFramework="net452" />
<package id="Microsoft.VisualStudio.Validation" version="14.1.111" targetFramework="net452" />
<package id="Microsoft.VSSDK.BuildTools" version="14.3.25407" targetFramework="net452" developmentDependency="true" />
<package id="Rothko" version="0.0.2-ghfvs" targetFramework="net461" />
<package id="Rx-Core" version="2.2.5-custom" targetFramework="net461" />
<package id="Rx-Interfaces" version="2.2.5-custom" targetFramework="net461" />
<package id="Rx-Linq" version="2.2.5-custom" targetFramework="net461" />

Просмотреть файл

@ -26,6 +26,10 @@
<assemblyIdentity name="Microsoft.VisualStudio.CoreUtility" publicKeyToken="b03f5f7f11d50a3a" culture="neutral" />
<bindingRedirect oldVersion="0.0.0.0-15.0.0.0" newVersion="15.0.0.0" />
</dependentAssembly>
<dependentAssembly>
<assemblyIdentity name="Microsoft.VisualStudio.ComponentModelHost" publicKeyToken="b03f5f7f11d50a3a" culture="neutral" />
<bindingRedirect oldVersion="0.0.0.0-14.0.0.0" newVersion="14.0.0.0" />
</dependentAssembly>
</assemblyBinding>
</runtime>
<startup><supportedRuntime version="v4.0" sku=".NETFramework,Version=v4.6.1" /></startup></configuration>

Просмотреть файл

@ -237,10 +237,6 @@
<HintPath>..\..\packages\Newtonsoft.Json.6.0.8\lib\net45\Newtonsoft.Json.dll</HintPath>
<Private>True</Private>
</Reference>
<Reference Include="rothko, Version=0.0.2.0, Culture=neutral, PublicKeyToken=9f664c41f503810a, processorArchitecture=MSIL">
<HintPath>..\..\packages\Rothko.0.0.2-ghfvs\lib\net45\rothko.dll</HintPath>
<Private>True</Private>
</Reference>
<Reference Include="Serilog, Version=2.0.0.0, Culture=neutral, PublicKeyToken=24c2f752a8e58a10, processorArchitecture=MSIL">
<HintPath>..\..\packages\Serilog.2.5.0\lib\net46\Serilog.dll</HintPath>
<Private>True</Private>
@ -599,6 +595,10 @@
<IncludeOutputGroupsInVSIX>BuiltProjectOutputGroup;GetCopyToOutputDirectoryItems;DebugSymbolsProjectOutputGroup;</IncludeOutputGroupsInVSIX>
<IncludeOutputGroupsInVSIXLocalOnly>DebugSymbolsProjectOutputGroup;</IncludeOutputGroupsInVSIXLocalOnly>
</ProjectReference>
<ProjectReference Include="..\..\submodules\Rothko\src\Rothko.csproj">
<Project>{4a84e568-ca86-4510-8cd0-90d3ef9b65f9}</Project>
<Name>Rothko</Name>
</ProjectReference>
<ProjectReference Include="..\..\submodules\splat\Splat\Splat-Net45.csproj">
<Project>{252ce1c2-027a-4445-a3c2-e4d6c80a935a}</Project>
<Name>Splat-Net45</Name>

Просмотреть файл

@ -214,10 +214,12 @@ namespace GitHub.VisualStudio
var serviceProvider = await GetServiceAsync(typeof(IGitHubServiceProvider)) as IGitHubServiceProvider;
var keychain = serviceProvider.GetService<IKeychain>();
var twoFaHandler = serviceProvider.GetService<ITwoFactorChallengeHandler>();
var oauthListener = serviceProvider.GetService<IOAuthCallbackListener>();
return new LoginManager(
keychain,
twoFaHandler,
oauthListener,
ApiClientConfiguration.ClientId,
ApiClientConfiguration.ClientSecret,
ApiClientConfiguration.AuthorizationNote,

Просмотреть файл

@ -12,6 +12,7 @@ using GitHub.Primitives;
using GitHub.Services;
using GitHubClient = Octokit.GitHubClient;
using IGitHubClient = Octokit.IGitHubClient;
using OauthClient = Octokit.OauthClient;
using User = Octokit.User;
namespace GitHub.VisualStudio
@ -29,6 +30,7 @@ namespace GitHub.VisualStudio
readonly TaskCompletionSource<object> loaded;
readonly Lazy<ObservableCollectionEx<IConnection>> connections;
readonly IUsageTracker usageTracker;
readonly IVisualStudioBrowser browser;
[ImportingConstructor]
public ConnectionManager(
@ -36,13 +38,15 @@ namespace GitHub.VisualStudio
IConnectionCache cache,
IKeychain keychain,
ILoginManager loginManager,
IUsageTracker usageTracker)
IUsageTracker usageTracker,
IVisualStudioBrowser browser)
{
this.program = program;
this.cache = cache;
this.keychain = keychain;
this.loginManager = loginManager;
this.usageTracker = usageTracker;
this.browser = browser;
loaded = new TaskCompletionSource<object>();
connections = new Lazy<ObservableCollectionEx<IConnection>>(
this.CreateConnections,
@ -84,6 +88,27 @@ namespace GitHub.VisualStudio
return connection;
}
/// <inheritdoc/>
public async Task<IConnection> LogInViaOAuth(HostAddress address, CancellationToken cancel)
{
var conns = await GetLoadedConnectionsInternal();
if (conns.Any(x => x.HostAddress == address))
{
throw new InvalidOperationException($"A connection to {address} already exists.");
}
var client = CreateClient(address);
var oauthClient = new OauthClient(client.Connection);
var user = await loginManager.LoginViaOAuth(address, client, oauthClient, OpenBrowser, cancel);
var connection = new Connection(address, user.Login, user, null);
conns.Add(connection);
await SaveConnections();
await usageTracker.IncrementLoginCount();
return connection;
}
/// <inheritdoc/>
public async Task LogOut(HostAddress address)
{
@ -159,5 +184,7 @@ namespace GitHub.VisualStudio
var details = conns.Select(x => new ConnectionDetails(x.HostAddress, x.Username));
await cache.Save(details);
}
void OpenBrowser(Uri uri) => browser.OpenUrl(uri);
}
}

Просмотреть файл

@ -1,5 +1,6 @@
using System;
using System.ComponentModel.Composition;
using System.Threading;
using System.Threading.Tasks;
using GitHub.Api;
using GitHub.Exports;
@ -27,6 +28,16 @@ namespace GitHub.Services
return inner.Login(hostAddress, client, userName, password);
}
public Task<User> LoginViaOAuth(
HostAddress hostAddress,
IGitHubClient client,
IOauthClient oauthClient,
Action<Uri> openBrowser,
CancellationToken cancel)
{
return inner.LoginViaOAuth(hostAddress, client, oauthClient, openBrowser, cancel);
}
public Task<User> LoginFromCache(HostAddress hostAddress, IGitHubClient client)
{
return inner.LoginFromCache(hostAddress, client);

Просмотреть файл

@ -110,7 +110,9 @@
<uirx:ValidationMessage
x:Name="dotComPasswordValidationMessage"
ValidatesControl="{Binding ElementName=dotComPassword}" />
<ui:GitHubActionLink x:Name="dotComSsaLogInButton">Login with your browser</ui:GitHubActionLink>
</StackPanel>
</StackPanel>
</DockPanel>
@ -157,6 +159,8 @@
<uirx:UserErrorMessages x:Name="enterpriseErrorMessage" Margin="0,10">
</uirx:UserErrorMessages>
<ui:GitHubActionLink x:Name="enterpriseSsaLogInButton">Login with your browser</ui:GitHubActionLink>
</StackPanel>
</DockPanel>
</TabItem>

Просмотреть файл

@ -51,6 +51,7 @@ namespace GitHub.VisualStudio.UI.Views.Controls
d(this.OneWayBind(ViewModel, vm => vm.GitHubLogin.PasswordValidator, v => v.dotComPasswordValidationMessage.ReactiveValidator));
d(this.OneWayBind(ViewModel, vm => vm.GitHubLogin.Login, v => v.dotComLogInButton.Command));
d(this.OneWayBind(ViewModel, vm => vm.GitHubLogin.LoginViaOAuth, v => v.dotComSsaLogInButton.Command));
d(this.OneWayBind(ViewModel, vm => vm.GitHubLogin.IsLoggingIn, v => v.dotComLogInButton.ShowSpinner));
d(this.OneWayBind(ViewModel, vm => vm.GitHubLogin.NavigatePricing, v => v.pricingLink.Command));
d(this.OneWayBind(ViewModel, vm => vm.GitHubLogin.Error, v => v.dotComErrorMessage.UserError));
@ -70,6 +71,7 @@ namespace GitHub.VisualStudio.UI.Views.Controls
d(this.OneWayBind(ViewModel, vm => vm.EnterpriseLogin.EnterpriseUrlValidator, v => v.enterpriseUrlValidationMessage.ReactiveValidator));
d(this.OneWayBind(ViewModel, vm => vm.EnterpriseLogin.Login, v => v.enterpriseLogInButton.Command));
d(this.OneWayBind(ViewModel, vm => vm.EnterpriseLogin.LoginViaOAuth, v => v.enterpriseSsaLogInButton.Command));
d(this.OneWayBind(ViewModel, vm => vm.EnterpriseLogin.IsLoggingIn, v => v.enterpriseLogInButton.ShowSpinner));
d(this.OneWayBind(ViewModel, vm => vm.EnterpriseLogin.NavigateLearnMore, v => v.learnMoreLink.Command));
d(this.OneWayBind(ViewModel, vm => vm.EnterpriseLogin.Error, v => v.enterpriseErrorMessage.UserError));

Просмотреть файл

@ -38,6 +38,10 @@
<assemblyIdentity name="System.Diagnostics.DiagnosticSource" publicKeyToken="cc7b13ffcd2ddd51" culture="neutral" />
<bindingRedirect oldVersion="0.0.0.0-4.0.1.0" newVersion="4.0.1.0" />
</dependentAssembly>
<dependentAssembly>
<assemblyIdentity name="Microsoft.VisualStudio.ComponentModelHost" publicKeyToken="b03f5f7f11d50a3a" culture="neutral" />
<bindingRedirect oldVersion="0.0.0.0-14.0.0.0" newVersion="14.0.0.0" />
</dependentAssembly>
</assemblyBinding>
</runtime>
</configuration>

Просмотреть файл

@ -35,7 +35,6 @@
<package id="Microsoft.VSSDK.BuildTools" version="15.0.26201" targetFramework="net461" developmentDependency="true" />
<package id="Microsoft.VSSDK.Vsixsigntool" version="14.1.24720" targetFramework="net45" />
<package id="Newtonsoft.Json" version="6.0.8" targetFramework="net45" />
<package id="Rothko" version="0.0.2-ghfvs" targetFramework="net461" />
<package id="Rx-Core" version="2.2.5-custom" targetFramework="net45" />
<package id="Rx-Interfaces" version="2.2.5-custom" targetFramework="net45" />
<package id="Rx-Linq" version="2.2.5-custom" targetFramework="net45" />

1
submodules/Rothko Submodule

@ -0,0 +1 @@
Subproject commit d7c647a8099d8c97adb592a2f6a57d9159b1e57b

Просмотреть файл

@ -23,8 +23,9 @@ public class LoginManagerTests
var keychain = Substitute.For<IKeychain>();
var tfa = Substitute.For<ITwoFactorChallengeHandler>();
var oauthListener = Substitute.For<IOAuthCallbackListener>();
var target = new LoginManager(keychain, tfa, "id", "secret");
var target = new LoginManager(keychain, tfa, oauthListener, "id", "secret");
await target.Login(host, client, "foo", "bar");
await keychain.Received().Save("foo", "123abc", host);
@ -41,8 +42,9 @@ public class LoginManagerTests
var keychain = Substitute.For<IKeychain>();
var tfa = Substitute.For<ITwoFactorChallengeHandler>();
var oauthListener = Substitute.For<IOAuthCallbackListener>();
var target = new LoginManager(keychain, tfa, "id", "secret");
var target = new LoginManager(keychain, tfa, oauthListener, "id", "secret");
var result = await target.Login(host, client, "foo", "bar");
Assert.Same(user, result);
@ -64,8 +66,9 @@ public class LoginManagerTests
var keychain = Substitute.For<IKeychain>();
var tfa = Substitute.For<ITwoFactorChallengeHandler>();
var oauthListener = Substitute.For<IOAuthCallbackListener>();
var target = new LoginManager(keychain, tfa, "id", "secret");
var target = new LoginManager(keychain, tfa, oauthListener, "id", "secret");
var result = await target.Login(host, client, "foo", "bar");
await client.Authorization.Received(2).GetOrCreateApplicationAuthentication("id", "secret", Arg.Any<NewAuthorization>());
@ -86,9 +89,10 @@ public class LoginManagerTests
var keychain = Substitute.For<IKeychain>();
var tfa = Substitute.For<ITwoFactorChallengeHandler>();
var oauthListener = Substitute.For<IOAuthCallbackListener>();
tfa.HandleTwoFactorException(exception).Returns(new TwoFactorChallengeResult("123456"));
var target = new LoginManager(keychain, tfa, "id", "secret");
var target = new LoginManager(keychain, tfa, oauthListener, "id", "secret");
await target.Login(host, client, "foo", "bar");
await client.Authorization.Received().GetOrCreateApplicationAuthentication(
@ -113,11 +117,12 @@ public class LoginManagerTests
var keychain = Substitute.For<IKeychain>();
var tfa = Substitute.For<ITwoFactorChallengeHandler>();
var oauthListener = Substitute.For<IOAuthCallbackListener>();
tfa.HandleTwoFactorException(exception).Returns(
new TwoFactorChallengeResult("111111"),
new TwoFactorChallengeResult("123456"));
var target = new LoginManager(keychain, tfa, "id", "secret");
var target = new LoginManager(keychain, tfa, oauthListener, "id", "secret");
await target.Login(host, client, "foo", "bar");
await client.Authorization.Received(1).GetOrCreateApplicationAuthentication(
@ -148,19 +153,20 @@ public class LoginManagerTests
var keychain = Substitute.For<IKeychain>();
var tfa = Substitute.For<ITwoFactorChallengeHandler>();
var oauthListener = Substitute.For<IOAuthCallbackListener>();
tfa.HandleTwoFactorException(twoFaException).Returns(
new TwoFactorChallengeResult("111111"),
new TwoFactorChallengeResult("123456"));
var target = new LoginManager(keychain, tfa, "id", "secret");
Assert.ThrowsAsync<LoginAttemptsExceededException>(async () => await target.Login(host, client, "foo", "bar"));
var target = new LoginManager(keychain, tfa, oauthListener, "id", "secret");
await Assert.ThrowsAsync<LoginAttemptsExceededException>(async () => await target.Login(host, client, "foo", "bar"));
await client.Authorization.Received(1).GetOrCreateApplicationAuthentication(
"id",
"secret",
Arg.Any<NewAuthorization>(),
"111111");
tfa.Received(1).ChallengeFailed(loginAttemptsException);
await tfa.Received(1).ChallengeFailed(loginAttemptsException);
}
[Fact]
@ -178,11 +184,12 @@ public class LoginManagerTests
var keychain = Substitute.For<IKeychain>();
var tfa = Substitute.For<ITwoFactorChallengeHandler>();
var oauthListener = Substitute.For<IOAuthCallbackListener>();
tfa.HandleTwoFactorException(exception).Returns(
TwoFactorChallengeResult.RequestResendCode,
new TwoFactorChallengeResult("123456"));
var target = new LoginManager(keychain, tfa, "id", "secret");
var target = new LoginManager(keychain, tfa, oauthListener, "id", "secret");
await target.Login(host, client, "foo", "bar");
await client.Authorization.Received(2).GetOrCreateApplicationAuthentication("id", "secret", Arg.Any<NewAuthorization>());
@ -203,8 +210,9 @@ public class LoginManagerTests
var keychain = Substitute.For<IKeychain>();
var tfa = Substitute.For<ITwoFactorChallengeHandler>();
var oauthListener = Substitute.For<IOAuthCallbackListener>();
var target = new LoginManager(keychain, tfa, "id", "secret");
var target = new LoginManager(keychain, tfa, oauthListener, "id", "secret");
await target.Login(enterprise, client, "foo", "bar");
await keychain.Received().Save("foo", "bar", enterprise);
@ -221,8 +229,9 @@ public class LoginManagerTests
var keychain = Substitute.For<IKeychain>();
var tfa = Substitute.For<ITwoFactorChallengeHandler>();
var oauthListener = Substitute.For<IOAuthCallbackListener>();
var target = new LoginManager(keychain, tfa, "id", "secret");
var target = new LoginManager(keychain, tfa, oauthListener, "id", "secret");
await Assert.ThrowsAsync<AuthorizationException>(async () => await target.Login(enterprise, client, "foo", "bar"));
await keychain.Received().Delete(enterprise);
@ -239,8 +248,9 @@ public class LoginManagerTests
var keychain = Substitute.For<IKeychain>();
var tfa = Substitute.For<ITwoFactorChallengeHandler>();
var oauthListener = Substitute.For<IOAuthCallbackListener>();
var target = new LoginManager(keychain, tfa, "id", "secret");
var target = new LoginManager(keychain, tfa, oauthListener, "id", "secret");
await Assert.ThrowsAsync<InvalidOperationException>(async () => await target.Login(host, client, "foo", "bar"));
await keychain.Received().Delete(host);
@ -261,9 +271,10 @@ public class LoginManagerTests
var keychain = Substitute.For<IKeychain>();
var tfa = Substitute.For<ITwoFactorChallengeHandler>();
var oauthListener = Substitute.For<IOAuthCallbackListener>();
tfa.HandleTwoFactorException(exception).Returns(new TwoFactorChallengeResult("123456"));
var target = new LoginManager(keychain, tfa, "id", "secret");
var target = new LoginManager(keychain, tfa, oauthListener, "id", "secret");
await Assert.ThrowsAsync<InvalidOperationException>(async () => await target.Login(host, client, "foo", "bar"));
await keychain.Received().Delete(host);

Просмотреть файл

@ -0,0 +1,97 @@
using System;
using System.Net;
using System.Threading;
using System.Threading.Tasks;
using GitHub.Extensions;
using GitHub.Services;
using NSubstitute;
using Rothko;
using Xunit;
namespace UnitTests.GitHub.App.Services
{
public class OAuthCallbackListenerTests
{
[Fact]
public void ListenStartsHttpListener()
{
var httpListener = CreateHttpListener("id1");
var target = new OAuthCallbackListener(httpListener);
target.Listen("id1", CancellationToken.None).Forget();
httpListener.Prefixes.Received(1).Add("http://localhost:42549/");
httpListener.Received(1).Start();
}
[Fact]
public async Task ListenStopsHttpListener()
{
var httpListener = CreateHttpListener("id1");
var target = new OAuthCallbackListener(httpListener);
await target.Listen("id1", CancellationToken.None);
httpListener.Received(1).Stop();
}
[Fact]
public void CancelStopsHttpListener()
{
var httpListener = CreateHttpListener(null);
var cts = new CancellationTokenSource();
var target = new OAuthCallbackListener(httpListener);
var task = target.Listen("id1", cts.Token);
httpListener.Received(0).Stop();
cts.Cancel();
httpListener.Received(1).Stop();
}
[Fact]
public void CallingListenWhenAlreadyListeningCancelsFirstListen()
{
var httpListener = CreateHttpListener(null);
var target = new OAuthCallbackListener(httpListener);
var task1 = target.Listen("id1", CancellationToken.None);
var task2 = target.Listen("id2", CancellationToken.None);
httpListener.Received(1).Stop();
}
[Fact]
public async Task SuccessfulResponseClosesResponse()
{
var httpListener = CreateHttpListener("id1");
var context = httpListener.GetContext();
var target = new OAuthCallbackListener(httpListener);
await target.Listen("id1", CancellationToken.None);
context.Response.Received(1).Close();
}
IHttpListener CreateHttpListener(string id)
{
var result = Substitute.For<IHttpListener>();
result.When(x => x.Start()).Do(_ => result.IsListening.Returns(true));
if (id != null)
{
var context = Substitute.For<IHttpListenerContext>();
context.Request.Url.Returns(new Uri($"https://localhost:42549?code=1234&state={id}"));
result.GetContext().Returns(context);
result.GetContextAsync().Returns(context);
}
else
{
var tcs = new TaskCompletionSource<IHttpListenerContext>();
result.GetContextAsync().Returns(tcs.Task);
}
return result;
}
}
}

Просмотреть файл

@ -23,7 +23,8 @@ public class ConnectionManagerTests
CreateConnectionCache("github", "valid"),
Substitute.For<IKeychain>(),
CreateLoginManager(),
Substitute.For<IUsageTracker>());
Substitute.For<IUsageTracker>(),
Substitute.For<IVisualStudioBrowser>());
var result = await target.GetLoadedConnections();
Assert.Equal(2, result.Count);
@ -41,7 +42,8 @@ public class ConnectionManagerTests
CreateConnectionCache("github", "invalid"),
Substitute.For<IKeychain>(),
CreateLoginManager(),
Substitute.For<IUsageTracker>());
Substitute.For<IUsageTracker>(),
Substitute.For<IVisualStudioBrowser>());
var result = await target.GetLoadedConnections();
Assert.Equal(2, result.Count);
@ -62,7 +64,8 @@ public class ConnectionManagerTests
CreateConnectionCache("github", "valid"),
Substitute.For<IKeychain>(),
CreateLoginManager(),
Substitute.For<IUsageTracker>());
Substitute.For<IUsageTracker>(),
Substitute.For<IVisualStudioBrowser>());
var result = await target.GetConnection(HostAddress.Create("valid.com"));
Assert.Equal("https://valid.com/", result.HostAddress.WebUri.ToString());
@ -76,7 +79,8 @@ public class ConnectionManagerTests
CreateConnectionCache("github", "valid"),
Substitute.For<IKeychain>(),
CreateLoginManager(),
Substitute.For<IUsageTracker>());
Substitute.For<IUsageTracker>(),
Substitute.For<IVisualStudioBrowser>());
var result = await target.GetConnection(HostAddress.Create("another.com"));
Assert.Null(result);
@ -93,7 +97,8 @@ public class ConnectionManagerTests
CreateConnectionCache(),
Substitute.For<IKeychain>(),
CreateLoginManager(),
Substitute.For<IUsageTracker>());
Substitute.For<IUsageTracker>(),
Substitute.For<IVisualStudioBrowser>());
var result = await target.LogIn(HostAddress.GitHubDotComHostAddress, "user", "pass");
Assert.NotNull(result);
@ -107,7 +112,8 @@ public class ConnectionManagerTests
CreateConnectionCache(),
Substitute.For<IKeychain>(),
CreateLoginManager(),
Substitute.For<IUsageTracker>());
Substitute.For<IUsageTracker>(),
Substitute.For<IVisualStudioBrowser>());
await target.LogIn(HostAddress.GitHubDotComHostAddress, "user", "pass");
@ -122,7 +128,8 @@ public class ConnectionManagerTests
CreateConnectionCache(),
Substitute.For<IKeychain>(),
CreateLoginManager(),
Substitute.For<IUsageTracker>());
Substitute.For<IUsageTracker>(),
Substitute.For<IVisualStudioBrowser>());
await Assert.ThrowsAsync<AuthorizationException>(async () =>
await target.LogIn(HostAddress.Create("invalid.com"), "user", "pass"));
@ -136,7 +143,8 @@ public class ConnectionManagerTests
CreateConnectionCache("github"),
Substitute.For<IKeychain>(),
CreateLoginManager(),
Substitute.For<IUsageTracker>());
Substitute.For<IUsageTracker>(),
Substitute.For<IVisualStudioBrowser>());
await Assert.ThrowsAsync<InvalidOperationException>(async () =>
await target.LogIn(HostAddress.GitHubDotComHostAddress, "user", "pass"));
@ -151,7 +159,8 @@ public class ConnectionManagerTests
cache,
Substitute.For<IKeychain>(),
CreateLoginManager(),
Substitute.For<IUsageTracker>());
Substitute.For<IUsageTracker>(),
Substitute.For<IVisualStudioBrowser>());
await target.LogIn(HostAddress.GitHubDotComHostAddress, "user", "pass");
@ -171,7 +180,8 @@ public class ConnectionManagerTests
CreateConnectionCache("github"),
Substitute.For<IKeychain>(),
loginManager,
Substitute.For<IUsageTracker>());
Substitute.For<IUsageTracker>(),
Substitute.For<IVisualStudioBrowser>());
await target.LogOut(HostAddress.GitHubDotComHostAddress);
@ -189,7 +199,8 @@ public class ConnectionManagerTests
CreateConnectionCache("github"),
Substitute.For<IKeychain>(),
loginManager,
Substitute.For<IUsageTracker>());
Substitute.For<IUsageTracker>(),
Substitute.For<IVisualStudioBrowser>());
await target.LogOut(HostAddress.GitHubDotComHostAddress);
@ -205,7 +216,8 @@ public class ConnectionManagerTests
CreateConnectionCache("valid"),
Substitute.For<IKeychain>(),
loginManager,
Substitute.For<IUsageTracker>());
Substitute.For<IUsageTracker>(),
Substitute.For<IVisualStudioBrowser>());
await Assert.ThrowsAsync<KeyNotFoundException>(async () =>
await target.LogOut(HostAddress.GitHubDotComHostAddress));
@ -220,7 +232,8 @@ public class ConnectionManagerTests
cache,
Substitute.For<IKeychain>(),
CreateLoginManager(),
Substitute.For<IUsageTracker>());
Substitute.For<IUsageTracker>(),
Substitute.For<IVisualStudioBrowser>());
await target.LogOut(HostAddress.GitHubDotComHostAddress);

Просмотреть файл

@ -164,10 +164,6 @@
<HintPath>..\..\packages\NSubstitute.2.0.3\lib\net45\NSubstitute.dll</HintPath>
<Private>True</Private>
</Reference>
<Reference Include="rothko, Version=0.0.2.0, Culture=neutral, PublicKeyToken=9f664c41f503810a, processorArchitecture=MSIL">
<HintPath>..\..\packages\Rothko.0.0.2-ghfvs\lib\net45\rothko.dll</HintPath>
<Private>True</Private>
</Reference>
<Reference Include="PresentationCore" />
<Reference Include="PresentationFramework" />
<Reference Include="System" />
@ -233,6 +229,7 @@
<Compile Include="GitHub.App\Models\RepositoryModelTests.cs" />
<Compile Include="GitHub.App\Services\AvatarProviderTests.cs" />
<Compile Include="GitHub.App\Services\GitClientTests.cs" />
<Compile Include="GitHub.App\Services\OAuthCallbackListenerTests.cs" />
<Compile Include="GitHub.App\Services\PullRequestServiceTests.cs" />
<Compile Include="GitHub.App\Services\RepositoryCloneServiceTests.cs" />
<Compile Include="GitHub.App\Services\RepositoryCreationServiceTests.cs" />
@ -305,6 +302,10 @@
<Project>{1ce2d235-8072-4649-ba5a-cfb1af8776e0}</Project>
<Name>ReactiveUI_Net45</Name>
</ProjectReference>
<ProjectReference Include="..\..\submodules\Rothko\src\Rothko.csproj">
<Project>{4a84e568-ca86-4510-8cd0-90d3ef9b65f9}</Project>
<Name>Rothko</Name>
</ProjectReference>
<ProjectReference Include="..\..\submodules\splat\Splat\Splat-Net45.csproj">
<Project>{252ce1c2-027a-4445-a3c2-e4d6c80a935a}</Project>
<Name>Splat-Net45</Name>