Google Api
This commit is contained in:
@ -9,6 +9,7 @@ using Microsoft.Data.Entity;
|
||||
using Yavsc.Models;
|
||||
using Yavsc.Models.Workflow;
|
||||
|
||||
|
||||
namespace Yavsc.Controllers
|
||||
{
|
||||
[Produces("application/json")]
|
||||
|
@ -120,6 +120,8 @@ namespace Yavsc.Controllers
|
||||
}
|
||||
public IActionResult Todo()
|
||||
{
|
||||
User.GetUserId();
|
||||
|
||||
return View();
|
||||
}
|
||||
|
||||
|
@ -0,0 +1,236 @@
|
||||
/*
|
||||
Copyright 2017 Google Inc
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
using Google.Apis.Auth.OAuth2;
|
||||
using Google.Apis.Json;
|
||||
using Google.Apis.Util;
|
||||
using Newtonsoft.Json.Linq;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Net.Http;
|
||||
using System.Security.Cryptography;
|
||||
using System.Text;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Google.Apis.Auth
|
||||
{
|
||||
/// <summary>
|
||||
/// Google JSON Web Signature as specified in https://developers.google.com/accounts/docs/OAuth2ServiceAccount.
|
||||
/// </summary>
|
||||
public class GoogleJsonWebSignature
|
||||
{
|
||||
|
||||
internal const int MaxJwtLength = 10000;
|
||||
internal readonly static TimeSpan CertCacheRefreshInterval = TimeSpan.FromHours(1);
|
||||
|
||||
// See http://oid-info.com/get/2.16.840.1.101.3.4.2.1
|
||||
private const string Sha256Oid = "2.16.840.1.101.3.4.2.1";
|
||||
|
||||
private const string SupportedJwtAlgorithm = "RS256";
|
||||
|
||||
private static readonly IEnumerable<string> ValidJwtIssuers = new[]
|
||||
{
|
||||
"https://accounts.google.com",
|
||||
"accounts.google.com"
|
||||
};
|
||||
|
||||
private static readonly DateTime UnixEpoch = new DateTime(1970, 1, 1, 0, 0, 0, DateTimeKind.Utc);
|
||||
|
||||
/// <summary>
|
||||
/// Validates a Google-issued Json Web Token (JWT).
|
||||
/// With throw a <see cref="InvalidJwtException"/> if the passed value is not valid JWT signed by Google.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// <para>Follows the procedure to
|
||||
/// <see href="https://developers.google.com/identity/protocols/OpenIDConnect#validatinganidtoken">validate a JWT ID token</see>.
|
||||
/// </para>
|
||||
/// <para>Google certificates are cached, and refreshed once per hour. This can be overridden by setting
|
||||
/// <paramref name="forceGoogleCertRefresh"/> to true.</para>
|
||||
/// </remarks>
|
||||
/// <param name="jwt">The JWT to validate.</param>
|
||||
/// <param name="clock">Optional. The <see cref="IClock"/> to use for JWT expiration verification. Defaults to the system clock.</param>
|
||||
/// <param name="forceGoogleCertRefresh">Optional. If true forces new certificates to be downloaded from Google. Defaults to false.</param>
|
||||
/// <returns>The JWT payload, if the JWT is valid. Throws an <see cref="InvalidJwtException"/> otherwise.</returns>
|
||||
/// <exception cref="InvalidJwtException">Thrown when passed a JWT that is not a valid JWT signed by Google.</exception>
|
||||
public static Task<Payload> ValidateAsync(string jwt, IClock clock = null, bool forceGoogleCertRefresh = false) =>
|
||||
ValidateInternalAsync(jwt, clock ?? SystemClock.Default, forceGoogleCertRefresh, null);
|
||||
|
||||
// internal for testing
|
||||
internal static async Task<Payload> ValidateInternalAsync(string jwt, IClock clock, bool forceGoogleCertRefresh, string certsJson)
|
||||
{
|
||||
// Check arguments
|
||||
jwt.ThrowIfNull(nameof(jwt));
|
||||
jwt.ThrowIfNullOrEmpty(nameof(jwt));
|
||||
if (jwt.Length > MaxJwtLength)
|
||||
{
|
||||
throw new InvalidJwtException($"JWT exceeds maximum allowed length of {MaxJwtLength}");
|
||||
}
|
||||
var parts = jwt.Split('.');
|
||||
if (parts.Length != 3)
|
||||
{
|
||||
throw new InvalidJwtException($"JWT must consist of Header, Payload, and Signature");
|
||||
}
|
||||
|
||||
// Decode the three parts of the JWT: header.payload.signature
|
||||
Header header = NewtonsoftJsonSerializer.Instance.Deserialize<Header>(Base64UrlToString(parts[0]));
|
||||
Payload payload = NewtonsoftJsonSerializer.Instance.Deserialize<Payload>(Base64UrlToString(parts[1]));
|
||||
byte[] signature = Base64UrlDecode(parts[2]);
|
||||
|
||||
// Verify algorithm in JWT
|
||||
if (header.Algorithm != SupportedJwtAlgorithm)
|
||||
{
|
||||
throw new InvalidJwtException($"JWT algorithm must be '{SupportedJwtAlgorithm}'");
|
||||
}
|
||||
|
||||
// Verify signature
|
||||
byte[] hash;
|
||||
using (var hashAlg = SHA256.Create())
|
||||
{
|
||||
hash = hashAlg.ComputeHash(Encoding.ASCII.GetBytes($"{parts[0]}.{parts[1]}"));
|
||||
}
|
||||
bool verifiedOk = false;
|
||||
foreach (var googleCert in await GetGoogleCertsAsync(clock, forceGoogleCertRefresh, certsJson))
|
||||
{
|
||||
#if NET45
|
||||
verifiedOk = ((RSACryptoServiceProvider)googleCert).VerifyHash(hash, Sha256Oid, signature);
|
||||
#elif DNX451
|
||||
verifiedOk = ((RSACryptoServiceProvider)googleCert).VerifyHash(hash, Sha256Oid, signature);
|
||||
#elif NETSTANDARD1_3
|
||||
verifiedOk = googleCert.VerifyHash(hash, signature, HashAlgorithmName.SHA256, RSASignaturePadding.Pkcs1);
|
||||
#else
|
||||
#error Unsupported platform
|
||||
#endif
|
||||
if (verifiedOk)
|
||||
{
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!verifiedOk)
|
||||
{
|
||||
throw new InvalidJwtException("JWT invalid: unable to verify signature.");
|
||||
}
|
||||
|
||||
// Verify iss, iat and exp claims
|
||||
if (!ValidJwtIssuers.Contains(payload.Issuer))
|
||||
{
|
||||
var validList = string.Join(", ", ValidJwtIssuers.Select(x => $"'{x}'"));
|
||||
throw new InvalidJwtException($"JWT issuer incorrect. Must be one of: {validList}");
|
||||
}
|
||||
if (payload.IssuedAtTimeSeconds == null || payload.ExpirationTimeSeconds == null)
|
||||
{
|
||||
throw new InvalidJwtException("JWT must contain 'iat' and 'exp' claims");
|
||||
}
|
||||
var nowSeconds = (clock.UtcNow - UnixEpoch).TotalSeconds;
|
||||
if (nowSeconds < payload.IssuedAtTimeSeconds.Value)
|
||||
{
|
||||
throw new InvalidJwtException("JWT is not yet valid.");
|
||||
}
|
||||
if (nowSeconds > payload.ExpirationTimeSeconds.Value)
|
||||
{
|
||||
throw new InvalidJwtException("JWT has expired.");
|
||||
}
|
||||
|
||||
// All verification passed, return payload.
|
||||
return payload;
|
||||
}
|
||||
|
||||
private static string Base64UrlToString(string base64Url) => Encoding.UTF8.GetString(Base64UrlDecode(base64Url));
|
||||
|
||||
private static byte[] Base64UrlDecode(string base64Url)
|
||||
{
|
||||
var base64 = base64Url.Replace('-', '+').Replace('_', '/');
|
||||
switch (base64.Length % 4)
|
||||
{
|
||||
case 2: base64 += "=="; break;
|
||||
case 3: base64 += "="; break;
|
||||
}
|
||||
return Convert.FromBase64String(base64);
|
||||
}
|
||||
|
||||
private static SemaphoreSlim _certCacheLock = new SemaphoreSlim(1);
|
||||
private static DateTime _certCacheDownloadTime;
|
||||
private static List<RSA> _certCache;
|
||||
|
||||
// internal for testing
|
||||
internal static async Task<List<RSA>> GetGoogleCertsAsync(IClock clock, bool forceGoogleCertRefresh, string certsJson)
|
||||
{
|
||||
var now = clock.UtcNow;
|
||||
await _certCacheLock.WaitAsync();
|
||||
try
|
||||
{
|
||||
if (forceGoogleCertRefresh || _certCache == null || (_certCacheDownloadTime + CertCacheRefreshInterval) < now)
|
||||
{
|
||||
using (var httpClient = new HttpClient())
|
||||
{
|
||||
// certsJson used for unit tests
|
||||
if (certsJson == null)
|
||||
{
|
||||
certsJson = await httpClient.GetStringAsync(GoogleAuthConsts.JsonWebKeySetUrl);
|
||||
}
|
||||
}
|
||||
_certCache = GetGoogleCertsFromJson(certsJson);
|
||||
_certCacheDownloadTime = now;
|
||||
}
|
||||
return _certCache;
|
||||
}
|
||||
finally
|
||||
{
|
||||
_certCacheLock.Release();
|
||||
}
|
||||
}
|
||||
|
||||
private static List<RSA> GetGoogleCertsFromJson(string json) =>
|
||||
JToken.Parse(json)["keys"].AsEnumerable().Select(key =>
|
||||
{
|
||||
var rsa = RSA.Create();
|
||||
rsa.ImportParameters(new RSAParameters
|
||||
{
|
||||
Modulus = Base64UrlDecode((string)key["n"]),
|
||||
Exponent = Base64UrlDecode((string)key["e"]),
|
||||
});
|
||||
return rsa;
|
||||
})
|
||||
.ToList();
|
||||
|
||||
/// <summary>
|
||||
/// The header as specified in https://developers.google.com/accounts/docs/OAuth2ServiceAccount#formingheader.
|
||||
/// </summary>
|
||||
public class Header : JsonWebSignature.Header
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The payload as specified in
|
||||
/// https://developers.google.com/accounts/docs/OAuth2ServiceAccount#formingclaimset.
|
||||
/// </summary>
|
||||
public class Payload : JsonWebSignature.Payload
|
||||
{
|
||||
/// <summary>
|
||||
/// a space-delimited list of the permissions the application requests or <c>null</c>.
|
||||
/// </summary>
|
||||
[Newtonsoft.Json.JsonPropertyAttribute("scope")]
|
||||
public string Scope { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The email address of the user for which the application is requesting delegated access.
|
||||
/// </summary>
|
||||
[Newtonsoft.Json.JsonPropertyAttribute("prn")]
|
||||
public string Prn { get; set; }
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,32 @@
|
||||
/*
|
||||
Copyright 2017 Google Inc
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
using System;
|
||||
|
||||
namespace Google.Apis.Auth
|
||||
{
|
||||
/// <summary>
|
||||
/// An exception that is thrown when a Json Web Token (JWT) is invalid.
|
||||
/// </summary>
|
||||
public class InvalidJwtException : Exception
|
||||
{
|
||||
/// <summary>
|
||||
/// Initializes a new InvalidJwtException instanc e with the specified error message.
|
||||
/// </summary>
|
||||
/// <param name="message">The error message that explains why the JWT was invalid.</param>
|
||||
public InvalidJwtException(string message) : base(message) { }
|
||||
}
|
||||
}
|
100
Yavsc/GoogleApiSupport/Google.Apis.Auth/JsonWebSignature.cs
Normal file
100
Yavsc/GoogleApiSupport/Google.Apis.Auth/JsonWebSignature.cs
Normal file
@ -0,0 +1,100 @@
|
||||
/*
|
||||
Copyright 2013 Google Inc
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace Google.Apis.Auth
|
||||
{
|
||||
/// <summary>
|
||||
/// JSON Web Signature (JWS) implementation as specified in
|
||||
/// http://tools.ietf.org/html/draft-ietf-jose-json-web-signature-11.
|
||||
/// </summary>
|
||||
public class JsonWebSignature
|
||||
{
|
||||
// TODO(peleyal): Implement some verify method:
|
||||
// http://tools.ietf.org/html/draft-ietf-oauth-json-web-token-08#section-7
|
||||
|
||||
/// <summary>
|
||||
/// Header as specified in http://tools.ietf.org/html/draft-ietf-jose-json-web-signature-11#section-4.1.
|
||||
/// </summary>
|
||||
public class Header : JsonWebToken.Header
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets or set the algorithm header parameter that identifies the cryptographic algorithm used to secure
|
||||
/// the JWS or <c>null</c>.
|
||||
/// </summary>
|
||||
[Newtonsoft.Json.JsonPropertyAttribute("alg")]
|
||||
public string Algorithm { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the JSON Web Key URL header parameter that is an absolute URL that refers to a resource
|
||||
/// for a set of JSON-encoded public keys, one of which corresponds to the key that was used to digitally
|
||||
/// sign the JWS or <c>null</c>.
|
||||
/// </summary>
|
||||
[Newtonsoft.Json.JsonPropertyAttribute("jku")]
|
||||
public string JwkUrl { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets JSON Web Key header parameter that is a public key that corresponds to the key used to
|
||||
/// digitally sign the JWS or <c>null</c>.
|
||||
/// </summary>
|
||||
[Newtonsoft.Json.JsonPropertyAttribute("jwk")]
|
||||
public string Jwk { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets key ID header parameter that is a hint indicating which specific key owned by the signer
|
||||
/// should be used to validate the digital signature or <c>null</c>.
|
||||
/// </summary>
|
||||
[Newtonsoft.Json.JsonPropertyAttribute("kid")]
|
||||
public string KeyId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets X.509 URL header parameter that is an absolute URL that refers to a resource for the X.509
|
||||
/// public key certificate or certificate chain corresponding to the key used to digitally sign the JWS or
|
||||
/// <c>null</c>.
|
||||
/// </summary>
|
||||
[Newtonsoft.Json.JsonPropertyAttribute("x5u")]
|
||||
public string X509Url { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets X.509 certificate thumb print header parameter that provides a base64url encoded SHA-1
|
||||
/// thumb-print (a.k.a. digest) of the DER encoding of an X.509 certificate that can be used to match the
|
||||
/// certificate or <c>null</c>.
|
||||
/// </summary>
|
||||
[Newtonsoft.Json.JsonPropertyAttribute("x5t")]
|
||||
public string X509Thumbprint { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets X.509 certificate chain header parameter contains the X.509 public key certificate or
|
||||
/// certificate chain corresponding to the key used to digitally sign the JWS or <c>null</c>.
|
||||
/// </summary>
|
||||
[Newtonsoft.Json.JsonPropertyAttribute("x5c")]
|
||||
public string X509Certificate { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets array listing the header parameter names that define extensions that are used in the JWS
|
||||
/// header that MUST be understood and processed or <c>null</c>.
|
||||
/// </summary>
|
||||
[Newtonsoft.Json.JsonPropertyAttribute("crit")]
|
||||
public IList<string> critical { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>JWS Payload.</summary>
|
||||
public class Payload : JsonWebToken.Payload
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
127
Yavsc/GoogleApiSupport/Google.Apis.Auth/JsonWebToken.cs
Normal file
127
Yavsc/GoogleApiSupport/Google.Apis.Auth/JsonWebToken.cs
Normal file
@ -0,0 +1,127 @@
|
||||
/*
|
||||
Copyright 2013 Google Inc
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace Google.Apis.Auth
|
||||
{
|
||||
/// <summary>
|
||||
/// JSON Web Token (JWT) implementation as specified in
|
||||
/// http://tools.ietf.org/html/draft-ietf-oauth-json-web-token-08.
|
||||
/// </summary>
|
||||
public class JsonWebToken
|
||||
{
|
||||
/// <summary>
|
||||
/// JWT Header as specified in http://tools.ietf.org/html/draft-ietf-oauth-json-web-token-08#section-5.
|
||||
/// </summary>
|
||||
public class Header
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets or sets type header parameter used to declare the type of this object or <c>null</c>.
|
||||
/// </summary>
|
||||
[Newtonsoft.Json.JsonPropertyAttribute("typ")]
|
||||
public string Type { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets content type header parameter used to declare structural information about the JWT or
|
||||
/// <c>null</c>.
|
||||
/// </summary>
|
||||
[Newtonsoft.Json.JsonPropertyAttribute("cty")]
|
||||
public string ContentType { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// JWT Payload as specified in http://tools.ietf.org/html/draft-ietf-oauth-json-web-token-08#section-4.1.
|
||||
/// </summary>
|
||||
public class Payload
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets or sets issuer claim that identifies the principal that issued the JWT or <c>null</c>.
|
||||
/// </summary>
|
||||
[Newtonsoft.Json.JsonPropertyAttribute("iss")]
|
||||
public string Issuer { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets subject claim identifying the principal that is the subject of the JWT or <c>null</c>.
|
||||
/// </summary>
|
||||
[Newtonsoft.Json.JsonPropertyAttribute("sub")]
|
||||
public string Subject { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets audience claim that identifies the audience that the JWT is intended for (should either be
|
||||
/// a string or list) or <c>null</c>.
|
||||
/// </summary>
|
||||
[Newtonsoft.Json.JsonPropertyAttribute("aud")]
|
||||
public object Audience { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets expiration time claim that identifies the expiration time (in seconds) on or after which
|
||||
/// the token MUST NOT be accepted for processing or <c>null</c>.
|
||||
/// </summary>
|
||||
[Newtonsoft.Json.JsonPropertyAttribute("exp")]
|
||||
public long? ExpirationTimeSeconds { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets not before claim that identifies the time (in seconds) before which the token MUST NOT be
|
||||
/// accepted for processing or <c>null</c>.
|
||||
/// </summary>
|
||||
[Newtonsoft.Json.JsonPropertyAttribute("nbf")]
|
||||
public long? NotBeforeTimeSeconds { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets issued at claim that identifies the time (in seconds) at which the JWT was issued or
|
||||
/// <c>null</c>.
|
||||
/// </summary>
|
||||
[Newtonsoft.Json.JsonPropertyAttribute("iat")]
|
||||
public long? IssuedAtTimeSeconds { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets JWT ID claim that provides a unique identifier for the JWT or <c>null</c>.
|
||||
/// </summary>
|
||||
[Newtonsoft.Json.JsonPropertyAttribute("jti")]
|
||||
public string JwtId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets type claim that is used to declare a type for the contents of this JWT Claims Set or
|
||||
/// <c>null</c>.
|
||||
/// </summary>
|
||||
[Newtonsoft.Json.JsonPropertyAttribute("typ")]
|
||||
public string Type { get; set; }
|
||||
|
||||
/// <summary>Gets the audience property as a list.</summary>
|
||||
[Newtonsoft.Json.JsonIgnoreAttribute]
|
||||
public IEnumerable<string> AudienceAsList
|
||||
{
|
||||
get
|
||||
{
|
||||
var asList = Audience as List<string>;
|
||||
if (asList != null)
|
||||
{
|
||||
return asList;
|
||||
}
|
||||
var list = new List<string>();
|
||||
var asString = Audience as string;
|
||||
if (asString != null)
|
||||
{
|
||||
list.Add(asString);
|
||||
}
|
||||
|
||||
return list;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,113 @@
|
||||
/*
|
||||
Copyright 2013 Google Inc
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
using Google.Apis.Auth.OAuth2.Flows;
|
||||
using Google.Apis.Auth.OAuth2.Responses;
|
||||
using Google.Apis.Auth.OAuth2.Requests;
|
||||
using Google.Apis.Logging;
|
||||
|
||||
namespace Google.Apis.Auth.OAuth2
|
||||
{
|
||||
/// <summary>
|
||||
/// Thread-safe OAuth 2.0 authorization code flow for an installed application that persists end-user credentials.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Incremental authorization (https://developers.google.com/+/web/api/rest/oauth) is currently not supported
|
||||
/// for Installed Apps.
|
||||
/// </remarks>
|
||||
public class AuthorizationCodeInstalledApp : IAuthorizationCodeInstalledApp
|
||||
{
|
||||
private static readonly ILogger Logger = ApplicationContext.Logger.ForType<AuthorizationCodeInstalledApp>();
|
||||
|
||||
private readonly IAuthorizationCodeFlow flow;
|
||||
private readonly ICodeReceiver codeReceiver;
|
||||
|
||||
/// <summary>
|
||||
/// Constructs a new authorization code installed application with the given flow and code receiver.
|
||||
/// </summary>
|
||||
public AuthorizationCodeInstalledApp(IAuthorizationCodeFlow flow, ICodeReceiver codeReceiver)
|
||||
{
|
||||
this.flow = flow;
|
||||
this.codeReceiver = codeReceiver;
|
||||
}
|
||||
|
||||
#region IAuthorizationCodeInstalledApp Members
|
||||
|
||||
/// <summary>Gets the authorization code flow.</summary>
|
||||
public IAuthorizationCodeFlow Flow
|
||||
{
|
||||
get { return flow; }
|
||||
}
|
||||
|
||||
/// <summary>Gets the code receiver which is responsible for receiving the authorization code.</summary>
|
||||
public ICodeReceiver CodeReceiver
|
||||
{
|
||||
get { return codeReceiver; }
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public async Task<UserCredential> AuthorizeAsync(string userId, CancellationToken taskCancellationToken)
|
||||
{
|
||||
// Try to load a token from the data store.
|
||||
var token = await Flow.LoadTokenAsync(userId, taskCancellationToken).ConfigureAwait(false);
|
||||
|
||||
// Check if a new authorization code is needed.
|
||||
if (ShouldRequestAuthorizationCode(token))
|
||||
{
|
||||
// Create an authorization code request.
|
||||
var redirectUri = CodeReceiver.RedirectUri;
|
||||
AuthorizationCodeRequestUrl codeRequest = Flow.CreateAuthorizationCodeRequest(redirectUri);
|
||||
|
||||
// Receive the code.
|
||||
var response = await CodeReceiver.ReceiveCodeAsync(codeRequest, taskCancellationToken)
|
||||
.ConfigureAwait(false);
|
||||
|
||||
if (string.IsNullOrEmpty(response.Code))
|
||||
{
|
||||
var errorResponse = new TokenErrorResponse(response);
|
||||
Logger.Info("Received an error. The response is: {0}", errorResponse);
|
||||
throw new TokenResponseException(errorResponse);
|
||||
}
|
||||
|
||||
Logger.Debug("Received \"{0}\" code", response.Code);
|
||||
|
||||
// Get the token based on the code.
|
||||
token = await Flow.ExchangeCodeForTokenAsync(userId, response.Code, CodeReceiver.RedirectUri,
|
||||
taskCancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
return new UserCredential(flow, userId, token);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Determines the need for retrieval of a new authorization code, based on the given token and the
|
||||
/// authorization code flow.
|
||||
/// </summary>
|
||||
public bool ShouldRequestAuthorizationCode(TokenResponse token)
|
||||
{
|
||||
// TODO: This code should be shared between this class and AuthorizationCodeWebApp.
|
||||
// If the flow includes a parameter that requires a new token, if the stored token is null or it doesn't
|
||||
// have a refresh token and the access token is expired we need to retrieve a new authorization code.
|
||||
return Flow.ShouldForceTokenRetrieval() || token == null || (token.RefreshToken == null
|
||||
&& token.IsExpired(flow.Clock));
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
}
|
@ -0,0 +1,94 @@
|
||||
/*
|
||||
Copyright 2013 Google Inc
|
||||
|
||||
Licensed under the Apache License, Version 2.0(the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
using System;
|
||||
using System.Net.Http;
|
||||
using System.Net.Http.Headers;
|
||||
|
||||
namespace Google.Apis.Auth.OAuth2
|
||||
{
|
||||
/// <summary>
|
||||
/// OAuth 2.0 helper for accessing protected resources using the Bearer token as specified in
|
||||
/// http://tools.ietf.org/html/rfc6750.
|
||||
/// </summary>
|
||||
public class BearerToken
|
||||
{
|
||||
/// <summary>
|
||||
/// Thread-safe OAuth 2.0 method for accessing protected resources using the Authorization header as specified
|
||||
/// in http://tools.ietf.org/html/rfc6750#section-2.1.
|
||||
/// </summary>
|
||||
public class AuthorizationHeaderAccessMethod : IAccessMethod
|
||||
{
|
||||
const string Schema = "Bearer";
|
||||
|
||||
/// <inheritdoc/>
|
||||
public void Intercept(HttpRequestMessage request, string accessToken)
|
||||
{
|
||||
request.Headers.Authorization = new AuthenticationHeaderValue(Schema, accessToken);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public string GetAccessToken(HttpRequestMessage request)
|
||||
{
|
||||
if (request.Headers.Authorization != null && request.Headers.Authorization.Scheme == Schema)
|
||||
{
|
||||
return request.Headers.Authorization.Parameter;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Thread-safe OAuth 2.0 method for accessing protected resources using an <c>access_token</c> query parameter
|
||||
/// as specified in http://tools.ietf.org/html/rfc6750#section-2.3.
|
||||
/// </summary>
|
||||
public class QueryParameterAccessMethod : IAccessMethod
|
||||
{
|
||||
const string AccessTokenKey = "access_token";
|
||||
|
||||
/// <inheritdoc/>
|
||||
public void Intercept(HttpRequestMessage request, string accessToken)
|
||||
{
|
||||
var uri = request.RequestUri;
|
||||
request.RequestUri = new Uri(string.Format("{0}{1}{2}={3}",
|
||||
uri.ToString(), string.IsNullOrEmpty(uri.Query) ? "?" : "&", AccessTokenKey,
|
||||
Uri.EscapeDataString(accessToken)));
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public string GetAccessToken(HttpRequestMessage request)
|
||||
{
|
||||
var query = request.RequestUri.Query;
|
||||
if (string.IsNullOrEmpty(query))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
// Remove the '?'.
|
||||
query = query.Substring(1);
|
||||
foreach (var parameter in query.Split('&'))
|
||||
{
|
||||
var keyValue = parameter.Split('=');
|
||||
if (keyValue[0].Equals(AccessTokenKey))
|
||||
{
|
||||
return keyValue[1];
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,30 @@
|
||||
/*
|
||||
Copyright 2013 Google Inc
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
namespace Google.Apis.Auth.OAuth2
|
||||
{
|
||||
/// <summary>Client credential details for installed and web applications.</summary>
|
||||
public sealed class ClientSecrets
|
||||
{
|
||||
/// <summary>Gets or sets the client identifier.</summary>
|
||||
[Newtonsoft.Json.JsonProperty("client_id")]
|
||||
public string ClientId { get; set; }
|
||||
|
||||
/// <summary>Gets or sets the client Secret.</summary>
|
||||
[Newtonsoft.Json.JsonProperty("client_secret")]
|
||||
public string ClientSecret { get; set; }
|
||||
}
|
||||
}
|
@ -0,0 +1,152 @@
|
||||
/*
|
||||
Copyright 2014 Google Inc
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Net;
|
||||
using System.Net.Http;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
using Google.Apis.Auth.OAuth2.Responses;
|
||||
|
||||
namespace Google.Apis.Auth.OAuth2
|
||||
{
|
||||
/// <summary>
|
||||
/// Google OAuth 2.0 credential for accessing protected resources using an access token. The Google OAuth 2.0
|
||||
/// Authorization Server supports server-to-server interactions such as those between a web application and Google
|
||||
/// Cloud Storage. The requesting application has to prove its own identity to gain access to an API, and an
|
||||
/// end-user doesn't have to be involved.
|
||||
/// <para>
|
||||
/// More details about Compute Engine authentication is available at:
|
||||
/// https://cloud.google.com/compute/docs/authentication.
|
||||
/// </para>
|
||||
/// </summary>
|
||||
public class ComputeCredential : ServiceCredential
|
||||
{
|
||||
/// <summary>The metadata server url.</summary>
|
||||
public const string MetadataServerUrl = "http://metadata.google.internal";
|
||||
|
||||
/// <summary>Caches result from first call to <c>IsRunningOnComputeEngine</c> </summary>
|
||||
private readonly static Lazy<Task<bool>> isRunningOnComputeEngineCached = new Lazy<Task<bool>>(
|
||||
() => IsRunningOnComputeEngineNoCache());
|
||||
|
||||
/// <summary>
|
||||
/// Experimentally, 200ms was found to be 99.9999% reliable.
|
||||
/// This is a conservative timeout to minimize hanging on some troublesome network.
|
||||
/// </summary>
|
||||
private const int MetadataServerPingTimeoutInMilliseconds = 1000;
|
||||
|
||||
/// <summary>The Metadata flavor header name.</summary>
|
||||
private const string MetadataFlavor = "Metadata-Flavor";
|
||||
|
||||
/// <summary>The Metadata header response indicating Google.</summary>
|
||||
private const string GoogleMetadataHeader = "Google";
|
||||
|
||||
private const string NotOnGceMessage = "Could not reach the Google Compute Engine metadata service. That is alright if this application is not running on GCE.";
|
||||
|
||||
/// <summary>
|
||||
/// An initializer class for the Compute credential. It uses <see cref="GoogleAuthConsts.ComputeTokenUrl"/>
|
||||
/// as the token server URL.
|
||||
/// </summary>
|
||||
new public class Initializer : ServiceCredential.Initializer
|
||||
{
|
||||
/// <summary>Constructs a new initializer using the default compute token URL.</summary>
|
||||
public Initializer()
|
||||
: this(GoogleAuthConsts.ComputeTokenUrl) {}
|
||||
|
||||
/// <summary>Constructs a new initializer using the given token URL.</summary>
|
||||
public Initializer(string tokenUrl)
|
||||
: base(tokenUrl) {}
|
||||
}
|
||||
|
||||
/// <summary>Constructs a new Compute credential instance.</summary>
|
||||
public ComputeCredential() : this(new Initializer()) { }
|
||||
|
||||
/// <summary>Constructs a new Compute credential instance.</summary>
|
||||
public ComputeCredential(Initializer initializer) : base(initializer) { }
|
||||
|
||||
#region ServiceCredential overrides
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override async Task<bool> RequestAccessTokenAsync(CancellationToken taskCancellationToken)
|
||||
{
|
||||
// Create and send the HTTP request to compute server token URL.
|
||||
var httpRequest = new HttpRequestMessage(HttpMethod.Get, TokenServerUrl);
|
||||
httpRequest.Headers.Add(MetadataFlavor, GoogleMetadataHeader);
|
||||
var response = await HttpClient.SendAsync(httpRequest, taskCancellationToken).ConfigureAwait(false);
|
||||
Token = await TokenResponse.FromHttpResponseAsync(response, Clock, Logger);
|
||||
return true;
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
/// <summary>
|
||||
/// Detects if application is running on Google Compute Engine. This is achieved by attempting to contact
|
||||
/// GCE metadata server, that is only available on GCE. The check is only performed the first time you
|
||||
/// call this method, subsequent invocations used cached result of the first call.
|
||||
/// </summary>
|
||||
public static Task<bool> IsRunningOnComputeEngine()
|
||||
{
|
||||
return isRunningOnComputeEngineCached.Value;
|
||||
}
|
||||
|
||||
private static async Task<bool> IsRunningOnComputeEngineNoCache()
|
||||
{
|
||||
try
|
||||
{
|
||||
Logger.Info("Checking connectivity to ComputeEngine metadata server.");
|
||||
var httpRequest = new HttpRequestMessage(HttpMethod.Get, MetadataServerUrl);
|
||||
var cts = new CancellationTokenSource();
|
||||
cts.CancelAfter(MetadataServerPingTimeoutInMilliseconds);
|
||||
|
||||
// Using the built-in HttpClient, as we want bare bones functionality without any retries.
|
||||
var httpClient = new HttpClient();
|
||||
var response = await httpClient.SendAsync(httpRequest, cts.Token).ConfigureAwait(false);
|
||||
|
||||
IEnumerable<string> headerValues = null;
|
||||
if (response.Headers.TryGetValues(MetadataFlavor, out headerValues))
|
||||
{
|
||||
foreach (var value in headerValues)
|
||||
{
|
||||
if (value == GoogleMetadataHeader)
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
// Response came from another source, possibly a proxy server in the caller's network.
|
||||
Logger.Info("Response came from a source other than the Google Compute Engine metadata server.");
|
||||
return false;
|
||||
}
|
||||
catch (HttpRequestException)
|
||||
{
|
||||
Logger.Debug(NotOnGceMessage);
|
||||
return false;
|
||||
}
|
||||
catch (WebException)
|
||||
{
|
||||
// On Mono, NameResolutionFailure is of System.Net.WebException.
|
||||
Logger.Debug(NotOnGceMessage);
|
||||
return false;
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
Logger.Warning("Could not reach the Google Compute Engine metadata service. Operation timed out.");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,288 @@
|
||||
/*
|
||||
Copyright 2015 Google Inc
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
using Google.Apis.Auth.OAuth2.Flows;
|
||||
using Google.Apis.Auth.OAuth2.Responses;
|
||||
using Google.Apis.Json;
|
||||
using Google.Apis.Logging;
|
||||
|
||||
namespace Google.Apis.Auth.OAuth2
|
||||
{
|
||||
// TODO(jtattermusch): look into getting rid of DefaultCredentialProvider and moving
|
||||
// the logic into GoogleCredential.
|
||||
|
||||
/// <summary>
|
||||
/// Provides the Application Default Credential from the environment.
|
||||
/// An instance of this class represents the per-process state used to get and cache
|
||||
/// the credential and allows overriding the state and environment for testing purposes.
|
||||
/// </summary>
|
||||
internal class DefaultCredentialProvider
|
||||
{
|
||||
private static readonly ILogger Logger = ApplicationContext.Logger.ForType<DefaultCredentialProvider>();
|
||||
|
||||
/// <summary>
|
||||
/// Environment variable override which stores the default application credentials file path.
|
||||
/// </summary>
|
||||
public const string CredentialEnvironmentVariable = "GOOGLE_APPLICATION_CREDENTIALS";
|
||||
|
||||
/// <summary>Well known file which stores the default application credentials.</summary>
|
||||
private const string WellKnownCredentialsFile = "application_default_credentials.json";
|
||||
|
||||
/// <summary>Environment variable which contains the Application Data settings.</summary>
|
||||
private const string AppdataEnvironmentVariable = "APPDATA";
|
||||
|
||||
/// <summary>Environment variable which contains the location of home directory on UNIX systems.</summary>
|
||||
private const string HomeEnvironmentVariable = "HOME";
|
||||
|
||||
/// <summary>GCloud configuration directory in Windows, relative to %APPDATA%.</summary>
|
||||
private const string CloudSDKConfigDirectoryWindows = "gcloud";
|
||||
|
||||
/// <summary>Help link to the application default credentials feature.</summary>
|
||||
private const string HelpPermalink =
|
||||
"https://developers.google.com/accounts/docs/application-default-credentials";
|
||||
|
||||
/// <summary>GCloud configuration directory on Linux/Mac, relative to $HOME.</summary>
|
||||
private static readonly string CloudSDKConfigDirectoryUnix = Path.Combine(".config", "gcloud");
|
||||
|
||||
/// <summary>Caches result from first call to <c>GetApplicationDefaultCredentialAsync</c> </summary>
|
||||
private readonly Lazy<Task<GoogleCredential>> cachedCredentialTask;
|
||||
|
||||
/// <summary>Constructs a new default credential provider.</summary>
|
||||
public DefaultCredentialProvider()
|
||||
{
|
||||
cachedCredentialTask = new Lazy<Task<GoogleCredential>>(CreateDefaultCredentialAsync);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns the Application Default Credentials. Subsequent invocations return cached value from
|
||||
/// first invocation.
|
||||
/// See <see cref="M:Google.Apis.Auth.OAuth2.GoogleCredential.GetApplicationDefaultAsync"/> for details.
|
||||
/// </summary>
|
||||
public Task<GoogleCredential> GetDefaultCredentialAsync()
|
||||
{
|
||||
return cachedCredentialTask.Value;
|
||||
}
|
||||
|
||||
/// <summary>Creates a new default credential.</summary>
|
||||
private async Task<GoogleCredential> CreateDefaultCredentialAsync()
|
||||
{
|
||||
// 1. First try the environment variable.
|
||||
string credentialPath = GetEnvironmentVariable(CredentialEnvironmentVariable);
|
||||
if (!String.IsNullOrWhiteSpace(credentialPath))
|
||||
{
|
||||
try
|
||||
{
|
||||
return CreateDefaultCredentialFromFile(credentialPath);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
// Catching generic exception type because any corrupted file could manifest in different ways
|
||||
// including but not limited to the System, System.IO or from the Newtonsoft.Json namespace.
|
||||
throw new InvalidOperationException(
|
||||
String.Format("Error reading credential file from location {0}: {1}"
|
||||
+ "\nPlease check the value of the Environment Variable {2}",
|
||||
credentialPath,
|
||||
e.Message,
|
||||
CredentialEnvironmentVariable));
|
||||
}
|
||||
}
|
||||
|
||||
// 2. Then try the well known file.
|
||||
credentialPath = GetWellKnownCredentialFilePath();
|
||||
if (!String.IsNullOrWhiteSpace(credentialPath))
|
||||
{
|
||||
try
|
||||
{
|
||||
return CreateDefaultCredentialFromFile(credentialPath);
|
||||
}
|
||||
catch (FileNotFoundException)
|
||||
{
|
||||
// File is not present, eat the exception and move on to the next check.
|
||||
Logger.Debug("Well-known credential file {0} not found.", credentialPath);
|
||||
}
|
||||
catch (DirectoryNotFoundException)
|
||||
{
|
||||
// Directory not present, eat the exception and move on to the next check.
|
||||
Logger.Debug("Well-known credential file {0} not found.", credentialPath);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
throw new InvalidOperationException(
|
||||
String.Format("Error reading credential file from location {0}: {1}"
|
||||
+ "\nPlease rerun 'gcloud auth login' to regenerate credentials file.",
|
||||
credentialPath,
|
||||
e.Message));
|
||||
}
|
||||
}
|
||||
|
||||
// 3. Then try the compute engine.
|
||||
Logger.Debug("Checking whether the application is running on ComputeEngine.");
|
||||
if (await ComputeCredential.IsRunningOnComputeEngine().ConfigureAwait(false))
|
||||
{
|
||||
Logger.Debug("ComputeEngine check passed. Using ComputeEngine Credentials.");
|
||||
return new GoogleCredential(new ComputeCredential());
|
||||
}
|
||||
|
||||
// If everything we tried has failed, throw an exception.
|
||||
throw new InvalidOperationException(
|
||||
String.Format("The Application Default Credentials are not available. They are available if running"
|
||||
+ " in Google Compute Engine. Otherwise, the environment variable {0} must be defined"
|
||||
+ " pointing to a file defining the credentials. See {1} for more information.",
|
||||
CredentialEnvironmentVariable,
|
||||
HelpPermalink));
|
||||
}
|
||||
|
||||
/// <summary>Creates a default credential from a JSON file.</summary>
|
||||
private GoogleCredential CreateDefaultCredentialFromFile(string credentialPath)
|
||||
{
|
||||
Logger.Debug("Loading Credential from file {0}", credentialPath);
|
||||
|
||||
using (Stream stream = GetStream(credentialPath))
|
||||
{
|
||||
return CreateDefaultCredentialFromStream(stream);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Creates a default credential from a stream that contains JSON credential data.</summary>
|
||||
internal GoogleCredential CreateDefaultCredentialFromStream(Stream stream)
|
||||
{
|
||||
JsonCredentialParameters credentialParameters;
|
||||
try
|
||||
{
|
||||
credentialParameters = NewtonsoftJsonSerializer.Instance.Deserialize<JsonCredentialParameters>(stream);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
throw new InvalidOperationException("Error deserializing JSON credential data.", e);
|
||||
}
|
||||
return CreateDefaultCredentialFromParameters(credentialParameters);
|
||||
}
|
||||
|
||||
/// <summary>Creates a default credential from a string that contains JSON credential data.</summary>
|
||||
internal GoogleCredential CreateDefaultCredentialFromJson(string json)
|
||||
{
|
||||
JsonCredentialParameters credentialParameters;
|
||||
try
|
||||
{
|
||||
credentialParameters = NewtonsoftJsonSerializer.Instance.Deserialize<JsonCredentialParameters>(json);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
throw new InvalidOperationException("Error deserializing JSON credential data.", e);
|
||||
}
|
||||
return CreateDefaultCredentialFromParameters(credentialParameters);
|
||||
}
|
||||
|
||||
|
||||
/// <summary>Creates a default credential from JSON data.</summary>
|
||||
private static GoogleCredential CreateDefaultCredentialFromParameters(JsonCredentialParameters credentialParameters)
|
||||
{
|
||||
switch (credentialParameters.Type)
|
||||
{
|
||||
case JsonCredentialParameters.AuthorizedUserCredentialType:
|
||||
return new GoogleCredential(CreateUserCredentialFromParameters(credentialParameters));
|
||||
case JsonCredentialParameters.ServiceAccountCredentialType:
|
||||
return GoogleCredential.FromCredential(
|
||||
CreateServiceAccountCredentialFromParameters(credentialParameters));
|
||||
default:
|
||||
throw new InvalidOperationException(
|
||||
String.Format("Error creating credential from JSON. Unrecognized credential type {0}.",
|
||||
credentialParameters.Type));
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Creates a user credential from JSON data.</summary>
|
||||
private static UserCredential CreateUserCredentialFromParameters(JsonCredentialParameters credentialParameters)
|
||||
{
|
||||
if (credentialParameters.Type != JsonCredentialParameters.AuthorizedUserCredentialType ||
|
||||
string.IsNullOrEmpty(credentialParameters.ClientId) ||
|
||||
string.IsNullOrEmpty(credentialParameters.ClientSecret))
|
||||
{
|
||||
throw new InvalidOperationException("JSON data does not represent a valid user credential.");
|
||||
}
|
||||
|
||||
var token = new TokenResponse
|
||||
{
|
||||
RefreshToken = credentialParameters.RefreshToken
|
||||
};
|
||||
|
||||
var initializer = new GoogleAuthorizationCodeFlow.Initializer
|
||||
{
|
||||
ClientSecrets = new ClientSecrets
|
||||
{
|
||||
ClientId = credentialParameters.ClientId,
|
||||
ClientSecret = credentialParameters.ClientSecret
|
||||
}
|
||||
};
|
||||
var flow = new GoogleAuthorizationCodeFlow(initializer);
|
||||
return new UserCredential(flow, "ApplicationDefaultCredentials", token);
|
||||
}
|
||||
|
||||
/// <summary>Creates a <see cref="ServiceAccountCredential"/> from JSON data.</summary>
|
||||
private static ServiceAccountCredential CreateServiceAccountCredentialFromParameters(
|
||||
JsonCredentialParameters credentialParameters)
|
||||
{
|
||||
if (credentialParameters.Type != JsonCredentialParameters.ServiceAccountCredentialType ||
|
||||
string.IsNullOrEmpty(credentialParameters.ClientEmail) ||
|
||||
string.IsNullOrEmpty(credentialParameters.PrivateKey))
|
||||
{
|
||||
throw new InvalidOperationException("JSON data does not represent a valid service account credential.");
|
||||
}
|
||||
var initializer = new ServiceAccountCredential.Initializer(credentialParameters.ClientEmail);
|
||||
return new ServiceAccountCredential(initializer.FromPrivateKey(credentialParameters.PrivateKey));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns platform-specific well known credential file path. This file is created by
|
||||
/// <a href="https://cloud.google.com/sdk/gcloud/reference/auth/login">gcloud auth login</a>
|
||||
/// </summary>
|
||||
private string GetWellKnownCredentialFilePath()
|
||||
{
|
||||
var appData = GetEnvironmentVariable(AppdataEnvironmentVariable);
|
||||
if (appData != null) {
|
||||
return Path.Combine(appData, CloudSDKConfigDirectoryWindows, WellKnownCredentialsFile);
|
||||
}
|
||||
var unixHome = GetEnvironmentVariable(HomeEnvironmentVariable);
|
||||
if (unixHome != null)
|
||||
{
|
||||
return Path.Combine(unixHome, CloudSDKConfigDirectoryUnix, WellKnownCredentialsFile);
|
||||
}
|
||||
return Path.Combine(CloudSDKConfigDirectoryWindows, WellKnownCredentialsFile);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the environment variable.
|
||||
/// This method is protected so it could be overriden for testing purposes only.
|
||||
/// </summary>
|
||||
protected virtual string GetEnvironmentVariable(string variableName)
|
||||
{
|
||||
return Environment.GetEnvironmentVariable(variableName);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Opens file as a stream.
|
||||
/// This method is protected so it could be overriden for testing purposes only.
|
||||
/// </summary>
|
||||
protected virtual Stream GetStream(string filePath)
|
||||
{
|
||||
return new FileStream(filePath, FileMode.Open, FileAccess.Read);
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,343 @@
|
||||
/*
|
||||
Copyright 2013 Google Inc
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
using Google.Apis.Auth.OAuth2.Requests;
|
||||
using Google.Apis.Auth.OAuth2.Responses;
|
||||
using Google.Apis.Http;
|
||||
using Google.Apis.Logging;
|
||||
using Google.Apis.Util;
|
||||
using Google.Apis.Util.Store;
|
||||
using Google.Apis.Testing;
|
||||
using System.Net;
|
||||
|
||||
namespace Google.Apis.Auth.OAuth2.Flows
|
||||
{
|
||||
/// <summary>
|
||||
/// Thread-safe OAuth 2.0 authorization code flow that manages and persists end-user credentials.
|
||||
/// <para>
|
||||
/// This is designed to simplify the flow in which an end-user authorizes the application to access their protected
|
||||
/// data, and then the application has access to their data based on an access token and a refresh token to refresh
|
||||
/// that access token when it expires.
|
||||
/// </para>
|
||||
/// </summary>
|
||||
public class AuthorizationCodeFlow : IAuthorizationCodeFlow
|
||||
{
|
||||
private static readonly ILogger Logger = ApplicationContext.Logger.ForType<AuthorizationCodeFlow>();
|
||||
|
||||
#region Initializer
|
||||
|
||||
/// <summary>An initializer class for the authorization code flow. </summary>
|
||||
public class Initializer
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets or sets the method for presenting the access token to the resource server.
|
||||
/// The default value is
|
||||
/// <see cref="Google.Apis.Auth.OAuth2.BearerToken.AuthorizationHeaderAccessMethod"/>.
|
||||
/// </summary>
|
||||
public IAccessMethod AccessMethod { get; set; }
|
||||
|
||||
/// <summary>Gets the token server URL.</summary>
|
||||
public string TokenServerUrl { get; private set; }
|
||||
|
||||
/// <summary>Gets or sets the authorization server URL.</summary>
|
||||
public string AuthorizationServerUrl { get; private set; }
|
||||
|
||||
/// <summary>Gets or sets the client secrets which includes the client identifier and its secret.</summary>
|
||||
public ClientSecrets ClientSecrets { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the client secrets stream which contains the client identifier and its secret.
|
||||
/// </summary>
|
||||
/// <remarks>The AuthorizationCodeFlow constructor is responsible for disposing the stream.</remarks>
|
||||
public Stream ClientSecretsStream { get; set; }
|
||||
|
||||
/// <summary>Gets or sets the data store used to store the token response.</summary>
|
||||
public IDataStore DataStore { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the scopes which indicate the API access your application is requesting.
|
||||
/// </summary>
|
||||
public IEnumerable<string> Scopes { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the factory for creating <see cref="System.Net.Http.HttpClient"/> instance.
|
||||
/// </summary>
|
||||
public IHttpClientFactory HttpClientFactory { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Get or sets the exponential back-off policy. Default value is <c>UnsuccessfulResponse503</c>, which
|
||||
/// means that exponential back-off is used on 503 abnormal HTTP responses.
|
||||
/// If the value is set to <c>None</c>, no exponential back-off policy is used, and it's up to user to
|
||||
/// configure the <see cref="Google.Apis.Http.ConfigurableMessageHandler"/> in an
|
||||
/// <see cref="Google.Apis.Http.IConfigurableHttpClientInitializer"/> to set a specific back-off
|
||||
/// implementation (using <see cref="Google.Apis.Http.BackOffHandler"/>).
|
||||
/// </summary>
|
||||
public ExponentialBackOffPolicy DefaultExponentialBackOffPolicy { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the clock. The clock is used to determine if the token has expired, if so we will try to
|
||||
/// refresh it. The default value is <see cref="Google.Apis.Util.SystemClock.Default"/>.
|
||||
/// </summary>
|
||||
public IClock Clock { get; set; }
|
||||
|
||||
/// <summary>Constructs a new initializer.</summary>
|
||||
/// <param name="authorizationServerUrl">Authorization server URL</param>
|
||||
/// <param name="tokenServerUrl">Token server URL</param>
|
||||
public Initializer(string authorizationServerUrl, string tokenServerUrl)
|
||||
{
|
||||
AuthorizationServerUrl = authorizationServerUrl;
|
||||
TokenServerUrl = tokenServerUrl;
|
||||
|
||||
Scopes = new List<string>();
|
||||
AccessMethod = new BearerToken.AuthorizationHeaderAccessMethod();
|
||||
DefaultExponentialBackOffPolicy = ExponentialBackOffPolicy.UnsuccessfulResponse503;
|
||||
Clock = SystemClock.Default;
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Readonly fields
|
||||
|
||||
private readonly IAccessMethod accessMethod;
|
||||
private readonly string tokenServerUrl;
|
||||
private readonly string authorizationServerUrl;
|
||||
private readonly ClientSecrets clientSecrets;
|
||||
private readonly IDataStore dataStore;
|
||||
private readonly IEnumerable<string> scopes;
|
||||
private readonly ConfigurableHttpClient httpClient;
|
||||
private readonly IClock clock;
|
||||
|
||||
#endregion
|
||||
|
||||
/// <summary>Gets the token server URL.</summary>
|
||||
public string TokenServerUrl { get { return tokenServerUrl; } }
|
||||
|
||||
/// <summary>Gets the authorization code server URL.</summary>
|
||||
public string AuthorizationServerUrl { get { return authorizationServerUrl; } }
|
||||
|
||||
/// <summary>Gets the client secrets which includes the client identifier and its secret.</summary>
|
||||
public ClientSecrets ClientSecrets { get { return clientSecrets; } }
|
||||
|
||||
/// <summary>Gets the data store used to store the credentials.</summary>
|
||||
public IDataStore DataStore { get { return dataStore; } }
|
||||
|
||||
/// <summary>Gets the scopes which indicate the API access your application is requesting.</summary>
|
||||
public IEnumerable<string> Scopes { get { return scopes; } }
|
||||
|
||||
/// <summary>Gets the HTTP client used to make authentication requests to the server.</summary>
|
||||
public ConfigurableHttpClient HttpClient { get { return httpClient; } }
|
||||
|
||||
/// <summary>Constructs a new flow using the initializer's properties.</summary>
|
||||
public AuthorizationCodeFlow(Initializer initializer)
|
||||
{
|
||||
clientSecrets = initializer.ClientSecrets;
|
||||
if (clientSecrets == null)
|
||||
{
|
||||
if (initializer.ClientSecretsStream == null)
|
||||
{
|
||||
throw new ArgumentException("You MUST set ClientSecret or ClientSecretStream on the initializer");
|
||||
}
|
||||
|
||||
using (initializer.ClientSecretsStream)
|
||||
{
|
||||
clientSecrets = GoogleClientSecrets.Load(initializer.ClientSecretsStream).Secrets;
|
||||
}
|
||||
}
|
||||
else if (initializer.ClientSecretsStream != null)
|
||||
{
|
||||
throw new ArgumentException(
|
||||
"You CAN'T set both ClientSecrets AND ClientSecretStream on the initializer");
|
||||
}
|
||||
|
||||
accessMethod = initializer.AccessMethod.ThrowIfNull("Initializer.AccessMethod");
|
||||
clock = initializer.Clock.ThrowIfNull("Initializer.Clock");
|
||||
tokenServerUrl = initializer.TokenServerUrl.ThrowIfNullOrEmpty("Initializer.TokenServerUrl");
|
||||
authorizationServerUrl = initializer.AuthorizationServerUrl.ThrowIfNullOrEmpty
|
||||
("Initializer.AuthorizationServerUrl");
|
||||
|
||||
dataStore = initializer.DataStore;
|
||||
if (dataStore == null)
|
||||
{
|
||||
Logger.Warning("Datastore is null, as a result the user's credential will not be stored");
|
||||
}
|
||||
scopes = initializer.Scopes;
|
||||
|
||||
// Set the HTTP client.
|
||||
var httpArgs = new CreateHttpClientArgs();
|
||||
|
||||
// Add exponential back-off initializer if necessary.
|
||||
if (initializer.DefaultExponentialBackOffPolicy != ExponentialBackOffPolicy.None)
|
||||
{
|
||||
httpArgs.Initializers.Add(
|
||||
new ExponentialBackOffInitializer(initializer.DefaultExponentialBackOffPolicy,
|
||||
() => new BackOffHandler(new ExponentialBackOff())));
|
||||
}
|
||||
httpClient = (initializer.HttpClientFactory ?? new HttpClientFactory()).CreateHttpClient(httpArgs);
|
||||
}
|
||||
|
||||
#region IAuthorizationCodeFlow overrides
|
||||
|
||||
/// <inheritdoc/>
|
||||
public IAccessMethod AccessMethod { get { return accessMethod; } }
|
||||
|
||||
/// <inheritdoc/>
|
||||
public IClock Clock { get { return clock; } }
|
||||
|
||||
/// <inheritdoc/>
|
||||
public async Task<TokenResponse> LoadTokenAsync(string userId, CancellationToken taskCancellationToken)
|
||||
{
|
||||
taskCancellationToken.ThrowIfCancellationRequested();
|
||||
if (DataStore == null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
return await DataStore.GetAsync<TokenResponse>(userId).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public async Task DeleteTokenAsync(string userId, CancellationToken taskCancellationToken)
|
||||
{
|
||||
taskCancellationToken.ThrowIfCancellationRequested();
|
||||
if (DataStore != null)
|
||||
{
|
||||
await DataStore.DeleteAsync<TokenResponse>(userId).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public virtual AuthorizationCodeRequestUrl CreateAuthorizationCodeRequest(string redirectUri)
|
||||
{
|
||||
return new AuthorizationCodeRequestUrl(new Uri(AuthorizationServerUrl))
|
||||
{
|
||||
ClientId = ClientSecrets.ClientId,
|
||||
Scope = string.Join(" ", Scopes),
|
||||
RedirectUri = redirectUri
|
||||
};
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public async Task<TokenResponse> ExchangeCodeForTokenAsync(string userId, string code, string redirectUri,
|
||||
CancellationToken taskCancellationToken)
|
||||
{
|
||||
var authorizationCodeTokenReq = new AuthorizationCodeTokenRequest
|
||||
{
|
||||
Scope = string.Join(" ", Scopes),
|
||||
RedirectUri = redirectUri,
|
||||
Code = code,
|
||||
};
|
||||
|
||||
var token = await FetchTokenAsync(userId, authorizationCodeTokenReq, taskCancellationToken)
|
||||
.ConfigureAwait(false);
|
||||
await StoreTokenAsync(userId, token, taskCancellationToken).ConfigureAwait(false);
|
||||
return token;
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public async Task<TokenResponse> RefreshTokenAsync(string userId, string refreshToken,
|
||||
CancellationToken taskCancellationToken)
|
||||
{
|
||||
var refreshTokenReq = new RefreshTokenRequest
|
||||
{
|
||||
RefreshToken = refreshToken,
|
||||
};
|
||||
var token = await FetchTokenAsync(userId, refreshTokenReq, taskCancellationToken).ConfigureAwait(false);
|
||||
|
||||
// The new token may not contain a refresh token, so set it with the given refresh token.
|
||||
if (token.RefreshToken == null)
|
||||
{
|
||||
token.RefreshToken = refreshToken;
|
||||
}
|
||||
|
||||
await StoreTokenAsync(userId, token, taskCancellationToken).ConfigureAwait(false);
|
||||
return token;
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public virtual Task RevokeTokenAsync(string userId, string token, CancellationToken taskCancellationToken)
|
||||
{
|
||||
throw new NotImplementedException("The OAuth 2.0 protocol does not support token revocation.");
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public virtual bool ShouldForceTokenRetrieval() { return false; }
|
||||
|
||||
#endregion
|
||||
|
||||
/// <summary>Stores the token in the <see cref="DataStore"/>.</summary>
|
||||
/// <param name="userId">User identifier.</param>
|
||||
/// <param name="token">Token to store.</param>
|
||||
/// <param name="taskCancellationToken">Cancellation token to cancel operation.</param>
|
||||
private async Task StoreTokenAsync(string userId, TokenResponse token, CancellationToken taskCancellationToken)
|
||||
{
|
||||
taskCancellationToken.ThrowIfCancellationRequested();
|
||||
if (DataStore != null)
|
||||
{
|
||||
await DataStore.StoreAsync<TokenResponse>(userId, token).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Retrieve a new token from the server using the specified request.</summary>
|
||||
/// <param name="userId">User identifier.</param>
|
||||
/// <param name="request">Token request.</param>
|
||||
/// <param name="taskCancellationToken">Cancellation token to cancel operation.</param>
|
||||
/// <returns>Token response with the new access token.</returns>
|
||||
[VisibleForTestOnly]
|
||||
public async Task<TokenResponse> FetchTokenAsync(string userId, TokenRequest request,
|
||||
CancellationToken taskCancellationToken)
|
||||
{
|
||||
// Add client id and client secret to requests.
|
||||
request.ClientId = ClientSecrets.ClientId;
|
||||
request.ClientSecret = ClientSecrets.ClientSecret;
|
||||
|
||||
try
|
||||
{
|
||||
var tokenResponse = await request.ExecuteAsync
|
||||
(httpClient, TokenServerUrl, taskCancellationToken, Clock).ConfigureAwait(false);
|
||||
return tokenResponse;
|
||||
}
|
||||
catch (TokenResponseException ex)
|
||||
{
|
||||
// In case there is an exception during getting the token, we delete any user's token information from
|
||||
// the data store if it's not a server-side error.
|
||||
int statusCode = (int)(ex.StatusCode ?? (HttpStatusCode)0);
|
||||
bool serverError = statusCode >= 500 && statusCode < 600;
|
||||
if (!serverError)
|
||||
{
|
||||
// If not a server error, then delete the user token information.
|
||||
// This is to guard against suspicious client-side behaviour.
|
||||
await DeleteTokenAsync(userId, taskCancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public void Dispose()
|
||||
{
|
||||
if (HttpClient != null)
|
||||
{
|
||||
HttpClient.Dispose();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,143 @@
|
||||
/*
|
||||
Copyright 2013 Google Inc
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Net.Http;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
using Google.Apis.Auth.OAuth2.Requests;
|
||||
using Google.Apis.Auth.OAuth2.Responses;
|
||||
using Google.Apis.Json;
|
||||
|
||||
namespace Google.Apis.Auth.OAuth2.Flows
|
||||
{
|
||||
/// <summary>
|
||||
/// Google specific authorization code flow which inherits from <see cref="AuthorizationCodeFlow"/>.
|
||||
/// </summary>
|
||||
public class GoogleAuthorizationCodeFlow : AuthorizationCodeFlow
|
||||
{
|
||||
private readonly string revokeTokenUrl;
|
||||
|
||||
/// <summary>Gets the token revocation URL.</summary>
|
||||
public string RevokeTokenUrl { get { return revokeTokenUrl; } }
|
||||
|
||||
/// <summary>Gets or sets the include granted scopes indicator.
|
||||
/// Do not use, use <see cref="IncludeGrantedScopes"/> instead.</summary>
|
||||
public readonly bool? includeGrantedScopes;
|
||||
|
||||
/// <summary>Gets or sets the include granted scopes indicator.</summary>
|
||||
public bool? IncludeGrantedScopes { get { return includeGrantedScopes; } }
|
||||
|
||||
private readonly IEnumerable<KeyValuePair<string, string>> userDefinedQueryParams;
|
||||
|
||||
/// <summary>Gets the user defined query parameters.</summary>
|
||||
public IEnumerable<KeyValuePair<string, string>> UserDefinedQueryParams
|
||||
{
|
||||
get { return userDefinedQueryParams; }
|
||||
}
|
||||
|
||||
/// <summary>Constructs a new Google authorization code flow.</summary>
|
||||
public GoogleAuthorizationCodeFlow(Initializer initializer)
|
||||
: base(initializer)
|
||||
{
|
||||
revokeTokenUrl = initializer.RevokeTokenUrl;
|
||||
includeGrantedScopes = initializer.IncludeGrantedScopes;
|
||||
userDefinedQueryParams = initializer.UserDefinedQueryParams;
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override AuthorizationCodeRequestUrl CreateAuthorizationCodeRequest(string redirectUri)
|
||||
{
|
||||
return new GoogleAuthorizationCodeRequestUrl(new Uri(AuthorizationServerUrl))
|
||||
{
|
||||
ClientId = ClientSecrets.ClientId,
|
||||
Scope = string.Join(" ", Scopes),
|
||||
RedirectUri = redirectUri,
|
||||
IncludeGrantedScopes = IncludeGrantedScopes.HasValue
|
||||
? IncludeGrantedScopes.Value.ToString().ToLower() : null,
|
||||
UserDefinedQueryParams = UserDefinedQueryParams
|
||||
};
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override async Task RevokeTokenAsync(string userId, string token,
|
||||
CancellationToken taskCancellationToken)
|
||||
{
|
||||
GoogleRevokeTokenRequest request = new GoogleRevokeTokenRequest(new Uri(RevokeTokenUrl))
|
||||
{
|
||||
Token = token
|
||||
};
|
||||
var httpRequest = new HttpRequestMessage(HttpMethod.Get, request.Build());
|
||||
|
||||
var response = await HttpClient.SendAsync(httpRequest, taskCancellationToken).ConfigureAwait(false);
|
||||
if (!response.IsSuccessStatusCode)
|
||||
{
|
||||
var content = await response.Content.ReadAsStringAsync().ConfigureAwait(false);
|
||||
var error = NewtonsoftJsonSerializer.Instance.Deserialize<TokenErrorResponse>(content);
|
||||
throw new TokenResponseException(error, response.StatusCode);
|
||||
}
|
||||
|
||||
await DeleteTokenAsync(userId, taskCancellationToken);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override bool ShouldForceTokenRetrieval()
|
||||
{
|
||||
return IncludeGrantedScopes.HasValue && IncludeGrantedScopes.Value;
|
||||
}
|
||||
|
||||
/// <summary>An initializer class for Google authorization code flow. </summary>
|
||||
public new class Initializer : AuthorizationCodeFlow.Initializer
|
||||
{
|
||||
/// <summary>Gets or sets the token revocation URL.</summary>
|
||||
public string RevokeTokenUrl { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the optional indicator for including granted scopes for incremental authorization.
|
||||
/// </summary>
|
||||
public bool? IncludeGrantedScopes { get; set; }
|
||||
|
||||
/// <summary>Gets or sets the optional user defined query parameters.</summary>
|
||||
public IEnumerable<KeyValuePair<string, string>> UserDefinedQueryParams { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Constructs a new initializer. Sets Authorization server URL to
|
||||
/// <see cref="Google.Apis.Auth.OAuth2.GoogleAuthConsts.OidcAuthorizationUrl"/>, and Token server URL to
|
||||
/// <see cref="Google.Apis.Auth.OAuth2.GoogleAuthConsts.OidcTokenUrl"/>.
|
||||
/// </summary>
|
||||
public Initializer() : this(
|
||||
GoogleAuthConsts.OidcAuthorizationUrl, GoogleAuthConsts.OidcTokenUrl, GoogleAuthConsts.RevokeTokenUrl)
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>Constructs a new initializer.</summary>
|
||||
/// <param name="authorizationServerUrl">Authorization server URL</param>
|
||||
/// <param name="tokenServerUrl">Token server URL</param>
|
||||
/// <param name="revokeTokenUrl">Revocation server URL</param>
|
||||
/// <remarks>
|
||||
/// This is mainly for internal testing at Google, where we occasionally need
|
||||
/// to use alternative oauth endpoints. This is not for general use.
|
||||
/// </remarks>
|
||||
protected Initializer(string authorizationServerUrl, string tokenServerUrl, string revokeTokenUrl)
|
||||
: base(authorizationServerUrl, tokenServerUrl)
|
||||
{
|
||||
RevokeTokenUrl = revokeTokenUrl;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,95 @@
|
||||
/*
|
||||
Copyright 2013 Google Inc
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
using System;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
using Google.Apis.Auth.OAuth2.Responses;
|
||||
using Google.Apis.Auth.OAuth2.Requests;
|
||||
using Google.Apis.Util;
|
||||
using Google.Apis.Util.Store;
|
||||
|
||||
namespace Google.Apis.Auth.OAuth2.Flows
|
||||
{
|
||||
/// <summary>OAuth 2.0 authorization code flow that manages and persists end-user credentials.</summary>
|
||||
public interface IAuthorizationCodeFlow : IDisposable
|
||||
{
|
||||
/// <summary>Gets the method for presenting the access token to the resource server.</summary>
|
||||
IAccessMethod AccessMethod { get; }
|
||||
|
||||
/// <summary>Gets the clock.</summary>
|
||||
IClock Clock { get; }
|
||||
|
||||
/// <summary>Gets the data store used to store the credentials.</summary>
|
||||
IDataStore DataStore { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Asynchronously loads the user's token using the flow's
|
||||
/// <see cref="Google.Apis.Util.Store.IDataStore"/>.
|
||||
/// </summary>
|
||||
/// <param name="userId">User identifier</param>
|
||||
/// <param name="taskCancellationToken">Cancellation token to cancel operation</param>
|
||||
/// <returns>Token response</returns>
|
||||
Task<TokenResponse> LoadTokenAsync(string userId, CancellationToken taskCancellationToken);
|
||||
|
||||
/// <summary>
|
||||
/// Asynchronously deletes the user's token using the flow's
|
||||
/// <see cref="Google.Apis.Util.Store.IDataStore"/>.
|
||||
/// </summary>
|
||||
/// <param name="userId">User identifier.</param>
|
||||
/// <param name="taskCancellationToken">Cancellation token to cancel operation.</param>
|
||||
Task DeleteTokenAsync(string userId, CancellationToken taskCancellationToken);
|
||||
|
||||
/// <summary>Creates an authorization code request with the specified redirect URI.</summary>
|
||||
AuthorizationCodeRequestUrl CreateAuthorizationCodeRequest(string redirectUri);
|
||||
|
||||
/// <summary>Asynchronously exchanges code with a token.</summary>
|
||||
/// <param name="userId">User identifier.</param>
|
||||
/// <param name="code">Authorization code received from the authorization server.</param>
|
||||
/// <param name="redirectUri">Redirect URI which is used in the token request.</param>
|
||||
/// <param name="taskCancellationToken">Cancellation token to cancel operation.</param>
|
||||
/// <returns>Token response which contains the access token.</returns>
|
||||
Task<TokenResponse> ExchangeCodeForTokenAsync(string userId, string code, string redirectUri,
|
||||
CancellationToken taskCancellationToken);
|
||||
|
||||
/// <summary>Asynchronously refreshes an access token using a refresh token.</summary>
|
||||
/// <param name="userId">User identifier.</param>
|
||||
/// <param name="refreshToken">Refresh token which is used to get a new access token.</param>
|
||||
/// <param name="taskCancellationToken">Cancellation token to cancel operation.</param>
|
||||
/// <returns>Token response which contains the access token and the input refresh token.</returns>
|
||||
Task<TokenResponse> RefreshTokenAsync(string userId, string refreshToken,
|
||||
CancellationToken taskCancellationToken);
|
||||
|
||||
/// <summary>
|
||||
/// Asynchronously revokes the specified token. This method disconnects the user's account from the OAuth 2.0
|
||||
/// application. It should be called upon removing the user account from the site.</summary>
|
||||
/// <remarks>
|
||||
/// If revoking the token succeeds, the user's credential is removed from the data store and the user MUST
|
||||
/// authorize the application again before the application can access the user's private resources.
|
||||
/// </remarks>
|
||||
/// <param name="userId">User identifier.</param>
|
||||
/// <param name="token">Access token to be revoked.</param>
|
||||
/// <param name="taskCancellationToken">Cancellation token to cancel operation.</param>
|
||||
/// <returns><c>true</c> if the token was revoked successfully.</returns>
|
||||
Task RevokeTokenAsync(string userId, string token, CancellationToken taskCancellationToken);
|
||||
|
||||
/// <summary>
|
||||
/// Indicates if a new token needs to be retrieved and stored regardless of normal circumstances.
|
||||
/// </summary>
|
||||
bool ShouldForceTokenRetrieval();
|
||||
}
|
||||
}
|
@ -0,0 +1,66 @@
|
||||
/*
|
||||
Copyright 2013 Google Inc
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
namespace Google.Apis.Auth.OAuth2
|
||||
{
|
||||
/// <summary>
|
||||
/// Google OAuth2 constants.
|
||||
/// Canonical source for these URLs is: https://accounts.google.com/.well-known/openid-configuration
|
||||
/// </summary>
|
||||
public static class GoogleAuthConsts
|
||||
{
|
||||
/// <summary>The authorization code server URL.</summary>
|
||||
public const string AuthorizationUrl = "https://accounts.google.com/o/oauth2/auth";
|
||||
|
||||
/// <summary>The OpenID Connect authorization code server URL.</summary>
|
||||
/// <remarks>
|
||||
/// Use of this <see cref="OidcAuthorizationUrl"/> is not 100% compatible with using
|
||||
/// <see cref="AuthorizationUrl"/>, so they are two distinct URLs.
|
||||
/// Internally within this library only this more up-to-date <see cref="OidcAuthorizationUrl"/> is used.
|
||||
/// </remarks>
|
||||
public const string OidcAuthorizationUrl = "https://accounts.google.com/o/oauth2/v2/auth";
|
||||
|
||||
/// <summary>The approval URL (used in the Windows solution as a callback).</summary>
|
||||
public const string ApprovalUrl = "https://accounts.google.com/o/oauth2/approval";
|
||||
|
||||
/// <summary>The authorization token server URL.</summary>
|
||||
public const string TokenUrl = "https://accounts.google.com/o/oauth2/token";
|
||||
|
||||
/// <summary>The OpenID Connect authorization token server URL.</summary>
|
||||
/// <remarks>
|
||||
/// Use of this <see cref="OidcTokenUrl"/> is not 100% compatible with using
|
||||
/// <see cref="TokenUrl"/>, so they are two distinct URLs.
|
||||
/// Internally within this library only this more up-to-date <see cref="OidcTokenUrl"/> is used.
|
||||
/// </remarks>
|
||||
public const string OidcTokenUrl = "https://www.googleapis.com/oauth2/v4/token";
|
||||
|
||||
/// <summary>The Compute Engine authorization token server URL</summary>
|
||||
public const string ComputeTokenUrl =
|
||||
"http://metadata/computeMetadata/v1/instance/service-accounts/default/token";
|
||||
|
||||
/// <summary>The path to the Google revocation endpoint.</summary>
|
||||
public const string RevokeTokenUrl = "https://accounts.google.com/o/oauth2/revoke";
|
||||
|
||||
/// <summary>The OpenID Connect Json Web Key Set (jwks) URL.</summary>
|
||||
public const string JsonWebKeySetUrl = "https://www.googleapis.com/oauth2/v3/certs";
|
||||
|
||||
/// <summary>Installed application redirect URI.</summary>
|
||||
public const string InstalledAppRedirectUri = "urn:ietf:wg:oauth:2.0:oob";
|
||||
|
||||
/// <summary>Installed application localhost redirect URI.</summary>
|
||||
public const string LocalhostRedirectUri = "http://localhost";
|
||||
}
|
||||
}
|
@ -0,0 +1,57 @@
|
||||
/*
|
||||
Copyright 2013 Google Inc
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
using System;
|
||||
using System.IO;
|
||||
|
||||
using Google.Apis.Json;
|
||||
|
||||
namespace Google.Apis.Auth.OAuth2
|
||||
{
|
||||
/// <summary>
|
||||
/// OAuth 2.0 client secrets model as specified in https://cloud.google.com/console/.
|
||||
/// </summary>
|
||||
public sealed class GoogleClientSecrets
|
||||
{
|
||||
/// <summary>Gets or sets the details for installed applications.</summary>
|
||||
[Newtonsoft.Json.JsonProperty("installed")]
|
||||
private ClientSecrets Installed { get; set; }
|
||||
|
||||
/// <summary>Gets or sets the details for web applications.</summary>
|
||||
[Newtonsoft.Json.JsonProperty("web")]
|
||||
private ClientSecrets Web { get; set; }
|
||||
|
||||
/// <summary>Gets the client secrets which contains the client identifier and client secret. </summary>
|
||||
public ClientSecrets Secrets
|
||||
{
|
||||
get
|
||||
{
|
||||
if (Installed == null && Web == null)
|
||||
{
|
||||
throw new InvalidOperationException(
|
||||
"At least one client secrets (Installed or Web) should be set");
|
||||
}
|
||||
return Installed ?? Web;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Loads the Google client secret from the input stream.</summary>
|
||||
public static GoogleClientSecrets Load(Stream stream)
|
||||
{
|
||||
return NewtonsoftJsonSerializer.Instance.Deserialize<GoogleClientSecrets>(stream);
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,222 @@
|
||||
/*
|
||||
Copyright 2015 Google Inc
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
using Google.Apis.Http;
|
||||
|
||||
namespace Google.Apis.Auth.OAuth2
|
||||
{
|
||||
/// <summary>
|
||||
/// Credential for authorizing calls using OAuth 2.0.
|
||||
/// It is a convenience wrapper that allows handling of different types of
|
||||
/// credentials (like <see cref="ServiceAccountCredential"/>, <see cref="ComputeCredential"/>
|
||||
/// or <see cref="UserCredential"/>) in a unified way.
|
||||
/// <para>
|
||||
/// See <see cref="GetApplicationDefaultAsync"/> for the credential retrieval logic.
|
||||
/// </para>
|
||||
/// </summary>
|
||||
public class GoogleCredential : ICredential
|
||||
{
|
||||
/// <summary>Provider implements the logic for creating the application default credential.</summary>
|
||||
private static DefaultCredentialProvider defaultCredentialProvider = new DefaultCredentialProvider();
|
||||
|
||||
/// <summary>The underlying credential being wrapped by this object.</summary>
|
||||
protected readonly ICredential credential;
|
||||
|
||||
/// <summary>Creates a new <c>GoogleCredential</c>.</summary>
|
||||
internal GoogleCredential(ICredential credential)
|
||||
{
|
||||
this.credential = credential;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// <para>Returns the Application Default Credentials which are ambient credentials that identify and authorize
|
||||
/// the whole application.</para>
|
||||
/// <para>The ambient credentials are determined as following order:</para>
|
||||
/// <list type="number">
|
||||
/// <item>
|
||||
/// <description>
|
||||
/// The environment variable GOOGLE_APPLICATION_CREDENTIALS is checked. If this variable is specified, it
|
||||
/// should point to a file that defines the credentials. The simplest way to get a credential for this purpose
|
||||
/// is to create a service account using the
|
||||
/// <a href="https://console.developers.google.com">Google Developers Console</a> in the section APIs &
|
||||
/// Auth, in the sub-section Credentials. Create a service account or choose an existing one and select
|
||||
/// Generate new JSON key. Set the environment variable to the path of the JSON file downloaded.
|
||||
/// </description>
|
||||
/// </item>
|
||||
/// <item>
|
||||
/// <description>
|
||||
/// If you have installed the Google Cloud SDK on your machine and have run the command
|
||||
/// <a href="https://cloud.google.com/sdk/gcloud/reference/auth/login">GCloud Auth Login</a>, your identity can
|
||||
/// be used as a proxy to test code calling APIs from that machine.
|
||||
/// </description>
|
||||
/// </item>
|
||||
/// <item>
|
||||
/// <description>
|
||||
/// If you are running in Google Compute Engine production, the built-in service account associated with the
|
||||
/// virtual machine instance will be used.
|
||||
/// </description>
|
||||
/// </item>
|
||||
/// <item>
|
||||
/// <description>
|
||||
/// If all previous steps have failed, <c>InvalidOperationException</c> is thrown.
|
||||
/// </description>
|
||||
/// </item>
|
||||
/// </list>
|
||||
/// </summary>
|
||||
/// <returns>A task which completes with the application default credentials.</returns>
|
||||
public static Task<GoogleCredential> GetApplicationDefaultAsync()
|
||||
{
|
||||
return defaultCredentialProvider.GetDefaultCredentialAsync();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// <para>Synchronously returns the Application Default Credentials which are ambient credentials that identify and authorize
|
||||
/// the whole application. See <see cref="GetApplicationDefaultAsync"/> for details on application default credentials.</para>
|
||||
/// <para>This method will block until the credentials are available (or an exception is thrown).
|
||||
/// It is highly preferable to call <see cref="GetApplicationDefaultAsync"/> where possible.</para>
|
||||
/// </summary>
|
||||
/// <returns>The application default credentials.</returns>
|
||||
public static GoogleCredential GetApplicationDefault() => Task.Run(() => GetApplicationDefaultAsync()).Result;
|
||||
|
||||
/// <summary>
|
||||
/// Loads credential from stream containing JSON credential data.
|
||||
/// <para>
|
||||
/// The stream can contain a Service Account key file in JSON format from the Google Developers
|
||||
/// Console or a stored user credential using the format supported by the Cloud SDK.
|
||||
/// </para>
|
||||
/// </summary>
|
||||
public static GoogleCredential FromStream(Stream stream)
|
||||
{
|
||||
return defaultCredentialProvider.CreateDefaultCredentialFromStream(stream);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Loads credential from a string containing JSON credential data.
|
||||
/// <para>
|
||||
/// The string can contain a Service Account key file in JSON format from the Google Developers
|
||||
/// Console or a stored user credential using the format supported by the Cloud SDK.
|
||||
/// </para>
|
||||
/// </summary>
|
||||
public static GoogleCredential FromJson(string json)
|
||||
{
|
||||
return defaultCredentialProvider.CreateDefaultCredentialFromJson(json);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// <para>Returns <c>true</c> only if this credential type has no scopes by default and requires
|
||||
/// a call to <see cref="o:CreateScoped"/> before use.</para>
|
||||
///
|
||||
/// <para>Credentials need to have scopes in them before they can be used to access Google services.
|
||||
/// Some Credential types have scopes built-in, and some don't. This property indicates whether
|
||||
/// the Credential type has scopes built-in.</para>
|
||||
///
|
||||
/// <list type="number">
|
||||
/// <item>
|
||||
/// <description>
|
||||
/// <see cref="ComputeCredential"/> has scopes built-in. Nothing additional is required.
|
||||
/// </description>
|
||||
/// </item>
|
||||
/// <item>
|
||||
/// <description>
|
||||
/// <see cref="UserCredential"/> has scopes built-in, as they were obtained during the consent
|
||||
/// screen. Nothing additional is required.</description>
|
||||
/// </item>
|
||||
/// <item>
|
||||
/// <description>
|
||||
/// <see cref="ServiceAccountCredential"/> does not have scopes built-in by default. Caller should
|
||||
/// invoke <see cref="o:CreateScoped"/> to add scopes to the credential.
|
||||
/// </description>
|
||||
/// </item>
|
||||
/// </list>
|
||||
/// </summary>
|
||||
public virtual bool IsCreateScopedRequired
|
||||
{
|
||||
get { return false; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// If the credential supports scopes, creates a copy with the specified scopes. Otherwise, it returns the same
|
||||
/// instance.
|
||||
/// </summary>
|
||||
public virtual GoogleCredential CreateScoped(IEnumerable<string> scopes)
|
||||
{
|
||||
return this;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// If the credential supports scopes, creates a copy with the specified scopes. Otherwise, it returns the same
|
||||
/// instance.
|
||||
/// </summary>
|
||||
public GoogleCredential CreateScoped(params string[] scopes)
|
||||
{
|
||||
return CreateScoped((IEnumerable<string>) scopes);
|
||||
}
|
||||
|
||||
void IConfigurableHttpClientInitializer.Initialize(ConfigurableHttpClient httpClient)
|
||||
{
|
||||
credential.Initialize(httpClient);
|
||||
}
|
||||
|
||||
Task<string> ITokenAccess.GetAccessTokenForRequestAsync(string authUri, CancellationToken cancellationToken)
|
||||
{
|
||||
return credential.GetAccessTokenForRequestAsync(authUri, cancellationToken);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the underlying credential instance being wrapped.
|
||||
/// </summary>
|
||||
public ICredential UnderlyingCredential => credential;
|
||||
|
||||
/// <summary>Creates a <c>GoogleCredential</c> wrapping a <see cref="ServiceAccountCredential"/>.</summary>
|
||||
internal static GoogleCredential FromCredential(ServiceAccountCredential credential)
|
||||
{
|
||||
return new ServiceAccountGoogleCredential(credential);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Wraps <c>ServiceAccountCredential</c> as <c>GoogleCredential</c>.
|
||||
/// We need this subclass because wrapping <c>ServiceAccountCredential</c> (unlike other wrapped credential
|
||||
/// types) requires special handling for <c>IsCreateScopedRequired</c> and <c>CreateScoped</c> members.
|
||||
/// </summary>
|
||||
internal class ServiceAccountGoogleCredential : GoogleCredential
|
||||
{
|
||||
public ServiceAccountGoogleCredential(ServiceAccountCredential credential)
|
||||
: base(credential) { }
|
||||
|
||||
public override bool IsCreateScopedRequired
|
||||
{
|
||||
get { return !(credential as ServiceAccountCredential).HasScopes; }
|
||||
}
|
||||
|
||||
public override GoogleCredential CreateScoped(IEnumerable<string> scopes)
|
||||
{
|
||||
var serviceAccountCredential = credential as ServiceAccountCredential;
|
||||
var initializer = new ServiceAccountCredential.Initializer(serviceAccountCredential.Id)
|
||||
{
|
||||
User = serviceAccountCredential.User,
|
||||
Key = serviceAccountCredential.Key,
|
||||
Scopes = scopes
|
||||
};
|
||||
return new ServiceAccountGoogleCredential(new ServiceAccountCredential(initializer));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,138 @@
|
||||
/*
|
||||
Copyright 2017 Google Inc
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
using Google.Apis.Auth.OAuth2.Flows;
|
||||
using Google.Apis.Util.Store;
|
||||
|
||||
namespace Google.Apis.Auth.OAuth2
|
||||
{
|
||||
/// <summary>A helper utility to manage the authorization code flow.</summary>
|
||||
public class GoogleWebAuthorizationBroker
|
||||
{
|
||||
// It's unforunate this is a public field. But it cannot be changed due to backward compatibility.
|
||||
/// <summary>The folder which is used by the <see cref="Google.Apis.Util.Store.FileDataStore"/>.</summary>
|
||||
/// <remarks>
|
||||
/// The reason that this is not 'private const' is that a user can change it and store the credentials in a
|
||||
/// different location.
|
||||
/// </remarks>
|
||||
public static string Folder = "Google.Apis.Auth";
|
||||
|
||||
/// <summary>Asynchronously authorizes the specified user.</summary>
|
||||
/// <remarks>
|
||||
/// In case no data store is specified, <see cref="Google.Apis.Util.Store.FileDataStore"/> will be used by
|
||||
/// default.
|
||||
/// </remarks>
|
||||
/// <param name="clientSecrets">The client secrets.</param>
|
||||
/// <param name="scopes">
|
||||
/// The scopes which indicate the Google API access your application is requesting.
|
||||
/// </param>
|
||||
/// <param name="user">The user to authorize.</param>
|
||||
/// <param name="taskCancellationToken">Cancellation token to cancel an operation.</param>
|
||||
/// <param name="dataStore">The data store, if not specified a file data store will be used.</param>
|
||||
/// <param name="codeReceiver">The code receiver, if not specified a local server code receiver will be used.</param>
|
||||
/// <returns>User credential.</returns>
|
||||
public static async Task<UserCredential> AuthorizeAsync(ClientSecrets clientSecrets,
|
||||
IEnumerable<string> scopes, string user, CancellationToken taskCancellationToken,
|
||||
IDataStore dataStore = null, ICodeReceiver codeReceiver = null)
|
||||
{
|
||||
var initializer = new GoogleAuthorizationCodeFlow.Initializer
|
||||
{
|
||||
ClientSecrets = clientSecrets,
|
||||
};
|
||||
return await AuthorizeAsync(initializer, scopes, user, taskCancellationToken, dataStore, codeReceiver)
|
||||
.ConfigureAwait(false);
|
||||
}
|
||||
|
||||
/// <summary>Asynchronously authorizes the specified user.</summary>
|
||||
/// <remarks>
|
||||
/// In case no data store is specified, <see cref="Google.Apis.Util.Store.FileDataStore"/> will be used by
|
||||
/// default.
|
||||
/// </remarks>
|
||||
/// <param name="clientSecretsStream">
|
||||
/// The client secrets stream. The authorization code flow constructor is responsible for disposing the stream.
|
||||
/// </param>
|
||||
/// <param name="scopes">
|
||||
/// The scopes which indicate the Google API access your application is requesting.
|
||||
/// </param>
|
||||
/// <param name="user">The user to authorize.</param>
|
||||
/// <param name="taskCancellationToken">Cancellation token to cancel an operation.</param>
|
||||
/// <param name="dataStore">The data store, if not specified a file data store will be used.</param>
|
||||
/// <param name="codeReceiver">The code receiver, if not specified a local server code receiver will be used.</param>
|
||||
/// <returns>User credential.</returns>
|
||||
public static async Task<UserCredential> AuthorizeAsync(Stream clientSecretsStream,
|
||||
IEnumerable<string> scopes, string user, CancellationToken taskCancellationToken,
|
||||
IDataStore dataStore = null, ICodeReceiver codeReceiver = null)
|
||||
{
|
||||
var initializer = new GoogleAuthorizationCodeFlow.Initializer
|
||||
{
|
||||
ClientSecretsStream = clientSecretsStream,
|
||||
};
|
||||
return await AuthorizeAsync(initializer, scopes, user, taskCancellationToken, dataStore, codeReceiver)
|
||||
.ConfigureAwait(false);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Asynchronously reauthorizes the user. This method should be called if the users want to authorize after
|
||||
/// they revoked the token.
|
||||
/// </summary>
|
||||
/// <param name="userCredential">The current user credential. Its <see cref="UserCredential.Token"/> will be
|
||||
/// updated. </param>
|
||||
/// <param name="taskCancellationToken">Cancellation token to cancel an operation.</param>
|
||||
/// <param name="codeReceiver">The code receiver, if not specified a local server code receiver will be used.</param>
|
||||
public static async Task ReauthorizeAsync(UserCredential userCredential,
|
||||
CancellationToken taskCancellationToken, ICodeReceiver codeReceiver = null)
|
||||
{
|
||||
codeReceiver = codeReceiver ?? new LocalServerCodeReceiver();
|
||||
|
||||
// Create an authorization code installed app instance and authorize the user.
|
||||
UserCredential newUserCredential = await new AuthorizationCodeInstalledApp(userCredential.Flow,
|
||||
codeReceiver).AuthorizeAsync
|
||||
(userCredential.UserId, taskCancellationToken).ConfigureAwait(false);
|
||||
userCredential.Token = newUserCredential.Token;
|
||||
}
|
||||
|
||||
/// <summary>The core logic for asynchronously authorizing the specified user.</summary>
|
||||
/// <param name="initializer">The authorization code initializer.</param>
|
||||
/// <param name="scopes">
|
||||
/// The scopes which indicate the Google API access your application is requesting.
|
||||
/// </param>
|
||||
/// <param name="user">The user to authorize.</param>
|
||||
/// <param name="taskCancellationToken">Cancellation token to cancel an operation.</param>
|
||||
/// <param name="dataStore">The data store, if not specified a file data store will be used.</param>
|
||||
/// <param name="codeReceiver">The code receiver, if not specified a local server code receiver will be used.</param>
|
||||
/// <returns>User credential.</returns>
|
||||
public static async Task<UserCredential> AuthorizeAsync(
|
||||
GoogleAuthorizationCodeFlow.Initializer initializer, IEnumerable<string> scopes, string user,
|
||||
CancellationToken taskCancellationToken, IDataStore dataStore = null,
|
||||
ICodeReceiver codeReceiver = null)
|
||||
{
|
||||
initializer.Scopes = scopes;
|
||||
initializer.DataStore = dataStore ?? new FileDataStore(Folder);
|
||||
|
||||
var flow = new GoogleAuthorizationCodeFlow(initializer);
|
||||
codeReceiver = codeReceiver ?? new LocalServerCodeReceiver();
|
||||
|
||||
// Create an authorization code installed app instance and authorize the user.
|
||||
return await new AuthorizationCodeInstalledApp(flow, codeReceiver).AuthorizeAsync
|
||||
(user, taskCancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,38 @@
|
||||
/*
|
||||
Copyright 2015 Google Inc
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
using System.Net.Http;
|
||||
|
||||
namespace Google.Apis.Auth.OAuth2
|
||||
{
|
||||
/// <summary>
|
||||
/// Method of presenting the access token to the resource server as specified in
|
||||
/// http://tools.ietf.org/html/rfc6749#section-7
|
||||
/// </summary>
|
||||
public interface IAccessMethod
|
||||
{
|
||||
/// <summary>
|
||||
/// Intercepts a HTTP request right before the HTTP request executes by providing the access token.
|
||||
/// </summary>
|
||||
void Intercept(HttpRequestMessage request, string accessToken);
|
||||
|
||||
/// <summary>
|
||||
/// Retrieves the original access token in the HTTP request, as provided in the <see cref=" Intercept"/>
|
||||
/// method.
|
||||
/// </summary>
|
||||
string GetAccessToken(HttpRequestMessage request);
|
||||
}
|
||||
}
|
@ -0,0 +1,41 @@
|
||||
/*
|
||||
Copyright 2013 Google Inc
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
using Google.Apis.Auth.OAuth2.Flows;
|
||||
|
||||
namespace Google.Apis.Auth.OAuth2
|
||||
{
|
||||
/// <summary>
|
||||
/// Authorization code flow for an installed application that persists end-user credentials.
|
||||
/// </summary>
|
||||
public interface IAuthorizationCodeInstalledApp
|
||||
{
|
||||
/// <summary>Gets the authorization code flow.</summary>
|
||||
IAuthorizationCodeFlow Flow { get; }
|
||||
|
||||
/// <summary>Gets the code receiver.</summary>
|
||||
ICodeReceiver CodeReceiver { get; }
|
||||
|
||||
/// <summary>Asynchronously authorizes the installed application to access user's protected data.</summary>
|
||||
/// <param name="userId">User identifier</param>
|
||||
/// <param name="taskCancellationToken">Cancellation token to cancel an operation</param>
|
||||
/// <returns>The user's credential</returns>
|
||||
Task<UserCredential> AuthorizeAsync(string userId, CancellationToken taskCancellationToken);
|
||||
}
|
||||
}
|
@ -0,0 +1,38 @@
|
||||
/*
|
||||
Copyright 2013 Google Inc
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
using Google.Apis.Auth.OAuth2.Requests;
|
||||
using Google.Apis.Auth.OAuth2.Responses;
|
||||
|
||||
namespace Google.Apis.Auth.OAuth2
|
||||
{
|
||||
/// <summary>OAuth 2.0 verification code receiver.</summary>
|
||||
public interface ICodeReceiver
|
||||
{
|
||||
/// <summary>Gets the redirected URI.</summary>
|
||||
string RedirectUri { get; }
|
||||
|
||||
/// <summary>Receives the authorization code.</summary>
|
||||
/// <param name="url">The authorization code request URL</param>
|
||||
/// <param name="taskCancellationToken">Cancellation token</param>
|
||||
/// <returns>The authorization code response</returns>
|
||||
Task<AuthorizationCodeResponseUrl> ReceiveCodeAsync(AuthorizationCodeRequestUrl url,
|
||||
CancellationToken taskCancellationToken);
|
||||
}
|
||||
}
|
@ -0,0 +1,31 @@
|
||||
/*
|
||||
Copyright 2015 Google Inc
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
using Google.Apis.Http;
|
||||
|
||||
namespace Google.Apis.Auth.OAuth2
|
||||
{
|
||||
/// <summary>
|
||||
/// The main interface to represent credential in the client library.
|
||||
/// Service account, User account and Compute credential inherit from this interface
|
||||
/// to provide access token functionality. In addition this interface inherits from
|
||||
/// <see cref="IConfigurableHttpClientInitializer"/> to be able to hook to http requests.
|
||||
/// More details are available in the specific implementations.
|
||||
/// </summary>
|
||||
public interface ICredential : IConfigurableHttpClientInitializer, ITokenAccess
|
||||
{
|
||||
}
|
||||
}
|
@ -0,0 +1,43 @@
|
||||
/*
|
||||
Copyright 2015 Google Inc
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Google.Apis.Auth.OAuth2
|
||||
{
|
||||
/// <summary>
|
||||
/// Allows direct retrieval of access tokens to authenticate requests.
|
||||
/// This is necessary for workflows where you don't want to use
|
||||
/// <see cref="T:Google.Apis.Services.BaseClientService"/> to access the API.
|
||||
/// (e.g. gRPC that implemenents the entire HTTP2 stack internally).
|
||||
/// </summary>
|
||||
public interface ITokenAccess
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets an access token to authorize a request.
|
||||
/// Implementations should handle automatic refreshes of the token
|
||||
/// if they are supported.
|
||||
/// The <paramref name="authUri"/> might be required by some credential types
|
||||
/// (e.g. the JWT access token) while other credential types
|
||||
/// migth just ignore it.
|
||||
/// </summary>
|
||||
/// <param name="authUri">The URI the returned token will grant access to.</param>
|
||||
/// <param name="cancellationToken">The cancellation token.</param>
|
||||
/// <returns>The access token.</returns>
|
||||
Task<string> GetAccessTokenForRequestAsync(string authUri = null, CancellationToken cancellationToken = default(CancellationToken));
|
||||
}
|
||||
}
|
@ -0,0 +1,78 @@
|
||||
/*
|
||||
Copyright 2015 Google Inc
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
|
||||
|
||||
namespace Google.Apis.Auth.OAuth2
|
||||
{
|
||||
/// <summary>
|
||||
/// Holder for credential parameters read from JSON credential file.
|
||||
/// Fields are union of parameters for all supported credential types.
|
||||
/// </summary>
|
||||
public class JsonCredentialParameters
|
||||
{
|
||||
/// <summary>
|
||||
/// UserCredential is created by the GCloud SDK tool when the user runs
|
||||
/// <a href="https://cloud.google.com/sdk/gcloud/reference/auth/login">GCloud Auth Login</a>.
|
||||
/// </summary>
|
||||
public const string AuthorizedUserCredentialType = "authorized_user";
|
||||
|
||||
/// <summary>
|
||||
/// ServiceAccountCredential is downloaded by the user from
|
||||
/// <a href="https://console.developers.google.com">Google Developers Console</a>.
|
||||
/// </summary>
|
||||
public const string ServiceAccountCredentialType = "service_account";
|
||||
|
||||
/// <summary>Type of the credential.</summary>
|
||||
[Newtonsoft.Json.JsonProperty("type")]
|
||||
public string Type { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Client Id associated with UserCredential created by
|
||||
/// <a href="https://cloud.google.com/sdk/gcloud/reference/auth/login">GCloud Auth Login</a>.
|
||||
/// </summary>
|
||||
[Newtonsoft.Json.JsonProperty("client_id")]
|
||||
public string ClientId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Client Secret associated with UserCredential created by
|
||||
/// <a href="https://cloud.google.com/sdk/gcloud/reference/auth/login">GCloud Auth Login</a>.
|
||||
/// </summary>
|
||||
[Newtonsoft.Json.JsonProperty("client_secret")]
|
||||
public string ClientSecret { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Client Email associated with ServiceAccountCredential obtained from
|
||||
/// <a href="https://console.developers.google.com">Google Developers Console</a>
|
||||
/// </summary>
|
||||
[Newtonsoft.Json.JsonProperty("client_email")]
|
||||
public string ClientEmail { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Private Key associated with ServiceAccountCredential obtained from
|
||||
/// <a href="https://console.developers.google.com">Google Developers Console</a>.
|
||||
/// </summary>
|
||||
[Newtonsoft.Json.JsonProperty("private_key")]
|
||||
public string PrivateKey { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Refresh Token associated with UserCredential created by
|
||||
/// <a href="https://cloud.google.com/sdk/gcloud/reference/auth/login">GCloud Auth Login</a>.
|
||||
/// </summary>
|
||||
[Newtonsoft.Json.JsonProperty("refresh_token")]
|
||||
public string RefreshToken { get; set; }
|
||||
}
|
||||
}
|
@ -0,0 +1,420 @@
|
||||
/*
|
||||
Copyright 2017 Google Inc
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
using Google.Apis.Auth.OAuth2.Requests;
|
||||
using Google.Apis.Auth.OAuth2.Responses;
|
||||
using Google.Apis.Logging;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.Specialized;
|
||||
using System.Diagnostics;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Net;
|
||||
using System.Net.Sockets;
|
||||
using System.Text;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Google.Apis.Auth.OAuth2
|
||||
{
|
||||
/// <summary>
|
||||
/// OAuth 2.0 verification code receiver that runs a local server on a free port and waits for a call with the
|
||||
/// authorization verification code.
|
||||
/// </summary>
|
||||
public class LocalServerCodeReceiver : ICodeReceiver
|
||||
{
|
||||
private static readonly ILogger Logger = ApplicationContext.Logger.ForType<LocalServerCodeReceiver>();
|
||||
|
||||
/// <summary>The call back request path.</summary>
|
||||
internal const string LoopbackCallbackPath = "/authorize/";
|
||||
|
||||
/// <summary>The call back format. Expects one port parameter.</summary>
|
||||
internal static readonly string LoopbackCallback = $"http://{IPAddress.Loopback}:{{0}}{LoopbackCallbackPath}";
|
||||
|
||||
/// <summary>Close HTML tag to return the browser so it will close itself.</summary>
|
||||
internal const string ClosePageResponse =
|
||||
@"<html>
|
||||
<head><title>OAuth 2.0 Authentication Token Received</title></head>
|
||||
<body>
|
||||
Received verification code. You may now close this window.
|
||||
<script type='text/javascript'>
|
||||
// This doesn't work on every browser.
|
||||
window.setTimeout(function() {
|
||||
this.focus();
|
||||
window.opener = this;
|
||||
window.open('', '_self', '');
|
||||
window.close();
|
||||
}, 1000);
|
||||
//if (window.opener) { window.opener.checkToken(); }
|
||||
</script>
|
||||
</body>
|
||||
</html>";
|
||||
|
||||
// Not required in NET45, but present for testing.
|
||||
/// <summary>
|
||||
/// An extremely limited HTTP server that can only do exactly what is required
|
||||
/// for this use-case.
|
||||
/// It can only serve localhost; receive a single GET request; read only the query paremters;
|
||||
/// send back a fixed response. Nothing else.
|
||||
/// </summary>
|
||||
internal class LimitedLocalhostHttpServer : IDisposable
|
||||
{
|
||||
private const int MaxRequestLineLength = 256;
|
||||
private const int MaxHeadersLength = 8192;
|
||||
private const int NetworkReadBufferSize = 1024;
|
||||
|
||||
private static ILogger Logger = ApplicationContext.Logger.ForType<LimitedLocalhostHttpServer>();
|
||||
|
||||
public class ServerException : Exception
|
||||
{
|
||||
public ServerException(string msg) : base(msg) { }
|
||||
}
|
||||
|
||||
public static LimitedLocalhostHttpServer Start(string url)
|
||||
{
|
||||
var uri = new Uri(url);
|
||||
if (!uri.IsLoopback)
|
||||
{
|
||||
throw new ArgumentException($"Url must be loopback, but given: '{url}'", nameof(url));
|
||||
}
|
||||
var listener = new TcpListener(IPAddress.Loopback, uri.Port);
|
||||
return new LimitedLocalhostHttpServer(listener);
|
||||
}
|
||||
|
||||
private LimitedLocalhostHttpServer(TcpListener listener)
|
||||
{
|
||||
_listener = listener;
|
||||
_cts = new CancellationTokenSource();
|
||||
_listener.Start();
|
||||
Port = ((IPEndPoint)_listener.LocalEndpoint).Port;
|
||||
}
|
||||
|
||||
private readonly TcpListener _listener;
|
||||
private readonly CancellationTokenSource _cts;
|
||||
|
||||
public int Port { get; }
|
||||
|
||||
public async Task<Dictionary<string, string>> GetQueryParamsAsync(CancellationToken cancellationToken = default(CancellationToken))
|
||||
{
|
||||
var ct = CancellationTokenSource.CreateLinkedTokenSource(_cts.Token, cancellationToken).Token;
|
||||
using (TcpClient client = await _listener.AcceptTcpClientAsync().ConfigureAwait(false))
|
||||
{
|
||||
try
|
||||
{
|
||||
return await GetQueryParamsFromClientAsync(client, ct).ConfigureAwait(false);
|
||||
}
|
||||
catch (ServerException e)
|
||||
{
|
||||
Logger.Warning("{0}", e.Message);
|
||||
throw;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private async Task<Dictionary<string, string>> GetQueryParamsFromClientAsync(TcpClient client, CancellationToken cancellationToken)
|
||||
{
|
||||
var stream = client.GetStream();
|
||||
|
||||
var buffer = new byte[NetworkReadBufferSize];
|
||||
int bufferOfs = 0;
|
||||
int bufferSize = 0;
|
||||
Func<Task<char?>> getChar = async () =>
|
||||
{
|
||||
if (bufferOfs == bufferSize)
|
||||
{
|
||||
bufferSize = await stream.ReadAsync(buffer, 0, buffer.Length, cancellationToken).ConfigureAwait(false);
|
||||
if (bufferSize == 0)
|
||||
{
|
||||
// End of stream
|
||||
return null;
|
||||
}
|
||||
bufferOfs = 0;
|
||||
}
|
||||
byte b = buffer[bufferOfs++];
|
||||
// HTTP headers are generally ASCII, but historically allowed ISO-8859-1.
|
||||
// Non-ASCII bytes should be treated opaquely, not further processed (e.g. as UTF8).
|
||||
return (char)b;
|
||||
};
|
||||
|
||||
string requestLine = await ReadRequestLine(getChar).ConfigureAwait(false);
|
||||
var requestParams = ValidateAndGetRequestParams(requestLine);
|
||||
await WaitForAllHeaders(getChar).ConfigureAwait(false);
|
||||
await WriteResponse(stream, cancellationToken).ConfigureAwait(false);
|
||||
|
||||
return requestParams;
|
||||
}
|
||||
|
||||
private async Task<string> ReadRequestLine(Func<Task<char?>> getChar)
|
||||
{
|
||||
var requestLine = new StringBuilder(MaxRequestLineLength);
|
||||
do
|
||||
{
|
||||
if (requestLine.Length >= MaxRequestLineLength)
|
||||
{
|
||||
throw new ServerException($"Request line too long: > {MaxRequestLineLength} bytes.");
|
||||
}
|
||||
char? c = await getChar().ConfigureAwait(false);
|
||||
if (c == null)
|
||||
{
|
||||
throw new ServerException("Unexpected end of network stream reading request line.");
|
||||
}
|
||||
requestLine.Append(c);
|
||||
} while (requestLine.Length < 2 || requestLine[requestLine.Length - 2] != '\r' || requestLine[requestLine.Length - 1] != '\n');
|
||||
requestLine.Length -= 2; // Remove \r\n
|
||||
return requestLine.ToString();
|
||||
}
|
||||
|
||||
private Dictionary<string, string> ValidateAndGetRequestParams(string requestLine)
|
||||
{
|
||||
var requestLineParts = requestLine.Split(' ');
|
||||
if (requestLineParts.Length != 3)
|
||||
{
|
||||
throw new ServerException("Request line ill-formatted. Should be '<request-method> <request-path> HTTP/1.1'");
|
||||
}
|
||||
string requestVerb = requestLineParts[0];
|
||||
if (requestVerb != "GET")
|
||||
{
|
||||
throw new ServerException($"Expected 'GET' request, got '{requestVerb}'");
|
||||
}
|
||||
string requestPath = requestLineParts[1];
|
||||
if (!requestPath.StartsWith(LoopbackCallbackPath))
|
||||
{
|
||||
throw new ServerException($"Expected request path to start '{LoopbackCallbackPath}', got '{requestPath}'");
|
||||
}
|
||||
var pathParts = requestPath.Split('?');
|
||||
if (pathParts.Length == 1)
|
||||
{
|
||||
return new Dictionary<string, string>();
|
||||
}
|
||||
if (pathParts.Length != 2)
|
||||
{
|
||||
throw new ServerException($"Expected a single '?' in request path, got '{requestPath}'");
|
||||
}
|
||||
var queryParams = pathParts[1];
|
||||
var result = queryParams.Split(new[] { '&' }, StringSplitOptions.RemoveEmptyEntries).Select(param =>
|
||||
{
|
||||
var keyValue = param.Split('=');
|
||||
if (keyValue.Length > 2)
|
||||
{
|
||||
throw new ServerException($"Invalid query parameter: '{param}'");
|
||||
}
|
||||
var key = WebUtility.UrlDecode(keyValue[0]);
|
||||
var value = keyValue.Length == 2 ? WebUtility.UrlDecode(keyValue[1]) : "";
|
||||
return new { key, value };
|
||||
}).ToDictionary(x => x.key, x => x.value);
|
||||
return result;
|
||||
}
|
||||
|
||||
private async Task WaitForAllHeaders(Func<Task<char?>> getChar)
|
||||
{
|
||||
// Looking for an empty line, terminated by \r\n
|
||||
int byteCount = 0;
|
||||
int lineLength = 0;
|
||||
char c0 = '\0';
|
||||
char c1 = '\0';
|
||||
while (true)
|
||||
{
|
||||
if (byteCount > MaxHeadersLength)
|
||||
{
|
||||
throw new ServerException($"Headers too long: > {MaxHeadersLength} bytes.");
|
||||
}
|
||||
char? c = await getChar().ConfigureAwait(false);
|
||||
if (c == null)
|
||||
{
|
||||
throw new ServerException("Unexpected end of network stream waiting for headers.");
|
||||
}
|
||||
c0 = c1;
|
||||
c1 = (char)c;
|
||||
lineLength += 1;
|
||||
byteCount += 1;
|
||||
if (c0 == '\r' && c1 == '\n')
|
||||
{
|
||||
// End of line
|
||||
if (lineLength == 2)
|
||||
{
|
||||
return;
|
||||
}
|
||||
lineLength = 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private async Task WriteResponse(NetworkStream stream, CancellationToken cancellationToken)
|
||||
{
|
||||
string fullResponse = $"HTTP/1.1 200 OK\r\n\r\n{ClosePageResponse}";
|
||||
var response = Encoding.ASCII.GetBytes(fullResponse);
|
||||
await stream.WriteAsync(response, 0, response.Length, cancellationToken).ConfigureAwait(false);
|
||||
await stream.FlushAsync(cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
_cts.Cancel();
|
||||
_listener.Stop();
|
||||
}
|
||||
}
|
||||
|
||||
// There is a race condition on the port used for the loopback callback.
|
||||
// This is not good, but is now difficult to change due to RedirecrUri and ReceiveCodeAsync
|
||||
// being public methods.
|
||||
|
||||
private string redirectUri;
|
||||
/// <inheritdoc />
|
||||
public string RedirectUri
|
||||
{
|
||||
get
|
||||
{
|
||||
if (!string.IsNullOrEmpty(redirectUri))
|
||||
{
|
||||
return redirectUri;
|
||||
}
|
||||
|
||||
return redirectUri = string.Format(LoopbackCallback, GetRandomUnusedPort());
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task<AuthorizationCodeResponseUrl> ReceiveCodeAsync(AuthorizationCodeRequestUrl url,
|
||||
CancellationToken taskCancellationToken)
|
||||
{
|
||||
var authorizationUrl = url.Build().ToString();
|
||||
// The listener type depends on platform:
|
||||
// * .NET desktop: System.Net.HttpListener
|
||||
// * .NET Core: LimitedLocalhostHttpServer (above, HttpListener is not available in any version of netstandard)
|
||||
using (var listener = StartListener())
|
||||
{
|
||||
Logger.Debug("Open a browser with \"{0}\" URL", authorizationUrl);
|
||||
bool browserOpenedOk;
|
||||
try
|
||||
{
|
||||
browserOpenedOk = OpenBrowser(authorizationUrl);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Logger.Error(e, "Failed to launch browser with \"{0}\" for authorization", authorizationUrl);
|
||||
throw new NotSupportedException(
|
||||
$"Failed to launch browser with \"{authorizationUrl}\" for authorization. See inner exception for details.", e);
|
||||
}
|
||||
if (!browserOpenedOk)
|
||||
{
|
||||
Logger.Error("Failed to launch browser with \"{0}\" for authorization; platform not supported.", authorizationUrl);
|
||||
throw new NotSupportedException(
|
||||
$"Failed to launch browser with \"{authorizationUrl}\" for authorization; platform not supported.");
|
||||
}
|
||||
|
||||
return await GetResponseFromListener(listener, taskCancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Returns a random, unused port.</summary>
|
||||
private static int GetRandomUnusedPort()
|
||||
{
|
||||
var listener = new TcpListener(IPAddress.Loopback, 0);
|
||||
try
|
||||
{
|
||||
listener.Start();
|
||||
return ((IPEndPoint)listener.LocalEndpoint).Port;
|
||||
}
|
||||
finally
|
||||
{
|
||||
listener.Stop();
|
||||
}
|
||||
}
|
||||
|
||||
#if NETSTANDARD1_3
|
||||
private LimitedLocalhostHttpServer StartListener() => LimitedLocalhostHttpServer.Start(RedirectUri);
|
||||
|
||||
private async Task<AuthorizationCodeResponseUrl> GetResponseFromListener(LimitedLocalhostHttpServer server, CancellationToken ct)
|
||||
{
|
||||
var queryParams = await server.GetQueryParamsAsync(ct).ConfigureAwait(false);
|
||||
|
||||
// Create a new response URL with a dictionary that contains all the response query parameters.
|
||||
return new AuthorizationCodeResponseUrl(queryParams);
|
||||
}
|
||||
|
||||
private bool OpenBrowser(string url)
|
||||
{
|
||||
// See https://github.com/dotnet/corefx/issues/10361
|
||||
// This is best-effort only, but should work most of the time.
|
||||
if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
|
||||
{
|
||||
Process.Start(new ProcessStartInfo("cmd", $"/c start {url.Replace("&", "^&")}") { CreateNoWindow = true });
|
||||
return true;
|
||||
}
|
||||
if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux))
|
||||
{
|
||||
Process.Start("xdg-open", url);
|
||||
return true;
|
||||
}
|
||||
if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX))
|
||||
{
|
||||
Process.Start("open", url);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
#else
|
||||
private HttpListener StartListener()
|
||||
{
|
||||
var listener = new HttpListener();
|
||||
listener.Prefixes.Add(RedirectUri);
|
||||
listener.Start();
|
||||
return listener;
|
||||
}
|
||||
|
||||
private async Task<AuthorizationCodeResponseUrl> GetResponseFromListener(HttpListener listener, CancellationToken ct)
|
||||
{
|
||||
HttpListenerContext context;
|
||||
// Set up cancellation. HttpListener.GetContextAsync() doesn't accept a cancellation token,
|
||||
// the HttpListener needs to be stopped which immediately aborts the GetContextAsync() call.
|
||||
using (ct.Register(listener.Stop))
|
||||
{
|
||||
// Wait to get the authorization code response.
|
||||
try
|
||||
{
|
||||
context = await listener.GetContextAsync().ConfigureAwait(false);
|
||||
}
|
||||
catch (Exception) when (ct.IsCancellationRequested)
|
||||
{
|
||||
ct.ThrowIfCancellationRequested();
|
||||
// Next line will never be reached because cancellation will always have been requested in this catch block.
|
||||
// But it's required to satisfy compiler.
|
||||
throw new InvalidOperationException();
|
||||
}
|
||||
}
|
||||
NameValueCollection coll = context.Request.QueryString;
|
||||
|
||||
// Write a "close" response.
|
||||
using (var writer = new StreamWriter(context.Response.OutputStream))
|
||||
{
|
||||
writer.WriteLine(ClosePageResponse);
|
||||
writer.Flush();
|
||||
}
|
||||
context.Response.OutputStream.Close();
|
||||
|
||||
// Create a new response URL with a dictionary that contains all the response query parameters.
|
||||
return new AuthorizationCodeResponseUrl(coll.AllKeys.ToDictionary(k => k, k => coll[k]));
|
||||
}
|
||||
|
||||
private bool OpenBrowser(string url)
|
||||
{
|
||||
Process.Start(url);
|
||||
return true;
|
||||
}
|
||||
#endif
|
||||
}
|
||||
}
|
287
Yavsc/GoogleApiSupport/Google.Apis.Auth/OAuth2/Pkcs8.cs
Normal file
287
Yavsc/GoogleApiSupport/Google.Apis.Auth/OAuth2/Pkcs8.cs
Normal file
@ -0,0 +1,287 @@
|
||||
/*
|
||||
Copyright 2016 Google Inc
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
using Google.Apis.Util;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Security.Cryptography;
|
||||
|
||||
namespace Google.Apis.Auth.OAuth2
|
||||
{
|
||||
internal class Pkcs8
|
||||
{
|
||||
// PKCS#8 specification: https://www.ietf.org/rfc/rfc5208.txt
|
||||
// ASN.1 specification: https://www.itu.int/ITU-T/studygroups/com17/languages/X.690-0207.pdf
|
||||
|
||||
/// <summary>
|
||||
/// An incomplete ASN.1 decoder, only implements what's required
|
||||
/// to decode a Service Credential.
|
||||
/// </summary>
|
||||
internal class Asn1
|
||||
{
|
||||
internal enum Tag
|
||||
{
|
||||
Integer = 2,
|
||||
OctetString = 4,
|
||||
Null = 5,
|
||||
ObjectIdentifier = 6,
|
||||
Sequence = 16,
|
||||
}
|
||||
|
||||
internal class Decoder
|
||||
{
|
||||
public Decoder(byte[] bytes)
|
||||
{
|
||||
_bytes = bytes;
|
||||
_index = 0;
|
||||
}
|
||||
|
||||
private byte[] _bytes;
|
||||
private int _index;
|
||||
|
||||
public object Decode()
|
||||
{
|
||||
Tag tag = ReadTag();
|
||||
switch (tag)
|
||||
{
|
||||
case Tag.Integer:
|
||||
return ReadInteger();
|
||||
case Tag.OctetString:
|
||||
return ReadOctetString();
|
||||
case Tag.Null:
|
||||
return ReadNull();
|
||||
case Tag.ObjectIdentifier:
|
||||
return ReadOid();
|
||||
case Tag.Sequence:
|
||||
return ReadSequence();
|
||||
default:
|
||||
throw new NotSupportedException($"Tag '{tag}' not supported.");
|
||||
}
|
||||
}
|
||||
|
||||
private byte NextByte() => _bytes[_index++];
|
||||
|
||||
private byte[] ReadLengthPrefixedBytes()
|
||||
{
|
||||
int length = ReadLength();
|
||||
return ReadBytes(length);
|
||||
}
|
||||
|
||||
private byte[] ReadInteger() => ReadLengthPrefixedBytes();
|
||||
|
||||
private object ReadOctetString()
|
||||
{
|
||||
byte[] bytes = ReadLengthPrefixedBytes();
|
||||
return new Decoder(bytes).Decode();
|
||||
}
|
||||
|
||||
private object ReadNull()
|
||||
{
|
||||
int length = ReadLength();
|
||||
if (length != 0)
|
||||
{
|
||||
throw new InvalidDataException("Invalid data, Null length must be 0.");
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private int[] ReadOid()
|
||||
{
|
||||
byte[] oidBytes = ReadLengthPrefixedBytes();
|
||||
List<int> result = new List<int>();
|
||||
bool first = true;
|
||||
int index = 0;
|
||||
while (index < oidBytes.Length)
|
||||
{
|
||||
int subId = 0;
|
||||
byte b;
|
||||
do
|
||||
{
|
||||
b = oidBytes[index++];
|
||||
if ((subId & 0xff000000) != 0)
|
||||
{
|
||||
throw new NotSupportedException("Oid subId > 2^31 not supported.");
|
||||
}
|
||||
subId = (subId << 7) | (b & 0x7f);
|
||||
} while ((b & 0x80) != 0);
|
||||
if (first)
|
||||
{
|
||||
first = false;
|
||||
result.Add(subId / 40);
|
||||
result.Add(subId % 40);
|
||||
}
|
||||
else
|
||||
{
|
||||
result.Add(subId);
|
||||
}
|
||||
}
|
||||
return result.ToArray();
|
||||
}
|
||||
|
||||
private object[] ReadSequence()
|
||||
{
|
||||
int length = ReadLength();
|
||||
int endOffset = _index + length;
|
||||
if (endOffset < 0 || endOffset > _bytes.Length)
|
||||
{
|
||||
throw new InvalidDataException("Invalid sequence, too long.");
|
||||
}
|
||||
List<object> sequence = new List<object>();
|
||||
while (_index < endOffset)
|
||||
{
|
||||
sequence.Add(Decode());
|
||||
}
|
||||
return sequence.ToArray();
|
||||
}
|
||||
|
||||
private byte[] ReadBytes(int length)
|
||||
{
|
||||
if (length <= 0)
|
||||
{
|
||||
throw new ArgumentOutOfRangeException(nameof(length), "length must be positive.");
|
||||
}
|
||||
if (_bytes.Length - length < 0)
|
||||
{
|
||||
throw new ArgumentException("Cannot read past end of buffer.");
|
||||
}
|
||||
byte[] result = new byte[length];
|
||||
Array.Copy(_bytes, _index, result, 0, length);
|
||||
_index += length;
|
||||
return result;
|
||||
}
|
||||
|
||||
private Tag ReadTag()
|
||||
{
|
||||
byte b = NextByte();
|
||||
int tag = b & 0x1f;
|
||||
if (tag == 0x1f)
|
||||
{
|
||||
// A tag value of 0x1f (31) indicates a tag value of >30 (spec section 8.1.2.4)
|
||||
throw new NotSupportedException("Tags of value > 30 not supported.");
|
||||
}
|
||||
else
|
||||
{
|
||||
return (Tag)tag;
|
||||
}
|
||||
}
|
||||
|
||||
private int ReadLength()
|
||||
{
|
||||
byte b0 = NextByte();
|
||||
if ((b0 & 0x80) == 0)
|
||||
{
|
||||
return b0;
|
||||
}
|
||||
else
|
||||
{
|
||||
if (b0 == 0xff)
|
||||
{
|
||||
throw new InvalidDataException("Invalid length byte: 0xff");
|
||||
}
|
||||
int byteCount = b0 & 0x7f;
|
||||
if (byteCount == 0)
|
||||
{
|
||||
throw new NotSupportedException("Lengths in Indefinite Form not supported.");
|
||||
}
|
||||
int result = 0;
|
||||
for (int i = 0; i < byteCount; i++)
|
||||
{
|
||||
if ((result & 0xff800000) != 0)
|
||||
{
|
||||
throw new NotSupportedException("Lengths > 2^31 not supported.");
|
||||
}
|
||||
result = (result << 8) | NextByte();
|
||||
}
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
public static object Decode(byte[] bs) => new Decoder(bs).Decode();
|
||||
|
||||
}
|
||||
|
||||
public static RSAParameters DecodeRsaParameters(string pkcs8PrivateKey)
|
||||
{
|
||||
const string PrivateKeyPrefix = "-----BEGIN PRIVATE KEY-----";
|
||||
const string PrivateKeySuffix = "-----END PRIVATE KEY-----";
|
||||
|
||||
Utilities.ThrowIfNullOrEmpty(pkcs8PrivateKey, nameof(pkcs8PrivateKey));
|
||||
pkcs8PrivateKey = pkcs8PrivateKey.Trim();
|
||||
if (!pkcs8PrivateKey.StartsWith(PrivateKeyPrefix) || !pkcs8PrivateKey.EndsWith(PrivateKeySuffix))
|
||||
{
|
||||
throw new ArgumentException(
|
||||
$"PKCS8 data must be contained within '{PrivateKeyPrefix}' and '{PrivateKeySuffix}'.", nameof(pkcs8PrivateKey));
|
||||
}
|
||||
string base64PrivateKey =
|
||||
pkcs8PrivateKey.Substring(PrivateKeyPrefix.Length, pkcs8PrivateKey.Length - PrivateKeyPrefix.Length - PrivateKeySuffix.Length);
|
||||
// FromBase64String() ignores whitespace, so further Trim()ing isn't required.
|
||||
byte[] pkcs8Bytes = Convert.FromBase64String(base64PrivateKey);
|
||||
|
||||
object ans1 = Asn1.Decode(pkcs8Bytes);
|
||||
object[] parameters = (object[])((object[])ans1)[2];
|
||||
|
||||
var rsaParmeters = new RSAParameters
|
||||
{
|
||||
Modulus = TrimLeadingZeroes((byte[])parameters[1]),
|
||||
Exponent = TrimLeadingZeroes((byte[])parameters[2], alignTo8Bytes: false),
|
||||
D = TrimLeadingZeroes((byte[])parameters[3]),
|
||||
P = TrimLeadingZeroes((byte[])parameters[4]),
|
||||
Q = TrimLeadingZeroes((byte[])parameters[5]),
|
||||
DP = TrimLeadingZeroes((byte[])parameters[6]),
|
||||
DQ = TrimLeadingZeroes((byte[])parameters[7]),
|
||||
InverseQ = TrimLeadingZeroes((byte[])parameters[8]),
|
||||
};
|
||||
|
||||
return rsaParmeters;
|
||||
}
|
||||
|
||||
internal static byte[] TrimLeadingZeroes(byte[] bs, bool alignTo8Bytes = true)
|
||||
{
|
||||
int zeroCount = 0;
|
||||
while (zeroCount < bs.Length && bs[zeroCount] == 0) zeroCount += 1;
|
||||
|
||||
int newLength = bs.Length - zeroCount;
|
||||
if (alignTo8Bytes)
|
||||
{
|
||||
int remainder = newLength & 0x07;
|
||||
if (remainder != 0)
|
||||
{
|
||||
newLength += 8 - remainder;
|
||||
}
|
||||
}
|
||||
|
||||
if (newLength == bs.Length)
|
||||
{
|
||||
return bs;
|
||||
}
|
||||
|
||||
byte[] result = new byte[newLength];
|
||||
if (newLength < bs.Length)
|
||||
{
|
||||
Buffer.BlockCopy(bs, bs.Length - newLength, result, 0, newLength);
|
||||
}
|
||||
else
|
||||
{
|
||||
Buffer.BlockCopy(bs, 0, result, newLength - bs.Length, bs.Length);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
}
|
||||
}
|
@ -0,0 +1,72 @@
|
||||
/*
|
||||
Copyright 2013 Google Inc
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
using System;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
using Google.Apis.Auth.OAuth2.Requests;
|
||||
using Google.Apis.Auth.OAuth2.Responses;
|
||||
using Google.Apis.Logging;
|
||||
|
||||
namespace Google.Apis.Auth.OAuth2
|
||||
{
|
||||
/// <summary>OAuth 2.0 verification code receiver that reads the authorization code from the user input.</summary>
|
||||
public class PromptCodeReceiver : ICodeReceiver
|
||||
{
|
||||
private static readonly ILogger Logger = ApplicationContext.Logger.ForType<PromptCodeReceiver>();
|
||||
|
||||
/// <inheritdoc/>
|
||||
public string RedirectUri
|
||||
{
|
||||
get { return GoogleAuthConsts.InstalledAppRedirectUri; }
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public Task<AuthorizationCodeResponseUrl> ReceiveCodeAsync(AuthorizationCodeRequestUrl url,
|
||||
CancellationToken taskCancellationToken)
|
||||
{
|
||||
var authorizationUrl = url.Build().ToString();
|
||||
|
||||
#if NETSTANDARD1_3
|
||||
Logger.Debug("Requested user open a browser with \"{0}\" URL", authorizationUrl);
|
||||
Console.WriteLine("Please visit the following URL in a web browser, then enter the code shown after authorization:");
|
||||
Console.WriteLine(authorizationUrl);
|
||||
Console.WriteLine();
|
||||
#elif NET45
|
||||
Logger.Debug("Open a browser with \"{0}\" URL", authorizationUrl);
|
||||
System.Diagnostics.Process.Start(authorizationUrl);
|
||||
|
||||
#elif DNX451
|
||||
Logger.Debug("Open a browser with \"{0}\" URL", authorizationUrl);
|
||||
System.Diagnostics.Process.Start(authorizationUrl);
|
||||
#else
|
||||
#error Unsupported target
|
||||
#endif
|
||||
|
||||
string code = string.Empty;
|
||||
while (string.IsNullOrEmpty(code))
|
||||
{
|
||||
Console.WriteLine("Please enter code: ");
|
||||
code = Console.ReadLine();
|
||||
}
|
||||
|
||||
Logger.Debug("Code is: \"{0}\"", code);
|
||||
|
||||
return Task.FromResult(new AuthorizationCodeResponseUrl { Code = code });
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,51 @@
|
||||
/*
|
||||
Copyright 2013 Google Inc
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
using System;
|
||||
|
||||
using Google.Apis.Requests;
|
||||
using Google.Apis.Requests.Parameters;
|
||||
|
||||
namespace Google.Apis.Auth.OAuth2.Requests
|
||||
{
|
||||
/// <summary>
|
||||
/// OAuth 2.0 request URL for an authorization web page to allow the end user to authorize the application to
|
||||
/// access their protected resources and that returns an authorization code, as specified in
|
||||
/// http://tools.ietf.org/html/rfc6749#section-4.1.
|
||||
/// </summary>
|
||||
public class AuthorizationCodeRequestUrl : AuthorizationRequestUrl
|
||||
{
|
||||
/// <summary>
|
||||
/// Constructs a new authorization code request with the specified URI and sets response_type to <c>code</c>.
|
||||
/// </summary>
|
||||
public AuthorizationCodeRequestUrl(Uri authorizationServerUrl)
|
||||
: base(authorizationServerUrl)
|
||||
{
|
||||
ResponseType = "code";
|
||||
}
|
||||
|
||||
/// <summary>Creates a <see cref="System.Uri"/> which is used to request the authorization code.</summary>
|
||||
public Uri Build()
|
||||
{
|
||||
var builder = new RequestBuilder()
|
||||
{
|
||||
BaseUri = AuthorizationServerUrl
|
||||
};
|
||||
ParameterUtils.InitParameters(builder, this);
|
||||
return builder.BuildUri();
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,43 @@
|
||||
/*
|
||||
Copyright 2013 Google Inc
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
namespace Google.Apis.Auth.OAuth2.Requests
|
||||
{
|
||||
/// <summary>
|
||||
/// OAuth 2.0 request for an access token using an authorization code as specified in
|
||||
/// http://tools.ietf.org/html/rfc6749#section-4.1.3.
|
||||
/// </summary>
|
||||
public class AuthorizationCodeTokenRequest : TokenRequest
|
||||
{
|
||||
/// <summary>Gets or sets the authorization code received from the authorization server.</summary>
|
||||
[Google.Apis.Util.RequestParameterAttribute("code")]
|
||||
public string Code { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the redirect URI parameter matching the redirect URI parameter in the authorization request.
|
||||
/// </summary>
|
||||
[Google.Apis.Util.RequestParameterAttribute("redirect_uri")]
|
||||
public string RedirectUri { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Constructs a new authorization code token request and sets grant_type to <c>authorization_code</c>.
|
||||
/// </summary>
|
||||
public AuthorizationCodeTokenRequest()
|
||||
{
|
||||
GrantType = "authorization_code";
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,75 @@
|
||||
/*
|
||||
Copyright 2013 Google Inc
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
using System;
|
||||
|
||||
namespace Google.Apis.Auth.OAuth2.Requests
|
||||
{
|
||||
/// <summary>
|
||||
/// OAuth 2.0 request URL for an authorization web page to allow the end user to authorize the application to
|
||||
/// access their protected resources, as specified in http://tools.ietf.org/html/rfc6749#section-3.1.
|
||||
/// </summary>
|
||||
public class AuthorizationRequestUrl
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets or sets the response type which must be <c>code</c> for requesting an authorization code or
|
||||
/// <c>token</c> for requesting an access token (implicit grant), or space separated registered extension
|
||||
/// values. See http://tools.ietf.org/html/rfc6749#section-3.1.1 for more details
|
||||
/// </summary>
|
||||
[Google.Apis.Util.RequestParameterAttribute("response_type", Google.Apis.Util.RequestParameterType.Query)]
|
||||
public string ResponseType { get; set; }
|
||||
|
||||
/// <summary>Gets or sets the client identifier.</summary>
|
||||
[Google.Apis.Util.RequestParameterAttribute("client_id", Google.Apis.Util.RequestParameterType.Query)]
|
||||
public string ClientId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the URI that the authorization server directs the resource owner's user-agent back to the
|
||||
/// client after a successful authorization grant, as specified in
|
||||
/// http://tools.ietf.org/html/rfc6749#section-3.1.2 or <c>null</c> for none.
|
||||
/// </summary>
|
||||
[Google.Apis.Util.RequestParameterAttribute("redirect_uri", Google.Apis.Util.RequestParameterType.Query)]
|
||||
public string RedirectUri { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets space-separated list of scopes, as specified in http://tools.ietf.org/html/rfc6749#section-3.3
|
||||
/// or <c>null</c> for none.
|
||||
/// </summary>
|
||||
[Google.Apis.Util.RequestParameterAttribute("scope", Google.Apis.Util.RequestParameterType.Query)]
|
||||
public string Scope { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the state (an opaque value used by the client to maintain state between the request and
|
||||
/// callback, as mentioned in http://tools.ietf.org/html/rfc6749#section-3.1.2.2 or <c>null</c> for none.
|
||||
/// </summary>
|
||||
[Google.Apis.Util.RequestParameterAttribute("state", Google.Apis.Util.RequestParameterType.Query)]
|
||||
public string State { get; set; }
|
||||
|
||||
private readonly Uri authorizationServerUrl;
|
||||
/// <summary>Gets the authorization server URI.</summary>
|
||||
public Uri AuthorizationServerUrl
|
||||
{
|
||||
get { return authorizationServerUrl; }
|
||||
}
|
||||
|
||||
/// <summary>Constructs a new authorization request with the specified URI.</summary>
|
||||
/// <param name="authorizationServerUrl">Authorization server URI</param>
|
||||
public AuthorizationRequestUrl(Uri authorizationServerUrl)
|
||||
{
|
||||
this.authorizationServerUrl = authorizationServerUrl;
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,39 @@
|
||||
/*
|
||||
Copyright 2013 Google Inc
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
|
||||
namespace Google.Apis.Auth.OAuth2.Requests
|
||||
{
|
||||
/// <summary>
|
||||
/// Service account assertion token request as specified in
|
||||
/// https://developers.google.com/accounts/docs/OAuth2ServiceAccount#makingrequest.
|
||||
/// </summary>
|
||||
public class GoogleAssertionTokenRequest : TokenRequest
|
||||
{
|
||||
/// <summary>Gets or sets the JWT (including signature).</summary>
|
||||
[Google.Apis.Util.RequestParameterAttribute("assertion")]
|
||||
public string Assertion { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Constructs a new refresh code token request and sets grant_type to
|
||||
/// <c>urn:ietf:params:oauth:grant-type:jwt-bearer</c>.
|
||||
/// </summary>
|
||||
public GoogleAssertionTokenRequest()
|
||||
{
|
||||
GrantType = "urn:ietf:params:oauth:grant-type:jwt-bearer";
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,85 @@
|
||||
/*
|
||||
Copyright 2013 Google Inc
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace Google.Apis.Auth.OAuth2.Requests
|
||||
{
|
||||
/// <summary>
|
||||
/// Google-specific implementation of the OAuth 2.0 URL for an authorization web page to allow the end user to
|
||||
/// authorize the application to access their protected resources and that returns an authorization code, as
|
||||
/// specified in https://developers.google.com/accounts/docs/OAuth2WebServer.
|
||||
/// </summary>
|
||||
public class GoogleAuthorizationCodeRequestUrl : AuthorizationCodeRequestUrl
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets or sets the access type. Set <c>online</c> to request on-line access or <c>offline</c> to request
|
||||
/// off-line access or <c>null</c> for the default behavior. The default value is <c>offline</c>.
|
||||
/// </summary>
|
||||
[Google.Apis.Util.RequestParameterAttribute("access_type", Google.Apis.Util.RequestParameterType.Query)]
|
||||
public string AccessType { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets prompt for consent behavior <c>auto</c> to request auto-approval or<c>force</c> to force the
|
||||
/// approval UI to show, or <c>null</c> for the default behavior.
|
||||
/// </summary>
|
||||
[Google.Apis.Util.RequestParameterAttribute("approval_prompt", Google.Apis.Util.RequestParameterType.Query)]
|
||||
public string ApprovalPrompt { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the login hint. Sets <c>email address</c> or sub <c>identifier</c>.
|
||||
/// When your application knows which user it is trying to authenticate, it may provide this parameter as a
|
||||
/// hint to the Authentication Server. Passing this hint will either pre-fill the email box on the sign-in form
|
||||
/// or select the proper multi-login session, thereby simplifying the login flow.
|
||||
/// </summary>
|
||||
[Google.Apis.Util.RequestParameterAttribute("login_hint", Google.Apis.Util.RequestParameterType.Query)]
|
||||
public string LoginHint { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the include granted scopes to determine if this authorization request should use
|
||||
/// incremental authorization (https://developers.google.com/+/web/api/rest/oauth#incremental-auth).
|
||||
/// If true and the authorization request is granted, the authorization will include any previous
|
||||
/// authorizations granted to this user/application combination for other scopes.
|
||||
/// </summary>
|
||||
/// <remarks>Currently unsupported for installed apps.</remarks>
|
||||
[Google.Apis.Util.RequestParameterAttribute("include_granted_scopes",
|
||||
Google.Apis.Util.RequestParameterType.Query)]
|
||||
public string IncludeGrantedScopes { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets a collection of user defined query parameters to facilitate any not explicitly supported
|
||||
/// by the library which will be included in the resultant authentication URL.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// The name of this parameter is used only for the constructor and will not end up in the resultant query
|
||||
/// string.
|
||||
/// </remarks>
|
||||
[Google.Apis.Util.RequestParameterAttribute("user_defined_query_params",
|
||||
Google.Apis.Util.RequestParameterType.UserDefinedQueries)]
|
||||
public IEnumerable<KeyValuePair<string, string>> UserDefinedQueryParams { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Constructs a new authorization code request with the given authorization server URL. This constructor sets
|
||||
/// the <see cref="AccessType"/> to <c>offline</c>.
|
||||
/// </summary>
|
||||
public GoogleAuthorizationCodeRequestUrl(Uri authorizationServerUrl)
|
||||
: base(authorizationServerUrl)
|
||||
{
|
||||
AccessType = "offline";
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,57 @@
|
||||
/*
|
||||
Copyright 2014 Google Inc
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
using System;
|
||||
|
||||
using Google.Apis.Requests;
|
||||
using Google.Apis.Requests.Parameters;
|
||||
|
||||
namespace Google.Apis.Auth.OAuth2.Requests
|
||||
{
|
||||
/// <summary>
|
||||
/// Google OAuth 2.0 request to revoke an access token as specified in
|
||||
/// https://developers.google.com/accounts/docs/OAuth2WebServer#tokenrevoke.
|
||||
/// </summary>
|
||||
class GoogleRevokeTokenRequest
|
||||
{
|
||||
private readonly Uri revokeTokenUrl;
|
||||
/// <summary>Gets the URI for token revocation.</summary>
|
||||
public Uri RevokeTokenUrl
|
||||
{
|
||||
get { return revokeTokenUrl; }
|
||||
}
|
||||
|
||||
/// <summary>Gets or sets the token to revoke.</summary>
|
||||
[Google.Apis.Util.RequestParameterAttribute("token")]
|
||||
public string Token { get; set; }
|
||||
|
||||
public GoogleRevokeTokenRequest(Uri revokeTokenUrl)
|
||||
{
|
||||
this.revokeTokenUrl = revokeTokenUrl;
|
||||
}
|
||||
|
||||
/// <summary>Creates a <see cref="System.Uri"/> which is used to request the authorization code.</summary>
|
||||
public Uri Build()
|
||||
{
|
||||
var builder = new RequestBuilder()
|
||||
{
|
||||
BaseUri = revokeTokenUrl
|
||||
};
|
||||
ParameterUtils.InitParameters(builder, this);
|
||||
return builder.BuildUri();
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,37 @@
|
||||
/*
|
||||
Copyright 2013 Google Inc
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
namespace Google.Apis.Auth.OAuth2.Requests
|
||||
{
|
||||
/// <summary>
|
||||
/// OAuth 2.0 request to refresh an access token using a refresh token as specified in
|
||||
/// http://tools.ietf.org/html/rfc6749#section-6.
|
||||
/// </summary>
|
||||
public class RefreshTokenRequest : TokenRequest
|
||||
{
|
||||
/// <summary>Gets or sets the Refresh token issued to the client.</summary>
|
||||
[Google.Apis.Util.RequestParameterAttribute("refresh_token")]
|
||||
public string RefreshToken { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Constructs a new refresh code token request and sets grant_type to <c>refresh_token</c>.
|
||||
/// </summary>
|
||||
public RefreshTokenRequest()
|
||||
{
|
||||
GrantType = "refresh_token";
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,45 @@
|
||||
/*
|
||||
Copyright 2013 Google Inc
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
namespace Google.Apis.Auth.OAuth2.Requests
|
||||
{
|
||||
/// <summary>
|
||||
/// OAuth 2.0 request for an access token as specified in http://tools.ietf.org/html/rfc6749#section-4.
|
||||
/// </summary>
|
||||
public class TokenRequest
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets or sets space-separated list of scopes as specified in http://tools.ietf.org/html/rfc6749#section-3.3.
|
||||
/// </summary>
|
||||
[Google.Apis.Util.RequestParameterAttribute("scope")]
|
||||
public string Scope { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the Grant type. Sets <c>authorization_code</c> or <c>password</c> or <c>client_credentials</c>
|
||||
/// or <c>refresh_token</c> or absolute URI of the extension grant type.
|
||||
/// </summary>
|
||||
[Google.Apis.Util.RequestParameterAttribute("grant_type")]
|
||||
public string GrantType { get; set; }
|
||||
|
||||
/// <summary>Gets or sets the client Identifier.</summary>
|
||||
[Google.Apis.Util.RequestParameterAttribute("client_id")]
|
||||
public string ClientId { get; set; }
|
||||
|
||||
/// <summary>Gets or sets the client Secret.</summary>
|
||||
[Google.Apis.Util.RequestParameterAttribute("client_secret")]
|
||||
public string ClientSecret { get; set; }
|
||||
}
|
||||
}
|
@ -0,0 +1,66 @@
|
||||
/*
|
||||
Copyright 2013 Google Inc
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
using System.Net.Http;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
using Google.Apis.Auth.OAuth2.Responses;
|
||||
using Google.Apis.Json;
|
||||
using Google.Apis.Requests.Parameters;
|
||||
using Google.Apis.Util;
|
||||
|
||||
namespace Google.Apis.Auth.OAuth2.Requests
|
||||
{
|
||||
/// <summary>Extension methods to <see cref="TokenRequest"/>.</summary>
|
||||
public static class TokenRequestExtenstions
|
||||
{
|
||||
/// <summary>
|
||||
/// Executes the token request in order to receive a
|
||||
/// <see cref="Google.Apis.Auth.OAuth2.Responses.TokenResponse"/>. In case the token server returns an
|
||||
/// error, a <see cref="Google.Apis.Auth.OAuth2.Responses.TokenResponseException"/> is thrown.
|
||||
/// </summary>
|
||||
/// <param name="request">The token request.</param>
|
||||
/// <param name="httpClient">The HTTP client used to create an HTTP request.</param>
|
||||
/// <param name="tokenServerUrl">The token server URL.</param>
|
||||
/// <param name="taskCancellationToken">Cancellation token to cancel operation.</param>
|
||||
/// <param name="clock">
|
||||
/// The clock which is used to set the
|
||||
/// <see cref="Google.Apis.Auth.OAuth2.Responses.TokenResponse.Issued"/> property.
|
||||
/// </param>
|
||||
/// <returns>Token response with the new access token.</returns>
|
||||
public static async Task<TokenResponse> ExecuteAsync(this TokenRequest request, HttpClient httpClient,
|
||||
string tokenServerUrl, CancellationToken taskCancellationToken, IClock clock)
|
||||
{
|
||||
var httpRequest = new HttpRequestMessage(HttpMethod.Post, tokenServerUrl);
|
||||
httpRequest.Content = ParameterUtils.CreateFormUrlEncodedContent(request);
|
||||
|
||||
var response = await httpClient.SendAsync(httpRequest, taskCancellationToken).ConfigureAwait(false);
|
||||
var content = await response.Content.ReadAsStringAsync().ConfigureAwait(false);
|
||||
|
||||
if (!response.IsSuccessStatusCode)
|
||||
{
|
||||
var error = NewtonsoftJsonSerializer.Instance.Deserialize<TokenErrorResponse>(content);
|
||||
throw new TokenResponseException(error, response.StatusCode);
|
||||
}
|
||||
|
||||
// Gets the token and sets its issued time.
|
||||
var newToken = NewtonsoftJsonSerializer.Instance.Deserialize<TokenResponse>(content);
|
||||
newToken.IssuedUtc = clock.UtcNow;
|
||||
return newToken;
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,107 @@
|
||||
/*
|
||||
Copyright 2013 Google Inc
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace Google.Apis.Auth.OAuth2.Responses
|
||||
{
|
||||
/// <summary>
|
||||
/// Authorization Code response for the redirect URL after end user grants or denies authorization as specified
|
||||
/// in http://tools.ietf.org/html/rfc6749#section-4.1.2.
|
||||
/// <para>
|
||||
/// Check that <see cref="Code"/> is not <c>null</c> or empty to verify the end-user granted authorization.
|
||||
/// </para>
|
||||
/// </summary>
|
||||
public class AuthorizationCodeResponseUrl
|
||||
{
|
||||
/// <summary>Gets or sets the authorization code generated by the authorization server.</summary>
|
||||
public string Code { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the state parameter matching the state parameter in the authorization request.
|
||||
/// </summary>
|
||||
public string State { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the error code (e.g. "invalid_request", "unauthorized_client", "access_denied",
|
||||
/// "unsupported_response_type", "invalid_scope", "server_error", "temporarily_unavailable") as specified in
|
||||
/// http://tools.ietf.org/html/rfc6749#section-4.1.2.1.
|
||||
/// </summary>
|
||||
public string Error { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the human-readable text which provides additional information used to assist the client
|
||||
/// developer in understanding the error occurred.
|
||||
/// </summary>
|
||||
public string ErrorDescription { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the URI identifying a human-readable web page with provides information about the error.
|
||||
/// </summary>
|
||||
public string ErrorUri { get; set; }
|
||||
|
||||
/// <summary>Constructs a new authorization code response URL from the specified dictionary.</summary>
|
||||
public AuthorizationCodeResponseUrl(IDictionary<string, string> queryString)
|
||||
{
|
||||
InitFromDictionary(queryString);
|
||||
}
|
||||
|
||||
#region Constructs
|
||||
|
||||
/// <summary>Constructs a new authorization code response URL from the specified query string.</summary>
|
||||
public AuthorizationCodeResponseUrl(string query)
|
||||
{
|
||||
var pairs = query.Split('&');
|
||||
var queryString = new Dictionary<string, string>();
|
||||
foreach (var pair in pairs)
|
||||
{
|
||||
var keyValue = pair.Split('=');
|
||||
queryString[keyValue[0]] = keyValue[1];
|
||||
}
|
||||
|
||||
InitFromDictionary(queryString);
|
||||
}
|
||||
|
||||
/// <summary>Initializes this instance from the input dictionary.</summary>
|
||||
private void InitFromDictionary(IDictionary<string, string> queryString)
|
||||
{
|
||||
//TODO(peleyal): improve the following code and make it a utility
|
||||
IDictionary<string, Action<string>> setters = new Dictionary<string, Action<string>>();
|
||||
setters["code"] = v => Code = v;
|
||||
setters["state"] = v => State = v;
|
||||
setters["error"] = v => Error = v;
|
||||
setters["error_description"] = v => ErrorDescription = v;
|
||||
setters["error_uri"] = v => ErrorUri = v;
|
||||
|
||||
Action<string> setter;
|
||||
foreach (var pair in queryString)
|
||||
{
|
||||
if (setters.TryGetValue(pair.Key, out setter))
|
||||
{
|
||||
setter(pair.Value);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Constructs a new empty authorization code response URL.</summary>
|
||||
public AuthorizationCodeResponseUrl()
|
||||
{
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
}
|
@ -0,0 +1,64 @@
|
||||
/*
|
||||
Copyright 2013 Google Inc
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
namespace Google.Apis.Auth.OAuth2.Responses
|
||||
{
|
||||
/// <summary>
|
||||
/// OAuth 2.0 model for a unsuccessful access token response as specified in
|
||||
/// http://tools.ietf.org/html/rfc6749#section-5.2.
|
||||
/// </summary>
|
||||
public class TokenErrorResponse
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets or sets error code (e.g. "invalid_request", "invalid_client", "invalid_grant", "unauthorized_client",
|
||||
/// "unsupported_grant_type", "invalid_scope") as specified in http://tools.ietf.org/html/rfc6749#section-5.2.
|
||||
/// </summary>
|
||||
[Newtonsoft.Json.JsonProperty("error")]
|
||||
public string Error { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets a human-readable text which provides additional information used to assist the client
|
||||
/// developer in understanding the error occurred.
|
||||
/// </summary>
|
||||
[Newtonsoft.Json.JsonProperty("error_description")]
|
||||
public string ErrorDescription { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the URI identifying a human-readable web page with provides information about the error.
|
||||
/// </summary>
|
||||
[Newtonsoft.Json.JsonProperty("error_uri")]
|
||||
public string ErrorUri { get; set; }
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override string ToString()
|
||||
{
|
||||
return string.Format("Error:\"{0}\", Description:\"{1}\", Uri:\"{2}\"", Error, ErrorDescription, ErrorUri);
|
||||
}
|
||||
|
||||
/// <summary>Constructs a new empty token error response.</summary>
|
||||
public TokenErrorResponse()
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>Constructs a new token error response from the given authorization code response.</summary>
|
||||
public TokenErrorResponse(AuthorizationCodeResponseUrl authorizationCode)
|
||||
{
|
||||
Error = authorizationCode.Error;
|
||||
ErrorDescription = authorizationCode.ErrorDescription;
|
||||
ErrorUri = authorizationCode.ErrorUri;
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,145 @@
|
||||
/*
|
||||
Copyright 2013 Google Inc
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
using System;
|
||||
|
||||
using Google.Apis.Util;
|
||||
using System.Threading.Tasks;
|
||||
using System.Net.Http;
|
||||
using Google.Apis.Json;
|
||||
using Google.Apis.Logging;
|
||||
|
||||
namespace Google.Apis.Auth.OAuth2.Responses
|
||||
{
|
||||
/// <summary>
|
||||
/// OAuth 2.0 model for a successful access token response as specified in
|
||||
/// http://tools.ietf.org/html/rfc6749#section-5.1.
|
||||
/// </summary>
|
||||
public class TokenResponse
|
||||
{
|
||||
private const int TokenExpiryTimeWindowSeconds = 60 * 5; // Refresh token 5 minutes before it expires.
|
||||
|
||||
/// <summary>Gets or sets the access token issued by the authorization server.</summary>
|
||||
[Newtonsoft.Json.JsonPropertyAttribute("access_token")]
|
||||
public string AccessToken { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the token type as specified in http://tools.ietf.org/html/rfc6749#section-7.1.
|
||||
/// </summary>
|
||||
[Newtonsoft.Json.JsonPropertyAttribute("token_type")]
|
||||
public string TokenType { get; set; }
|
||||
|
||||
/// <summary>Gets or sets the lifetime in seconds of the access token.</summary>
|
||||
[Newtonsoft.Json.JsonPropertyAttribute("expires_in")]
|
||||
public Nullable<long> ExpiresInSeconds { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the refresh token which can be used to obtain a new access token.
|
||||
/// For example, the value "3600" denotes that the access token will expire in one hour from the time the
|
||||
/// response was generated.
|
||||
/// </summary>
|
||||
[Newtonsoft.Json.JsonPropertyAttribute("refresh_token")]
|
||||
public string RefreshToken { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the scope of the access token as specified in http://tools.ietf.org/html/rfc6749#section-3.3.
|
||||
/// </summary>
|
||||
[Newtonsoft.Json.JsonPropertyAttribute("scope")]
|
||||
public string Scope { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the id_token, which is a JSON Web Token (JWT) as specified in http://tools.ietf.org/html/draft-ietf-oauth-json-web-token
|
||||
/// </summary>
|
||||
[Newtonsoft.Json.JsonPropertyAttribute("id_token")]
|
||||
public string IdToken { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The date and time that this token was issued, expressed in the system time zone.
|
||||
/// This property only exists for backward compatibility; it can cause inappropriate behavior around
|
||||
/// time zone transitions (e.g. daylight saving transitions).
|
||||
/// </summary>
|
||||
[Obsolete("Use IssuedUtc instead")]
|
||||
[Newtonsoft.Json.JsonPropertyAttribute(Order = 1)] // Serialize this before IssuedUtc, so that IssuedUtc takes priority when deserializing
|
||||
public DateTime Issued
|
||||
{
|
||||
get { return IssuedUtc.ToLocalTime(); }
|
||||
set { IssuedUtc = value.ToUniversalTime(); }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The date and time that this token was issued, expressed in UTC.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// This should be set by the CLIENT after the token was received from the server.
|
||||
/// </remarks>
|
||||
[Newtonsoft.Json.JsonPropertyAttribute(Order = 2)]
|
||||
public DateTime IssuedUtc { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Returns <c>true</c> if the token is expired or it's going to be expired in the next minute.
|
||||
/// </summary>
|
||||
public bool IsExpired(IClock clock)
|
||||
{
|
||||
if (AccessToken == null || !ExpiresInSeconds.HasValue)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
return IssuedUtc.AddSeconds(ExpiresInSeconds.Value - TokenExpiryTimeWindowSeconds) <= clock.UtcNow;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Asynchronously parses a <see cref="TokenResponse"/> instance from the specified <see cref="HttpResponseMessage"/>.
|
||||
/// </summary>
|
||||
/// <param name="response">The http response from which to parse the token.</param>
|
||||
/// <param name="clock">The clock used to set the <see cref="Issued"/> value of the token.</param>
|
||||
/// <param name="logger">The logger used to output messages incase of error.</param>
|
||||
/// <exception cref="TokenResponseException">
|
||||
/// The response was not successful or there is an error parsing the response into valid <see cref="TokenResponse"/> instance.
|
||||
/// </exception>
|
||||
/// <returns>
|
||||
/// A task containing the <see cref="TokenResponse"/> parsed form the response message.
|
||||
/// </returns>
|
||||
public static async Task<TokenResponse> FromHttpResponseAsync(HttpResponseMessage response, Util.IClock clock, ILogger logger)
|
||||
{
|
||||
var content = await response.Content.ReadAsStringAsync().ConfigureAwait(false);
|
||||
var typeName = "";
|
||||
try
|
||||
{
|
||||
if (!response.IsSuccessStatusCode)
|
||||
{
|
||||
typeName = nameof(TokenErrorResponse);
|
||||
var error = NewtonsoftJsonSerializer.Instance.Deserialize<TokenErrorResponse>(content);
|
||||
throw new TokenResponseException(error, response.StatusCode);
|
||||
}
|
||||
|
||||
// Gets the token and sets its issued time.
|
||||
typeName = nameof(TokenResponse);
|
||||
var newToken = NewtonsoftJsonSerializer.Instance.Deserialize<TokenResponse>(content);
|
||||
newToken.IssuedUtc = clock.UtcNow;
|
||||
return newToken;
|
||||
}
|
||||
catch (Newtonsoft.Json.JsonException ex)
|
||||
{
|
||||
logger.Error(ex, $"Exception was caught when deserializing {typeName}. Content is: {content}");
|
||||
throw new TokenResponseException(new TokenErrorResponse
|
||||
{
|
||||
Error = "Server response does not contain a JSON object. Status code is: " + response.StatusCode
|
||||
}, response.StatusCode);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,46 @@
|
||||
/*
|
||||
Copyright 2013 Google Inc
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
using System;
|
||||
using System.Net;
|
||||
|
||||
namespace Google.Apis.Auth.OAuth2.Responses
|
||||
{
|
||||
/// <summary>
|
||||
/// Token response exception which is thrown in case of receiving a token error when an authorization code or an
|
||||
/// access token is expected.
|
||||
/// </summary>
|
||||
public class TokenResponseException : Exception
|
||||
{
|
||||
/// <summary>The error information.</summary>
|
||||
public TokenErrorResponse Error { get; }
|
||||
|
||||
/// <summary>HTTP status code of error, or null if unknown.</summary>
|
||||
public HttpStatusCode? StatusCode { get; }
|
||||
|
||||
/// <summary>Constructs a new token response exception from the given error.</summary>
|
||||
public TokenResponseException(TokenErrorResponse error)
|
||||
: this(error, null) { }
|
||||
|
||||
/// <summary>Constructs a new token response exception from the given error nad optional HTTP status code.</summary>
|
||||
public TokenResponseException(TokenErrorResponse error, HttpStatusCode? statusCode)
|
||||
: base(error.ToString())
|
||||
{
|
||||
Error = error;
|
||||
StatusCode = statusCode;
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,349 @@
|
||||
/*
|
||||
Copyright 2013 Google Inc
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.IO;
|
||||
using System.Security.Cryptography;
|
||||
using System.Security.Cryptography.X509Certificates;
|
||||
using System.Text;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Google.Apis.Auth.OAuth2.Requests;
|
||||
using Google.Apis.Json;
|
||||
using Google.Apis.Util;
|
||||
|
||||
#if NETSTANDARD1_3
|
||||
using RsaKey = System.Security.Cryptography.RSA;
|
||||
#elif NET45
|
||||
using RsaKey = System.Security.Cryptography.RSACryptoServiceProvider;
|
||||
#elif DNX451
|
||||
using RsaKey = System.Security.Cryptography.RSACryptoServiceProvider;
|
||||
#else
|
||||
#error Unsupported target
|
||||
#endif
|
||||
|
||||
namespace Google.Apis.Auth.OAuth2
|
||||
{
|
||||
/// <summary>
|
||||
/// Google OAuth 2.0 credential for accessing protected resources using an access token. The Google OAuth 2.0
|
||||
/// Authorization Server supports server-to-server interactions such as those between a web application and Google
|
||||
/// Cloud Storage. The requesting application has to prove its own identity to gain access to an API, and an
|
||||
/// end-user doesn't have to be involved.
|
||||
/// <para>
|
||||
/// Take a look in https://developers.google.com/accounts/docs/OAuth2ServiceAccount for more details.
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// Since version 1.9.3, service account credential also supports JSON Web Token access token scenario.
|
||||
/// In this scenario, instead of sending a signed JWT claim to a token server and exchanging it for
|
||||
/// an access token, a locally signed JWT claim bound to an appropriate URI is used as an access token
|
||||
/// directly.
|
||||
/// See <see cref="GetAccessTokenForRequestAsync"/> for explanation when JWT access token
|
||||
/// is used and when regular OAuth2 token is used.
|
||||
/// </para>
|
||||
/// </summary>
|
||||
public class ServiceAccountCredential : ServiceCredential
|
||||
{
|
||||
private const string Sha256Oid = "2.16.840.1.101.3.4.2.1";
|
||||
/// <summary>An initializer class for the service account credential. </summary>
|
||||
new public class Initializer : ServiceCredential.Initializer
|
||||
{
|
||||
/// <summary>Gets the service account ID (typically an e-mail address).</summary>
|
||||
public string Id { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the email address of the user the application is trying to impersonate in the service
|
||||
/// account flow or <c>null</c>.
|
||||
/// </summary>
|
||||
public string User { get; set; }
|
||||
|
||||
/// <summary>Gets the scopes which indicate API access your application is requesting.</summary>
|
||||
public IEnumerable<string> Scopes { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the key which is used to sign the request, as specified in
|
||||
/// https://developers.google.com/accounts/docs/OAuth2ServiceAccount#computingsignature.
|
||||
/// </summary>
|
||||
public RsaKey Key { get; set; }
|
||||
|
||||
/// <summary>Constructs a new initializer using the given id.</summary>
|
||||
public Initializer(string id)
|
||||
: this(id, GoogleAuthConsts.OidcTokenUrl) { }
|
||||
|
||||
/// <summary>Constructs a new initializer using the given id and the token server URL.</summary>
|
||||
public Initializer(string id, string tokenServerUrl) : base(tokenServerUrl)
|
||||
{
|
||||
Id = id;
|
||||
Scopes = new List<string>();
|
||||
}
|
||||
|
||||
/// <summary>Extracts the <see cref="Key"/> from the given PKCS8 private key.</summary>
|
||||
public Initializer FromPrivateKey(string privateKey)
|
||||
{
|
||||
RSAParameters rsaParameters = Pkcs8.DecodeRsaParameters(privateKey);
|
||||
Key = (RsaKey)RSA.Create();
|
||||
Key.ImportParameters(rsaParameters);
|
||||
return this;
|
||||
}
|
||||
|
||||
/// <summary>Extracts a <see cref="Key"/> from the given certificate.</summary>
|
||||
public Initializer FromCertificate(X509Certificate2 certificate)
|
||||
{
|
||||
#if NETSTANDARD1_3
|
||||
Key = certificate.GetRSAPrivateKey();
|
||||
#elif NET45
|
||||
// Workaround to correctly cast the private key as a RSACryptoServiceProvider type 24.
|
||||
RSACryptoServiceProvider rsa = (RSACryptoServiceProvider) certificate.PrivateKey;
|
||||
byte[] privateKeyBlob = rsa.ExportCspBlob(true);
|
||||
Key = new RSACryptoServiceProvider();
|
||||
Key.ImportCspBlob(privateKeyBlob);
|
||||
#elif DNX451
|
||||
// Workaround to correctly cast the private key as a RSACryptoServiceProvider type 24.
|
||||
RSACryptoServiceProvider rsa = (RSACryptoServiceProvider)certificate.PrivateKey;
|
||||
byte[] privateKeyBlob = rsa.ExportCspBlob(true);
|
||||
Key = new RSACryptoServiceProvider();
|
||||
Key.ImportCspBlob(privateKeyBlob);
|
||||
#else
|
||||
#error Unsupported target
|
||||
#endif
|
||||
return this;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Unix epoch as a <c>DateTime</c></summary>
|
||||
protected static readonly DateTime UnixEpoch = new DateTime(1970, 1, 1, 0, 0, 0, DateTimeKind.Utc);
|
||||
|
||||
private readonly string id;
|
||||
private readonly string user;
|
||||
private readonly IEnumerable<string> scopes;
|
||||
private readonly RsaKey key;
|
||||
|
||||
/// <summary>Gets the service account ID (typically an e-mail address).</summary>
|
||||
public string Id { get { return id; } }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the email address of the user the application is trying to impersonate in the service account flow
|
||||
/// or <c>null</c>.
|
||||
/// </summary>
|
||||
public string User { get { return user; } }
|
||||
|
||||
/// <summary>Gets the service account scopes.</summary>
|
||||
public IEnumerable<string> Scopes { get { return scopes; } }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the key which is used to sign the request, as specified in
|
||||
/// https://developers.google.com/accounts/docs/OAuth2ServiceAccount#computingsignature.
|
||||
/// </summary>
|
||||
public RsaKey Key { get { return key; } }
|
||||
|
||||
/// <summary><c>true</c> if this credential has any scopes associated with it.</summary>
|
||||
internal bool HasScopes { get { return scopes != null && scopes.Any(); } }
|
||||
|
||||
/// <summary>Constructs a new service account credential using the given initializer.</summary>
|
||||
public ServiceAccountCredential(Initializer initializer) : base(initializer)
|
||||
{
|
||||
id = initializer.Id.ThrowIfNullOrEmpty("initializer.Id");
|
||||
user = initializer.User;
|
||||
scopes = initializer.Scopes;
|
||||
key = initializer.Key.ThrowIfNull("initializer.Key");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates a new <see cref="ServiceAccountCredential"/> instance from JSON credential data.
|
||||
/// </summary>
|
||||
/// <param name="credentialData">The stream from which to read the JSON key data for a service account. Must not be null.</param>
|
||||
/// <exception cref="InvalidOperationException">
|
||||
/// The <paramref name="credentialData"/> does not contain valid JSON service account key data.
|
||||
/// </exception>
|
||||
/// <returns>The credentials parsed from the service account key data.</returns>
|
||||
public static ServiceAccountCredential FromServiceAccountData(Stream credentialData)
|
||||
{
|
||||
var credential = GoogleCredential.FromStream(credentialData);
|
||||
var result = credential.UnderlyingCredential as ServiceAccountCredential;
|
||||
if (result == null)
|
||||
{
|
||||
throw new InvalidOperationException("JSON data does not represent a valid service account credential.");
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Requests a new token as specified in
|
||||
/// https://developers.google.com/accounts/docs/OAuth2ServiceAccount#makingrequest.
|
||||
/// </summary>
|
||||
/// <param name="taskCancellationToken">Cancellation token to cancel operation.</param>
|
||||
/// <returns><c>true</c> if a new token was received successfully.</returns>
|
||||
public override async Task<bool> RequestAccessTokenAsync(CancellationToken taskCancellationToken)
|
||||
{
|
||||
// Create the request.
|
||||
var request = new GoogleAssertionTokenRequest()
|
||||
{
|
||||
Assertion = CreateAssertionFromPayload(CreatePayload())
|
||||
};
|
||||
|
||||
Logger.Debug("Request a new access token. Assertion data is: " + request.Assertion);
|
||||
|
||||
var newToken = await request.ExecuteAsync(HttpClient, TokenServerUrl, taskCancellationToken, Clock)
|
||||
.ConfigureAwait(false);
|
||||
Token = newToken;
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets an access token to authorize a request.
|
||||
/// If <paramref name="authUri"/> is set and this credential has no scopes associated
|
||||
/// with it, a locally signed JWT access token for given <paramref name="authUri"/>
|
||||
/// is returned. Otherwise, an OAuth2 access token obtained from token server will be returned.
|
||||
/// A cached token is used if possible and the token is only refreshed once it's close to its expiry.
|
||||
/// </summary>
|
||||
/// <param name="authUri">The URI the returned token will grant access to.</param>
|
||||
/// <param name="cancellationToken">The cancellation token.</param>
|
||||
/// <returns>The access token.</returns>
|
||||
public override async Task<string> GetAccessTokenForRequestAsync(string authUri = null,
|
||||
CancellationToken cancellationToken = default(CancellationToken))
|
||||
{
|
||||
if (!HasScopes && authUri != null)
|
||||
{
|
||||
// TODO(jtattermusch): support caching of JWT access tokens per authUri, currently a new
|
||||
// JWT access token is created each time, which can hurt performance.
|
||||
return CreateJwtAccessToken(authUri);
|
||||
}
|
||||
return await base.GetAccessTokenForRequestAsync(authUri, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates a JWT access token than can be used in request headers instead of an OAuth2 token.
|
||||
/// This is achieved by signing a special JWT using this service account's private key.
|
||||
/// <param name="authUri">The URI for which the access token will be valid.</param>
|
||||
/// </summary>
|
||||
private string CreateJwtAccessToken(string authUri)
|
||||
{
|
||||
var issuedDateTime = Clock.UtcNow;
|
||||
var issued = (int)(issuedDateTime - UnixEpoch).TotalSeconds;
|
||||
var payload = new JsonWebSignature.Payload()
|
||||
{
|
||||
Issuer = Id,
|
||||
Subject = Id,
|
||||
Audience = authUri,
|
||||
IssuedAtTimeSeconds = issued,
|
||||
ExpirationTimeSeconds = issued + 3600,
|
||||
};
|
||||
|
||||
return CreateAssertionFromPayload(payload);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Signs JWT token using the private key and returns the serialized assertion.
|
||||
/// </summary>
|
||||
/// <param name="payload">the JWT payload to sign.</param>
|
||||
private string CreateAssertionFromPayload(JsonWebSignature.Payload payload)
|
||||
{
|
||||
string serializedHeader = CreateSerializedHeader();
|
||||
string serializedPayload = NewtonsoftJsonSerializer.Instance.Serialize(payload);
|
||||
|
||||
var assertion = new StringBuilder();
|
||||
assertion.Append(UrlSafeBase64Encode(serializedHeader))
|
||||
.Append('.')
|
||||
.Append(UrlSafeBase64Encode(serializedPayload));
|
||||
var signature = CreateSignature(Encoding.ASCII.GetBytes(assertion.ToString()));
|
||||
assertion.Append('.') .Append(UrlSafeEncode(signature));
|
||||
return assertion.ToString();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates a base64 encoded signature for the SHA-256 hash of the specified data.
|
||||
/// </summary>
|
||||
/// <param name="data">The data to hash and sign. Must not be null.</param>
|
||||
/// <returns>The base-64 encoded signature.</returns>
|
||||
public string CreateSignature(byte[] data)
|
||||
{
|
||||
data.ThrowIfNull(nameof(data));
|
||||
|
||||
using (var hashAlg = SHA256.Create())
|
||||
{
|
||||
byte[] assertionHash = hashAlg.ComputeHash(data);
|
||||
#if NETSTANDARD1_3
|
||||
var sigBytes = key.SignHash(assertionHash, HashAlgorithmName.SHA256, RSASignaturePadding.Pkcs1);
|
||||
#elif NET45
|
||||
var sigBytes = key.SignHash(assertionHash, Sha256Oid);
|
||||
#elif DNX451
|
||||
var sigBytes = key.SignHash(assertionHash, Sha256Oid);
|
||||
#else
|
||||
#error Unsupported target
|
||||
#endif
|
||||
return Convert.ToBase64String(sigBytes);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates a serialized header as specified in
|
||||
/// https://developers.google.com/accounts/docs/OAuth2ServiceAccount#formingheader.
|
||||
/// </summary>
|
||||
private static string CreateSerializedHeader()
|
||||
{
|
||||
var header = new GoogleJsonWebSignature.Header()
|
||||
{
|
||||
Algorithm = "RS256",
|
||||
Type = "JWT"
|
||||
};
|
||||
|
||||
return NewtonsoftJsonSerializer.Instance.Serialize(header);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates a claim set as specified in
|
||||
/// https://developers.google.com/accounts/docs/OAuth2ServiceAccount#formingclaimset.
|
||||
/// </summary>
|
||||
private GoogleJsonWebSignature.Payload CreatePayload()
|
||||
{
|
||||
var issued = (int)(Clock.UtcNow - UnixEpoch).TotalSeconds;
|
||||
return new GoogleJsonWebSignature.Payload()
|
||||
{
|
||||
Issuer = Id,
|
||||
Audience = TokenServerUrl,
|
||||
IssuedAtTimeSeconds = issued,
|
||||
ExpirationTimeSeconds = issued + 3600,
|
||||
Subject = User,
|
||||
Scope = String.Join(" ", Scopes)
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>Encodes the provided UTF8 string into an URL safe base64 string.</summary>
|
||||
/// <param name="value">Value to encode.</param>
|
||||
/// <returns>The URL safe base64 string.</returns>
|
||||
private string UrlSafeBase64Encode(string value)
|
||||
{
|
||||
return UrlSafeBase64Encode(Encoding.UTF8.GetBytes(value));
|
||||
}
|
||||
|
||||
/// <summary>Encodes the byte array into an URL safe base64 string.</summary>
|
||||
/// <param name="bytes">Byte array to encode.</param>
|
||||
/// <returns>The URL safe base64 string.</returns>
|
||||
private string UrlSafeBase64Encode(byte[] bytes)
|
||||
{
|
||||
return UrlSafeEncode(Convert.ToBase64String(bytes));
|
||||
}
|
||||
|
||||
/// <summary>Encodes the base64 string into an URL safe string.</summary>
|
||||
/// <param name="base64Value">The base64 string to make URL safe.</param>
|
||||
/// <returns>The URL safe base64 string.</returns>
|
||||
private string UrlSafeEncode(string base64Value)
|
||||
{
|
||||
return base64Value.Replace("=", String.Empty).Replace('+', '-').Replace('/', '_');
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,234 @@
|
||||
/*
|
||||
Copyright 2014 Google Inc
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
using System;
|
||||
using System.Net;
|
||||
using System.Net.Http;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
using Google.Apis.Auth.OAuth2.Responses;
|
||||
using Google.Apis.Http;
|
||||
using Google.Apis.Logging;
|
||||
using Google.Apis.Util;
|
||||
|
||||
namespace Google.Apis.Auth.OAuth2
|
||||
{
|
||||
/// <summary>
|
||||
/// This type of Google OAuth 2.0 credential enables access to protected resources using an access token when
|
||||
/// interacting server to server. For example, a service account credential could be used to access Google Cloud
|
||||
/// Storage from a web application without a user's involvement.
|
||||
/// <para>
|
||||
/// <code>ServiceAccountCredential</code> inherits from this class in order to support Service Account. More
|
||||
/// details available at: https://developers.google.com/accounts/docs/OAuth2ServiceAccount.
|
||||
/// <see cref="Google.Apis.Auth.OAuth2.ComputeCredential"/> is another example for a class that inherits from this
|
||||
/// class in order to support Compute credentials. For more information about Compute authentication, see:
|
||||
/// https://cloud.google.com/compute/docs/authentication.
|
||||
/// </para>
|
||||
/// </summary>
|
||||
public abstract class ServiceCredential : ICredential, IHttpExecuteInterceptor, IHttpUnsuccessfulResponseHandler
|
||||
{
|
||||
/// <summary>Logger for this class</summary>
|
||||
protected static readonly ILogger Logger = ApplicationContext.Logger.ForType<ServiceCredential>();
|
||||
|
||||
/// <summary>An initializer class for the service credential. </summary>
|
||||
public class Initializer
|
||||
{
|
||||
/// <summary>Gets the token server URL.</summary>
|
||||
public string TokenServerUrl { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the clock used to refresh the token when it expires. The default value is
|
||||
/// <see cref="Google.Apis.Util.SystemClock.Default"/>.
|
||||
/// </summary>
|
||||
public IClock Clock { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the method for presenting the access token to the resource server.
|
||||
/// The default value is <see cref="BearerToken.AuthorizationHeaderAccessMethod"/>.
|
||||
/// </summary>
|
||||
public IAccessMethod AccessMethod { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the factory for creating a <see cref="System.Net.Http.HttpClient"/> instance.
|
||||
/// </summary>
|
||||
public IHttpClientFactory HttpClientFactory { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Get or sets the exponential back-off policy. Default value is <c>UnsuccessfulResponse503</c>, which
|
||||
/// means that exponential back-off is used on 503 abnormal HTTP responses.
|
||||
/// If the value is set to <c>None</c>, no exponential back-off policy is used, and it's up to the user to
|
||||
/// configure the <see cref="Google.Apis.Http.ConfigurableMessageHandler"/> in an
|
||||
/// <see cref="Google.Apis.Http.IConfigurableHttpClientInitializer"/> to set a specific back-off
|
||||
/// implementation (using <see cref="Google.Apis.Http.BackOffHandler"/>).
|
||||
/// </summary>
|
||||
public ExponentialBackOffPolicy DefaultExponentialBackOffPolicy { get; set; }
|
||||
|
||||
/// <summary>Constructs a new initializer using the given token server URL.</summary>
|
||||
public Initializer(string tokenServerUrl)
|
||||
{
|
||||
TokenServerUrl = tokenServerUrl;
|
||||
|
||||
AccessMethod = new BearerToken.AuthorizationHeaderAccessMethod();
|
||||
Clock = SystemClock.Default;
|
||||
DefaultExponentialBackOffPolicy = ExponentialBackOffPolicy.UnsuccessfulResponse503;
|
||||
}
|
||||
}
|
||||
|
||||
#region Readonly fields
|
||||
|
||||
private readonly string tokenServerUrl;
|
||||
private readonly IClock clock;
|
||||
private readonly IAccessMethod accessMethod;
|
||||
private readonly ConfigurableHttpClient httpClient;
|
||||
|
||||
#endregion
|
||||
|
||||
/// <summary>Gets the token server URL.</summary>
|
||||
public string TokenServerUrl { get { return tokenServerUrl; } }
|
||||
|
||||
/// <summary>Gets the clock used to refresh the token if it expires.</summary>
|
||||
public IClock Clock { get { return clock; } }
|
||||
|
||||
/// <summary>Gets the method for presenting the access token to the resource server.</summary>
|
||||
public IAccessMethod AccessMethod { get { return accessMethod; } }
|
||||
|
||||
/// <summary>Gets the HTTP client used to make authentication requests to the server.</summary>
|
||||
public ConfigurableHttpClient HttpClient { get { return httpClient; } }
|
||||
|
||||
private TokenResponse token;
|
||||
private object lockObject = new object();
|
||||
|
||||
/// <summary>Gets the token response which contains the access token.</summary>
|
||||
public TokenResponse Token
|
||||
{
|
||||
get
|
||||
{
|
||||
lock (lockObject)
|
||||
{
|
||||
return token;
|
||||
}
|
||||
}
|
||||
protected set
|
||||
{
|
||||
lock (lockObject)
|
||||
{
|
||||
token = value;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Constructs a new service account credential using the given initializer.</summary>
|
||||
public ServiceCredential(Initializer initializer)
|
||||
{
|
||||
tokenServerUrl = initializer.TokenServerUrl;
|
||||
accessMethod = initializer.AccessMethod.ThrowIfNull("initializer.AccessMethod");
|
||||
clock = initializer.Clock.ThrowIfNull("initializer.Clock");
|
||||
|
||||
// Set the HTTP client.
|
||||
var httpArgs = new CreateHttpClientArgs();
|
||||
|
||||
// Add exponential back-off initializer if necessary.
|
||||
if (initializer.DefaultExponentialBackOffPolicy != ExponentialBackOffPolicy.None)
|
||||
{
|
||||
httpArgs.Initializers.Add(
|
||||
new ExponentialBackOffInitializer(initializer.DefaultExponentialBackOffPolicy,
|
||||
() => new BackOffHandler(new ExponentialBackOff())));
|
||||
}
|
||||
httpClient = (initializer.HttpClientFactory ?? new HttpClientFactory()).CreateHttpClient(httpArgs);
|
||||
}
|
||||
|
||||
#region IConfigurableHttpClientInitializer
|
||||
|
||||
/// <inheritdoc/>
|
||||
public void Initialize(ConfigurableHttpClient httpClient)
|
||||
{
|
||||
httpClient.MessageHandler.AddExecuteInterceptor(this);
|
||||
httpClient.MessageHandler.AddUnsuccessfulResponseHandler(this);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region IHttpExecuteInterceptor implementation
|
||||
|
||||
/// <inheritdoc/>
|
||||
public async Task InterceptAsync(HttpRequestMessage request, CancellationToken cancellationToken)
|
||||
{
|
||||
var accessToken = await GetAccessTokenForRequestAsync(request.RequestUri.ToString(), cancellationToken)
|
||||
.ConfigureAwait(false);
|
||||
AccessMethod.Intercept(request, accessToken);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region IHttpUnsuccessfulResponseHandler
|
||||
|
||||
/// <summary>
|
||||
/// Decorates unsuccessful responses, returns true if the response gets modified.
|
||||
/// See IHttpUnsuccessfulResponseHandler for more information.
|
||||
/// </summary>
|
||||
public async Task<bool> HandleResponseAsync(HandleUnsuccessfulResponseArgs args)
|
||||
{
|
||||
// If the response was unauthorized, request a new access token so that the original
|
||||
// request can be retried.
|
||||
// TODO(peleyal): check WWW-Authenticate header.
|
||||
if (args.Response.StatusCode == HttpStatusCode.Unauthorized)
|
||||
{
|
||||
bool tokensEqual = false;
|
||||
if (Token != null)
|
||||
{
|
||||
tokensEqual = Object.Equals(
|
||||
Token.AccessToken, AccessMethod.GetAccessToken(args.Request));
|
||||
}
|
||||
return !tokensEqual
|
||||
|| await RequestAccessTokenAsync(args.CancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
|
||||
#region ITokenAccess implementation
|
||||
|
||||
/// <summary>
|
||||
/// Gets an access token to authorize a request. If the existing token has expired, try to refresh it first.
|
||||
/// <seealso cref="ITokenAccess.GetAccessTokenForRequestAsync"/>
|
||||
/// </summary>
|
||||
public virtual async Task<string> GetAccessTokenForRequestAsync(string authUri = null,
|
||||
CancellationToken cancellationToken = default(CancellationToken))
|
||||
{
|
||||
if (Token == null || Token.IsExpired(Clock))
|
||||
{
|
||||
Logger.Debug("Token has expired, trying to get a new one.");
|
||||
if (!await RequestAccessTokenAsync(cancellationToken).ConfigureAwait(false))
|
||||
{
|
||||
throw new InvalidOperationException("The access token has expired but we can't refresh it");
|
||||
}
|
||||
Logger.Info("New access token was received successfully");
|
||||
}
|
||||
return Token.AccessToken;
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
/// <summary>Requests a new token.</summary>
|
||||
/// <param name="taskCancellationToken">Cancellation token to cancel operation.</param>
|
||||
/// <returns><c>true</c> if a new token was received successfully.</returns>
|
||||
public abstract Task<bool> RequestAccessTokenAsync(CancellationToken taskCancellationToken);
|
||||
}
|
||||
}
|
199
Yavsc/GoogleApiSupport/Google.Apis.Auth/OAuth2/UserCredential.cs
Normal file
199
Yavsc/GoogleApiSupport/Google.Apis.Auth/OAuth2/UserCredential.cs
Normal file
@ -0,0 +1,199 @@
|
||||
/*
|
||||
Copyright 2013 Google Inc
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
using System;
|
||||
using System.Net;
|
||||
using System.Net.Http;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
using Google.Apis.Auth.OAuth2.Flows;
|
||||
using Google.Apis.Auth.OAuth2.Responses;
|
||||
using Google.Apis.Http;
|
||||
using Google.Apis.Logging;
|
||||
|
||||
namespace Google.Apis.Auth.OAuth2
|
||||
{
|
||||
/// <summary>
|
||||
/// OAuth 2.0 credential for accessing protected resources using an access token, as well as optionally refreshing
|
||||
/// the access token when it expires using a refresh token.
|
||||
/// </summary>
|
||||
public class UserCredential : ICredential, IHttpExecuteInterceptor, IHttpUnsuccessfulResponseHandler
|
||||
{
|
||||
/// <summary>Logger for this class.</summary>
|
||||
protected static readonly ILogger Logger = ApplicationContext.Logger.ForType<UserCredential>();
|
||||
|
||||
private TokenResponse token;
|
||||
private object lockObject = new object();
|
||||
|
||||
/// <summary>Gets or sets the token response which contains the access token.</summary>
|
||||
public TokenResponse Token
|
||||
{
|
||||
get
|
||||
{
|
||||
lock (lockObject)
|
||||
{
|
||||
return token;
|
||||
}
|
||||
}
|
||||
set
|
||||
{
|
||||
lock (lockObject)
|
||||
{
|
||||
token = value;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Gets the authorization code flow.</summary>
|
||||
public IAuthorizationCodeFlow Flow
|
||||
{
|
||||
get { return flow; }
|
||||
}
|
||||
|
||||
/// <summary>Gets the user identity.</summary>
|
||||
public string UserId
|
||||
{
|
||||
get { return userId; }
|
||||
}
|
||||
|
||||
private readonly IAuthorizationCodeFlow flow;
|
||||
private readonly string userId;
|
||||
|
||||
/// <summary>Constructs a new credential instance.</summary>
|
||||
/// <param name="flow">Authorization code flow.</param>
|
||||
/// <param name="userId">User identifier.</param>
|
||||
/// <param name="token">An initial token for the user.</param>
|
||||
public UserCredential(IAuthorizationCodeFlow flow, string userId, TokenResponse token)
|
||||
{
|
||||
this.flow = flow;
|
||||
this.userId = userId;
|
||||
this.token = token;
|
||||
}
|
||||
|
||||
#region IHttpExecuteInterceptor
|
||||
|
||||
/// <summary>
|
||||
/// Default implementation is to try to refresh the access token if there is no access token or if we are 1
|
||||
/// minute away from expiration. If token server is unavailable, it will try to use the access token even if
|
||||
/// has expired. If successful, it will call <see cref="IAccessMethod.Intercept"/>.
|
||||
/// </summary>
|
||||
public async Task InterceptAsync(HttpRequestMessage request, CancellationToken taskCancellationToken)
|
||||
{
|
||||
var accessToken = await GetAccessTokenForRequestAsync(request.RequestUri.ToString(), taskCancellationToken).ConfigureAwait(false);
|
||||
flow.AccessMethod.Intercept(request, Token.AccessToken);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region IHttpUnsuccessfulResponseHandler
|
||||
|
||||
/// <inheritdoc/>
|
||||
public async Task<bool> HandleResponseAsync(HandleUnsuccessfulResponseArgs args)
|
||||
{
|
||||
// TODO(peleyal): check WWW-Authenticate header.
|
||||
if (args.Response.StatusCode == HttpStatusCode.Unauthorized)
|
||||
{
|
||||
return !Object.Equals(Token.AccessToken, flow.AccessMethod.GetAccessToken(args.Request))
|
||||
|| await RefreshTokenAsync(args.CancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region IConfigurableHttpClientInitializer
|
||||
|
||||
/// <inheritdoc/>
|
||||
public void Initialize(ConfigurableHttpClient httpClient)
|
||||
{
|
||||
httpClient.MessageHandler.AddExecuteInterceptor(this);
|
||||
httpClient.MessageHandler.AddUnsuccessfulResponseHandler(this);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region ITokenAccess implementation
|
||||
|
||||
/// <inheritdoc/>
|
||||
public virtual async Task<string> GetAccessTokenForRequestAsync(string authUri = null, CancellationToken cancellationToken = default(CancellationToken))
|
||||
{
|
||||
if (Token.IsExpired(flow.Clock))
|
||||
{
|
||||
Logger.Debug("Token has expired, trying to refresh it.");
|
||||
if (!await RefreshTokenAsync(cancellationToken).ConfigureAwait(false))
|
||||
{
|
||||
throw new InvalidOperationException("The access token has expired but we can't refresh it");
|
||||
}
|
||||
}
|
||||
return token.AccessToken;
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
/// <summary>
|
||||
/// Refreshes the token by calling to
|
||||
/// <see cref="Google.Apis.Auth.OAuth2.Flows.IAuthorizationCodeFlow.RefreshTokenAsync"/>.
|
||||
/// Then it updates the <see cref="TokenResponse"/> with the new token instance.
|
||||
/// </summary>
|
||||
/// <param name="taskCancellationToken">Cancellation token to cancel an operation.</param>
|
||||
/// <returns><c>true</c> if the token was refreshed.</returns>
|
||||
public async Task<bool> RefreshTokenAsync(CancellationToken taskCancellationToken)
|
||||
{
|
||||
if (Token.RefreshToken == null)
|
||||
{
|
||||
Logger.Warning("Refresh token is null, can't refresh the token!");
|
||||
return false;
|
||||
}
|
||||
|
||||
// It's possible that two concurrent calls will be made to refresh the token, in that case the last one
|
||||
// will win.
|
||||
var newToken = await flow.RefreshTokenAsync(userId, Token.RefreshToken, taskCancellationToken)
|
||||
.ConfigureAwait(false);
|
||||
|
||||
Logger.Info("Access token was refreshed successfully");
|
||||
|
||||
if (newToken.RefreshToken == null)
|
||||
{
|
||||
newToken.RefreshToken = Token.RefreshToken;
|
||||
}
|
||||
|
||||
Token = newToken;
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Asynchronously revokes the token by calling
|
||||
/// <see cref="Google.Apis.Auth.OAuth2.Flows.IAuthorizationCodeFlow.RevokeTokenAsync"/>.
|
||||
/// </summary>
|
||||
/// <param name="taskCancellationToken">Cancellation token to cancel an operation.</param>
|
||||
/// <returns><c>true</c> if the token was revoked successfully.</returns>
|
||||
public async Task<bool> RevokeTokenAsync(CancellationToken taskCancellationToken)
|
||||
{
|
||||
if (Token == null)
|
||||
{
|
||||
Logger.Warning("Token is already null, no need to revoke it.");
|
||||
return false;
|
||||
}
|
||||
|
||||
await flow.RevokeTokenAsync(userId, Token.AccessToken, taskCancellationToken).ConfigureAwait(false);
|
||||
Logger.Info("Access token was revoked successfully");
|
||||
// We don't set the token to null, cause we want that the next request (without reauthorizing) will fail).
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,62 @@
|
||||
/*
|
||||
Copyright 2013 Google Inc
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
using System;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
using Google.Apis.Auth.OAuth2.Responses;
|
||||
using Google.Apis.Util.Store;
|
||||
|
||||
namespace Google.Apis.Auth.OAuth2.Web
|
||||
{
|
||||
/// <summary>Auth Utility methods for web development.</summary>
|
||||
public class AuthWebUtility
|
||||
{
|
||||
/// <summary>Extracts the redirect URI from the state OAuth2 parameter.</summary>
|
||||
/// <remarks>
|
||||
/// If the data store is not <c>null</c>, this method verifies that the state parameter which was returned
|
||||
/// from the authorization server is the same as the one we set before redirecting to the authorization server.
|
||||
/// </remarks>
|
||||
/// <param name="dataStore">The data store which contains the original state parameter.</param>
|
||||
/// <param name="userId">User identifier.</param>
|
||||
/// <param name="state">
|
||||
/// The authorization state parameter which we got back from the authorization server.
|
||||
/// </param>
|
||||
/// <returns>Redirect URI to the address which initializes the authorization code flow.</returns>
|
||||
public static async Task<string> ExtracRedirectFromState(IDataStore dataStore, string userId, string state)
|
||||
{
|
||||
var oauthState = state;
|
||||
if (dataStore != null)
|
||||
{
|
||||
var userKey = AuthorizationCodeWebApp.StateKey + userId;
|
||||
var expectedState = await dataStore.GetAsync<string>(userKey).ConfigureAwait(false);
|
||||
|
||||
// Verify that the stored state is equal to the one we got back from the authorization server.
|
||||
if (!Object.Equals(oauthState, expectedState))
|
||||
{
|
||||
throw new TokenResponseException(new TokenErrorResponse
|
||||
{
|
||||
Error = "State is invalid"
|
||||
});
|
||||
}
|
||||
await dataStore.DeleteAsync<string>(userKey).ConfigureAwait(false);
|
||||
oauthState = oauthState.Substring(0, oauthState.Length - AuthorizationCodeWebApp.StateRandomLength);
|
||||
}
|
||||
|
||||
return oauthState;
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,141 @@
|
||||
/*
|
||||
Copyright 2013 Google Inc
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
using System;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
using Google.Apis.Auth.OAuth2.Flows;
|
||||
using Google.Apis.Auth.OAuth2.Requests;
|
||||
using Google.Apis.Auth.OAuth2.Responses;
|
||||
|
||||
namespace Google.Apis.Auth.OAuth2.Web
|
||||
{
|
||||
/// <summary>
|
||||
/// Thread safe OAuth 2.0 authorization code flow for a web application that persists end-user credentials.
|
||||
/// </summary>
|
||||
public class AuthorizationCodeWebApp
|
||||
{
|
||||
/// <summary>
|
||||
/// The state key. As part of making the request for authorization code we save the original request to verify
|
||||
/// that this server create the original request.
|
||||
/// </summary>
|
||||
public const string StateKey = "oauth_";
|
||||
|
||||
/// <summary>The length of the random number which will be added to the end of the state parameter.</summary>
|
||||
public const int StateRandomLength = 8;
|
||||
|
||||
/// <summary>
|
||||
/// AuthResult which contains the user's credentials if it was loaded successfully from the store. Otherwise
|
||||
/// it contains the redirect URI for the authorization server.
|
||||
/// </summary>
|
||||
public class AuthResult
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets or sets the user's credentials or <c>null</c> in case the end user needs to authorize.
|
||||
/// </summary>
|
||||
public UserCredential Credential { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the redirect URI to for the user to authorize against the authorization server or
|
||||
/// <c>null</c> in case the <see cref="Google.Apis.Auth.OAuth2.UserCredential"/> was loaded from the data
|
||||
/// store.
|
||||
/// </summary>
|
||||
public string RedirectUri { get; set; }
|
||||
}
|
||||
|
||||
private readonly IAuthorizationCodeFlow flow;
|
||||
private readonly string redirectUri;
|
||||
private readonly string state;
|
||||
|
||||
/// <summary>Gets the authorization code flow.</summary>
|
||||
public IAuthorizationCodeFlow Flow
|
||||
{
|
||||
get { return flow; }
|
||||
}
|
||||
|
||||
/// <summary>Gets the OAuth2 callback redirect URI.</summary>
|
||||
public string RedirectUri
|
||||
{
|
||||
get { return redirectUri; }
|
||||
}
|
||||
|
||||
/// <summary>Gets the state which is used to navigate back to the page that started the OAuth flow.</summary>
|
||||
public string State
|
||||
{
|
||||
get { return state; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Constructs a new authorization code installed application with the given flow and code receiver.
|
||||
/// </summary>
|
||||
public AuthorizationCodeWebApp(IAuthorizationCodeFlow flow, string redirectUri, string state)
|
||||
{
|
||||
// TODO(peleyal): Provide a way to disable to random number in the end of the state parameter.
|
||||
this.flow = flow;
|
||||
this.redirectUri = redirectUri;
|
||||
this.state = state;
|
||||
}
|
||||
|
||||
/// <summary>Asynchronously authorizes the web application to access user's protected data.</summary>
|
||||
/// <param name="userId">User identifier</param>
|
||||
/// <param name="taskCancellationToken">Cancellation token to cancel an operation</param>
|
||||
/// <returns>
|
||||
/// Auth result object which contains the user's credential or redirect URI for the authorization server
|
||||
/// </returns>
|
||||
public async Task<AuthResult> AuthorizeAsync(string userId, CancellationToken taskCancellationToken)
|
||||
{
|
||||
// Try to load a token from the data store.
|
||||
var token = await Flow.LoadTokenAsync(userId, taskCancellationToken).ConfigureAwait(false);
|
||||
|
||||
// Check if a new authorization code is needed.
|
||||
if (ShouldRequestAuthorizationCode(token))
|
||||
{
|
||||
// Create an authorization code request.
|
||||
AuthorizationCodeRequestUrl codeRequest = Flow.CreateAuthorizationCodeRequest(redirectUri);
|
||||
|
||||
// Add a random number to the end of the state so we can indicate the original request was made by this
|
||||
// call.
|
||||
var oauthState = state;
|
||||
if (Flow.DataStore != null)
|
||||
{
|
||||
var rndString = new string('9', StateRandomLength);
|
||||
var random = new Random().Next(int.Parse(rndString)).ToString("D" + StateRandomLength);
|
||||
oauthState += random;
|
||||
await Flow.DataStore.StoreAsync(StateKey + userId, oauthState).ConfigureAwait(false);
|
||||
}
|
||||
codeRequest.State = oauthState;
|
||||
|
||||
return new AuthResult { RedirectUri = codeRequest.Build().ToString() };
|
||||
}
|
||||
|
||||
return new AuthResult { Credential = new UserCredential(flow, userId, token) };
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Determines the need for retrieval of a new authorization code, based on the given token and the
|
||||
/// authorization code flow.
|
||||
/// </summary>
|
||||
public bool ShouldRequestAuthorizationCode(TokenResponse token)
|
||||
{
|
||||
// TODO: This code should be shared between this class and AuthorizationCodeInstalledApp.
|
||||
// If the flow includes a parameter that requires a new token, if the stored token is null or it doesn't
|
||||
// have a refresh token and the access token is expired we need to retrieve a new authorization code.
|
||||
return Flow.ShouldForceTokenRetrieval() || token == null || (token.RefreshToken == null
|
||||
&& token.IsExpired(flow.Clock));
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,53 @@
|
||||
/*
|
||||
Copyright 2011 Google Inc
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
using Google.Apis.Logging;
|
||||
using System;
|
||||
|
||||
namespace Google
|
||||
{
|
||||
/// <summary>Defines the context in which this library runs. It allows setting up custom loggers.</summary>
|
||||
public static class ApplicationContext
|
||||
{
|
||||
private static ILogger logger;
|
||||
|
||||
// For testing
|
||||
internal static void Reset() => logger = null;
|
||||
|
||||
/// <summary>Returns the logger used within this application context.</summary>
|
||||
/// <remarks>It creates a <see cref="NullLogger"/> if no logger was registered previously</remarks>
|
||||
public static ILogger Logger
|
||||
{
|
||||
get
|
||||
{
|
||||
// Register the default null-logger if no other one was set.
|
||||
return logger ?? (logger = new NullLogger());
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Registers a logger with this application context.</summary>
|
||||
/// <exception cref="InvalidOperationException">Thrown if a logger was already registered.</exception>
|
||||
public static void RegisterLogger(ILogger loggerToRegister)
|
||||
{
|
||||
// TODO(peleyal): Reconsider why the library should contain only one logger. Also consider using Tracing!
|
||||
if (logger != null && !(logger is NullLogger))
|
||||
{
|
||||
throw new InvalidOperationException("A logger was already registered with this context.");
|
||||
}
|
||||
logger = loggerToRegister;
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,25 @@
|
||||
/*
|
||||
Copyright 2010 Google Inc
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
namespace Google.Apis.Discovery
|
||||
{
|
||||
/// <summary>An enumeration of all supported discovery versions.</summary>
|
||||
public enum DiscoveryVersion
|
||||
{
|
||||
/// <summary>Discovery version 1.0.</summary>
|
||||
Version_1_0,
|
||||
}
|
||||
}
|
@ -0,0 +1,32 @@
|
||||
/*
|
||||
Copyright 2011 Google Inc
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
using Google.Apis.Util;
|
||||
|
||||
namespace Google.Apis.Discovery
|
||||
{
|
||||
/// <summary>
|
||||
/// Specifies a list of features which can be defined within the discovery document of a service.
|
||||
/// </summary>
|
||||
public enum Features
|
||||
{
|
||||
/// <summary>
|
||||
/// If this feature is specified, then the data of a response is encapsulated within a "data" resource.
|
||||
/// </summary>
|
||||
[StringValue("dataWrapper")]
|
||||
LegacyDataResponse,
|
||||
}
|
||||
}
|
@ -0,0 +1,37 @@
|
||||
/*
|
||||
Copyright 2010 Google Inc
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
namespace Google.Apis.Discovery
|
||||
{
|
||||
/// <summary>Represents a parameter for a method.</summary>
|
||||
public interface IParameter
|
||||
{
|
||||
/// <summary>Gets the name of the parameter.</summary>
|
||||
string Name { get; }
|
||||
|
||||
/// <summary>Gets the pattern that this parameter must follow.</summary>
|
||||
string Pattern { get; }
|
||||
|
||||
/// <summary>Gets an indication whether this parameter is optional or required.</summary>
|
||||
bool IsRequired { get; }
|
||||
|
||||
/// <summary>Gets the default value of this parameter.</summary>
|
||||
string DefaultValue { get; }
|
||||
|
||||
/// <summary>Gets the type of the parameter.</summary>
|
||||
string ParameterType { get; }
|
||||
}
|
||||
}
|
@ -0,0 +1,36 @@
|
||||
/*
|
||||
Copyright 2013 Google Inc.
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
namespace Google.Apis.Discovery
|
||||
{
|
||||
/// <summary>Represents a method's parameter.</summary>
|
||||
public class Parameter : IParameter
|
||||
{
|
||||
/// <inheritdoc/>
|
||||
public string Name { get; set; }
|
||||
|
||||
/// <inheritdoc/>
|
||||
public string Pattern { get; set; }
|
||||
|
||||
/// <inheritdoc/>
|
||||
public bool IsRequired { get; set; }
|
||||
|
||||
/// <inheritdoc/>
|
||||
public string ParameterType { get; set; }
|
||||
|
||||
/// <inheritdoc/>
|
||||
public string DefaultValue { get; set; }
|
||||
}
|
||||
}
|
@ -0,0 +1,62 @@
|
||||
/*
|
||||
Copyright 2010 Google Inc
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
using System;
|
||||
using System.Net;
|
||||
|
||||
using Google.Apis.Requests;
|
||||
using Google.Apis.Util;
|
||||
|
||||
namespace Google
|
||||
{
|
||||
/// <summary>Represents an exception thrown by an API Service.</summary>
|
||||
public class GoogleApiException : Exception
|
||||
{
|
||||
private readonly string serviceName;
|
||||
|
||||
/// <summary>Gets the service name which related to this exception.</summary>
|
||||
public string ServiceName
|
||||
{
|
||||
get { return serviceName; }
|
||||
}
|
||||
|
||||
/// <summary>Creates an API Service exception.</summary>
|
||||
public GoogleApiException(string serviceName, string message, Exception inner)
|
||||
: base(message, inner)
|
||||
{
|
||||
serviceName.ThrowIfNull("serviceName");
|
||||
this.serviceName = serviceName;
|
||||
}
|
||||
|
||||
/// <summary>Creates an API Service exception.</summary>
|
||||
public GoogleApiException(string serviceName, string message) : this(serviceName, message, null) { }
|
||||
|
||||
/// <summary>The Error which was returned from the server, or <c>null</c> if unavailable.</summary>
|
||||
public RequestError Error { get; set; }
|
||||
|
||||
/// <summary>The HTTP status code which was returned along with this error, or 0 if unavailable.</summary>
|
||||
public HttpStatusCode HttpStatusCode { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Returns a summary of this exception.
|
||||
/// </summary>
|
||||
/// <returns>A summary of this exception.</returns>
|
||||
public override string ToString()
|
||||
{
|
||||
return string.Format("The service {1} has thrown an exception: {0}", base.ToString(), serviceName);
|
||||
}
|
||||
}
|
||||
}
|
186
Yavsc/GoogleApiSupport/Google.Apis.Core/Http/BackOffHandler.cs
Normal file
186
Yavsc/GoogleApiSupport/Google.Apis.Core/Http/BackOffHandler.cs
Normal file
@ -0,0 +1,186 @@
|
||||
/*
|
||||
Copyright 2013 Google Inc
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
using System;
|
||||
using System.Net.Http;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
using Google.Apis.Logging;
|
||||
using Google.Apis.Util;
|
||||
|
||||
namespace Google.Apis.Http
|
||||
{
|
||||
/// <summary>
|
||||
/// A thread-safe back-off handler which handles an abnormal HTTP response or an exception with
|
||||
/// <see cref="Google.Apis.Util.IBackOff"/>.
|
||||
/// </summary>
|
||||
public class BackOffHandler : IHttpUnsuccessfulResponseHandler, IHttpExceptionHandler
|
||||
{
|
||||
private static readonly ILogger Logger = ApplicationContext.Logger.ForType<BackOffHandler>();
|
||||
|
||||
/// <summary>An initializer class to initialize a back-off handler.</summary>
|
||||
public class Initializer
|
||||
{
|
||||
/// <summary>Gets the back-off policy used by this back-off handler.</summary>
|
||||
public IBackOff BackOff { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the maximum time span to wait. If the back-off instance returns a greater time span than
|
||||
/// this value, this handler returns <c>false</c> to both <c>HandleExceptionAsync</c> and
|
||||
/// <c>HandleResponseAsync</c>. Default value is 16 seconds per a retry request.
|
||||
/// </summary>
|
||||
public TimeSpan MaxTimeSpan { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets a delegate function which indicates whether this back-off handler should handle an
|
||||
/// abnormal HTTP response. The default is <see cref="DefaultHandleUnsuccessfulResponseFunc"/>.
|
||||
/// </summary>
|
||||
public Func<HttpResponseMessage, bool> HandleUnsuccessfulResponseFunc { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets a delegate function which indicates whether this back-off handler should handle an
|
||||
/// exception. The default is <see cref="DefaultHandleExceptionFunc"/>.
|
||||
/// </summary>
|
||||
public Func<Exception, bool> HandleExceptionFunc { get; set; }
|
||||
|
||||
/// <summary>Default function which handles server errors (503).</summary>
|
||||
public static readonly Func<HttpResponseMessage, bool> DefaultHandleUnsuccessfulResponseFunc =
|
||||
(r) => (int)r.StatusCode == 503;
|
||||
|
||||
/// <summary>
|
||||
/// Default function which handles exception which aren't
|
||||
/// <see cref="System.Threading.Tasks.TaskCanceledException"/> or
|
||||
/// <see cref="System.OperationCanceledException"/>. Those exceptions represent a task or an operation
|
||||
/// which was canceled and shouldn't be retried.
|
||||
/// </summary>
|
||||
public static readonly Func<Exception, bool> DefaultHandleExceptionFunc =
|
||||
(ex) => !(ex is TaskCanceledException || ex is OperationCanceledException);
|
||||
|
||||
/// <summary>Constructs a new initializer by the given back-off.</summary>
|
||||
public Initializer(IBackOff backOff)
|
||||
{
|
||||
BackOff = backOff;
|
||||
HandleExceptionFunc = DefaultHandleExceptionFunc;
|
||||
HandleUnsuccessfulResponseFunc = DefaultHandleUnsuccessfulResponseFunc;
|
||||
MaxTimeSpan = TimeSpan.FromSeconds(16);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Gets the back-off policy used by this back-off handler.</summary>
|
||||
public IBackOff BackOff { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the maximum time span to wait. If the back-off instance returns a greater time span, the handle method
|
||||
/// returns <c>false</c>. Default value is 16 seconds per a retry request.
|
||||
/// </summary>
|
||||
public TimeSpan MaxTimeSpan { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets a delegate function which indicates whether this back-off handler should handle an abnormal HTTP
|
||||
/// response. The default is <see cref="Initializer.DefaultHandleUnsuccessfulResponseFunc"/>.
|
||||
/// </summary>
|
||||
public Func<HttpResponseMessage, bool> HandleUnsuccessfulResponseFunc { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets a delegate function which indicates whether this back-off handler should handle an exception. The
|
||||
/// default is <see cref="Initializer.DefaultHandleExceptionFunc"/>.
|
||||
/// </summary>
|
||||
public Func<Exception, bool> HandleExceptionFunc { get; private set; }
|
||||
|
||||
/// <summary>Constructs a new back-off handler with the given back-off.</summary>
|
||||
/// <param name="backOff">The back-off policy.</param>
|
||||
public BackOffHandler(IBackOff backOff)
|
||||
: this(new Initializer(backOff))
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>Constructs a new back-off handler with the given initializer.</summary>
|
||||
public BackOffHandler(Initializer initializer)
|
||||
{
|
||||
BackOff = initializer.BackOff;
|
||||
MaxTimeSpan = initializer.MaxTimeSpan;
|
||||
HandleExceptionFunc = initializer.HandleExceptionFunc;
|
||||
HandleUnsuccessfulResponseFunc = initializer.HandleUnsuccessfulResponseFunc;
|
||||
}
|
||||
|
||||
#region IHttpUnsuccessfulResponseHandler
|
||||
|
||||
/// <inheritdoc/>
|
||||
public virtual async Task<bool> HandleResponseAsync(HandleUnsuccessfulResponseArgs args)
|
||||
{
|
||||
// if the func returns true try to handle this current failed try
|
||||
if (HandleUnsuccessfulResponseFunc != null && HandleUnsuccessfulResponseFunc(args.Response))
|
||||
{
|
||||
return await HandleAsync(args.SupportsRetry, args.CurrentFailedTry, args.CancellationToken)
|
||||
.ConfigureAwait(false);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region IHttpExceptionHandler
|
||||
|
||||
/// <inheritdoc/>
|
||||
public virtual async Task<bool> HandleExceptionAsync(HandleExceptionArgs args)
|
||||
{
|
||||
// if the func returns true try to handle this current failed try
|
||||
if (HandleExceptionFunc != null && HandleExceptionFunc(args.Exception))
|
||||
{
|
||||
return await HandleAsync(args.SupportsRetry, args.CurrentFailedTry, args.CancellationToken)
|
||||
.ConfigureAwait(false);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
/// <summary>
|
||||
/// Handles back-off. In case the request doesn't support retry or the back-off time span is greater than the
|
||||
/// maximum time span allowed for a request, the handler returns <c>false</c>. Otherwise the current thread
|
||||
/// will block for x milliseconds (x is defined by the <see cref="BackOff"/> instance), and this handler
|
||||
/// returns <c>true</c>.
|
||||
/// </summary>
|
||||
private async Task<bool> HandleAsync(bool supportsRetry, int currentFailedTry,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
if (!supportsRetry || BackOff.MaxNumOfRetries < currentFailedTry)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
TimeSpan ts = BackOff.GetNextBackOff(currentFailedTry);
|
||||
if (ts > MaxTimeSpan || ts < TimeSpan.Zero)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
await Wait(ts, cancellationToken).ConfigureAwait(false);
|
||||
Logger.Debug("Back-Off handled the error. Waited {0}ms before next retry...", ts.TotalMilliseconds);
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <summary>Waits the given time span. Overriding this method is recommended for mocking purposes.</summary>
|
||||
/// <param name="ts">TimeSpan to wait (and block the current thread).</param>
|
||||
/// <param name="cancellationToken">The cancellation token in case the user wants to cancel the operation in
|
||||
/// the middle.</param>
|
||||
protected virtual async Task Wait(TimeSpan ts, CancellationToken cancellationToken)
|
||||
{
|
||||
await Task.Delay(ts, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,38 @@
|
||||
/*
|
||||
Copyright 2013 Google Inc
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
using System.Net.Http;
|
||||
|
||||
namespace Google.Apis.Http
|
||||
{
|
||||
/// <summary>
|
||||
/// Configurable HTTP client inherits from <see cref="System.Net.Http.HttpClient"/> and contains a reference to
|
||||
/// <see cref="Google.Apis.Http.ConfigurableMessageHandler"/>.
|
||||
/// </summary>
|
||||
public class ConfigurableHttpClient : HttpClient
|
||||
{
|
||||
/// <summary>Gets the configurable message handler.</summary>
|
||||
public ConfigurableMessageHandler MessageHandler { get; private set; }
|
||||
|
||||
/// <summary>Constructs a new HTTP client.</summary>
|
||||
public ConfigurableHttpClient(ConfigurableMessageHandler handler)
|
||||
: base(handler)
|
||||
{
|
||||
MessageHandler = handler;
|
||||
DefaultRequestHeaders.ExpectContinue = false;
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,593 @@
|
||||
/*
|
||||
Copyright 2013 Google Inc
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Net;
|
||||
using System.Net.Http;
|
||||
using System.Text;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
using Google.Apis.Logging;
|
||||
using Google.Apis.Testing;
|
||||
using System.Net.Http.Headers;
|
||||
|
||||
namespace Google.Apis.Http
|
||||
{
|
||||
/// <summary>
|
||||
/// A message handler which contains the main logic of our HTTP requests. It contains a list of
|
||||
/// <see cref="IHttpUnsuccessfulResponseHandler"/>s for handling abnormal responses, a list of
|
||||
/// <see cref="IHttpExceptionHandler"/>s for handling exception in a request and a list of
|
||||
/// <see cref="IHttpExecuteInterceptor"/>s for intercepting a request before it has been sent to the server.
|
||||
/// It also contains important properties like number of tries, follow redirect, etc.
|
||||
/// </summary>
|
||||
public class ConfigurableMessageHandler : DelegatingHandler
|
||||
{
|
||||
/// <summary>The class logger.</summary>
|
||||
private static readonly ILogger Logger = ApplicationContext.Logger.ForType<ConfigurableMessageHandler>();
|
||||
|
||||
/// <summary>Maximum allowed number of tries.</summary>
|
||||
[VisibleForTestOnly]
|
||||
public const int MaxAllowedNumTries = 20;
|
||||
|
||||
/// <summary>The current API version of this client library.</summary>
|
||||
private static readonly string ApiVersion = Google.Apis.Util.Utilities.GetLibraryVersion();
|
||||
|
||||
/// <summary>The User-Agent suffix header which contains the <see cref="ApiVersion"/>.</summary>
|
||||
private static readonly string UserAgentSuffix = "google-api-dotnet-client/" + ApiVersion + " (gzip)";
|
||||
|
||||
#region IHttpUnsuccessfulResponseHandler, IHttpExceptionHandler and IHttpExecuteInterceptor lists
|
||||
|
||||
#region Lock objects
|
||||
|
||||
// The following lock objects are used to lock the list of handlers and interceptors in order to be able to
|
||||
// iterate over them from several threads and to keep this class thread-safe.
|
||||
private readonly object unsuccessfulResponseHandlersLock = new object();
|
||||
private readonly object exceptionHandlersLock = new object();
|
||||
private readonly object executeInterceptorsLock = new object();
|
||||
|
||||
#endregion
|
||||
|
||||
/// <summary>A list of <see cref="IHttpUnsuccessfulResponseHandler"/>.</summary>
|
||||
private readonly IList<IHttpUnsuccessfulResponseHandler> unsuccessfulResponseHandlers =
|
||||
new List<IHttpUnsuccessfulResponseHandler>();
|
||||
|
||||
/// <summary>A list of <see cref="IHttpExceptionHandler"/>.</summary>
|
||||
private readonly IList<IHttpExceptionHandler> exceptionHandlers =
|
||||
new List<IHttpExceptionHandler>();
|
||||
|
||||
/// <summary>A list of <see cref="IHttpExecuteInterceptor"/>.</summary>
|
||||
private readonly IList<IHttpExecuteInterceptor> executeInterceptors =
|
||||
new List<IHttpExecuteInterceptor>();
|
||||
|
||||
/// <summary>
|
||||
/// Gets a list of <see cref="IHttpUnsuccessfulResponseHandler"/>s.
|
||||
/// <remarks>
|
||||
/// Since version 1.10, <see cref="AddUnsuccessfulResponseHandler"/> and
|
||||
/// <see cref="RemoveUnsuccessfulResponseHandler"/> were added in order to keep this class thread-safe.
|
||||
/// More information is available on
|
||||
/// <a href="https://github.com/google/google-api-dotnet-client/issues/592">#592</a>.
|
||||
/// </remarks>
|
||||
/// </summary>
|
||||
[Obsolete("Use AddUnsuccessfulResponseHandler or RemoveUnsuccessfulResponseHandler instead.")]
|
||||
public IList<IHttpUnsuccessfulResponseHandler> UnsuccessfulResponseHandlers
|
||||
{
|
||||
get { return unsuccessfulResponseHandlers; }
|
||||
}
|
||||
|
||||
/// <summary>Adds the specified handler to the list of unsuccessful response handlers.</summary>
|
||||
public void AddUnsuccessfulResponseHandler(IHttpUnsuccessfulResponseHandler handler)
|
||||
{
|
||||
lock (unsuccessfulResponseHandlersLock)
|
||||
{
|
||||
unsuccessfulResponseHandlers.Add(handler);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Removes the specified handler from the list of unsuccessful response handlers.</summary>
|
||||
public void RemoveUnsuccessfulResponseHandler(IHttpUnsuccessfulResponseHandler handler)
|
||||
{
|
||||
lock (unsuccessfulResponseHandlersLock)
|
||||
{
|
||||
unsuccessfulResponseHandlers.Remove(handler);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets a list of <see cref="IHttpExceptionHandler"/>s.
|
||||
/// <remarks>
|
||||
/// Since version 1.10, <see cref="AddExceptionHandler"/> and <see cref="RemoveExceptionHandler"/> were added
|
||||
/// in order to keep this class thread-safe. More information is available on
|
||||
/// <a href="https://github.com/google/google-api-dotnet-client/issues/592">#592</a>.
|
||||
/// </remarks>
|
||||
/// </summary>
|
||||
[Obsolete("Use AddExceptionHandler or RemoveExceptionHandler instead.")]
|
||||
public IList<IHttpExceptionHandler> ExceptionHandlers
|
||||
{
|
||||
get { return exceptionHandlers; }
|
||||
}
|
||||
|
||||
/// <summary>Adds the specified handler to the list of exception handlers.</summary>
|
||||
public void AddExceptionHandler(IHttpExceptionHandler handler)
|
||||
{
|
||||
lock (exceptionHandlersLock)
|
||||
{
|
||||
exceptionHandlers.Add(handler);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Removes the specified handler from the list of exception handlers.</summary>
|
||||
public void RemoveExceptionHandler(IHttpExceptionHandler handler)
|
||||
{
|
||||
lock (exceptionHandlersLock)
|
||||
{
|
||||
exceptionHandlers.Remove(handler);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets a list of <see cref="IHttpExecuteInterceptor"/>s.
|
||||
/// <remarks>
|
||||
/// Since version 1.10, <see cref="AddExecuteInterceptor"/> and <see cref="RemoveExecuteInterceptor"/> were
|
||||
/// added in order to keep this class thread-safe. More information is available on
|
||||
/// <a href="https://github.com/google/google-api-dotnet-client/issues/592">#592</a>.
|
||||
/// </remarks>
|
||||
/// </summary>
|
||||
[Obsolete("Use AddExecuteInterceptor or RemoveExecuteInterceptor instead.")]
|
||||
public IList<IHttpExecuteInterceptor> ExecuteInterceptors
|
||||
{
|
||||
get { return executeInterceptors; }
|
||||
}
|
||||
|
||||
/// <summary>Adds the specified interceptor to the list of execute interceptors.</summary>
|
||||
public void AddExecuteInterceptor(IHttpExecuteInterceptor interceptor)
|
||||
{
|
||||
lock (executeInterceptorsLock)
|
||||
{
|
||||
executeInterceptors.Add(interceptor);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Removes the specified interceptor from the list of execute interceptors.</summary>
|
||||
public void RemoveExecuteInterceptor(IHttpExecuteInterceptor interceptor)
|
||||
{
|
||||
lock (executeInterceptorsLock)
|
||||
{
|
||||
executeInterceptors.Remove(interceptor);
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
private int _loggingRequestId = 0;
|
||||
|
||||
private ILogger _instanceLogger = Logger;
|
||||
|
||||
/// <summary>
|
||||
/// For testing only.
|
||||
/// This defaults to the static <see cref="Logger"/>, but can be overridden for fine-grain testing.
|
||||
/// </summary>
|
||||
internal ILogger InstanceLogger
|
||||
{
|
||||
get { return _instanceLogger; }
|
||||
set { _instanceLogger = value.ForType<ConfigurableMessageHandler>(); }
|
||||
}
|
||||
|
||||
/// <summary>Number of tries. Default is <c>3</c>.</summary>
|
||||
private int numTries = 3;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the number of tries that will be allowed to execute. Retries occur as a result of either
|
||||
/// <see cref="IHttpUnsuccessfulResponseHandler"/> or <see cref="IHttpExceptionHandler"/> which handles the
|
||||
/// abnormal HTTP response or exception before being terminated.
|
||||
/// Set <c>1</c> for not retrying requests. The default value is <c>3</c>.
|
||||
/// <remarks>
|
||||
/// The number of allowed redirects (3xx) is defined by <see cref="NumRedirects"/>. This property defines
|
||||
/// only the allowed tries for >=400 responses, or when an exception is thrown. For example if you set
|
||||
/// <see cref="NumTries"/> to 1 and <see cref="NumRedirects"/> to 5, the library will send up to five redirect
|
||||
/// requests, but will not send any retry requests due to an error HTTP status code.
|
||||
/// </remarks>
|
||||
/// </summary>
|
||||
public int NumTries
|
||||
{
|
||||
get { return numTries; }
|
||||
set
|
||||
{
|
||||
if (value > MaxAllowedNumTries || value < 1)
|
||||
{
|
||||
throw new ArgumentOutOfRangeException("NumTries");
|
||||
}
|
||||
numTries = value;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Number of redirects allowed. Default is <c>10</c>.</summary>
|
||||
private int numRedirects = 10;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the number of redirects that will be allowed to execute. The default value is <c>10</c>.
|
||||
/// See <see cref="NumTries"/> for more information.
|
||||
/// </summary>
|
||||
public int NumRedirects
|
||||
{
|
||||
get { return numRedirects; }
|
||||
set
|
||||
{
|
||||
if (value > MaxAllowedNumTries || value < 1)
|
||||
{
|
||||
throw new ArgumentOutOfRangeException("NumRedirects");
|
||||
}
|
||||
numRedirects = value;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets whether the handler should follow a redirect when a redirect response is received. Default
|
||||
/// value is <c>true</c>.
|
||||
/// </summary>
|
||||
public bool FollowRedirect { get; set; }
|
||||
|
||||
/// <summary>Gets or sets whether logging is enabled. Default value is <c>true</c>.</summary>
|
||||
public bool IsLoggingEnabled { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Specifies the type(s) of request/response events to log.
|
||||
/// </summary>
|
||||
[Flags]
|
||||
public enum LogEventType
|
||||
{
|
||||
/// <summary>
|
||||
/// Log no request/response information.
|
||||
/// </summary>
|
||||
None = 0,
|
||||
|
||||
/// <summary>
|
||||
/// Log the request URI.
|
||||
/// </summary>
|
||||
RequestUri = 1,
|
||||
|
||||
/// <summary>
|
||||
/// Log the request headers.
|
||||
/// </summary>
|
||||
RequestHeaders = 2,
|
||||
|
||||
/// <summary>
|
||||
/// Log the request body. The body is assumed to be ASCII, and non-printable charaters are replaced by '.'.
|
||||
/// Warning: This causes the body content to be buffered in memory, so use with care for large requests.
|
||||
/// </summary>
|
||||
RequestBody = 4,
|
||||
|
||||
/// <summary>
|
||||
/// Log the response status.
|
||||
/// </summary>
|
||||
ResponseStatus = 8,
|
||||
|
||||
/// <summary>
|
||||
/// Log the response headers.
|
||||
/// </summary>
|
||||
ResponseHeaders = 16,
|
||||
|
||||
/// <summary>
|
||||
/// Log the response body. The body is assumed to be ASCII, and non-printable characters are replaced by '.'.
|
||||
/// Warning: This causes the body content to be buffered in memory, so use with care for large responses.
|
||||
/// </summary>
|
||||
ResponseBody = 32,
|
||||
|
||||
/// <summary>
|
||||
/// Log abnormal response messages.
|
||||
/// </summary>
|
||||
ResponseAbnormal = 64,
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The request/response types to log.
|
||||
/// </summary>
|
||||
public LogEventType LogEvents { get; set; }
|
||||
|
||||
/// <summary>Gets or sets the application name which will be used on the User-Agent header.</summary>
|
||||
public string ApplicationName { get; set; }
|
||||
|
||||
/// <summary>Constructs a new configurable message handler.</summary>
|
||||
public ConfigurableMessageHandler(HttpMessageHandler httpMessageHandler)
|
||||
: base(httpMessageHandler)
|
||||
{
|
||||
// set default values
|
||||
FollowRedirect = true;
|
||||
IsLoggingEnabled = true;
|
||||
LogEvents = LogEventType.RequestUri | LogEventType.ResponseStatus | LogEventType.ResponseAbnormal;
|
||||
}
|
||||
|
||||
private void LogHeaders(string initialText, HttpHeaders headers1, HttpHeaders headers2)
|
||||
{
|
||||
var headers = (headers1 ?? Enumerable.Empty<KeyValuePair<string, IEnumerable<string>>>())
|
||||
.Concat(headers2 ?? Enumerable.Empty<KeyValuePair<string, IEnumerable<string>>>()).ToList();
|
||||
var args = new object[headers.Count * 2];
|
||||
var fmt = new StringBuilder(headers.Count * 32);
|
||||
fmt.Append(initialText);
|
||||
var argBuilder = new StringBuilder();
|
||||
for (int i = 0; i < headers.Count; i++)
|
||||
{
|
||||
fmt.Append($"\n [{{{i * 2}}}] '{{{1 + i * 2}}}'");
|
||||
args[i * 2] = headers[i].Key;
|
||||
argBuilder.Clear();
|
||||
args[1 + i * 2] = string.Join("; ", headers[i].Value);
|
||||
}
|
||||
InstanceLogger.Debug(fmt.ToString(), args);
|
||||
}
|
||||
|
||||
private async Task LogBody(string fmtText, HttpContent content)
|
||||
{
|
||||
// This buffers the body content within the HttpContent if required.
|
||||
var bodyBytes = content != null ? await content.ReadAsByteArrayAsync() : new byte[0];
|
||||
char[] bodyChars = new char[bodyBytes.Length];
|
||||
for (int i = 0; i < bodyBytes.Length; i++)
|
||||
{
|
||||
var b = bodyBytes[i];
|
||||
bodyChars[i] = b >= 32 && b <= 126 ? (char)b : '.';
|
||||
}
|
||||
InstanceLogger.Debug(fmtText, new string(bodyChars));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The main logic of sending a request to the server. This send method adds the User-Agent header to a request
|
||||
/// with <see cref="ApplicationName"/> and the library version. It also calls interceptors before each attempt,
|
||||
/// and unsuccessful response handler or exception handlers when abnormal response or exception occurred.
|
||||
/// </summary>
|
||||
protected override async Task<HttpResponseMessage> SendAsync(HttpRequestMessage request,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
var loggable = IsLoggingEnabled && InstanceLogger.IsDebugEnabled;
|
||||
string loggingRequestId = "";
|
||||
if (loggable)
|
||||
{
|
||||
loggingRequestId = Interlocked.Increment(ref _loggingRequestId).ToString("X8");
|
||||
}
|
||||
|
||||
int triesRemaining = NumTries;
|
||||
int redirectRemaining = NumRedirects;
|
||||
|
||||
Exception lastException = null;
|
||||
|
||||
// Set User-Agent header.
|
||||
var userAgent = (ApplicationName == null ? "" : ApplicationName + " ") + UserAgentSuffix;
|
||||
// TODO: setting the User-Agent won't work on Silverlight. We may need to create a special callback here to
|
||||
// set it correctly.
|
||||
request.Headers.Add("User-Agent", userAgent);
|
||||
|
||||
HttpResponseMessage response = null;
|
||||
do // While (triesRemaining > 0)
|
||||
{
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
|
||||
if (response != null)
|
||||
{
|
||||
response.Dispose();
|
||||
response = null;
|
||||
}
|
||||
lastException = null;
|
||||
|
||||
// We keep a local list of the interceptors, since we can't call await inside lock.
|
||||
IEnumerable<IHttpExecuteInterceptor> interceptors;
|
||||
lock (executeInterceptorsLock)
|
||||
{
|
||||
interceptors = executeInterceptors.ToList();
|
||||
}
|
||||
|
||||
// Intercept the request.
|
||||
foreach (var interceptor in interceptors)
|
||||
{
|
||||
await interceptor.InterceptAsync(request, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
if (loggable)
|
||||
{
|
||||
if ((LogEvents & LogEventType.RequestUri) != 0)
|
||||
{
|
||||
InstanceLogger.Debug("Request[{0}] (triesRemaining={1}) URI: '{2}'", loggingRequestId, triesRemaining, request.RequestUri);
|
||||
}
|
||||
if ((LogEvents & LogEventType.RequestHeaders) != 0)
|
||||
{
|
||||
LogHeaders($"Request[{loggingRequestId}] Headers:", request.Headers, request.Content?.Headers);
|
||||
}
|
||||
if ((LogEvents & LogEventType.RequestBody) != 0)
|
||||
{
|
||||
await LogBody($"Request[{loggingRequestId}] Body: '{{0}}'", request.Content);
|
||||
}
|
||||
}
|
||||
try
|
||||
{
|
||||
// Send the request!
|
||||
response = await base.SendAsync(request, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
lastException = ex;
|
||||
}
|
||||
|
||||
// Decrease the number of retries.
|
||||
if (response == null || ((int)response.StatusCode >= 400 || (int)response.StatusCode < 200))
|
||||
{
|
||||
triesRemaining--;
|
||||
}
|
||||
|
||||
// Exception was thrown, try to handle it.
|
||||
if (response == null)
|
||||
{
|
||||
var exceptionHandled = false;
|
||||
|
||||
// We keep a local list of the handlers, since we can't call await inside lock.
|
||||
IEnumerable<IHttpExceptionHandler> handlers;
|
||||
lock (exceptionHandlersLock)
|
||||
{
|
||||
handlers = exceptionHandlers.ToList();
|
||||
}
|
||||
|
||||
// Try to handle the exception with each handler.
|
||||
foreach (var handler in handlers)
|
||||
{
|
||||
exceptionHandled |= await handler.HandleExceptionAsync(new HandleExceptionArgs
|
||||
{
|
||||
Request = request,
|
||||
Exception = lastException,
|
||||
TotalTries = NumTries,
|
||||
CurrentFailedTry = NumTries - triesRemaining,
|
||||
CancellationToken = cancellationToken
|
||||
}).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
if (!exceptionHandled)
|
||||
{
|
||||
InstanceLogger.Error(lastException,
|
||||
"Response[{0}] Exception was thrown while executing a HTTP request and it wasn't handled", loggingRequestId);
|
||||
throw lastException;
|
||||
}
|
||||
else if (loggable && (LogEvents & LogEventType.ResponseAbnormal) != 0)
|
||||
{
|
||||
InstanceLogger.Debug("Response[{0}] Exception {1} was thrown, but it was handled by an exception handler",
|
||||
loggingRequestId, lastException.Message);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
if (loggable)
|
||||
{
|
||||
if ((LogEvents & LogEventType.ResponseStatus) != 0)
|
||||
{
|
||||
InstanceLogger.Debug("Response[{0}] Response status: {1} '{2}'", loggingRequestId, response.StatusCode, response.ReasonPhrase);
|
||||
}
|
||||
if ((LogEvents & LogEventType.ResponseHeaders) != 0)
|
||||
{
|
||||
LogHeaders($"Response[{loggingRequestId}] Headers:", response.Headers, response.Content?.Headers);
|
||||
}
|
||||
if ((LogEvents & LogEventType.ResponseBody) != 0)
|
||||
{
|
||||
await LogBody($"Response[{loggingRequestId}] Body: '{{0}}'", response.Content);
|
||||
}
|
||||
}
|
||||
if (response.IsSuccessStatusCode)
|
||||
{
|
||||
// No need to retry, the response was successful.
|
||||
triesRemaining = 0;
|
||||
}
|
||||
else
|
||||
{
|
||||
bool errorHandled = false;
|
||||
|
||||
// We keep a local list of the handlers, since we can't call await inside lock.
|
||||
IEnumerable<IHttpUnsuccessfulResponseHandler> handlers;
|
||||
lock (unsuccessfulResponseHandlersLock)
|
||||
{
|
||||
handlers = unsuccessfulResponseHandlers.ToList();
|
||||
}
|
||||
// Try to handle the abnormal HTTP response with each handler.
|
||||
foreach (var handler in handlers)
|
||||
{
|
||||
errorHandled |= await handler.HandleResponseAsync(new HandleUnsuccessfulResponseArgs
|
||||
{
|
||||
Request = request,
|
||||
Response = response,
|
||||
TotalTries = NumTries,
|
||||
CurrentFailedTry = NumTries - triesRemaining,
|
||||
CancellationToken = cancellationToken
|
||||
}).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
if (!errorHandled)
|
||||
{
|
||||
if (FollowRedirect && HandleRedirect(response))
|
||||
{
|
||||
if (redirectRemaining-- == 0)
|
||||
{
|
||||
triesRemaining = 0;
|
||||
}
|
||||
|
||||
errorHandled = true;
|
||||
if (loggable && (LogEvents & LogEventType.ResponseAbnormal) != 0)
|
||||
{
|
||||
InstanceLogger.Debug("Response[{0}] Redirect response was handled successfully. Redirect to {1}",
|
||||
loggingRequestId, response.Headers.Location);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
if (loggable && (LogEvents & LogEventType.ResponseAbnormal) != 0)
|
||||
{
|
||||
InstanceLogger.Debug("Response[{0}] An abnormal response wasn't handled. Status code is {1}",
|
||||
loggingRequestId, response.StatusCode);
|
||||
}
|
||||
|
||||
// No need to retry, because no handler handled the abnormal response.
|
||||
triesRemaining = 0;
|
||||
}
|
||||
}
|
||||
else if (loggable && (LogEvents & LogEventType.ResponseAbnormal) != 0)
|
||||
{
|
||||
InstanceLogger.Debug("Response[{0}] An abnormal response was handled by an unsuccessful response handler. " +
|
||||
"Status Code is {1}", loggingRequestId, response.StatusCode);
|
||||
}
|
||||
}
|
||||
}
|
||||
} while (triesRemaining > 0); // Not a successful status code but it was handled.
|
||||
|
||||
// If the response is null, we should throw the last exception.
|
||||
if (response == null)
|
||||
{
|
||||
InstanceLogger.Error(lastException, "Request[{0}] Exception was thrown while executing a HTTP request", loggingRequestId);
|
||||
throw lastException;
|
||||
}
|
||||
else if (!response.IsSuccessStatusCode && loggable && (LogEvents & LogEventType.ResponseAbnormal) != 0)
|
||||
{
|
||||
InstanceLogger.Debug("Response[{0}] Abnormal response is being returned. Status Code is {1}", loggingRequestId, response.StatusCode);
|
||||
}
|
||||
|
||||
return response;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Handles redirect if the response's status code is redirect, redirects are turned on, and the header has
|
||||
/// a location.
|
||||
/// When the status code is <c>303</c> the method on the request is changed to a GET as per the RFC2616
|
||||
/// specification. On a redirect, it also removes the <c>Authorization</c> and all <c>If-*</c> request headers.
|
||||
/// </summary>
|
||||
/// <returns> Whether this method changed the request and handled redirect successfully. </returns>
|
||||
private bool HandleRedirect(HttpResponseMessage message)
|
||||
{
|
||||
// TODO(peleyal): think if it's better to move that code to RedirectUnsucessfulResponseHandler
|
||||
var uri = message.Headers.Location;
|
||||
if (!message.IsRedirectStatusCode() || uri == null)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var request = message.RequestMessage;
|
||||
request.RequestUri = new Uri(request.RequestUri, uri);
|
||||
// Status code for a resource that has moved to a new URI and should be retrieved using GET.
|
||||
if (message.StatusCode == HttpStatusCode.SeeOther)
|
||||
{
|
||||
request.Method = HttpMethod.Get;
|
||||
}
|
||||
// Clear Authorization and If-* headers.
|
||||
request.Headers.Remove("Authorization");
|
||||
request.Headers.IfMatch.Clear();
|
||||
request.Headers.IfNoneMatch.Clear();
|
||||
request.Headers.IfModifiedSince = null;
|
||||
request.Headers.IfUnmodifiedSince = null;
|
||||
request.Headers.Remove("If-Range");
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,76 @@
|
||||
/*
|
||||
Copyright 2013 Google Inc
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
using System;
|
||||
|
||||
namespace Google.Apis.Http
|
||||
{
|
||||
/// <summary>
|
||||
/// Indicates if exponential back-off is used automatically on exceptions in a service requests and \ or when 503
|
||||
/// responses is returned form the server.
|
||||
/// </summary>
|
||||
[Flags]
|
||||
public enum ExponentialBackOffPolicy
|
||||
{
|
||||
/// <summary>Exponential back-off is disabled.</summary>
|
||||
None = 0,
|
||||
/// <summary>Exponential back-off is enabled only for exceptions.</summary>
|
||||
Exception = 1,
|
||||
/// <summary>Exponential back-off is enabled only for 503 HTTP Status code.</summary>
|
||||
UnsuccessfulResponse503 = 2
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// An initializer which adds exponential back-off as exception handler and \ or unsuccessful response handler by
|
||||
/// the given <see cref="ExponentialBackOffPolicy"/>.
|
||||
/// </summary>
|
||||
public class ExponentialBackOffInitializer : IConfigurableHttpClientInitializer
|
||||
{
|
||||
/// <summary>Gets or sets the used back-off policy.</summary>
|
||||
private ExponentialBackOffPolicy Policy { get; set; }
|
||||
|
||||
/// <summary>Gets or sets the back-off handler creation function.</summary>
|
||||
private Func<BackOffHandler> CreateBackOff { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Constructs a new back-off initializer with the given policy and back-off handler create function.
|
||||
/// </summary>
|
||||
public ExponentialBackOffInitializer(ExponentialBackOffPolicy policy, Func<BackOffHandler> createBackOff)
|
||||
{
|
||||
Policy = policy;
|
||||
CreateBackOff = createBackOff;
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public void Initialize(ConfigurableHttpClient httpClient)
|
||||
{
|
||||
var backOff = CreateBackOff();
|
||||
|
||||
// Add exception handler and \ or unsuccessful response handler.
|
||||
if ((Policy & ExponentialBackOffPolicy.Exception) == ExponentialBackOffPolicy.Exception)
|
||||
{
|
||||
httpClient.MessageHandler.AddExceptionHandler(backOff);
|
||||
}
|
||||
|
||||
if ((Policy & ExponentialBackOffPolicy.UnsuccessfulResponse503) ==
|
||||
ExponentialBackOffPolicy.UnsuccessfulResponse503)
|
||||
{
|
||||
httpClient.MessageHandler.AddUnsuccessfulResponseHandler(backOff);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,74 @@
|
||||
/*
|
||||
Copyright 2013 Google Inc
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
using System.Net.Http;
|
||||
|
||||
using Google.Apis.Logging;
|
||||
|
||||
namespace Google.Apis.Http
|
||||
{
|
||||
/// <summary>The default implementation of the HTTP client factory.</summary>
|
||||
public class HttpClientFactory : IHttpClientFactory
|
||||
{
|
||||
/// <summary>The class logger.</summary>
|
||||
private static readonly ILogger Logger = ApplicationContext.Logger.ForType<HttpClientFactory>();
|
||||
|
||||
/// <inheritdoc/>
|
||||
public ConfigurableHttpClient CreateHttpClient(CreateHttpClientArgs args)
|
||||
{
|
||||
// Create the handler.
|
||||
var handler = CreateHandler(args);
|
||||
var configurableHandler = new ConfigurableMessageHandler(handler)
|
||||
{
|
||||
ApplicationName = args.ApplicationName
|
||||
};
|
||||
|
||||
// Create the client.
|
||||
var client = new ConfigurableHttpClient(configurableHandler);
|
||||
foreach (var initializer in args.Initializers)
|
||||
{
|
||||
initializer.Initialize(client);
|
||||
}
|
||||
|
||||
return client;
|
||||
}
|
||||
|
||||
/// <summary>Creates a HTTP message handler. Override this method to mock a message handler.</summary>
|
||||
protected virtual HttpMessageHandler CreateHandler(CreateHttpClientArgs args)
|
||||
{
|
||||
var handler = new HttpClientHandler();
|
||||
|
||||
// If the framework supports redirect configuration, set it to false, because ConfigurableMessageHandler
|
||||
// handles redirect.
|
||||
if (handler.SupportsRedirectConfiguration)
|
||||
{
|
||||
handler.AllowAutoRedirect = false;
|
||||
}
|
||||
|
||||
// If the framework supports automatic decompression and GZip is enabled, set automatic decompression.
|
||||
if (handler.SupportsAutomaticDecompression && args.GZipEnabled)
|
||||
{
|
||||
handler.AutomaticDecompression = System.Net.DecompressionMethods.GZip |
|
||||
System.Net.DecompressionMethods.Deflate;
|
||||
}
|
||||
|
||||
Logger.Debug("Handler was created. SupportsRedirectConfiguration={0}, SupportsAutomaticDecompression={1}",
|
||||
handler.SupportsRedirectConfiguration, handler.SupportsAutomaticDecompression);
|
||||
|
||||
return handler;
|
||||
}
|
||||
}
|
||||
}
|
37
Yavsc/GoogleApiSupport/Google.Apis.Core/Http/HttpConsts.cs
Normal file
37
Yavsc/GoogleApiSupport/Google.Apis.Core/Http/HttpConsts.cs
Normal file
@ -0,0 +1,37 @@
|
||||
/*
|
||||
Copyright 2013 Google Inc
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
namespace Google.Apis.Http
|
||||
{
|
||||
/// <summary>HTTP constants.</summary>
|
||||
public static class HttpConsts
|
||||
{
|
||||
/// <summary>Http GET request</summary>
|
||||
public const string Get = "GET";
|
||||
|
||||
/// <summary>Http DELETE request</summary>
|
||||
public const string Delete = "DELETE";
|
||||
|
||||
/// <summary>Http PUT request</summary>
|
||||
public const string Put = "PUT";
|
||||
|
||||
/// <summary>Http POST request</summary>
|
||||
public const string Post = "POST";
|
||||
|
||||
/// <summary>Http PATCH request</summary>
|
||||
public const string Patch = "PATCH";
|
||||
}
|
||||
}
|
@ -0,0 +1,51 @@
|
||||
/*
|
||||
Copyright 2013 Google Inc
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
using System.Net;
|
||||
using System.Net.Http;
|
||||
|
||||
namespace Google.Apis.Http
|
||||
{
|
||||
/// <summary>
|
||||
/// Extension methods to <see cref="System.Net.Http.HttpRequestMessage"/> and
|
||||
/// <see cref="System.Net.Http.HttpResponseMessage"/>.
|
||||
/// </summary>
|
||||
public static class HttpExtenstions
|
||||
{
|
||||
/// <summary>Returns <c>true</c> if the response contains one of the redirect status codes.</summary>
|
||||
internal static bool IsRedirectStatusCode(this HttpResponseMessage message)
|
||||
{
|
||||
switch (message.StatusCode)
|
||||
{
|
||||
case HttpStatusCode.Moved:
|
||||
case HttpStatusCode.Redirect:
|
||||
case HttpStatusCode.RedirectMethod:
|
||||
case HttpStatusCode.TemporaryRedirect:
|
||||
return true;
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>A Google.Apis utility method for setting an empty HTTP content.</summary>
|
||||
public static HttpContent SetEmptyContent(this HttpRequestMessage request)
|
||||
{
|
||||
request.Content = new ByteArrayContent(new byte[0]);
|
||||
request.Content.Headers.ContentLength = 0;
|
||||
return request.Content;
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,30 @@
|
||||
/*
|
||||
Copyright 2013 Google Inc
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
namespace Google.Apis.Http
|
||||
{
|
||||
/// <summary>
|
||||
/// HTTP client initializer for changing the default behavior of HTTP client.
|
||||
/// Use this initializer to change default values like timeout and number of tries.
|
||||
/// You can also set different handlers and interceptors like <see cref="IHttpUnsuccessfulResponseHandler"/>s,
|
||||
/// <see cref="IHttpExceptionHandler"/>s and <see cref="IHttpExecuteInterceptor"/>s.
|
||||
/// </summary>
|
||||
public interface IConfigurableHttpClientInitializer
|
||||
{
|
||||
/// <summary>Initializes a HTTP client after it was created.</summary>
|
||||
void Initialize(ConfigurableHttpClient httpClient);
|
||||
}
|
||||
}
|
@ -0,0 +1,48 @@
|
||||
/*
|
||||
Copyright 2013 Google Inc
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace Google.Apis.Http
|
||||
{
|
||||
/// <summary>Arguments for creating a HTTP client.</summary>
|
||||
public class CreateHttpClientArgs
|
||||
{
|
||||
/// <summary>Gets or sets whether GZip is enabled.</summary>
|
||||
public bool GZipEnabled { get; set; }
|
||||
|
||||
/// <summary>Gets or sets the application name that is sent in the User-Agent header.</summary>
|
||||
public string ApplicationName { get; set; }
|
||||
|
||||
/// <summary>Gets a list of initializers to initialize the HTTP client instance.</summary>
|
||||
public IList<IConfigurableHttpClientInitializer> Initializers { get; private set; }
|
||||
|
||||
/// <summary>Constructs a new argument instance.</summary>
|
||||
public CreateHttpClientArgs()
|
||||
{
|
||||
Initializers = new List<IConfigurableHttpClientInitializer>();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// HTTP client factory creates configurable HTTP clients. A unique HTTP client should be created for each service.
|
||||
/// </summary>
|
||||
public interface IHttpClientFactory
|
||||
{
|
||||
/// <summary>Creates a new configurable HTTP client.</summary>
|
||||
ConfigurableHttpClient CreateHttpClient(CreateHttpClientArgs args);
|
||||
}
|
||||
}
|
@ -0,0 +1,63 @@
|
||||
/*
|
||||
Copyright 2013 Google Inc
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
using System;
|
||||
using System.Net.Http;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Google.Apis.Http
|
||||
{
|
||||
/// <summary>Argument class to <see cref="IHttpExceptionHandler.HandleExceptionAsync"/>.</summary>
|
||||
public class HandleExceptionArgs
|
||||
{
|
||||
/// <summary>Gets or sets the sent request.</summary>
|
||||
public HttpRequestMessage Request { get; set; }
|
||||
|
||||
/// <summary>Gets or sets the exception which occurred during sending the request.</summary>
|
||||
public Exception Exception { get; set; }
|
||||
|
||||
/// <summary>Gets or sets the total number of tries to send the request.</summary>
|
||||
public int TotalTries { get; set; }
|
||||
|
||||
/// <summary>Gets or sets the current failed try.</summary>
|
||||
public int CurrentFailedTry { get; set; }
|
||||
|
||||
/// <summary>Gets an indication whether a retry will occur if the handler returns <c>true</c>.</summary>
|
||||
public bool SupportsRetry
|
||||
{
|
||||
get { return TotalTries - CurrentFailedTry > 0; }
|
||||
}
|
||||
|
||||
/// <summary>Gets or sets the request's cancellation token.</summary>
|
||||
public CancellationToken CancellationToken { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>Exception handler is invoked when an exception is thrown during a HTTP request.</summary>
|
||||
public interface IHttpExceptionHandler
|
||||
{
|
||||
/// <summary>
|
||||
/// Handles an exception thrown when sending a HTTP request.
|
||||
/// A simple rule must be followed, if you modify the request object in a way that the exception can be
|
||||
/// resolved, you must return <c>true</c>.
|
||||
/// </summary>
|
||||
/// <param name="args">
|
||||
/// Handle exception argument which properties such as the request, exception, current failed try.
|
||||
/// </param>
|
||||
/// <returns>Whether this handler has made a change that requires the request to be resent.</returns>
|
||||
Task<bool> HandleExceptionAsync(HandleExceptionArgs args);
|
||||
}
|
||||
}
|
@ -0,0 +1,36 @@
|
||||
/*
|
||||
Copyright 2013 Google Inc
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
using System.Net.Http;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Google.Apis.Http
|
||||
{
|
||||
/// <summary>
|
||||
/// HTTP request execute interceptor to intercept a <see cref="System.Net.Http.HttpRequestMessage"/> before it has
|
||||
/// been sent. Sample usage is attaching "Authorization" header to a request.
|
||||
/// </summary>
|
||||
public interface IHttpExecuteInterceptor
|
||||
{
|
||||
/// <summary>
|
||||
/// <summary>Invoked before the request is being sent.</summary>
|
||||
/// </summary>
|
||||
/// <param name="request">The HTTP request message.</param>
|
||||
/// <param name="cancellationToken">Cancellation token to cancel the operation.</param>
|
||||
Task InterceptAsync(HttpRequestMessage request, CancellationToken cancellationToken);
|
||||
}
|
||||
}
|
@ -0,0 +1,65 @@
|
||||
/*
|
||||
Copyright 2013 Google Inc
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
using System.Net.Http;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Google.Apis.Http
|
||||
{
|
||||
/// <summary>Argument class to <see cref="IHttpUnsuccessfulResponseHandler.HandleResponseAsync"/>.</summary>
|
||||
public class HandleUnsuccessfulResponseArgs
|
||||
{
|
||||
/// <summary>Gets or sets the sent request.</summary>
|
||||
public HttpRequestMessage Request { get; set; }
|
||||
|
||||
/// <summary>Gets or sets the abnormal response.</summary>
|
||||
public HttpResponseMessage Response { get; set; }
|
||||
|
||||
/// <summary>Gets or sets the total number of tries to send the request.</summary>
|
||||
public int TotalTries { get; set; }
|
||||
|
||||
/// <summary>Gets or sets the current failed try.</summary>
|
||||
public int CurrentFailedTry { get; set; }
|
||||
|
||||
/// <summary>Gets an indication whether a retry will occur if the handler returns <c>true</c>.</summary>
|
||||
public bool SupportsRetry
|
||||
{
|
||||
get { return TotalTries - CurrentFailedTry > 0; }
|
||||
}
|
||||
|
||||
/// <summary>Gets or sets the request's cancellation token.</summary>
|
||||
public CancellationToken CancellationToken { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Unsuccessful response handler which is invoked when an abnormal HTTP response is returned when sending a HTTP
|
||||
/// request.
|
||||
/// </summary>
|
||||
public interface IHttpUnsuccessfulResponseHandler
|
||||
{
|
||||
/// <summary>
|
||||
/// Handles an abnormal response when sending a HTTP request.
|
||||
/// A simple rule must be followed, if you modify the request object in a way that the abnormal response can
|
||||
/// be resolved, you must return <c>true</c>.
|
||||
/// </summary>
|
||||
/// <param name="args">
|
||||
/// Handle response argument which contains properties such as the request, response, current failed try.
|
||||
/// </param>
|
||||
/// <returns>Whether this handler has made a change that requires the request to be resent.</returns>
|
||||
Task<bool> HandleResponseAsync(HandleUnsuccessfulResponseArgs args);
|
||||
}
|
||||
}
|
@ -0,0 +1,72 @@
|
||||
/*
|
||||
Copyright 2013 Google Inc
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
using System;
|
||||
using System.Net.Http;
|
||||
using System.Net.Http.Headers;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
using Google.Apis.Testing;
|
||||
|
||||
namespace Google.Apis.Http
|
||||
{
|
||||
/// <summary>
|
||||
/// Intercepts HTTP GET requests with a URLs longer than a specified maximum number of characters.
|
||||
/// The interceptor will change such requests as follows:
|
||||
/// <list type="bullet">
|
||||
/// <item>The request's method will be changed to POST</item>
|
||||
/// <item>A <c>X-HTTP-Method-Override</c> header will be added with the value <c>GET</c></item>
|
||||
/// <item>Any query parameters from the URI will be moved into the body of the request.</item>
|
||||
/// <item>If query parameters are moved, the content type is set to <c>application/x-www-form-urlencoded</c></item>
|
||||
/// </list>
|
||||
/// </summary>
|
||||
[VisibleForTestOnly]
|
||||
public class MaxUrlLengthInterceptor : IHttpExecuteInterceptor
|
||||
{
|
||||
private readonly uint maxUrlLength;
|
||||
|
||||
///<summary>Constructs a new Max URL length interceptor with the given max length.</summary>
|
||||
public MaxUrlLengthInterceptor(uint maxUrlLength)
|
||||
{
|
||||
this.maxUrlLength = maxUrlLength;
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public Task InterceptAsync(HttpRequestMessage request, CancellationToken cancellationToken)
|
||||
{
|
||||
if (request.Method != HttpMethod.Get || request.RequestUri.AbsoluteUri.Length <= maxUrlLength)
|
||||
{
|
||||
return Task.FromResult(0);
|
||||
}
|
||||
// Change the method to POST.
|
||||
request.Method = HttpMethod.Post;
|
||||
var query = request.RequestUri.Query;
|
||||
if (!String.IsNullOrEmpty(query))
|
||||
{
|
||||
// Move query parameters to the body (without the "?").
|
||||
request.Content = new StringContent(query.Substring(1));
|
||||
request.Content.Headers.ContentType = new MediaTypeHeaderValue("application/x-www-form-urlencoded");
|
||||
var requestString = request.RequestUri.ToString();
|
||||
// The new request URI is the old one minus the "?" and everything that follows, since we moved the
|
||||
// query params to the body. For example: "www.example.com/?q=foo" => "www.example.com/".
|
||||
request.RequestUri = new Uri(requestString.Remove(requestString.IndexOf("?")));
|
||||
}
|
||||
request.Headers.Add("X-HTTP-Method-Override", "GET");
|
||||
return Task.FromResult(0);
|
||||
}
|
||||
}
|
||||
}
|
43
Yavsc/GoogleApiSupport/Google.Apis.Core/ISerializer.cs
Normal file
43
Yavsc/GoogleApiSupport/Google.Apis.Core/ISerializer.cs
Normal file
@ -0,0 +1,43 @@
|
||||
/*
|
||||
Copyright 2011 Google Inc
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
using System;
|
||||
using System.IO;
|
||||
|
||||
namespace Google.Apis
|
||||
{
|
||||
/// <summary>Serialization interface that supports serialize and deserialize methods.</summary>
|
||||
public interface ISerializer
|
||||
{
|
||||
/// <summary>Gets the application format this serializer supports (e.g. "json", "xml", etc.).</summary>
|
||||
string Format { get; }
|
||||
|
||||
/// <summary>Serializes the specified object into a Stream.</summary>
|
||||
void Serialize(object obj, Stream target);
|
||||
|
||||
/// <summary>Serializes the specified object into a string.</summary>
|
||||
string Serialize(object obj);
|
||||
|
||||
/// <summary>Deserializes the string into an object.</summary>
|
||||
T Deserialize<T>(string input);
|
||||
|
||||
/// <summary>Deserializes the string into an object.</summary>
|
||||
object Deserialize(string input, Type type);
|
||||
|
||||
/// <summary>Deserializes the stream into an object.</summary>
|
||||
T Deserialize<T>(Stream input);
|
||||
}
|
||||
}
|
@ -0,0 +1,23 @@
|
||||
/*
|
||||
Copyright 2011 Google Inc
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
namespace Google.Apis.Json
|
||||
{
|
||||
/// <summary>Represents a JSON serializer.</summary>
|
||||
public interface IJsonSerializer : ISerializer
|
||||
{
|
||||
}
|
||||
}
|
100
Yavsc/GoogleApiSupport/Google.Apis.Core/Json/JsonExplicitNull.cs
Normal file
100
Yavsc/GoogleApiSupport/Google.Apis.Core/Json/JsonExplicitNull.cs
Normal file
@ -0,0 +1,100 @@
|
||||
/*
|
||||
Copyright 2010 Google Inc
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
using System;
|
||||
using System.Collections;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace Google.Apis.Json
|
||||
{
|
||||
/// <summary>
|
||||
/// Provides values which are explicitly expressed as <c>null</c> when converted to JSON.
|
||||
/// </summary>
|
||||
public static class JsonExplicitNull
|
||||
{
|
||||
/// <summary>
|
||||
/// Get an <see cref="IList{T}"/> that is explicitly expressed as <c>null</c> when converted to JSON.
|
||||
/// </summary>
|
||||
/// <returns>An <see cref="IList{T}"/> that is explicitly expressed as <c>null</c> when converted to JSON.</returns>
|
||||
public static IList<T> ForIList<T>() => ExplicitNullList<T>.Instance;
|
||||
|
||||
[JsonExplicitNull]
|
||||
private sealed class ExplicitNullList<T> : IList<T>
|
||||
{
|
||||
public static ExplicitNullList<T> Instance = new ExplicitNullList<T>();
|
||||
|
||||
public T this[int index]
|
||||
{
|
||||
get { throw new NotSupportedException(); }
|
||||
set { throw new NotSupportedException(); }
|
||||
}
|
||||
|
||||
public int Count { get { throw new NotSupportedException(); } }
|
||||
|
||||
public bool IsReadOnly { get { throw new NotSupportedException(); } }
|
||||
|
||||
public void Add(T item)
|
||||
{
|
||||
throw new NotSupportedException();
|
||||
}
|
||||
|
||||
public void Clear()
|
||||
{
|
||||
throw new NotSupportedException();
|
||||
}
|
||||
|
||||
public bool Contains(T item)
|
||||
{
|
||||
throw new NotSupportedException();
|
||||
}
|
||||
|
||||
public void CopyTo(T[] array, int arrayIndex)
|
||||
{
|
||||
throw new NotSupportedException();
|
||||
}
|
||||
|
||||
public IEnumerator<T> GetEnumerator()
|
||||
{
|
||||
throw new NotSupportedException();
|
||||
}
|
||||
|
||||
public int IndexOf(T item)
|
||||
{
|
||||
throw new NotSupportedException();
|
||||
}
|
||||
|
||||
public void Insert(int index, T item)
|
||||
{
|
||||
throw new NotSupportedException();
|
||||
}
|
||||
|
||||
public bool Remove(T item)
|
||||
{
|
||||
throw new NotSupportedException();
|
||||
}
|
||||
|
||||
public void RemoveAt(int index)
|
||||
{
|
||||
throw new NotSupportedException();
|
||||
}
|
||||
|
||||
IEnumerator IEnumerable.GetEnumerator()
|
||||
{
|
||||
throw new NotSupportedException();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,26 @@
|
||||
/*
|
||||
Copyright 2016 Google Inc
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
using System;
|
||||
|
||||
namespace Google.Apis.Json
|
||||
{
|
||||
/// <summary>
|
||||
/// All values of a type with this attribute are represented as a literal <c>null</c> in JSON.
|
||||
/// </summary>
|
||||
[AttributeUsage(AttributeTargets.Class)]
|
||||
public class JsonExplicitNullAttribute : Attribute { }
|
||||
}
|
@ -0,0 +1,179 @@
|
||||
/*
|
||||
Copyright 2017 Google Inc
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
using Google.Apis.Util;
|
||||
using Newtonsoft.Json;
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.Reflection;
|
||||
using System.Linq;
|
||||
|
||||
namespace Google.Apis.Json
|
||||
{
|
||||
/// <summary>
|
||||
/// A JSON converter which honers RFC 3339 and the serialized date is accepted by Google services.
|
||||
/// </summary>
|
||||
public class RFC3339DateTimeConverter : JsonConverter
|
||||
{
|
||||
/// <inheritdoc/>
|
||||
public override bool CanRead => false;
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override object ReadJson(Newtonsoft.Json.JsonReader reader, Type objectType, object existingValue,
|
||||
JsonSerializer serializer)
|
||||
{
|
||||
throw new NotImplementedException("Unnecessary because CanRead is false.");
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override bool CanConvert(Type objectType) =>
|
||||
// Convert DateTime only.
|
||||
objectType == typeof(DateTime) || objectType == typeof(Nullable<DateTime>);
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
|
||||
{
|
||||
if (value != null)
|
||||
{
|
||||
DateTime date = (DateTime)value;
|
||||
serializer.Serialize(writer, Utilities.ConvertToRFC3339(date));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// A JSON converter to write <c>null</c> literals into JSON when explicitly requested.
|
||||
/// </summary>
|
||||
public class ExplicitNullConverter : JsonConverter
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public override bool CanRead => false;
|
||||
|
||||
/// <inheritdoc />
|
||||
public override bool CanConvert(Type objectType) => objectType.GetTypeInfo().GetCustomAttributes(typeof(JsonExplicitNullAttribute), false).Any();
|
||||
|
||||
/// <inheritdoc />
|
||||
public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
|
||||
{
|
||||
throw new NotImplementedException("Unnecessary because CanRead is false.");
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer) => writer.WriteNull();
|
||||
}
|
||||
|
||||
/// <summary>Class for serialization and deserialization of JSON documents using the Newtonsoft Library.</summary>
|
||||
public class NewtonsoftJsonSerializer : IJsonSerializer
|
||||
{
|
||||
private readonly JsonSerializerSettings settings;
|
||||
private readonly JsonSerializer serializer;
|
||||
|
||||
/// <summary>The default instance of the Newtonsoft JSON Serializer, with default settings.</summary>
|
||||
public static NewtonsoftJsonSerializer Instance { get; } = new NewtonsoftJsonSerializer();
|
||||
|
||||
/// <summary>
|
||||
/// Constructs a new instance with the default serialization settings, equivalent to <see cref="Instance"/>.
|
||||
/// </summary>
|
||||
public NewtonsoftJsonSerializer() : this(CreateDefaultSettings())
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Constructs a new instance with the given settings.
|
||||
/// </summary>
|
||||
/// <param name="settings">The settings to apply when serializing and deserializing. Must not be null.</param>
|
||||
public NewtonsoftJsonSerializer(JsonSerializerSettings settings)
|
||||
{
|
||||
Utilities.ThrowIfNull(settings, nameof(settings));
|
||||
this.settings = settings;
|
||||
serializer = JsonSerializer.Create(settings);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates a new instance of <see cref="JsonSerializerSettings"/> with the same behavior
|
||||
/// as the ones used in <see cref="Instance"/>. This method is expected to be used to construct
|
||||
/// settings which are then passed to <see cref="NewtonsoftJsonSerializer.NewtonsoftJsonSerializer(JsonSerializerSettings)"/>.
|
||||
/// </summary>
|
||||
/// <returns>A new set of default settings.</returns>
|
||||
public static JsonSerializerSettings CreateDefaultSettings() =>
|
||||
new JsonSerializerSettings
|
||||
{
|
||||
NullValueHandling = NullValueHandling.Ignore,
|
||||
MetadataPropertyHandling = MetadataPropertyHandling.Ignore,
|
||||
Converters = { new RFC3339DateTimeConverter(), new ExplicitNullConverter() }
|
||||
};
|
||||
|
||||
/// <inheritdoc/>
|
||||
public string Format => "json";
|
||||
|
||||
/// <inheritdoc/>
|
||||
public void Serialize(object obj, Stream target)
|
||||
{
|
||||
using (var writer = new StreamWriter(target))
|
||||
{
|
||||
if (obj == null)
|
||||
{
|
||||
obj = string.Empty;
|
||||
}
|
||||
serializer.Serialize(writer, obj);
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public string Serialize(object obj)
|
||||
{
|
||||
using (TextWriter tw = new StringWriter())
|
||||
{
|
||||
if (obj == null)
|
||||
{
|
||||
obj = string.Empty;
|
||||
}
|
||||
serializer.Serialize(tw, obj);
|
||||
return tw.ToString();
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public T Deserialize<T>(string input)
|
||||
{
|
||||
if (string.IsNullOrEmpty(input))
|
||||
{
|
||||
return default(T);
|
||||
}
|
||||
return JsonConvert.DeserializeObject<T>(input, settings);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public object Deserialize(string input, Type type)
|
||||
{
|
||||
if (string.IsNullOrEmpty(input))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
return JsonConvert.DeserializeObject(input, type, settings);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public T Deserialize<T>(Stream input)
|
||||
{
|
||||
// Convert the JSON document into an object.
|
||||
using (StreamReader streamReader = new StreamReader(input))
|
||||
{
|
||||
return (T)serializer.Deserialize(streamReader, typeof(T));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
172
Yavsc/GoogleApiSupport/Google.Apis.Core/Logging/BaseLogger.cs
Normal file
172
Yavsc/GoogleApiSupport/Google.Apis.Core/Logging/BaseLogger.cs
Normal file
@ -0,0 +1,172 @@
|
||||
/*
|
||||
Copyright 2011 Google Inc
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
using Google.Apis.Util;
|
||||
using System;
|
||||
using System.Globalization;
|
||||
|
||||
namespace Google.Apis.Logging
|
||||
{
|
||||
/// <summary>
|
||||
/// An abstract base logger, upon which real loggers may be built.
|
||||
/// </summary>
|
||||
public abstract class BaseLogger : ILogger
|
||||
{
|
||||
// Does not match gRPC datetime log format, which is "MMdd HH:mm:ss.ffffff"
|
||||
private const string DateTimeFormatString = "yyyy-MM-dd HH:mm:ss.ffffff";
|
||||
|
||||
/// <summary>
|
||||
/// Construct a <see cref="BaseLogger"/>.
|
||||
/// </summary>
|
||||
/// <param name="minimumLogLevel">Logging will be enabled at this level and all higher levels.</param>
|
||||
/// <param name="clock">The <see cref="IClock"/> to use to timestamp log entries.</param>
|
||||
/// <param name="forType">The type from which entries are being logged. May be <c>null</c>.</param>
|
||||
protected BaseLogger(LogLevel minimumLogLevel, IClock clock, Type forType)
|
||||
{
|
||||
MinimumLogLevel = minimumLogLevel;
|
||||
IsDebugEnabled = minimumLogLevel <= LogLevel.Debug;
|
||||
IsInfoEnabled = minimumLogLevel <= LogLevel.Info;
|
||||
IsWarningEnabled = minimumLogLevel <= LogLevel.Warning;
|
||||
IsErrorEnabled = minimumLogLevel <= LogLevel.Error;
|
||||
Clock = clock ?? SystemClock.Default;
|
||||
LoggerForType = forType;
|
||||
if (forType != null)
|
||||
{
|
||||
var namespaceStr = forType.Namespace ?? "";
|
||||
if (namespaceStr.Length > 0)
|
||||
{
|
||||
namespaceStr += ".";
|
||||
}
|
||||
_loggerForTypeString = namespaceStr + forType.Name + " ";
|
||||
}
|
||||
else
|
||||
{
|
||||
_loggerForTypeString = "";
|
||||
}
|
||||
}
|
||||
|
||||
private readonly string _loggerForTypeString;
|
||||
|
||||
/// <summary>
|
||||
/// The <see cref="IClock"/> being used to timestamp log entries.
|
||||
/// </summary>
|
||||
public IClock Clock { get; }
|
||||
|
||||
/// <summary>
|
||||
/// The type from which entries are being logged. May be <c>null</c>.
|
||||
/// </summary>
|
||||
public Type LoggerForType { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Logging is enabled at this level and all higher levels.
|
||||
/// </summary>
|
||||
public LogLevel MinimumLogLevel { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Is Debug level logging enabled?
|
||||
/// </summary>
|
||||
public bool IsDebugEnabled { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Is info level logging enabled?
|
||||
/// </summary>
|
||||
public bool IsInfoEnabled { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Is warning level logging enabled?
|
||||
/// </summary>
|
||||
public bool IsWarningEnabled { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Is error level logging enabled?
|
||||
/// </summary>
|
||||
public bool IsErrorEnabled { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Build a new logger of the derived concrete type, for use to log from the specified type.
|
||||
/// </summary>
|
||||
/// <param name="type">The type from which entries are being logged.</param>
|
||||
/// <returns>A new <see cref="ILogger"/> instance, logging from the specified type.</returns>
|
||||
protected abstract ILogger BuildNewLogger(Type type);
|
||||
|
||||
/// <inheritdoc/>
|
||||
public ILogger ForType<T>() => ForType(typeof(T));
|
||||
|
||||
/// <inheritdoc/>
|
||||
public ILogger ForType(Type type) => type == LoggerForType ? this : BuildNewLogger(type);
|
||||
|
||||
/// <summary>
|
||||
/// Perform the actual logging.
|
||||
/// </summary>
|
||||
/// <param name="logLevel">The <see cref="LogLevel"/> of this log entry.</param>
|
||||
/// <param name="formattedMessage">The fully formatted log message, ready for logging.</param>
|
||||
protected abstract void Log(LogLevel logLevel, string formattedMessage);
|
||||
|
||||
private string FormatLogEntry(string severityString, string message, params object[] formatArgs)
|
||||
{
|
||||
var msg = string.Format(message, formatArgs);
|
||||
var when = Clock.UtcNow.ToString(DateTimeFormatString, CultureInfo.InvariantCulture);
|
||||
// Matches gRPC log format
|
||||
return $"{severityString}{when} {_loggerForTypeString}{msg}";
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public void Debug(string message, params object[] formatArgs)
|
||||
{
|
||||
if (IsDebugEnabled)
|
||||
{
|
||||
Log(LogLevel.Debug, FormatLogEntry("D", message, formatArgs));
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public void Info(string message, params object[] formatArgs)
|
||||
{
|
||||
if (IsInfoEnabled)
|
||||
{
|
||||
Log(LogLevel.Info, FormatLogEntry("I", message, formatArgs));
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public void Warning(string message, params object[] formatArgs)
|
||||
{
|
||||
if (IsWarningEnabled)
|
||||
{
|
||||
Log(LogLevel.Warning, FormatLogEntry("W", message, formatArgs));
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public void Error(Exception exception, string message, params object[] formatArgs)
|
||||
{
|
||||
if (IsErrorEnabled)
|
||||
{
|
||||
Log(LogLevel.Error, $"{FormatLogEntry("E", message, formatArgs)} {exception}");
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public void Error(string message, params object[] formatArgs)
|
||||
{
|
||||
if (IsErrorEnabled)
|
||||
{
|
||||
Log(LogLevel.Error, FormatLogEntry("E", message, formatArgs));
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
@ -0,0 +1,54 @@
|
||||
/*
|
||||
Copyright 2017 Google Inc
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
using Google.Apis.Util;
|
||||
using System;
|
||||
|
||||
namespace Google.Apis.Logging
|
||||
{
|
||||
/// <summary>
|
||||
/// A logger than logs to StdError or StdOut.
|
||||
/// </summary>
|
||||
public sealed class ConsoleLogger : BaseLogger, ILogger
|
||||
{
|
||||
/// <summary>
|
||||
/// Construct a <see cref="ConsoleLogger"/>.
|
||||
/// </summary>
|
||||
/// <param name="minimumLogLevel">Logging will be enabled at this level and all higher levels.</param>
|
||||
/// <param name="logToStdOut"><c>true</c> to log to StdOut, defaults to logging to StdError.</param>
|
||||
/// <param name="clock">Optional <see cref="IClock"/>; will use the system clock if <c>null</c>.</param>
|
||||
public ConsoleLogger(LogLevel minimumLogLevel, bool logToStdOut = false, IClock clock = null) : this(minimumLogLevel, logToStdOut, clock, null) { }
|
||||
|
||||
private ConsoleLogger(LogLevel minimumLogLevel, bool logToStdOut, IClock clock, Type forType) : base(minimumLogLevel, clock, forType)
|
||||
{
|
||||
LogToStdOut = logToStdOut;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// <c>false</c> to log to StdError; <c>true</c> to log to StdOut.
|
||||
/// </summary>
|
||||
public bool LogToStdOut { get; }
|
||||
|
||||
/// <inheritdoc/>
|
||||
protected override ILogger BuildNewLogger(Type type) => new ConsoleLogger(MinimumLogLevel, LogToStdOut, Clock, type);
|
||||
|
||||
/// <inheritdoc/>
|
||||
protected override void Log(LogLevel logLevel, string formattedMessage)
|
||||
{
|
||||
(LogToStdOut ? Console.Out : Console.Error).WriteLine(formattedMessage);
|
||||
}
|
||||
}
|
||||
}
|
62
Yavsc/GoogleApiSupport/Google.Apis.Core/Logging/ILogger.cs
Normal file
62
Yavsc/GoogleApiSupport/Google.Apis.Core/Logging/ILogger.cs
Normal file
@ -0,0 +1,62 @@
|
||||
/*
|
||||
Copyright 2011 Google Inc
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
using System;
|
||||
|
||||
namespace Google.Apis.Logging
|
||||
{
|
||||
/// <summary>Describes a logging interface which is used for outputting messages.</summary>
|
||||
public interface ILogger
|
||||
{
|
||||
/// <summary>Gets an indication whether debug output is logged or not.</summary>
|
||||
bool IsDebugEnabled { get; }
|
||||
|
||||
/// <summary>Returns a logger which will be associated with the specified type.</summary>
|
||||
/// <param name="type">Type to which this logger belongs.</param>
|
||||
/// <returns>A type-associated logger.</returns>
|
||||
ILogger ForType(Type type);
|
||||
|
||||
/// <summary>Returns a logger which will be associated with the specified type.</summary>
|
||||
/// <returns>A type-associated logger.</returns>
|
||||
ILogger ForType<T>();
|
||||
|
||||
/// <summary>Logs a debug message.</summary>
|
||||
/// <param name="message">The message to log.</param>
|
||||
/// <param name="formatArgs">String.Format arguments (if applicable).</param>
|
||||
void Debug(string message, params object[] formatArgs);
|
||||
|
||||
/// <summary>Logs an info message.</summary>
|
||||
/// <param name="message">The message to log.</param>
|
||||
/// <param name="formatArgs">String.Format arguments (if applicable).</param>
|
||||
void Info(string message, params object[] formatArgs);
|
||||
|
||||
/// <summary>Logs a warning.</summary>
|
||||
/// <param name="message">The message to log.</param>
|
||||
/// <param name="formatArgs">String.Format arguments (if applicable).</param>
|
||||
void Warning(string message, params object[] formatArgs);
|
||||
|
||||
/// <summary>Logs an error message resulting from an exception.</summary>
|
||||
/// <param name="exception"></param>
|
||||
/// <param name="message">The message to log.</param>
|
||||
/// <param name="formatArgs">String.Format arguments (if applicable).</param>
|
||||
void Error(Exception exception, string message, params object[] formatArgs);
|
||||
|
||||
/// <summary>Logs an error message.</summary>
|
||||
/// <param name="message">The message to log.</param>
|
||||
/// <param name="formatArgs">String.Format arguments (if applicable).</param>
|
||||
void Error(string message, params object[] formatArgs);
|
||||
}
|
||||
}
|
54
Yavsc/GoogleApiSupport/Google.Apis.Core/Logging/LogLevel.cs
Normal file
54
Yavsc/GoogleApiSupport/Google.Apis.Core/Logging/LogLevel.cs
Normal file
@ -0,0 +1,54 @@
|
||||
/*
|
||||
Copyright 2011 Google Inc
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
namespace Google.Apis.Logging
|
||||
{
|
||||
/// <summary>
|
||||
/// The supported logging levels.
|
||||
/// </summary>
|
||||
public enum LogLevel
|
||||
{
|
||||
/// <summary>
|
||||
/// A value lower than all logging levels.
|
||||
/// </summary>
|
||||
All = 0,
|
||||
|
||||
/// <summary>
|
||||
/// Debug logging.
|
||||
/// </summary>
|
||||
Debug = 100,
|
||||
|
||||
/// <summary>
|
||||
/// Info logging.
|
||||
/// </summary>
|
||||
Info = 200,
|
||||
|
||||
/// <summary>
|
||||
/// Warning logging.
|
||||
/// </summary>
|
||||
Warning = 300,
|
||||
|
||||
/// <summary>
|
||||
/// Error logging.
|
||||
/// </summary>
|
||||
Error = 400,
|
||||
|
||||
/// <summary>
|
||||
/// A value higher than all logging levels.
|
||||
/// </summary>
|
||||
None = 1000,
|
||||
}
|
||||
}
|
@ -0,0 +1,72 @@
|
||||
/*
|
||||
Copyright 2011 Google Inc
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
using Google.Apis.Util;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.ObjectModel;
|
||||
|
||||
namespace Google.Apis.Logging
|
||||
{
|
||||
/// <summary>
|
||||
/// A logger than logs to an in-memory buffer.
|
||||
/// Generally for use during tests.
|
||||
/// </summary>
|
||||
public sealed class MemoryLogger : BaseLogger, ILogger
|
||||
{
|
||||
/// <summary>
|
||||
/// Construct a <see cref="MemoryLogger"/>.
|
||||
/// </summary>
|
||||
/// <param name="minimumLogLevel">Logging will be enabled at this level and all higher levels.</param>
|
||||
/// <param name="maximumEntryCount">The maximum number of log entries. Further log entries will be silently discarded.</param>
|
||||
/// <param name="clock">Optional <see cref="IClock"/>; will use the system clock if <c>null</c>.</param>
|
||||
public MemoryLogger(LogLevel minimumLogLevel, int maximumEntryCount = 1000, IClock clock = null) :
|
||||
this(minimumLogLevel, maximumEntryCount, clock, new List<string>(), null) { }
|
||||
|
||||
private MemoryLogger(LogLevel minimumLogLevel, int maximumEntryCount, IClock clock, List<string> logEntries, Type forType) : base(minimumLogLevel, clock, forType)
|
||||
{
|
||||
_logEntries = logEntries;
|
||||
LogEntries = new ReadOnlyCollection<string>(_logEntries);
|
||||
_maximumEntryCount = maximumEntryCount;
|
||||
}
|
||||
|
||||
private readonly int _maximumEntryCount;
|
||||
|
||||
// This list is shared between all derived MemoryLogger instances
|
||||
private readonly List<string> _logEntries;
|
||||
|
||||
/// <summary>
|
||||
/// The list of log entries.
|
||||
/// </summary>
|
||||
public IList<string> LogEntries { get; }
|
||||
|
||||
/// <inheritdoc/>
|
||||
protected override ILogger BuildNewLogger(Type type) => new MemoryLogger(MinimumLogLevel, _maximumEntryCount, Clock, _logEntries, type);
|
||||
|
||||
/// <inheritdoc/>
|
||||
protected override void Log(LogLevel logLevel, string formattedMessage)
|
||||
{
|
||||
lock (_logEntries)
|
||||
{
|
||||
if (_logEntries.Count < _maximumEntryCount)
|
||||
{
|
||||
_logEntries.Add(formattedMessage);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
@ -0,0 +1,59 @@
|
||||
/*
|
||||
Copyright 2011 Google Inc
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
using System;
|
||||
|
||||
namespace Google.Apis.Logging
|
||||
{
|
||||
/// <summary>
|
||||
/// Represents a NullLogger which does not do any logging.
|
||||
/// </summary>
|
||||
public class NullLogger : ILogger
|
||||
{
|
||||
/// <inheritdoc/>
|
||||
public bool IsDebugEnabled
|
||||
{
|
||||
get { return false; }
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public ILogger ForType(Type type)
|
||||
{
|
||||
return new NullLogger();
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public ILogger ForType<T>()
|
||||
{
|
||||
return new NullLogger();
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public void Info(string message, params object[] formatArgs) {}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public void Warning(string message, params object[] formatArgs) {}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public void Debug(string message, params object[] formatArgs) {}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public void Error(Exception exception, string message, params object[] formatArgs) {}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public void Error(string message, params object[] formatArgs) {}
|
||||
}
|
||||
}
|
@ -0,0 +1,166 @@
|
||||
/*
|
||||
Copyright 2011 Google Inc
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
using System;
|
||||
using System.Collections;
|
||||
using System.Collections.Generic;
|
||||
|
||||
using Google.Apis.Util;
|
||||
|
||||
namespace Google.Apis.Requests.Parameters
|
||||
{
|
||||
/// <summary>A collection of parameters (key value pairs). May contain duplicate keys.</summary>
|
||||
public class ParameterCollection : List<KeyValuePair<string, string>>
|
||||
{
|
||||
/// <summary>Constructs a new parameter collection.</summary>
|
||||
public ParameterCollection() : base() { }
|
||||
|
||||
/// <summary>Constructs a new parameter collection from the given collection.</summary>
|
||||
public ParameterCollection(IEnumerable<KeyValuePair<string, string>> collection) : base(collection) { }
|
||||
|
||||
/// <summary>Adds a single parameter to this collection.</summary>
|
||||
public void Add(string key, string value)
|
||||
{
|
||||
Add(new KeyValuePair<string, string>(key, value));
|
||||
}
|
||||
|
||||
/// <summary>Returns <c>true</c> if this parameter is set within the collection.</summary>
|
||||
public bool ContainsKey(string key)
|
||||
{
|
||||
key.ThrowIfNullOrEmpty("key");
|
||||
string value;
|
||||
return TryGetValue(key, out value);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Tries to find the a key within the specified key value collection. Returns true if the key was found.
|
||||
/// If a pair was found the out parameter value will contain the value of that pair.
|
||||
/// </summary>
|
||||
public bool TryGetValue(string key, out string value)
|
||||
{
|
||||
key.ThrowIfNullOrEmpty("key");
|
||||
|
||||
foreach (KeyValuePair<string, string> pair in this)
|
||||
{
|
||||
// Check if this pair matches the specified key name.
|
||||
if (pair.Key.Equals(key))
|
||||
{
|
||||
value = pair.Value;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
// No result found.
|
||||
value = null;
|
||||
return false;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns the value of the first matching key, or throws a KeyNotFoundException if the parameter is not
|
||||
/// present within the collection.
|
||||
/// </summary>
|
||||
public string GetFirstMatch(string key)
|
||||
{
|
||||
string val;
|
||||
if (!TryGetValue(key, out val))
|
||||
{
|
||||
throw new KeyNotFoundException("Parameter with the name '" + key + "' was not found.");
|
||||
}
|
||||
return val;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns all matches for the specified key. May return an empty enumeration if the key is not present.
|
||||
/// </summary>
|
||||
public IEnumerable<string> GetAllMatches(string key)
|
||||
{
|
||||
key.ThrowIfNullOrEmpty("key");
|
||||
|
||||
foreach (KeyValuePair<string, string> pair in this)
|
||||
{
|
||||
// Check if this pair matches the specified key name.
|
||||
if (pair.Key.Equals(key))
|
||||
{
|
||||
yield return pair.Value;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns all matches for the specified key. May return an empty enumeration if the key is not present.
|
||||
/// </summary>
|
||||
public IEnumerable<string> this[string key]
|
||||
{
|
||||
get { return GetAllMatches(key); }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates a parameter collection from the specified URL encoded query string.
|
||||
/// Example:
|
||||
/// The query string "foo=bar&chocolate=cookie" would result in two parameters (foo and bar)
|
||||
/// with the values "bar" and "cookie" set.
|
||||
/// </summary>
|
||||
public static ParameterCollection FromQueryString(string qs)
|
||||
{
|
||||
var collection = new ParameterCollection();
|
||||
var qsParam = qs.Split('&');
|
||||
foreach (var param in qsParam)
|
||||
{
|
||||
// Split the parameter into key and value.
|
||||
var info = param.Split(new[] { '=' });
|
||||
if (info.Length == 2)
|
||||
{
|
||||
collection.Add(Uri.UnescapeDataString(info[0]), Uri.UnescapeDataString(info[1]));
|
||||
}
|
||||
else
|
||||
{
|
||||
throw new ArgumentException(string.Format(
|
||||
"Invalid query string [{0}]. Invalid part [{1}]", qs, param));
|
||||
}
|
||||
}
|
||||
|
||||
return collection;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates a parameter collection from the specified dictionary.
|
||||
/// If the value is an enumerable, a parameter pair will be added for each value.
|
||||
/// Otherwise the value will be converted into a string using the .ToString() method.
|
||||
/// </summary>
|
||||
public static ParameterCollection FromDictionary(IDictionary<string, object> dictionary)
|
||||
{
|
||||
var collection = new ParameterCollection();
|
||||
foreach (KeyValuePair<string, object> pair in dictionary)
|
||||
{
|
||||
// Try parsing the value of the pair as an enumerable.
|
||||
var valueAsEnumerable = pair.Value as IEnumerable;
|
||||
if (!(pair.Value is string) && valueAsEnumerable != null)
|
||||
{
|
||||
foreach (var value in valueAsEnumerable)
|
||||
{
|
||||
collection.Add(pair.Key, Util.Utilities.ConvertToString(value));
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
// Otherwise just convert it to a string.
|
||||
collection.Add(pair.Key, pair.Value == null ? null : Util.Utilities.ConvertToString(pair.Value));
|
||||
}
|
||||
}
|
||||
return collection;
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,150 @@
|
||||
/*
|
||||
Copyright 2013 Google Inc
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Net.Http;
|
||||
using System.Linq;
|
||||
using System.Reflection;
|
||||
|
||||
using Google.Apis.Logging;
|
||||
using Google.Apis.Util;
|
||||
|
||||
namespace Google.Apis.Requests.Parameters
|
||||
{
|
||||
/// <summary>
|
||||
/// Utility class for iterating on <see cref="RequestParameterAttribute"/> properties in a request object.
|
||||
/// </summary>
|
||||
public static class ParameterUtils
|
||||
{
|
||||
private static readonly ILogger Logger = ApplicationContext.Logger.ForType(typeof(ParameterUtils));
|
||||
|
||||
/// <summary>
|
||||
/// Creates a <see cref="System.Net.Http.FormUrlEncodedContent"/> with all the specified parameters in
|
||||
/// the input request. It uses reflection to iterate over all properties with
|
||||
/// <see cref="Google.Apis.Util.RequestParameterAttribute"/> attribute.
|
||||
/// </summary>
|
||||
/// <param name="request">
|
||||
/// A request object which contains properties with
|
||||
/// <see cref="Google.Apis.Util.RequestParameterAttribute"/> attribute. Those properties will be serialized
|
||||
/// to the returned <see cref="System.Net.Http.FormUrlEncodedContent"/>.
|
||||
/// </param>
|
||||
/// <returns>
|
||||
/// A <see cref="System.Net.Http.FormUrlEncodedContent"/> which contains the all the given object required
|
||||
/// values.
|
||||
/// </returns>
|
||||
public static FormUrlEncodedContent CreateFormUrlEncodedContent(object request)
|
||||
{
|
||||
IList<KeyValuePair<string, string>> list = new List<KeyValuePair<string, string>>();
|
||||
IterateParameters(request, (type, name, value) =>
|
||||
{
|
||||
list.Add(new KeyValuePair<string, string>(name, value.ToString()));
|
||||
});
|
||||
return new FormUrlEncodedContent(list);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates a parameter dictionary by using reflection to iterate over all properties with
|
||||
/// <see cref="Google.Apis.Util.RequestParameterAttribute"/> attribute.
|
||||
/// </summary>
|
||||
/// <param name="request">
|
||||
/// A request object which contains properties with
|
||||
/// <see cref="Google.Apis.Util.RequestParameterAttribute"/> attribute. Those properties will be set
|
||||
/// in the output dictionary.
|
||||
/// </param>
|
||||
public static IDictionary<string, object> CreateParameterDictionary(object request)
|
||||
{
|
||||
var dict = new Dictionary<string, object>();
|
||||
IterateParameters(request, (type, name, value) =>
|
||||
{
|
||||
dict.Add(name, value);
|
||||
});
|
||||
return dict;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sets query parameters in the given builder with all all properties with the
|
||||
/// <see cref="Google.Apis.Util.RequestParameterAttribute"/> attribute.
|
||||
/// </summary>
|
||||
/// <param name="builder">The request builder</param>
|
||||
/// <param name="request">
|
||||
/// A request object which contains properties with
|
||||
/// <see cref="Google.Apis.Util.RequestParameterAttribute"/> attribute. Those properties will be set in the
|
||||
/// given request builder object
|
||||
/// </param>
|
||||
public static void InitParameters(RequestBuilder builder, object request)
|
||||
{
|
||||
IterateParameters(request, (type, name, value) =>
|
||||
{
|
||||
builder.AddParameter(type, name, value.ToString());
|
||||
});
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Iterates over all <see cref="Google.Apis.Util.RequestParameterAttribute"/> properties in the request
|
||||
/// object and invokes the specified action for each of them.
|
||||
/// </summary>
|
||||
/// <param name="request">A request object</param>
|
||||
/// <param name="action">An action to invoke which gets the parameter type, name and its value</param>
|
||||
private static void IterateParameters(object request, Action<RequestParameterType, string, object> action)
|
||||
{
|
||||
// Use reflection to build the parameter dictionary.
|
||||
foreach (PropertyInfo property in request.GetType().GetProperties(BindingFlags.Instance |
|
||||
BindingFlags.Public))
|
||||
{
|
||||
// Retrieve the RequestParameterAttribute.
|
||||
RequestParameterAttribute attribute =
|
||||
property.GetCustomAttributes(typeof(RequestParameterAttribute), false).FirstOrDefault() as
|
||||
RequestParameterAttribute;
|
||||
if (attribute == null)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
// Get the name of this parameter from the attribute, if it doesn't exist take a lower-case variant of
|
||||
// property name.
|
||||
string name = attribute.Name ?? property.Name.ToLower();
|
||||
|
||||
var propertyType = property.PropertyType;
|
||||
var value = property.GetValue(request, null);
|
||||
|
||||
// Call action with the type name and value.
|
||||
if (propertyType.GetTypeInfo().IsValueType || value != null)
|
||||
{
|
||||
if (attribute.Type == RequestParameterType.UserDefinedQueries)
|
||||
{
|
||||
if (typeof(IEnumerable<KeyValuePair<string, string>>).IsAssignableFrom(value.GetType()))
|
||||
{
|
||||
foreach (var pair in (IEnumerable<KeyValuePair<string, string>>)value)
|
||||
{
|
||||
action(RequestParameterType.Query, pair.Key, pair.Value);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
Logger.Warning("Parameter marked with RequestParameterType.UserDefinedQueries attribute " +
|
||||
"was not of type IEnumerable<KeyValuePair<string, string>> and will be skipped.");
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
action(attribute.Type, name, value);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,48 @@
|
||||
/*
|
||||
Copyright 2010 Google Inc
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
using System;
|
||||
using System.Text.RegularExpressions;
|
||||
|
||||
using Google.Apis.Discovery;
|
||||
using Google.Apis.Testing;
|
||||
|
||||
namespace Google.Apis.Requests.Parameters
|
||||
{
|
||||
/// <summary>Logic for validating a parameter.</summary>
|
||||
public static class ParameterValidator
|
||||
{
|
||||
/// <summary>Validates a parameter value against the methods regex.</summary>
|
||||
[VisibleForTestOnly]
|
||||
public static bool ValidateRegex(IParameter param, string paramValue)
|
||||
{
|
||||
return string.IsNullOrEmpty(param.Pattern) || new Regex(param.Pattern).IsMatch(paramValue);
|
||||
}
|
||||
|
||||
/// <summary>Validates if a parameter is valid.</summary>
|
||||
public static bool ValidateParameter(IParameter parameter, string value)
|
||||
{
|
||||
// Fail if a required parameter is not present.
|
||||
if (String.IsNullOrEmpty(value))
|
||||
{
|
||||
return !parameter.IsRequired;
|
||||
}
|
||||
|
||||
// The parameter has value so validate the regex.
|
||||
return ValidateRegex(parameter, value);
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,308 @@
|
||||
/*
|
||||
Copyright 2012 Google Inc
|
||||
|
||||
Licensed under the Apache License, Version 2.0(the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Net.Http;
|
||||
using System.Text;
|
||||
using System.Text.RegularExpressions;
|
||||
|
||||
using Google.Apis.Http;
|
||||
using Google.Apis.Logging;
|
||||
using Google.Apis.Util;
|
||||
|
||||
namespace Google.Apis.Requests
|
||||
{
|
||||
/// <summary>Utility class for building a URI using <see cref="BuildUri"/> or a HTTP request using
|
||||
/// <see cref="CreateRequest"/> from the query and path parameters of a REST call.</summary>
|
||||
public class RequestBuilder
|
||||
{
|
||||
static RequestBuilder()
|
||||
{
|
||||
UriPatcher.PatchUriQuirks();
|
||||
}
|
||||
|
||||
private static readonly ILogger Logger = ApplicationContext.Logger.ForType<RequestBuilder>();
|
||||
|
||||
/// <summary>Pattern to get the groups that are part of the path.</summary>
|
||||
private static Regex PathParametersPattern = new Regex(@"{[^{}]*}*");
|
||||
|
||||
/// <summary>Supported HTTP methods.</summary>
|
||||
private static IEnumerable<string> SupportedMethods = new List<string>
|
||||
{
|
||||
HttpConsts.Get, HttpConsts.Post, HttpConsts.Put, HttpConsts.Delete, HttpConsts.Patch
|
||||
};
|
||||
|
||||
/// <summary>
|
||||
/// A dictionary containing the parameters which will be inserted into the path of the URI. These parameters
|
||||
/// will be substituted into the URI path where the path contains "{key}". See
|
||||
/// http://tools.ietf.org/html/rfc6570 for more information.
|
||||
/// </summary>
|
||||
private IDictionary<string, IList<string>> PathParameters { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// A dictionary containing the parameters which will apply to the query portion of this request.
|
||||
/// </summary>
|
||||
private List<KeyValuePair<string, string>> QueryParameters { get; set; }
|
||||
|
||||
/// <summary>The base URI for this request (usually applies to the service itself).</summary>
|
||||
public Uri BaseUri { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The path portion of this request. It's appended to the <see cref="BaseUri"/> and the parameters are
|
||||
/// substituted from the <see cref="PathParameters"/> dictionary.
|
||||
/// </summary>
|
||||
public string Path { get; set; }
|
||||
|
||||
/// <summary>The HTTP method used for this request.</summary>
|
||||
private string method;
|
||||
|
||||
/// <summary>The HTTP method used for this request (such as GET, PUT, POST, etc...).</summary>
|
||||
/// <remarks>The default Value is <see cref="Google.Apis.Http.HttpConsts.Get"/>.</remarks>
|
||||
public string Method
|
||||
{
|
||||
get { return method; }
|
||||
set
|
||||
{
|
||||
if (!SupportedMethods.Contains(value))
|
||||
throw new ArgumentOutOfRangeException("Method");
|
||||
method = value;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Construct a new request builder.</summary>
|
||||
/// TODO(peleyal): Consider using the Factory pattern here.
|
||||
public RequestBuilder()
|
||||
{
|
||||
PathParameters = new Dictionary<string, IList<string>>();
|
||||
QueryParameters = new List<KeyValuePair<string, string>>();
|
||||
Method = HttpConsts.Get;
|
||||
}
|
||||
|
||||
/// <summary>Constructs a Uri as defined by the parts of this request builder.</summary>
|
||||
public Uri BuildUri()
|
||||
{
|
||||
var restPath = BuildRestPath();
|
||||
|
||||
if (QueryParameters.Count > 0)
|
||||
{
|
||||
// In case the path already contains '?' - we should add '&'. Otherwise add '?'.
|
||||
restPath.Append(restPath.ToString().Contains("?") ? "&" : "?");
|
||||
|
||||
// If parameter value is empty - just add the "name", otherwise "name=value"
|
||||
restPath.Append(String.Join("&", QueryParameters.Select(
|
||||
x => string.IsNullOrEmpty(x.Value) ?
|
||||
Uri.EscapeDataString(x.Key) :
|
||||
String.Format("{0}={1}", Uri.EscapeDataString(x.Key), Uri.EscapeDataString(x.Value)))
|
||||
.ToArray()));
|
||||
}
|
||||
|
||||
return new Uri(this.BaseUri, restPath.ToString());
|
||||
}
|
||||
|
||||
/// <summary>Operator list that can appear in the path argument.</summary>
|
||||
private const string OPERATORS = "+#./;?&|!@=";
|
||||
|
||||
/// <summary>
|
||||
/// Builds the REST path string builder based on <see cref="PathParameters"/> and the URI template spec
|
||||
/// http://tools.ietf.org/html/rfc6570.
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
private StringBuilder BuildRestPath()
|
||||
{
|
||||
if (string.IsNullOrEmpty(Path))
|
||||
{
|
||||
return new StringBuilder(string.Empty);
|
||||
}
|
||||
|
||||
var restPath = new StringBuilder(Path);
|
||||
var matches = PathParametersPattern.Matches(restPath.ToString());
|
||||
foreach (var match in matches)
|
||||
{
|
||||
var matchStr = match.ToString();
|
||||
// Strip the first and last characters: '{' and '}'.
|
||||
var content = matchStr.Substring(1, matchStr.Length - 2);
|
||||
|
||||
var op = string.Empty;
|
||||
// If the content's first character is an operator, save and remove it from the content string.
|
||||
if (OPERATORS.Contains(content[0].ToString()))
|
||||
{
|
||||
op = content[0].ToString();
|
||||
content = content.Substring(1);
|
||||
}
|
||||
|
||||
var newContent = new StringBuilder();
|
||||
|
||||
// Iterate over all possible parameters.
|
||||
var parameters = content.Split(',');
|
||||
for (var index = 0; index < parameters.Length; ++index)
|
||||
{
|
||||
var parameter = parameters[index];
|
||||
|
||||
var parameterName = parameter;
|
||||
var containStar = false;
|
||||
var numOfChars = 0;
|
||||
|
||||
// Check if it ends with '*'.
|
||||
if (parameterName[parameterName.Length - 1] == '*')
|
||||
{
|
||||
containStar = true;
|
||||
parameterName = parameterName.Substring(0, parameterName.Length - 1);
|
||||
}
|
||||
// Check if it contains :n which means we should only use the first n characters of this parameter.
|
||||
if (parameterName.Contains(":"))
|
||||
{
|
||||
if (!int.TryParse(parameterName.Substring(parameterName.IndexOf(":") + 1), out numOfChars))
|
||||
{
|
||||
throw new ArgumentException(
|
||||
string.Format("Can't parse number after ':' in Path \"{0}\". Parameter is \"{1}\"",
|
||||
Path, parameterName), Path);
|
||||
}
|
||||
parameterName = parameterName.Substring(0, parameterName.IndexOf(":"));
|
||||
}
|
||||
|
||||
// We can improve the following if statement, but for readability we will leave it like that.
|
||||
var joiner = op;
|
||||
var start = op;
|
||||
switch (op)
|
||||
{
|
||||
case "+":
|
||||
start = index == 0 ? "" : ",";
|
||||
joiner = ",";
|
||||
break;
|
||||
case ".":
|
||||
if (!containStar)
|
||||
{
|
||||
joiner = ",";
|
||||
}
|
||||
break;
|
||||
case "/":
|
||||
if (!containStar)
|
||||
{
|
||||
joiner = ",";
|
||||
}
|
||||
break;
|
||||
case "#":
|
||||
start = index == 0 ? "#" : ",";
|
||||
joiner = ",";
|
||||
break;
|
||||
|
||||
case "?":
|
||||
start = (index == 0 ? "?" : "&") + parameterName + "=";
|
||||
joiner = ",";
|
||||
if (containStar)
|
||||
{
|
||||
joiner = "&" + parameterName + "=";
|
||||
}
|
||||
break;
|
||||
case "&":
|
||||
case ";":
|
||||
start = op + parameterName + "=";
|
||||
joiner = ",";
|
||||
if (containStar)
|
||||
{
|
||||
joiner = op + parameterName + "=";
|
||||
}
|
||||
break;
|
||||
// No operator, in that case just ','.
|
||||
default:
|
||||
if (index > 0)
|
||||
{
|
||||
start = ",";
|
||||
}
|
||||
joiner = ",";
|
||||
break;
|
||||
}
|
||||
|
||||
// Check if a path parameter equals the name which appears in the REST path.
|
||||
if (PathParameters.ContainsKey(parameterName))
|
||||
{
|
||||
var value = string.Join(joiner, PathParameters[parameterName]);
|
||||
|
||||
// Check if we need to use a substring of the value.
|
||||
if (numOfChars != 0 && numOfChars < value.Length)
|
||||
{
|
||||
value = value.Substring(0, numOfChars);
|
||||
}
|
||||
|
||||
if (op != "+" && op != "#" && PathParameters[parameterName].Count == 1)
|
||||
{
|
||||
value = Uri.EscapeDataString(value);
|
||||
}
|
||||
|
||||
value = start + value;
|
||||
newContent.Append(value);
|
||||
}
|
||||
else
|
||||
{
|
||||
throw new ArgumentException(
|
||||
string.Format("Path \"{0}\" misses a \"{1}\" parameter", Path, parameterName), Path);
|
||||
}
|
||||
}
|
||||
|
||||
if (op == ";")
|
||||
{
|
||||
if (newContent[newContent.Length - 1] == '=')
|
||||
{
|
||||
newContent = newContent.Remove(newContent.Length - 1, 1);
|
||||
}
|
||||
newContent = newContent.Replace("=;", ";");
|
||||
}
|
||||
restPath = restPath.Replace(matchStr, newContent.ToString());
|
||||
}
|
||||
return restPath;
|
||||
}
|
||||
|
||||
/// <summary>Adds a parameter value.</summary>
|
||||
/// <param name="type">Type of the parameter (must be 'Path' or 'Query').</param>
|
||||
/// <param name="name">Parameter name.</param>
|
||||
/// <param name="value">Parameter value.</param>
|
||||
public void AddParameter(RequestParameterType type, string name, string value)
|
||||
{
|
||||
name.ThrowIfNull("name");
|
||||
if (value == null)
|
||||
{
|
||||
Logger.Warning("Add parameter should not get null values. type={0}, name={1}", type, name);
|
||||
return;
|
||||
}
|
||||
switch (type)
|
||||
{
|
||||
case RequestParameterType.Path:
|
||||
if (!PathParameters.ContainsKey(name))
|
||||
{
|
||||
PathParameters[name] = new List<string> { value };
|
||||
}
|
||||
else
|
||||
{
|
||||
PathParameters[name].Add(value);
|
||||
}
|
||||
break;
|
||||
case RequestParameterType.Query:
|
||||
QueryParameters.Add(new KeyValuePair<string, string>(name, value));
|
||||
break;
|
||||
default:
|
||||
throw new ArgumentOutOfRangeException("type");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Creates a new HTTP request message.</summary>
|
||||
public HttpRequestMessage CreateRequest()
|
||||
{
|
||||
return new HttpRequestMessage(new HttpMethod(Method), BuildUri());
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,82 @@
|
||||
/*
|
||||
Copyright 2011 Google Inc
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
using System.Collections.Generic;
|
||||
using System.Text;
|
||||
|
||||
using Google.Apis.Util;
|
||||
|
||||
namespace Google.Apis.Requests
|
||||
{
|
||||
/// <summary>
|
||||
/// Collection of server errors
|
||||
/// </summary>
|
||||
public class RequestError
|
||||
{
|
||||
/// <summary>
|
||||
/// Enumeration of known error codes which may occur during a request.
|
||||
/// </summary>
|
||||
public enum ErrorCodes
|
||||
{
|
||||
/// <summary>
|
||||
/// The ETag condition specified caused the ETag verification to fail.
|
||||
/// Depending on the ETagAction of the request this either means that a change to the object has been
|
||||
/// made on the server, or that the object in question is still the same and has not been changed.
|
||||
/// </summary>
|
||||
ETagConditionFailed = 412
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Contains a list of all errors
|
||||
/// </summary>
|
||||
public IList<SingleError> Errors { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The error code returned
|
||||
/// </summary>
|
||||
public int Code { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The error message returned
|
||||
/// </summary>
|
||||
public string Message { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Returns a string summary of this error
|
||||
/// </summary>
|
||||
/// <returns>A string summary of this error</returns>
|
||||
public override string ToString()
|
||||
{
|
||||
StringBuilder sb = new StringBuilder();
|
||||
sb.AppendLine(GetType().FullName).Append(Message).AppendFormat(" [{0}]", Code).AppendLine();
|
||||
if (Errors.IsNullOrEmpty())
|
||||
{
|
||||
sb.AppendLine("No individual errors");
|
||||
}
|
||||
else
|
||||
{
|
||||
sb.AppendLine("Errors [");
|
||||
foreach (SingleError err in Errors)
|
||||
{
|
||||
sb.Append('\t').AppendLine(err.ToString());
|
||||
}
|
||||
sb.AppendLine("]");
|
||||
}
|
||||
|
||||
return sb.ToString();
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,60 @@
|
||||
/*
|
||||
Copyright 2011 Google Inc
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
namespace Google.Apis.Requests
|
||||
{
|
||||
/// <summary>
|
||||
/// A single server error
|
||||
/// </summary>
|
||||
public class SingleError
|
||||
{
|
||||
/// <summary>
|
||||
/// The domain in which the error occured
|
||||
/// </summary>
|
||||
public string Domain { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The reason the error was thrown
|
||||
/// </summary>
|
||||
public string Reason { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The error message
|
||||
/// </summary>
|
||||
public string Message { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Type of the location
|
||||
/// </summary>
|
||||
public string LocationType { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Location where the error was thrown
|
||||
/// </summary>
|
||||
public string Location { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Returns a string summary of this error
|
||||
/// </summary>
|
||||
/// <returns>A string summary of this error</returns>
|
||||
public override string ToString()
|
||||
{
|
||||
return string.Format(
|
||||
"Message[{0}] Location[{1} - {2}] Reason[{3}] Domain[{4}]", Message, Location, LocationType, Reason,
|
||||
Domain);
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,29 @@
|
||||
/*
|
||||
Copyright 2010 Google Inc
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
using System;
|
||||
|
||||
namespace Google.Apis.Testing
|
||||
{
|
||||
/// <summary>
|
||||
/// Marker Attribute to indicate a Method/Class/Property has been made more visible for purpose of testing.
|
||||
/// Mark the member as internal and make the testing assembly a friend using
|
||||
/// <code>[assembly: InternalsVisibleTo("Full.Name.Of.Testing.Assembly")]</code>
|
||||
/// </summary>
|
||||
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method | AttributeTargets.Property |
|
||||
AttributeTargets.Field)]
|
||||
public class VisibleForTestOnly : Attribute { }
|
||||
}
|
@ -0,0 +1,99 @@
|
||||
/*
|
||||
Copyright 2013 Google Inc
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
using System;
|
||||
|
||||
namespace Google.Apis.Util
|
||||
{
|
||||
/// <summary>
|
||||
/// Implementation of <see cref="IBackOff"/> that increases the back-off period for each retry attempt using a
|
||||
/// randomization function that grows exponentially. In addition, it also adds a randomize number of milliseconds
|
||||
/// for each attempt.
|
||||
/// </summary>
|
||||
public class ExponentialBackOff : IBackOff
|
||||
{
|
||||
/// <summary>The maximum allowed number of retries.</summary>
|
||||
private const int MaxAllowedNumRetries = 20;
|
||||
|
||||
private readonly TimeSpan deltaBackOff;
|
||||
/// <summary>
|
||||
/// Gets the delta time span used to generate a random milliseconds to add to the next back-off.
|
||||
/// If the value is <see cref="System.TimeSpan.Zero"/> then the generated back-off will be exactly 1, 2, 4,
|
||||
/// 8, 16, etc. seconds. A valid value is between zero and one second. The default value is 250ms, which means
|
||||
/// that the generated back-off will be [0.75-1.25]sec, [1.75-2.25]sec, [3.75-4.25]sec, and so on.
|
||||
/// </summary>
|
||||
public TimeSpan DeltaBackOff
|
||||
{
|
||||
get { return deltaBackOff; }
|
||||
}
|
||||
|
||||
private readonly int maxNumOfRetries;
|
||||
/// <summary>Gets the maximum number of retries. Default value is <c>10</c>.</summary>
|
||||
public int MaxNumOfRetries
|
||||
{
|
||||
get { return maxNumOfRetries; }
|
||||
}
|
||||
|
||||
/// <summary>The random instance which generates a random number to add the to next back-off.</summary>
|
||||
private Random random = new Random();
|
||||
|
||||
/// <summary>Constructs a new exponential back-off with default values.</summary>
|
||||
public ExponentialBackOff()
|
||||
: this(TimeSpan.FromMilliseconds(250))
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>Constructs a new exponential back-off with the given delta and maximum retries.</summary>
|
||||
public ExponentialBackOff(TimeSpan deltaBackOff, int maximumNumOfRetries = 10)
|
||||
{
|
||||
if (deltaBackOff < TimeSpan.Zero || deltaBackOff > TimeSpan.FromSeconds(1))
|
||||
{
|
||||
throw new ArgumentOutOfRangeException("deltaBackOff");
|
||||
}
|
||||
if (maximumNumOfRetries < 0 || maximumNumOfRetries > MaxAllowedNumRetries)
|
||||
{
|
||||
throw new ArgumentOutOfRangeException("deltaBackOff");
|
||||
}
|
||||
|
||||
this.deltaBackOff = deltaBackOff;
|
||||
this.maxNumOfRetries = maximumNumOfRetries;
|
||||
}
|
||||
|
||||
#region IBackOff Members
|
||||
|
||||
/// <inheritdoc/>
|
||||
public TimeSpan GetNextBackOff(int currentRetry)
|
||||
{
|
||||
if (currentRetry <= 0)
|
||||
{
|
||||
throw new ArgumentOutOfRangeException("currentRetry");
|
||||
}
|
||||
if (currentRetry > MaxNumOfRetries)
|
||||
{
|
||||
return TimeSpan.MinValue;
|
||||
}
|
||||
|
||||
// Generate a random number of milliseconds and add it to the current exponential number.
|
||||
var randomMilli = (double)random.Next(
|
||||
(int)(DeltaBackOff.TotalMilliseconds * -1),
|
||||
(int)(DeltaBackOff.TotalMilliseconds * 1));
|
||||
int backOffMilli = (int)(Math.Pow(2.0, (double)currentRetry - 1) * 1000 + randomMilli);
|
||||
return TimeSpan.FromMilliseconds(backOffMilli);
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
}
|
33
Yavsc/GoogleApiSupport/Google.Apis.Core/Util/IBackOff.cs
Normal file
33
Yavsc/GoogleApiSupport/Google.Apis.Core/Util/IBackOff.cs
Normal file
@ -0,0 +1,33 @@
|
||||
/*
|
||||
Copyright 2013 Google Inc
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
using System;
|
||||
|
||||
namespace Google.Apis.Util
|
||||
{
|
||||
/// <summary>Strategy interface to control back-off between retry attempts.</summary>
|
||||
public interface IBackOff
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets the a time span to wait before next retry. If the current retry reached the maximum number of retries,
|
||||
/// the returned value is <see cref="TimeSpan.MinValue"/>.
|
||||
/// </summary>
|
||||
TimeSpan GetNextBackOff(int currentRetry);
|
||||
|
||||
/// <summary>Gets the maximum number of retries.</summary>
|
||||
int MaxNumOfRetries { get; }
|
||||
}
|
||||
}
|
56
Yavsc/GoogleApiSupport/Google.Apis.Core/Util/IClock.cs
Normal file
56
Yavsc/GoogleApiSupport/Google.Apis.Core/Util/IClock.cs
Normal file
@ -0,0 +1,56 @@
|
||||
/*
|
||||
Copyright 2013 Google Inc
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
using System;
|
||||
|
||||
namespace Google.Apis.Util
|
||||
{
|
||||
/// <summary>Clock wrapper for getting the current time.</summary>
|
||||
public interface IClock
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets a <see cref="System.DateTime"/> object that is set to the current date and time on this computer,
|
||||
/// expressed as the local time.
|
||||
/// </summary>
|
||||
[Obsolete("System local time is almost always inappropriate to use. If you really need this, call UtcNow and then call ToLocalTime on the result")]
|
||||
DateTime Now { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets a <see cref="System.DateTime"/> object that is set to the current date and time on this computer,
|
||||
/// expressed as UTC time.
|
||||
/// </summary>
|
||||
DateTime UtcNow { get; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// A default clock implementation that wraps the <see cref="System.DateTime.UtcNow"/>
|
||||
/// and <see cref="System.DateTime.Now"/> properties.
|
||||
/// </summary>
|
||||
public class SystemClock : IClock
|
||||
{
|
||||
/// <summary>Constructs a new system clock.</summary>
|
||||
protected SystemClock() { }
|
||||
|
||||
/// <summary>The default instance.</summary>
|
||||
public static readonly IClock Default = new SystemClock();
|
||||
|
||||
/// <inheritdoc/>
|
||||
public DateTime Now => DateTime.Now;
|
||||
|
||||
/// <inheritdoc/>
|
||||
public DateTime UtcNow => DateTime.UtcNow;
|
||||
}
|
||||
}
|
75
Yavsc/GoogleApiSupport/Google.Apis.Core/Util/Repeatable.cs
Normal file
75
Yavsc/GoogleApiSupport/Google.Apis.Core/Util/Repeatable.cs
Normal file
@ -0,0 +1,75 @@
|
||||
/*
|
||||
Copyright 2011 Google Inc
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
using System.Collections;
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.ObjectModel;
|
||||
|
||||
namespace Google.Apis.Util
|
||||
{
|
||||
/// <summary>
|
||||
/// Repeatable class which allows you to both pass a single element, as well as an array, as a parameter value.
|
||||
/// </summary>
|
||||
public class Repeatable<T> : IEnumerable<T>
|
||||
{
|
||||
private readonly IList<T> values;
|
||||
|
||||
/// <summary>Creates a repeatable value.</summary>
|
||||
public Repeatable(IEnumerable<T> enumeration)
|
||||
{
|
||||
values = new ReadOnlyCollection<T>(new List<T>(enumeration));
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public IEnumerator<T> GetEnumerator()
|
||||
{
|
||||
return values.GetEnumerator();
|
||||
}
|
||||
|
||||
IEnumerator IEnumerable.GetEnumerator()
|
||||
{
|
||||
return GetEnumerator();
|
||||
}
|
||||
|
||||
/// <summary>Converts the single element into a repeatable.</summary>
|
||||
public static implicit operator Repeatable<T>(T elem)
|
||||
{
|
||||
if (elem == null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
return new Repeatable<T>(new[] { elem });
|
||||
}
|
||||
|
||||
/// <summary>Converts a number of elements into a repeatable.</summary>
|
||||
public static implicit operator Repeatable<T>(T[] elem)
|
||||
{
|
||||
if (elem.Length == 0)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
return new Repeatable<T>(elem);
|
||||
}
|
||||
|
||||
/// <summary>Converts a number of elements into a repeatable.</summary>
|
||||
public static implicit operator Repeatable<T>(List<T> elem)
|
||||
{
|
||||
return new Repeatable<T>(elem);
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,82 @@
|
||||
/*
|
||||
Copyright 2011 Google Inc
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
using System;
|
||||
|
||||
namespace Google.Apis.Util
|
||||
{
|
||||
/// <summary>
|
||||
/// An attribute which is used to specially mark a property for reflective purposes,
|
||||
/// assign a name to the property and indicate it's location in the request as either
|
||||
/// in the path or query portion of the request URL.
|
||||
/// </summary>
|
||||
[AttributeUsage(AttributeTargets.Property, AllowMultiple = false)]
|
||||
public class RequestParameterAttribute : Attribute
|
||||
{
|
||||
private readonly string name;
|
||||
private readonly RequestParameterType type;
|
||||
|
||||
/// <summary>Gets the name of the parameter.</summary>
|
||||
public string Name { get { return name; } }
|
||||
|
||||
/// <summary>Gets the type of the parameter, Path or Query.</summary>
|
||||
public RequestParameterType Type { get { return type; } }
|
||||
|
||||
/// <summary>
|
||||
/// Constructs a new property attribute to be a part of a REST URI.
|
||||
/// This constructor uses <see cref="RequestParameterType.Query"/> as the parameter's type.
|
||||
/// </summary>
|
||||
/// <param name="name">
|
||||
/// The name of the parameter. If the parameter is a path parameter this name will be used to substitute the
|
||||
/// string value into the path, replacing {name}. If the parameter is a query parameter, this parameter will be
|
||||
/// added to the query string, in the format "name=value".
|
||||
/// </param>
|
||||
public RequestParameterAttribute(string name)
|
||||
: this(name, RequestParameterType.Query)
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
/// <summary>Constructs a new property attribute to be a part of a REST URI.</summary>
|
||||
/// <param name="name">
|
||||
/// The name of the parameter. If the parameter is a path parameter this name will be used to substitute the
|
||||
/// string value into the path, replacing {name}. If the parameter is a query parameter, this parameter will be
|
||||
/// added to the query string, in the format "name=value".
|
||||
/// </param>
|
||||
/// <param name="type">The type of the parameter, either Path, Query or UserDefinedQueries.</param>
|
||||
public RequestParameterAttribute(string name, RequestParameterType type)
|
||||
{
|
||||
this.name = name;
|
||||
this.type = type;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Describe the type of this parameter (Path, Query or UserDefinedQueries).</summary>
|
||||
public enum RequestParameterType
|
||||
{
|
||||
/// <summary>A path parameter which is inserted into the path portion of the request URI.</summary>
|
||||
Path,
|
||||
|
||||
/// <summary>A query parameter which is inserted into the query portion of the request URI.</summary>
|
||||
Query,
|
||||
|
||||
/// <summary>
|
||||
/// A group of user-defined parameters that will be added in to the query portion of the request URI. If this
|
||||
/// type is being used, the name of the RequestParameterAttirbute is meaningless.
|
||||
/// </summary>
|
||||
UserDefinedQueries
|
||||
}
|
||||
}
|
@ -0,0 +1,37 @@
|
||||
/*
|
||||
Copyright 2010 Google Inc
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
using Google.Apis.Requests;
|
||||
using Newtonsoft.Json;
|
||||
|
||||
namespace Google.Apis.Util
|
||||
{
|
||||
/// <summary>
|
||||
/// Calls to Google Api return StandardResponses as Json with
|
||||
/// two properties Data, being the return type of the method called
|
||||
/// and Error, being any errors that occure.
|
||||
/// </summary>
|
||||
public sealed class StandardResponse<InnerType>
|
||||
{
|
||||
/// <summary>May be null if call failed.</summary>
|
||||
[JsonProperty("data")]
|
||||
public InnerType Data { get; set; }
|
||||
|
||||
/// <summary>May be null if call succedded.</summary>
|
||||
[JsonProperty("error")]
|
||||
public RequestError Error { get; set; }
|
||||
}
|
||||
}
|
@ -0,0 +1,52 @@
|
||||
/*
|
||||
Copyright 2013 Google Inc
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Google.Apis.Util.Store
|
||||
{
|
||||
/// <summary>
|
||||
/// Stores and manages data objects, where the key is a string and the value is an object.
|
||||
/// <para>
|
||||
/// <c>null</c> keys are not allowed.
|
||||
/// </para>
|
||||
/// </summary>
|
||||
public interface IDataStore
|
||||
{
|
||||
/// <summary>Asynchronously stores the given value for the given key (replacing any existing value).</summary>
|
||||
/// <typeparam name="T">The type to store in the data store.</typeparam>
|
||||
/// <param name="key">The key.</param>
|
||||
/// <param name="value">The value to store.</param>
|
||||
Task StoreAsync<T>(string key, T value);
|
||||
|
||||
/// <summary>
|
||||
/// Asynchronously deletes the given key. The type is provided here as well because the "real" saved key should
|
||||
/// contain type information as well, so the data store will be able to store the same key for different types.
|
||||
/// </summary>
|
||||
/// <typeparam name="T">The type to delete from the data store.</typeparam>
|
||||
/// <param name="key">The key to delete.</param>
|
||||
Task DeleteAsync<T>(string key);
|
||||
|
||||
/// <summary>Asynchronously returns the stored value for the given key or <c>null</c> if not found.</summary>
|
||||
/// <typeparam name="T">The type to retrieve from the data store.</typeparam>
|
||||
/// <param name="key">The key to retrieve its value.</param>
|
||||
/// <returns>The stored object.</returns>
|
||||
Task<T> GetAsync<T>(string key);
|
||||
|
||||
/// <summary>Asynchronously clears all values in the data store.</summary>
|
||||
Task ClearAsync();
|
||||
}
|
||||
}
|
@ -0,0 +1,36 @@
|
||||
/*
|
||||
Copyright 2011 Google Inc
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
using System;
|
||||
|
||||
namespace Google.Apis.Util
|
||||
{
|
||||
/// <summary>Defines an attribute containing a string representation of the member.</summary>
|
||||
[AttributeUsage(AttributeTargets.Field, AllowMultiple = false)]
|
||||
public class StringValueAttribute : Attribute
|
||||
{
|
||||
private readonly string text;
|
||||
/// <summary>The text which belongs to this member.</summary>
|
||||
public string Text { get { return text; } }
|
||||
|
||||
/// <summary>Creates a new string value attribute with the specified text.</summary>
|
||||
public StringValueAttribute(string text)
|
||||
{
|
||||
text.ThrowIfNull("text");
|
||||
this.text = text;
|
||||
}
|
||||
}
|
||||
}
|
122
Yavsc/GoogleApiSupport/Google.Apis.Core/Util/UriPatcher.cs
Normal file
122
Yavsc/GoogleApiSupport/Google.Apis.Core/Util/UriPatcher.cs
Normal file
@ -0,0 +1,122 @@
|
||||
/*
|
||||
Copyright 2016 Google Inc
|
||||
|
||||
Licensed under the Apache License, Version 2.0(the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
using System;
|
||||
using System.Reflection;
|
||||
|
||||
namespace Google.Apis.Util
|
||||
{
|
||||
/// <summary>
|
||||
/// Workarounds for some unfortunate behaviors in the .NET Framework's
|
||||
/// implementation of System.Uri
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// UriPatcher lets us work around some unfortunate behaviors in the .NET Framework's
|
||||
/// implementation of System.Uri.
|
||||
///
|
||||
/// == Problem 1: Slashes and dots
|
||||
///
|
||||
/// Prior to .NET 4.5, System.Uri would always unescape "%2f" ("/") and "%5c" ("\\").
|
||||
/// Relative path components were also compressed.
|
||||
///
|
||||
/// As a result, this: "http://www.example.com/.%2f.%5c./"
|
||||
/// ... turned into this: "http://www.example.com/"
|
||||
///
|
||||
/// This breaks API requests where slashes or dots appear in path parameters. Such requests
|
||||
/// arise, for example, when these characters appear in the name of a GCS object.
|
||||
///
|
||||
/// == Problem 2: Fewer unreserved characters
|
||||
///
|
||||
/// Unless IDN/IRI parsing is enabled -- which it is not, by default, prior to .NET 4.5 --
|
||||
/// Uri.EscapeDataString uses the set of "unreserved" characters from RFC 2396 instead of the
|
||||
/// newer, *smaller* list from RFC 3986. We build requests using URI templating as described
|
||||
/// by RFC 6570, which specifies that the latter definition (RFC 3986) should be used.
|
||||
///
|
||||
/// This breaks API requests with parameters including any of: !*'()
|
||||
///
|
||||
/// == Solutions
|
||||
///
|
||||
/// Though the default behaviors changed in .NET 4.5, these "quirks" remain for compatibility
|
||||
/// unless the application explicitly targets the new runtime. Usually, that means adding a
|
||||
/// TargetFrameworkAttribute to the entry assembly.
|
||||
///
|
||||
/// Applications running on .NET 4.0 or later can also set "DontUnescapePathDotsAndSlashes"
|
||||
/// and enable IDN/IRI parsing using app.config or web.config.
|
||||
///
|
||||
/// As a class library, we can't control app.config or the entry assembly, so we can't take
|
||||
/// either approach. Instead, we resort to reflection trickery to try to solve these problems
|
||||
/// if we detect they exist. Sorry.
|
||||
/// </remarks>
|
||||
public static class UriPatcher
|
||||
{
|
||||
/// <summary>
|
||||
/// Patch URI quirks in System.Uri. See class summary for details.
|
||||
/// </summary>
|
||||
public static void PatchUriQuirks()
|
||||
{
|
||||
var uriParser = typeof(System.Uri).GetTypeInfo().Assembly.GetType("System.UriParser");
|
||||
if (uriParser == null) { return; }
|
||||
|
||||
// Is "%2f" unescaped for http: or https: URIs?
|
||||
if (new Uri("http://example.com/%2f").AbsolutePath == "//" ||
|
||||
new Uri("https://example.com/%2f").AbsolutePath == "//")
|
||||
{
|
||||
// Call System.UriParser.Http[s]Uri.SetUpdatableFlags(UriSyntaxFlags.None)
|
||||
// https://github.com/Microsoft/referencesource/blob/d925d870f3cb3f6a/System/net/System/_UriSyntax.cs#L87
|
||||
// https://github.com/Microsoft/referencesource/blob/d925d870f3cb3f6a/System/net/System/_UriSyntax.cs#L77
|
||||
// https://github.com/Microsoft/referencesource/blob/d925d870f3cb3f6a/System/net/System/_UriSyntax.cs#L352
|
||||
|
||||
var setUpdatableFlagsMethod = uriParser.GetMethod("SetUpdatableFlags",
|
||||
BindingFlags.Instance | BindingFlags.NonPublic);
|
||||
if (setUpdatableFlagsMethod != null)
|
||||
{
|
||||
Action<string> setUriParserUpdatableFlags = (fieldName) =>
|
||||
{
|
||||
var parserField = uriParser.GetField(fieldName,
|
||||
BindingFlags.Static | BindingFlags.NonPublic);
|
||||
if (parserField == null) { return; }
|
||||
var parserInstance = parserField.GetValue(null);
|
||||
if (parserInstance == null) { return; }
|
||||
setUpdatableFlagsMethod.Invoke(parserInstance, new object[] { 0 });
|
||||
};
|
||||
|
||||
// Make the change for the http: and https: URI parsers.
|
||||
setUriParserUpdatableFlags("HttpUri");
|
||||
setUriParserUpdatableFlags("HttpsUri");
|
||||
}
|
||||
}
|
||||
|
||||
// Is "*" considered "unreserved"?
|
||||
if (Uri.EscapeDataString("*") == "*")
|
||||
{
|
||||
// Set UriParser.s_QuirksVersion to at least UriQuirksVersion.V3
|
||||
// https://github.com/Microsoft/referencesource/blob/d925d870f3cb3f6a/System/net/System/_UriSyntax.cs#L114
|
||||
// https://github.com/Microsoft/referencesource/blob/d925d870f3cb3f6a/System/net/System/UriHelper.cs#L701
|
||||
|
||||
var quirksField = uriParser.GetField("s_QuirksVersion",
|
||||
BindingFlags.Static | BindingFlags.NonPublic);
|
||||
if (quirksField != null)
|
||||
{
|
||||
int quirksVersion = (int)quirksField.GetValue(null);
|
||||
if (quirksVersion <= 2)
|
||||
{
|
||||
quirksField.SetValue(null, 3);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
175
Yavsc/GoogleApiSupport/Google.Apis.Core/Util/Utilities.cs
Normal file
175
Yavsc/GoogleApiSupport/Google.Apis.Core/Util/Utilities.cs
Normal file
@ -0,0 +1,175 @@
|
||||
/*
|
||||
Copyright 2010 Google Inc
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
using Google.Apis.Testing;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Globalization;
|
||||
using System.Linq;
|
||||
using System.Reflection;
|
||||
using System.Text.RegularExpressions;
|
||||
|
||||
namespace Google.Apis.Util
|
||||
{
|
||||
/// <summary>A utility class which contains helper methods and extension methods.</summary>
|
||||
public static class Utilities
|
||||
{
|
||||
/// <summary>Returns the version of the core library.</summary>
|
||||
[VisibleForTestOnly]
|
||||
public static string GetLibraryVersion()
|
||||
{
|
||||
return Regex.Match(typeof(Utilities).GetTypeInfo().Assembly.FullName, "Version=([\\d\\.]+)").Groups[1].ToString();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// A Google.Apis utility method for throwing an <see cref="System.ArgumentNullException"/> if the object is
|
||||
/// <c>null</c>.
|
||||
/// </summary>
|
||||
public static T ThrowIfNull<T>(this T obj, string paramName)
|
||||
{
|
||||
if (obj == null)
|
||||
{
|
||||
throw new ArgumentNullException(paramName);
|
||||
}
|
||||
|
||||
return obj;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// A Google.Apis utility method for throwing an <see cref="System.ArgumentNullException"/> if the string is
|
||||
/// <c>null</c> or empty.
|
||||
/// </summary>
|
||||
/// <returns>The original string.</returns>
|
||||
public static string ThrowIfNullOrEmpty(this string str, string paramName)
|
||||
{
|
||||
if (string.IsNullOrEmpty(str))
|
||||
{
|
||||
throw new ArgumentException("Parameter was empty", paramName);
|
||||
}
|
||||
return str;
|
||||
}
|
||||
|
||||
/// <summary>Returns <c>true</c> in case the enumerable is <c>null</c> or empty.</summary>
|
||||
internal static bool IsNullOrEmpty<T>(this IEnumerable<T> coll)
|
||||
{
|
||||
return coll == null || coll.Count() == 0;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// A Google.Apis utility method for returning the first matching custom attribute (or <c>null</c>) of the specified member.
|
||||
/// </summary>
|
||||
public static T GetCustomAttribute<T>(this MemberInfo info) where T : Attribute
|
||||
{
|
||||
object[] results = info.GetCustomAttributes(typeof(T), false).ToArray();
|
||||
return results.Length == 0 ? null : (T)results[0];
|
||||
}
|
||||
|
||||
/// <summary>Returns the defined string value of an Enum.</summary>
|
||||
internal static string GetStringValue(this Enum value)
|
||||
{
|
||||
FieldInfo entry = value.GetType().GetField(value.ToString());
|
||||
entry.ThrowIfNull("value");
|
||||
|
||||
// If set, return the value.
|
||||
var attribute = entry.GetCustomAttribute<StringValueAttribute>();
|
||||
if (attribute != null)
|
||||
{
|
||||
return attribute.Text;
|
||||
}
|
||||
|
||||
// Otherwise, throw an exception.
|
||||
throw new ArgumentException(
|
||||
string.Format("Enum value '{0}' does not contain a StringValue attribute", entry), "value");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns the defined string value of an Enum. Use for test purposes or in other Google.Apis projects.
|
||||
/// </summary>
|
||||
public static string GetEnumStringValue(Enum value)
|
||||
{
|
||||
return value.GetStringValue();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Tries to convert the specified object to a string. Uses custom type converters if available.
|
||||
/// Returns null for a null object.
|
||||
/// </summary>
|
||||
[VisibleForTestOnly]
|
||||
public static string ConvertToString(object o)
|
||||
{
|
||||
if (o == null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
if (o.GetType().GetTypeInfo().IsEnum)
|
||||
{
|
||||
// Try to convert the Enum value using the StringValue attribute.
|
||||
var enumType = o.GetType();
|
||||
FieldInfo field = enumType.GetField(o.ToString());
|
||||
StringValueAttribute attribute = field.GetCustomAttribute<StringValueAttribute>();
|
||||
return attribute != null ? attribute.Text : o.ToString();
|
||||
}
|
||||
|
||||
if (o is DateTime)
|
||||
{
|
||||
// Honor RFC3339.
|
||||
return ConvertToRFC3339((DateTime)o);
|
||||
}
|
||||
|
||||
if (o is bool)
|
||||
{
|
||||
return o.ToString().ToLowerInvariant();
|
||||
}
|
||||
|
||||
return o.ToString();
|
||||
}
|
||||
|
||||
/// <summary>Converts the input date into a RFC3339 string (http://www.ietf.org/rfc/rfc3339.txt).</summary>
|
||||
internal static string ConvertToRFC3339(DateTime date)
|
||||
{
|
||||
if (date.Kind == DateTimeKind.Unspecified)
|
||||
{
|
||||
date = date.ToUniversalTime();
|
||||
}
|
||||
return date.ToString("yyyy-MM-dd'T'HH:mm:ss.fffK", DateTimeFormatInfo.InvariantInfo);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Parses the input string and returns <see cref="System.DateTime"/> if the input is a valid
|
||||
/// representation of a date. Otherwise it returns <c>null</c>.
|
||||
/// </summary>
|
||||
public static DateTime? GetDateTimeFromString(string raw)
|
||||
{
|
||||
DateTime result;
|
||||
if (!DateTime.TryParse(raw, out result))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
/// <summary>Returns a string (by RFC3339) form the input <see cref="DateTime"/> instance.</summary>
|
||||
public static string GetStringFromDateTime(DateTime? date)
|
||||
{
|
||||
if (!date.HasValue)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
return ConvertToRFC3339(date.Value);
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,49 @@
|
||||
/*
|
||||
Copyright 2013 Google Inc
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
using System;
|
||||
|
||||
namespace Google.Apis.Download
|
||||
{
|
||||
/// <summary>Enum which represents the status of the current download.</summary>
|
||||
public enum DownloadStatus
|
||||
{
|
||||
/// <summary>The download has not started.</summary>
|
||||
NotStarted,
|
||||
|
||||
/// <summary>Data is being downloaded.</summary>
|
||||
Downloading,
|
||||
|
||||
/// <summary>The download was completed successfully.</summary>
|
||||
Completed,
|
||||
|
||||
/// <summary>The download failed.</summary>
|
||||
Failed
|
||||
};
|
||||
|
||||
/// <summary>Reports download progress.</summary>
|
||||
public interface IDownloadProgress
|
||||
{
|
||||
/// <summary>Gets the current status of the upload.</summary>
|
||||
DownloadStatus Status { get; }
|
||||
|
||||
/// <summary>Gets the number of bytes received from the server.</summary>
|
||||
long BytesDownloaded { get; }
|
||||
|
||||
/// <summary>Gets an exception if one occurred.</summary>
|
||||
Exception Exception { get; }
|
||||
}
|
||||
}
|
@ -0,0 +1,50 @@
|
||||
/*
|
||||
Copyright 2013 Google Inc
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Google.Apis.Download
|
||||
{
|
||||
/// <summary>Media download which uses download file part by part, by <see cref="ChunkSize"/>.</summary>
|
||||
public interface IMediaDownloader
|
||||
{
|
||||
/// <summary>An event which notifies when the download status has been changed.</summary>
|
||||
event Action<IDownloadProgress> ProgressChanged;
|
||||
|
||||
/// <summary>Gets or sets the chunk size to download, it defines the size of each part.</summary>
|
||||
int ChunkSize { get; set; }
|
||||
|
||||
/// <summary>Downloads synchronously the given URL to the given stream.</summary>
|
||||
IDownloadProgress Download(string url, Stream stream);
|
||||
|
||||
/// <summary>Downloads asynchronously the given URL to the given stream.</summary>
|
||||
Task<IDownloadProgress> DownloadAsync(string url, Stream stream);
|
||||
|
||||
/// <summary>
|
||||
/// Downloads asynchronously the given URL to the given stream. This download method supports a cancellation
|
||||
/// token to cancel a request before it was completed.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// In case the download fails <see cref="IDownloadProgress.Exception "/> will contain the exception that
|
||||
/// cause the failure. The only exception which will be thrown is
|
||||
/// <see cref="System.Threading.Tasks.TaskCanceledException"/> which indicates that the task was canceled.
|
||||
/// </remarks>
|
||||
Task<IDownloadProgress> DownloadAsync(string url, Stream stream, CancellationToken cancellationToken);
|
||||
}
|
||||
}
|
367
Yavsc/GoogleApiSupport/Google.Apis/Download/MediaDownloader.cs
Normal file
367
Yavsc/GoogleApiSupport/Google.Apis/Download/MediaDownloader.cs
Normal file
@ -0,0 +1,367 @@
|
||||
/*
|
||||
Copyright 2013 Google Inc
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.Net.Http;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
using Google.Apis.Logging;
|
||||
using Google.Apis.Media;
|
||||
using Google.Apis.Services;
|
||||
using Google.Apis.Util;
|
||||
using System.Net.Http.Headers;
|
||||
|
||||
namespace Google.Apis.Download
|
||||
{
|
||||
/// <summary>
|
||||
/// A media downloader implementation which handles media downloads.
|
||||
/// </summary>
|
||||
public class MediaDownloader : IMediaDownloader
|
||||
{
|
||||
static MediaDownloader()
|
||||
{
|
||||
UriPatcher.PatchUriQuirks();
|
||||
}
|
||||
|
||||
private static readonly ILogger Logger = ApplicationContext.Logger.ForType<MediaDownloader>();
|
||||
|
||||
/// <summary>The service which this downloader belongs to.</summary>
|
||||
private readonly IClientService service;
|
||||
|
||||
private const int MB = 0x100000;
|
||||
|
||||
/// <summary>Maximum chunk size. Default value is 10*MB.</summary>
|
||||
public const int MaximumChunkSize = 10 * MB;
|
||||
|
||||
private int chunkSize = MaximumChunkSize;
|
||||
/// <summary>
|
||||
/// Gets or sets the amount of data that will be downloaded before notifying the caller of
|
||||
/// the download's progress.
|
||||
/// Must not exceed <see cref="MaximumChunkSize"/>.
|
||||
/// Default value is <see cref="MaximumChunkSize"/>.
|
||||
/// </summary>
|
||||
public int ChunkSize
|
||||
{
|
||||
get { return chunkSize; }
|
||||
set
|
||||
{
|
||||
if (value > MaximumChunkSize)
|
||||
{
|
||||
throw new ArgumentOutOfRangeException("ChunkSize");
|
||||
}
|
||||
chunkSize = value;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The range header for the request, if any. This can be used to download specific parts
|
||||
/// of the requested media.
|
||||
/// </summary>
|
||||
public RangeHeaderValue Range { get; set; }
|
||||
|
||||
#region Progress
|
||||
|
||||
/// <summary>
|
||||
/// Download progress model, which contains the status of the download, the amount of bytes whose where
|
||||
/// downloaded so far, and an exception in case an error had occurred.
|
||||
/// </summary>
|
||||
private class DownloadProgress : IDownloadProgress
|
||||
{
|
||||
/// <summary>Constructs a new progress instance.</summary>
|
||||
/// <param name="status">The status of the download.</param>
|
||||
/// <param name="bytes">The number of bytes received so far.</param>
|
||||
public DownloadProgress(DownloadStatus status, long bytes)
|
||||
{
|
||||
Status = status;
|
||||
BytesDownloaded = bytes;
|
||||
}
|
||||
|
||||
/// <summary>Constructs a new progress instance.</summary>
|
||||
/// <param name="exception">An exception which occurred during the download.</param>
|
||||
/// <param name="bytes">The number of bytes received before the exception occurred.</param>
|
||||
public DownloadProgress(Exception exception, long bytes)
|
||||
{
|
||||
Status = DownloadStatus.Failed;
|
||||
BytesDownloaded = bytes;
|
||||
Exception = exception;
|
||||
}
|
||||
|
||||
/// <summary>Gets or sets the status of the download.</summary>
|
||||
public DownloadStatus Status { get; private set; }
|
||||
|
||||
/// <summary>Gets or sets the amount of bytes that have been downloaded so far.</summary>
|
||||
public long BytesDownloaded { get; private set; }
|
||||
|
||||
/// <summary>Gets or sets the exception which occurred during the download or <c>null</c>.</summary>
|
||||
public Exception Exception { get; private set; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Updates the current progress and call the <see cref="ProgressChanged"/> event to notify listeners.
|
||||
/// </summary>
|
||||
private void UpdateProgress(IDownloadProgress progress)
|
||||
{
|
||||
ProgressChanged?.Invoke(progress);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
/// <summary>Constructs a new downloader with the given client service.</summary>
|
||||
public MediaDownloader(IClientService service)
|
||||
{
|
||||
this.service = service;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the callback for modifying requests made when downloading.
|
||||
/// </summary>
|
||||
public Action<HttpRequestMessage> ModifyRequest { get; set; }
|
||||
|
||||
#region IMediaDownloader Overrides
|
||||
|
||||
/// <inheritdoc/>
|
||||
public event Action<IDownloadProgress> ProgressChanged;
|
||||
|
||||
#region Download (sync and async)
|
||||
|
||||
/// <inheritdoc/>
|
||||
public IDownloadProgress Download(string url, Stream stream)
|
||||
{
|
||||
return DownloadCoreAsync(url, stream, CancellationToken.None).Result;
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public async Task<IDownloadProgress> DownloadAsync(string url, Stream stream)
|
||||
{
|
||||
return await DownloadAsync(url, stream, CancellationToken.None).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public async Task<IDownloadProgress> DownloadAsync(string url, Stream stream,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
return await DownloadCoreAsync(url, stream, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#endregion
|
||||
|
||||
/// <summary>
|
||||
/// CountedBuffer bundles together a byte buffer and a count of valid bytes.
|
||||
/// </summary>
|
||||
private class CountedBuffer
|
||||
{
|
||||
public byte[] Data { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// How many bytes at the beginning of Data are valid.
|
||||
/// </summary>
|
||||
public int Count { get; private set; }
|
||||
|
||||
public CountedBuffer(int size)
|
||||
{
|
||||
Data = new byte[size];
|
||||
Count = 0;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns true if the buffer contains no data.
|
||||
/// </summary>
|
||||
public bool IsEmpty { get { return Count == 0; } }
|
||||
|
||||
/// <summary>
|
||||
/// Read data from stream until the stream is empty or the buffer is full.
|
||||
/// </summary>
|
||||
/// <param name="stream">Stream from which to read.</param>
|
||||
/// <param name="cancellationToken">Cancellation token for the operation.</param>
|
||||
public async Task Fill(Stream stream, CancellationToken cancellationToken)
|
||||
{
|
||||
// ReadAsync may return if it has *any* data available, so we loop.
|
||||
while (Count < Data.Length)
|
||||
{
|
||||
int read = await stream.ReadAsync(Data, Count, Data.Length - Count, cancellationToken).ConfigureAwait(false);
|
||||
if (read == 0) { break; }
|
||||
Count += read;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Remove the first n bytes of the buffer. Move any remaining valid bytes to the beginning.
|
||||
/// Trying to remove more bytes than the buffer contains just clears the buffer.
|
||||
/// </summary>
|
||||
/// <param name="n">The number of bytes to remove.</param>
|
||||
public void RemoveFromFront(int n)
|
||||
{
|
||||
if (n >= Count)
|
||||
{
|
||||
Count = 0;
|
||||
}
|
||||
else
|
||||
{
|
||||
// Some valid data remains.
|
||||
Array.Copy(Data, n, Data, 0, Count - n);
|
||||
Count -= n;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The core download logic. We download the media and write it to an output stream
|
||||
/// ChunkSize bytes at a time, raising the ProgressChanged event after each chunk.
|
||||
///
|
||||
/// The chunking behavior is largely a historical artifact: a previous implementation
|
||||
/// issued multiple web requests, each for ChunkSize bytes. Now we do everything in
|
||||
/// one request, but the API and client-visible behavior are retained for compatibility.
|
||||
/// </summary>
|
||||
/// <param name="url">The URL of the resource to download.</param>
|
||||
/// <param name="stream">The download will download the resource into this stream.</param>
|
||||
/// <param name="cancellationToken">A cancellation token to cancel this download in the middle.</param>
|
||||
/// <returns>A task with the download progress object. If an exception occurred during the download, its
|
||||
/// <see cref="IDownloadProgress.Exception "/> property will contain the exception.</returns>
|
||||
private async Task<IDownloadProgress> DownloadCoreAsync(string url, Stream stream,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
url.ThrowIfNull("url");
|
||||
stream.ThrowIfNull("stream");
|
||||
if (!stream.CanWrite)
|
||||
{
|
||||
throw new ArgumentException("stream doesn't support write operations");
|
||||
}
|
||||
|
||||
// Add alt=media to the query parameters.
|
||||
var uri = new UriBuilder(url);
|
||||
if (uri.Query == null || uri.Query.Length <= 1)
|
||||
{
|
||||
uri.Query = "alt=media";
|
||||
}
|
||||
else
|
||||
{
|
||||
// Remove the leading '?'. UriBuilder.Query doesn't round-trip.
|
||||
uri.Query = uri.Query.Substring(1) + "&alt=media";
|
||||
}
|
||||
|
||||
var request = new HttpRequestMessage(HttpMethod.Get, uri.ToString());
|
||||
request.Headers.Range = Range;
|
||||
ModifyRequest?.Invoke(request);
|
||||
|
||||
// Number of bytes sent to the caller's stream.
|
||||
long bytesReturned = 0;
|
||||
|
||||
try
|
||||
{
|
||||
// Signal SendAsync to return as soon as the response headers are read.
|
||||
// We'll stream the content ourselves as it becomes available.
|
||||
var completionOption = HttpCompletionOption.ResponseHeadersRead;
|
||||
|
||||
using (var response = await service.HttpClient.SendAsync(request, completionOption, cancellationToken).ConfigureAwait(false))
|
||||
{
|
||||
if (!response.IsSuccessStatusCode)
|
||||
{
|
||||
throw await MediaApiErrorHandling.ExceptionForResponseAsync(service, response).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
OnResponseReceived(response);
|
||||
|
||||
using (var responseStream = await response.Content.ReadAsStreamAsync().ConfigureAwait(false))
|
||||
{
|
||||
// We send ChunkSize bytes at a time to the caller, but we keep ChunkSize + 1 bytes
|
||||
// buffered. That way we can tell when we've reached the end of the response, even if the
|
||||
// response length is evenly divisible by ChunkSize, and we can avoid sending a Downloading
|
||||
// event followed by a Completed event with no bytes downloaded in between.
|
||||
//
|
||||
// This maintains the client-visible behavior of a previous implementation.
|
||||
var buffer = new CountedBuffer(ChunkSize + 1);
|
||||
|
||||
while (true)
|
||||
{
|
||||
await buffer.Fill(responseStream, cancellationToken).ConfigureAwait(false);
|
||||
|
||||
// Send one chunk to the caller's stream.
|
||||
int bytesToReturn = Math.Min(ChunkSize, buffer.Count);
|
||||
OnDataReceived(buffer.Data, bytesToReturn);
|
||||
await stream.WriteAsync(buffer.Data, 0, bytesToReturn, cancellationToken).ConfigureAwait(false);
|
||||
bytesReturned += bytesToReturn;
|
||||
|
||||
buffer.RemoveFromFront(ChunkSize);
|
||||
if (buffer.IsEmpty)
|
||||
{
|
||||
// We had <= ChunkSize bytes buffered, so we've read and returned the entire response.
|
||||
// Skip sending a Downloading event. We'll send Completed instead.
|
||||
break;
|
||||
}
|
||||
|
||||
UpdateProgress(new DownloadProgress(DownloadStatus.Downloading, bytesReturned));
|
||||
}
|
||||
}
|
||||
OnDownloadCompleted();
|
||||
|
||||
var finalProgress = new DownloadProgress(DownloadStatus.Completed, bytesReturned);
|
||||
UpdateProgress(finalProgress);
|
||||
return finalProgress;
|
||||
}
|
||||
}
|
||||
catch (TaskCanceledException ex)
|
||||
{
|
||||
Logger.Error(ex, "Download media was canceled");
|
||||
UpdateProgress(new DownloadProgress(ex, bytesReturned));
|
||||
throw;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logger.Error(ex, "Exception occurred while downloading media");
|
||||
var progress = new DownloadProgress(ex, bytesReturned);
|
||||
UpdateProgress(progress);
|
||||
return progress;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Called when a successful HTTP response is received, allowing subclasses to examine headers.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// For unsuccessful responses, an appropriate exception is thrown immediately, without this method
|
||||
/// being called.
|
||||
/// </remarks>
|
||||
/// <param name="response">HTTP response received.</param>
|
||||
protected virtual void OnResponseReceived(HttpResponseMessage response)
|
||||
{
|
||||
// No-op
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Called when an HTTP response is received, allowing subclasses to examine data before it's
|
||||
/// written to the client stream.
|
||||
/// </summary>
|
||||
/// <param name="data">Byte array containing the data downloaded.</param>
|
||||
/// <param name="length">Length of data downloaded in this chunk, in bytes.</param>
|
||||
protected virtual void OnDataReceived(byte[] data, int length)
|
||||
{
|
||||
// No-op
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Called when a download has completed, allowing subclasses to perform any final validation
|
||||
/// or transformation.
|
||||
/// </summary>
|
||||
protected virtual void OnDownloadCompleted()
|
||||
{
|
||||
// No-op
|
||||
}
|
||||
}
|
||||
}
|
46
Yavsc/GoogleApiSupport/Google.Apis/ETagAction.cs
Normal file
46
Yavsc/GoogleApiSupport/Google.Apis/ETagAction.cs
Normal file
@ -0,0 +1,46 @@
|
||||
/*
|
||||
Copyright 2011 Google Inc
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
namespace Google.Apis
|
||||
{
|
||||
/// <summary>
|
||||
/// Defines the behaviour/header used for sending an etag along with a request.
|
||||
/// </summary>
|
||||
public enum ETagAction
|
||||
{
|
||||
/// <summary>
|
||||
/// The default etag behaviour will be determined by the type of the request.
|
||||
/// </summary>
|
||||
Default,
|
||||
|
||||
/// <summary>
|
||||
/// The ETag won't be added to the header of the request.
|
||||
/// </summary>
|
||||
Ignore,
|
||||
|
||||
/// <summary>
|
||||
/// The ETag will be added as an "If-Match" header.
|
||||
/// A request sent with an "If-Match" header will only succeed if both ETags are identical.
|
||||
/// </summary>
|
||||
IfMatch,
|
||||
|
||||
/// <summary>
|
||||
/// The ETag will be added as an "If-None-Match" header.
|
||||
/// A request sent with an "If-Match" header will only succeed if both ETags are not identical.
|
||||
/// </summary>
|
||||
IfNoneMatch,
|
||||
}
|
||||
}
|
@ -0,0 +1,82 @@
|
||||
/*
|
||||
Copyright 2015 Google Inc
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
using System.Net.Http;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
using Newtonsoft.Json;
|
||||
|
||||
using Google.Apis.Json;
|
||||
using Google.Apis.Requests;
|
||||
using Google.Apis.Services;
|
||||
using Google.Apis.Util;
|
||||
|
||||
namespace Google.Apis.Media
|
||||
{
|
||||
/// <summary>
|
||||
/// Common error handling code for the Media API.
|
||||
/// </summary>
|
||||
internal static class MediaApiErrorHandling
|
||||
{
|
||||
/// <summary>
|
||||
/// Creates a suitable exception for an HTTP response, attempting to parse the body as
|
||||
/// JSON but falling back to just using the text as the message.
|
||||
/// </summary>
|
||||
internal static Task<GoogleApiException> ExceptionForResponseAsync(
|
||||
IClientService service,
|
||||
HttpResponseMessage response)
|
||||
{
|
||||
return ExceptionForResponseAsync(service.Serializer, service.Name, response);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates a suitable exception for an HTTP response, attempting to parse the body as
|
||||
/// JSON but falling back to just using the text as the message.
|
||||
/// </summary>
|
||||
internal static async Task<GoogleApiException> ExceptionForResponseAsync(
|
||||
ISerializer serializer,
|
||||
string name,
|
||||
HttpResponseMessage response)
|
||||
{
|
||||
// If we can't even read the response, let that exception bubble up, just as it would have done
|
||||
// if the error had been occurred when sending the request.
|
||||
string responseText = await response.Content.ReadAsStringAsync().ConfigureAwait(false);
|
||||
RequestError parsedError = null;
|
||||
string message = responseText;
|
||||
try
|
||||
{
|
||||
var parsedResponse = (serializer ?? NewtonsoftJsonSerializer.Instance).Deserialize<StandardResponse<object>>(responseText);
|
||||
if (parsedResponse != null && parsedResponse.Error != null)
|
||||
{
|
||||
parsedError = parsedResponse.Error;
|
||||
message = parsedError.ToString();
|
||||
}
|
||||
}
|
||||
catch (JsonException)
|
||||
{
|
||||
// Just make do with a null RequestError, and the message set to the body of the response.
|
||||
// The contents of the caught exception aren't particularly useful - we don't need to include it
|
||||
// as a cause, for example. The expectation is that the exception returned by this method (below)
|
||||
// will be thrown by the caller.
|
||||
}
|
||||
return new GoogleApiException(name ?? "", message)
|
||||
{
|
||||
Error = parsedError,
|
||||
HttpStatusCode = response.StatusCode
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
383
Yavsc/GoogleApiSupport/Google.Apis/Requests/BatchRequest.cs
Normal file
383
Yavsc/GoogleApiSupport/Google.Apis/Requests/BatchRequest.cs
Normal file
@ -0,0 +1,383 @@
|
||||
/*
|
||||
Copyright 2013 Google Inc
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Net;
|
||||
using System.Net.Http;
|
||||
using System.Net.Http.Headers;
|
||||
using System.Text;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
using Google.Apis.Http;
|
||||
using Google.Apis.Services;
|
||||
using Google.Apis.Testing;
|
||||
|
||||
namespace Google.Apis.Requests
|
||||
{
|
||||
/// <summary>
|
||||
/// A batch request which represents individual requests to Google servers. You should add a single service
|
||||
/// request using the <see cref="Queue"/> method and execute all individual requests using
|
||||
/// <see cref="ExecuteAsync()"/>. More information about the batch protocol is available in
|
||||
/// https://developers.google.com/storage/docs/json_api/v1/how-tos/batch.
|
||||
/// <remarks>
|
||||
/// Current implementation doesn't retry on unsuccessful individual response and doesn't support requests with
|
||||
/// different access tokens (different users or scopes).
|
||||
/// </remarks>
|
||||
/// </summary>
|
||||
public sealed class BatchRequest
|
||||
{
|
||||
private const string DefaultBatchUrl = "https://www.googleapis.com/batch";
|
||||
private const int QueueLimit = 1000;
|
||||
|
||||
private readonly IList<InnerRequest> allRequests = new List<InnerRequest>();
|
||||
|
||||
private readonly string batchUrl;
|
||||
private readonly IClientService service;
|
||||
|
||||
// For testing
|
||||
internal string BatchUrl => batchUrl;
|
||||
|
||||
/// <summary>A concrete type callback for an individual response.</summary>
|
||||
/// <typeparam name="TResponse">The response type.</typeparam>
|
||||
/// <param name="content">The content response or <c>null</c> if the request failed.</param>
|
||||
/// <param name="error">Error or <c>null</c> if the request succeeded.</param>
|
||||
/// <param name="index">The request index.</param>
|
||||
/// <param name="message">The HTTP individual response.</param>
|
||||
public delegate void OnResponse<in TResponse>
|
||||
(TResponse content, RequestError error, int index, HttpResponseMessage message) where TResponse : class;
|
||||
|
||||
#region Inner Request
|
||||
|
||||
/// <summary>This inner class represents an individual inner request.</summary>
|
||||
private class InnerRequest
|
||||
{
|
||||
/// <summary>Gets or sets the client service request.</summary>
|
||||
public IClientServiceRequest ClientRequest { get; set; }
|
||||
|
||||
/// <summary>Gets or sets the response class type.</summary>
|
||||
public Type ResponseType { get; set; }
|
||||
|
||||
/// <summary>A callback method which will be called after an individual response was parsed.</summary>
|
||||
/// <param name="content">The content response or <c>null</c> if the request failed.</param>
|
||||
/// <param name="error">Error or <c>null</c> if the request succeeded.</param>
|
||||
/// <param name="index">The request index.</param>
|
||||
/// <param name="message">The HTTP individual response.</param>
|
||||
public virtual void OnResponse(object content, RequestError error, int index, HttpResponseMessage message)
|
||||
{
|
||||
// Set ETag on the response.
|
||||
var eTagValue = message.Headers.ETag != null ? message.Headers.ETag.Tag : null;
|
||||
var eTagContainer = content as IDirectResponseSchema;
|
||||
if (eTagContainer != null && eTagContainer.ETag == null && eTagValue != null)
|
||||
{
|
||||
eTagContainer.ETag = eTagValue;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// This generic inner class represents an individual inner request with a generic response type.
|
||||
/// </summary>
|
||||
private class InnerRequest<TResponse> : InnerRequest
|
||||
where TResponse : class
|
||||
{
|
||||
/// <summary>Gets or sets a concrete type callback for an individual response. </summary>
|
||||
public OnResponse<TResponse> OnResponseCallback { get; set; }
|
||||
|
||||
public override void OnResponse(object content, RequestError error, int index,
|
||||
HttpResponseMessage message)
|
||||
{
|
||||
base.OnResponse(content, error, index, message);
|
||||
if (OnResponseCallback == null)
|
||||
return;
|
||||
|
||||
OnResponseCallback(content as TResponse, error, index, message);
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
/// <summary>
|
||||
/// Constructs a new batch request using the given service. See
|
||||
/// <see cref="BatchRequest(IClientService, string)"/> for more information.
|
||||
/// </summary>
|
||||
public BatchRequest(IClientService service)
|
||||
: this(service, (service as BaseClientService)?.BatchUri ?? DefaultBatchUrl) { }
|
||||
|
||||
/// <summary>
|
||||
/// Constructs a new batch request using the given service. The service's HTTP client is used to create a
|
||||
/// request to the given server URL and its serializer members are used to serialize the request and
|
||||
/// deserialize the response.
|
||||
/// </summary>
|
||||
public BatchRequest(IClientService service, string batchUrl)
|
||||
{
|
||||
this.batchUrl = batchUrl;
|
||||
this.service = service;
|
||||
}
|
||||
|
||||
/// <summary>Gets the count of all queued requests.</summary>
|
||||
public int Count
|
||||
{
|
||||
get { return allRequests.Count; }
|
||||
}
|
||||
|
||||
/// <summary>Queues an individual request.</summary>
|
||||
/// <typeparam name="TResponse">The response's type.</typeparam>
|
||||
/// <param name="request">The individual request.</param>
|
||||
/// <param name="callback">A callback which will be called after a response was parsed.</param>
|
||||
public void Queue<TResponse>(IClientServiceRequest request, OnResponse<TResponse> callback)
|
||||
where TResponse : class
|
||||
{
|
||||
if (Count > QueueLimit)
|
||||
{
|
||||
throw new InvalidOperationException("A batch request cannot contain more than 1000 single requests");
|
||||
}
|
||||
|
||||
allRequests.Add(new InnerRequest<TResponse>
|
||||
{
|
||||
ClientRequest = request,
|
||||
ResponseType = typeof(TResponse),
|
||||
OnResponseCallback = callback,
|
||||
});
|
||||
}
|
||||
|
||||
/// <summary>Asynchronously executes the batch request.</summary>
|
||||
public Task ExecuteAsync()
|
||||
{
|
||||
return ExecuteAsync(CancellationToken.None);
|
||||
}
|
||||
|
||||
/// <summary>Asynchronously executes the batch request.</summary>
|
||||
/// <param name="cancellationToken">Cancellation token to cancel operation.</param>
|
||||
public async Task ExecuteAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
if (Count < 1)
|
||||
return;
|
||||
|
||||
ConfigurableHttpClient httpClient = service.HttpClient;
|
||||
|
||||
var requests = from r in allRequests
|
||||
select r.ClientRequest;
|
||||
HttpContent outerContent = await CreateOuterRequestContent(requests).ConfigureAwait(false);
|
||||
var result = await httpClient.PostAsync(new Uri(batchUrl), outerContent, cancellationToken)
|
||||
.ConfigureAwait(false);
|
||||
|
||||
result.EnsureSuccessStatusCode();
|
||||
|
||||
// Get the boundary separator.
|
||||
const string boundaryKey = "boundary=";
|
||||
var fullContent = await result.Content.ReadAsStringAsync().ConfigureAwait(false);
|
||||
var contentType = result.Content.Headers.GetValues("Content-Type").First();
|
||||
var boundary = contentType.Substring(contentType.IndexOf(boundaryKey) + boundaryKey.Length);
|
||||
|
||||
int requestIndex = 0;
|
||||
// While there is still content to read, parse the current HTTP response.
|
||||
while (true)
|
||||
{
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
|
||||
var startIndex = fullContent.IndexOf("--" + boundary);
|
||||
if (startIndex == -1)
|
||||
{
|
||||
break;
|
||||
}
|
||||
fullContent = fullContent.Substring(startIndex + boundary.Length + 2);
|
||||
var endIndex = fullContent.IndexOf("--" + boundary);
|
||||
if (endIndex == -1)
|
||||
{
|
||||
break;
|
||||
}
|
||||
|
||||
HttpResponseMessage responseMessage = ParseAsHttpResponse(fullContent.Substring(0, endIndex));
|
||||
|
||||
if (responseMessage.IsSuccessStatusCode)
|
||||
{
|
||||
// Parse the current content object.
|
||||
var responseContent = await responseMessage.Content.ReadAsStringAsync().ConfigureAwait(false);
|
||||
var content = service.Serializer.Deserialize(responseContent,
|
||||
allRequests[requestIndex].ResponseType);
|
||||
|
||||
allRequests[requestIndex].OnResponse(content, null, requestIndex, responseMessage);
|
||||
}
|
||||
else
|
||||
{
|
||||
// Parse the error from the current response.
|
||||
var error = await service.DeserializeError(responseMessage).ConfigureAwait(false);
|
||||
allRequests[requestIndex].OnResponse(null, error, requestIndex, responseMessage);
|
||||
}
|
||||
|
||||
requestIndex++;
|
||||
fullContent = fullContent.Substring(endIndex);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Parses the given string content to a HTTP response.</summary>
|
||||
[VisibleForTestOnly]
|
||||
internal static HttpResponseMessage ParseAsHttpResponse(string content)
|
||||
{
|
||||
var response = new HttpResponseMessage();
|
||||
|
||||
using (var reader = new StringReader(content))
|
||||
{
|
||||
string line = reader.ReadLine();
|
||||
|
||||
// Extract empty lines.
|
||||
while (string.IsNullOrEmpty(line))
|
||||
line = reader.ReadLine();
|
||||
|
||||
// Extract the outer header.
|
||||
while (!string.IsNullOrEmpty(line))
|
||||
line = reader.ReadLine();
|
||||
|
||||
// Extract the status code.
|
||||
line = reader.ReadLine();
|
||||
while (string.IsNullOrEmpty(line))
|
||||
line = reader.ReadLine();
|
||||
int code = int.Parse(line.Split(' ')[1]);
|
||||
response.StatusCode = (HttpStatusCode)code;
|
||||
|
||||
// Extract the headers.
|
||||
IDictionary<string, string> headersDic = new Dictionary<string, string>();
|
||||
while (!string.IsNullOrEmpty((line = reader.ReadLine())))
|
||||
{
|
||||
var separatorIndex = line.IndexOf(':');
|
||||
var key = line.Substring(0, separatorIndex).Trim();
|
||||
var value = line.Substring(separatorIndex + 1).Trim();
|
||||
// Check if the header already exists, and if so append its value
|
||||
// to the existing value. Fixes issue #548.
|
||||
if (headersDic.ContainsKey(key)) {
|
||||
headersDic[key] = headersDic[key] + ", " + value;
|
||||
} else {
|
||||
headersDic.Add(key, value);
|
||||
}
|
||||
}
|
||||
|
||||
// Set the content.
|
||||
string mediaType = null;
|
||||
if (headersDic.ContainsKey("Content-Type"))
|
||||
{
|
||||
mediaType = headersDic["Content-Type"].Split(';', ' ')[0];
|
||||
headersDic.Remove("Content-Type");
|
||||
}
|
||||
response.Content = new StringContent(reader.ReadToEnd(), Encoding.UTF8, mediaType);
|
||||
|
||||
// Add the headers to the response.
|
||||
foreach (var keyValue in headersDic)
|
||||
{
|
||||
HttpHeaders headers = response.Headers;
|
||||
// Check if we need to add the current header to the content headers.
|
||||
if (typeof(HttpContentHeaders).GetProperty(keyValue.Key.Replace("-", "")) != null)
|
||||
{
|
||||
headers = response.Content.Headers;
|
||||
}
|
||||
|
||||
// Use TryAddWithoutValidation rather than Add because Mono's validation is
|
||||
// improperly strict. https://bugzilla.xamarin.com/show_bug.cgi?id=39569
|
||||
if (!headers.TryAddWithoutValidation(keyValue.Key, keyValue.Value))
|
||||
{
|
||||
throw new FormatException(String.Format(
|
||||
"Could not parse header {0} from batch reply", keyValue.Key));
|
||||
}
|
||||
}
|
||||
|
||||
// TODO(peleyal): ContentLength header is x while the "real" content that we read from the stream is
|
||||
// Content.ReadStringAsAsync().Length is x+2
|
||||
}
|
||||
|
||||
return response;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates the batch outer request content which includes all the individual requests to Google servers.
|
||||
/// </summary>
|
||||
[VisibleForTestOnly]
|
||||
internal async static Task<HttpContent> CreateOuterRequestContent(IEnumerable<IClientServiceRequest> requests)
|
||||
{
|
||||
var mixedContent = new MultipartContent("mixed");
|
||||
foreach (var request in requests)
|
||||
{
|
||||
mixedContent.Add(await CreateIndividualRequest(request).ConfigureAwait(false));
|
||||
}
|
||||
|
||||
// Batch request currently doesn't support GZip. Uncomment when the issue will be resolved.
|
||||
// https://code.google.com/p/google-api-dotnet-client/issues/detail?id=409
|
||||
/*if (service.GZipEnabled)
|
||||
{
|
||||
var content = HttpServiceExtenstions.CreateZipContent(await mixedContent.ReadAsStringAsync()
|
||||
.ConfigureAwait(false));
|
||||
content.Headers.ContentType = mixedContent.Headers.ContentType;
|
||||
return content;
|
||||
}*/
|
||||
return mixedContent;
|
||||
|
||||
}
|
||||
|
||||
/// <summary>Creates the individual server request.</summary>
|
||||
[VisibleForTestOnly]
|
||||
internal static async Task<HttpContent> CreateIndividualRequest(IClientServiceRequest request)
|
||||
{
|
||||
HttpRequestMessage requestMessage = request.CreateRequest(false);
|
||||
string requestContent = await CreateRequestContentString(requestMessage).ConfigureAwait(false);
|
||||
|
||||
var content = new StringContent(requestContent);
|
||||
content.Headers.ContentType = new System.Net.Http.Headers.MediaTypeHeaderValue("application/http");
|
||||
return content;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates a string representation that includes the request's headers and content based on the input HTTP
|
||||
/// request message.
|
||||
/// </summary>
|
||||
[VisibleForTestOnly]
|
||||
internal static async Task<string> CreateRequestContentString(HttpRequestMessage requestMessage)
|
||||
{
|
||||
var sb = new StringBuilder();
|
||||
sb.AppendFormat("{0} {1}", requestMessage.Method, requestMessage.RequestUri.AbsoluteUri);
|
||||
|
||||
// Add Headers.
|
||||
foreach (var otherHeader in requestMessage.Headers)
|
||||
{
|
||||
sb.Append(Environment.NewLine)
|
||||
.AppendFormat(("{0}: {1}"), otherHeader.Key, String.Join(", ", otherHeader.Value.ToArray()));
|
||||
}
|
||||
|
||||
// Add content headers.
|
||||
if (requestMessage.Content != null)
|
||||
{
|
||||
foreach (var contentHeader in requestMessage.Content.Headers)
|
||||
{
|
||||
sb.Append(Environment.NewLine)
|
||||
.AppendFormat("{0}: {1}", contentHeader.Key, String.Join(", ", contentHeader.Value.ToArray()));
|
||||
}
|
||||
}
|
||||
|
||||
// Content.
|
||||
if (requestMessage.Content != null)
|
||||
{
|
||||
sb.Append(Environment.NewLine);
|
||||
var content = await requestMessage.Content.ReadAsStringAsync().ConfigureAwait(false);
|
||||
sb.Append("Content-Length: ").Append(content.Length);
|
||||
|
||||
sb.Append(Environment.NewLine).Append(Environment.NewLine).Append(content);
|
||||
}
|
||||
|
||||
return sb.Append(Environment.NewLine).ToString();
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,366 @@
|
||||
/*
|
||||
Copyright 2011 Google Inc
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Net.Http;
|
||||
using System.Net.Http.Headers;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
using Google.Apis.Discovery;
|
||||
using Google.Apis.Http;
|
||||
using Google.Apis.Logging;
|
||||
using Google.Apis.Services;
|
||||
using Google.Apis.Testing;
|
||||
using Google.Apis.Util;
|
||||
using Google.Apis.Requests.Parameters;
|
||||
|
||||
namespace Google.Apis.Requests
|
||||
{
|
||||
/// <summary>
|
||||
/// Represents an abstract, strongly typed request base class to make requests to a service.
|
||||
/// Supports a strongly typed response.
|
||||
/// </summary>
|
||||
/// <typeparam name="TResponse">The type of the response object</typeparam>
|
||||
public abstract class ClientServiceRequest<TResponse> : IClientServiceRequest<TResponse>
|
||||
{
|
||||
/// <summary>The class logger.</summary>
|
||||
private static readonly ILogger Logger = ApplicationContext.Logger.ForType<ClientServiceRequest<TResponse>>();
|
||||
|
||||
/// <summary>The service on which this request will be executed.</summary>
|
||||
private readonly IClientService service;
|
||||
|
||||
/// <summary>Defines whether the E-Tag will be used in a specified way or be ignored.</summary>
|
||||
public ETagAction ETagAction { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the callback for modifying HTTP requests made by this service request.
|
||||
/// </summary>
|
||||
public Action<HttpRequestMessage> ModifyRequest { get; set; }
|
||||
|
||||
#region IClientServiceRequest Properties
|
||||
|
||||
/// <inheritdoc/>
|
||||
public abstract string MethodName { get; }
|
||||
|
||||
/// <inheritdoc/>
|
||||
public abstract string RestPath { get; }
|
||||
|
||||
/// <inheritdoc/>
|
||||
public abstract string HttpMethod { get; }
|
||||
|
||||
/// <inheritdoc/>
|
||||
public IDictionary<string, IParameter> RequestParameters { get; private set; }
|
||||
|
||||
/// <inheritdoc/>
|
||||
public IClientService Service
|
||||
{
|
||||
get { return service; }
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
/// <summary>Creates a new service request.</summary>
|
||||
protected ClientServiceRequest(IClientService service)
|
||||
{
|
||||
this.service = service;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initializes request's parameters. Inherited classes MUST override this method to add parameters to the
|
||||
/// <see cref="RequestParameters"/> dictionary.
|
||||
/// </summary>
|
||||
protected virtual void InitParameters()
|
||||
{
|
||||
RequestParameters = new Dictionary<string, IParameter>();
|
||||
}
|
||||
|
||||
#region Execution
|
||||
|
||||
/// <inheritdoc/>
|
||||
public TResponse Execute()
|
||||
{
|
||||
try
|
||||
{
|
||||
using (var response = ExecuteUnparsedAsync(CancellationToken.None).Result)
|
||||
{
|
||||
return ParseResponse(response).Result;
|
||||
}
|
||||
}
|
||||
catch (AggregateException aex)
|
||||
{
|
||||
// If an exception was thrown during the tasks, unwrap and throw it.
|
||||
throw aex.InnerException;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
throw ex;
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public Stream ExecuteAsStream()
|
||||
{
|
||||
// TODO(peleyal): should we copy the stream, and dispose the response?
|
||||
try
|
||||
{
|
||||
// Sync call.
|
||||
var response = ExecuteUnparsedAsync(CancellationToken.None).Result;
|
||||
return response.Content.ReadAsStreamAsync().Result;
|
||||
}
|
||||
catch (AggregateException aex)
|
||||
{
|
||||
// If an exception was thrown during the tasks, unwrap and throw it.
|
||||
throw aex.InnerException;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
throw ex;
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public async Task<TResponse> ExecuteAsync()
|
||||
{
|
||||
return await ExecuteAsync(CancellationToken.None).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public async Task<TResponse> ExecuteAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
using (var response = await ExecuteUnparsedAsync(cancellationToken).ConfigureAwait(false))
|
||||
{
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
return await ParseResponse(response).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public async Task<Stream> ExecuteAsStreamAsync()
|
||||
{
|
||||
return await ExecuteAsStreamAsync(CancellationToken.None).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public async Task<Stream> ExecuteAsStreamAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
// TODO(peleyal): should we copy the stream, and dispose the response?
|
||||
var response = await ExecuteUnparsedAsync(cancellationToken).ConfigureAwait(false);
|
||||
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
return await response.Content.ReadAsStreamAsync().ConfigureAwait(false);
|
||||
}
|
||||
|
||||
#region Helpers
|
||||
|
||||
/// <summary>Sync executes the request without parsing the result. </summary>
|
||||
private async Task<HttpResponseMessage> ExecuteUnparsedAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
using (var request = CreateRequest())
|
||||
{
|
||||
return await service.HttpClient.SendAsync(request, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Parses the response and deserialize the content into the requested response object. </summary>
|
||||
private async Task<TResponse> ParseResponse(HttpResponseMessage response)
|
||||
{
|
||||
if (response.IsSuccessStatusCode)
|
||||
{
|
||||
return await service.DeserializeResponse<TResponse>(response).ConfigureAwait(false);
|
||||
}
|
||||
var error = await service.DeserializeError(response).ConfigureAwait(false);
|
||||
throw new GoogleApiException(service.Name, error.ToString())
|
||||
{
|
||||
Error = error,
|
||||
HttpStatusCode = response.StatusCode
|
||||
};
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#endregion
|
||||
|
||||
/// <inheritdoc/>
|
||||
public HttpRequestMessage CreateRequest(Nullable<bool> overrideGZipEnabled = null)
|
||||
{
|
||||
var builder = CreateBuilder();
|
||||
var request = builder.CreateRequest();
|
||||
object body = GetBody();
|
||||
request.SetRequestSerailizedContent(service, body, overrideGZipEnabled.HasValue
|
||||
? overrideGZipEnabled.Value : service.GZipEnabled);
|
||||
AddETag(request);
|
||||
ModifyRequest?.Invoke(request);
|
||||
return request;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates the <see cref="Google.Apis.Requests.RequestBuilder"/> which is used to generate a request.
|
||||
/// </summary>
|
||||
/// <returns>
|
||||
/// A new builder instance which contains the HTTP method and the right Uri with its path and query parameters.
|
||||
/// </returns>
|
||||
private RequestBuilder CreateBuilder()
|
||||
{
|
||||
var builder = new RequestBuilder()
|
||||
{
|
||||
BaseUri = new Uri(Service.BaseUri),
|
||||
Path = RestPath,
|
||||
Method = HttpMethod,
|
||||
};
|
||||
|
||||
// Init parameters.
|
||||
if (service.ApiKey != null)
|
||||
{
|
||||
builder.AddParameter(RequestParameterType.Query, "key", service.ApiKey);
|
||||
}
|
||||
var parameters = ParameterUtils.CreateParameterDictionary(this);
|
||||
AddParameters(builder, ParameterCollection.FromDictionary(parameters));
|
||||
return builder;
|
||||
}
|
||||
|
||||
/// <summary>Generates the right URL for this request.</summary>
|
||||
protected string GenerateRequestUri()
|
||||
{
|
||||
return CreateBuilder().BuildUri().ToString();
|
||||
}
|
||||
|
||||
/// <summary>Returns the body of this request.</summary>
|
||||
/// <returns>The body of this request.</returns>
|
||||
protected virtual object GetBody()
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
#region ETag
|
||||
|
||||
/// <summary>
|
||||
/// Adds the right ETag action (e.g. If-Match) header to the given HTTP request if the body contains ETag.
|
||||
/// </summary>
|
||||
private void AddETag(HttpRequestMessage request)
|
||||
{
|
||||
IDirectResponseSchema body = GetBody() as IDirectResponseSchema;
|
||||
if (body != null && !string.IsNullOrEmpty(body.ETag))
|
||||
{
|
||||
var etag = body.ETag;
|
||||
ETagAction action = ETagAction == ETagAction.Default ? GetDefaultETagAction(HttpMethod) : ETagAction;
|
||||
try
|
||||
{
|
||||
switch (action)
|
||||
{
|
||||
case ETagAction.IfMatch:
|
||||
request.Headers.IfMatch.Add(new EntityTagHeaderValue(etag));
|
||||
break;
|
||||
case ETagAction.IfNoneMatch:
|
||||
request.Headers.IfNoneMatch.Add(new EntityTagHeaderValue(etag));
|
||||
break;
|
||||
}
|
||||
}
|
||||
// When ETag is invalid we are going to create a request anyway.
|
||||
// See https://code.google.com/p/google-api-dotnet-client/issues/detail?id=464 for more details.
|
||||
catch (FormatException ex)
|
||||
{
|
||||
Logger.Error(ex, "Can't set {0}. Etag is: {1}.", action, etag);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Returns the default ETagAction for a specific HTTP verb.</summary>
|
||||
[VisibleForTestOnly]
|
||||
public static ETagAction GetDefaultETagAction(string httpMethod)
|
||||
{
|
||||
switch (httpMethod)
|
||||
{
|
||||
// Incoming data should only be updated if it has been changed on the server.
|
||||
case HttpConsts.Get:
|
||||
return ETagAction.IfNoneMatch;
|
||||
|
||||
// Outgoing data should only be committed if it hasn't been changed on the server.
|
||||
case HttpConsts.Put:
|
||||
case HttpConsts.Post:
|
||||
case HttpConsts.Patch:
|
||||
case HttpConsts.Delete:
|
||||
return ETagAction.IfMatch;
|
||||
|
||||
default:
|
||||
return ETagAction.Ignore;
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Parameters
|
||||
|
||||
/// <summary>Adds path and query parameters to the given <c>requestBuilder</c>.</summary>
|
||||
private void AddParameters(RequestBuilder requestBuilder, ParameterCollection inputParameters)
|
||||
{
|
||||
foreach (var parameter in inputParameters)
|
||||
{
|
||||
IParameter parameterDefinition;
|
||||
|
||||
if (!RequestParameters.TryGetValue(parameter.Key, out parameterDefinition))
|
||||
{
|
||||
throw new GoogleApiException(Service.Name,
|
||||
String.Format("Invalid parameter \"{0}\" was specified", parameter.Key));
|
||||
}
|
||||
|
||||
string value = parameter.Value;
|
||||
if (!ParameterValidator.ValidateParameter(parameterDefinition, value))
|
||||
{
|
||||
throw new GoogleApiException(Service.Name,
|
||||
string.Format("Parameter validation failed for \"{0}\"", parameterDefinition.Name));
|
||||
}
|
||||
|
||||
if (value == null) // If the parameter is null, use the default value.
|
||||
{
|
||||
value = parameterDefinition.DefaultValue;
|
||||
}
|
||||
|
||||
switch (parameterDefinition.ParameterType)
|
||||
{
|
||||
case "path":
|
||||
requestBuilder.AddParameter(RequestParameterType.Path, parameter.Key, value);
|
||||
break;
|
||||
case "query":
|
||||
// If the parameter is optional and no value is given, don't add to url.
|
||||
if (!Object.Equals(value, parameterDefinition.DefaultValue) || parameterDefinition.IsRequired)
|
||||
{
|
||||
requestBuilder.AddParameter(RequestParameterType.Query, parameter.Key, value);
|
||||
}
|
||||
break;
|
||||
default:
|
||||
throw new GoogleApiException(service.Name,
|
||||
string.Format("Unsupported parameter type \"{0}\" for \"{1}\"",
|
||||
parameterDefinition.ParameterType, parameterDefinition.Name));
|
||||
}
|
||||
}
|
||||
|
||||
// Check if there is a required parameter which wasn't set.
|
||||
foreach (var parameter in RequestParameters.Values)
|
||||
{
|
||||
if (parameter.IsRequired && !inputParameters.ContainsKey(parameter.Name))
|
||||
{
|
||||
throw new GoogleApiException(service.Name,
|
||||
string.Format("Parameter \"{0}\" is missing", parameter.Name));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
}
|
@ -0,0 +1,97 @@
|
||||
/*
|
||||
Copyright 2013 Google Inc
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
using System.IO;
|
||||
using System.Net.Http;
|
||||
using System.Net.Http.Headers;
|
||||
using System.Text;
|
||||
|
||||
using System.IO.Compression;
|
||||
|
||||
using Google.Apis.Services;
|
||||
|
||||
namespace Google.Apis.Requests
|
||||
{
|
||||
/// <summary>Extension methods to <see cref="System.Net.Http.HttpRequestMessage"/>.</summary>
|
||||
static class HttpRequestMessageExtenstions
|
||||
{
|
||||
/// <summary>
|
||||
/// Sets the content of the request by the given body and the the required GZip configuration.
|
||||
/// </summary>
|
||||
/// <param name="request">The request.</param>
|
||||
/// <param name="service">The service.</param>
|
||||
/// <param name="body">The body of the future request. If <c>null</c> do nothing.</param>
|
||||
/// <param name="gzipEnabled">
|
||||
/// Indicates if the content will be wrapped in a GZip stream, or a regular string stream will be used.
|
||||
/// </param>
|
||||
internal static void SetRequestSerailizedContent(this HttpRequestMessage request,
|
||||
IClientService service, object body, bool gzipEnabled)
|
||||
{
|
||||
if (body == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
HttpContent content = null;
|
||||
|
||||
var mediaType = "application/" + service.Serializer.Format;
|
||||
var serializedObject = service.SerializeObject(body);
|
||||
if (gzipEnabled)
|
||||
{
|
||||
content = CreateZipContent(serializedObject);
|
||||
content.Headers.ContentType = new MediaTypeHeaderValue(mediaType)
|
||||
{
|
||||
CharSet = Encoding.UTF8.WebName
|
||||
};
|
||||
}
|
||||
else
|
||||
{
|
||||
content = new StringContent(serializedObject, Encoding.UTF8, mediaType);
|
||||
}
|
||||
|
||||
request.Content = content;
|
||||
}
|
||||
|
||||
/// <summary>Creates a GZip content based on the given content.</summary>
|
||||
/// <param name="content">Content to GZip.</param>
|
||||
/// <returns>GZiped HTTP content.</returns>
|
||||
internal static HttpContent CreateZipContent(string content)
|
||||
{
|
||||
var stream = CreateGZipStream(content);
|
||||
var sc = new StreamContent(stream);
|
||||
sc.Headers.ContentEncoding.Add("gzip");
|
||||
return sc;
|
||||
}
|
||||
|
||||
/// <summary>Creates a GZip stream by the given serialized object.</summary>
|
||||
private static Stream CreateGZipStream(string serializedObject)
|
||||
{
|
||||
byte[] bytes = System.Text.Encoding.UTF8.GetBytes(serializedObject);
|
||||
using (System.IO.MemoryStream ms = new System.IO.MemoryStream())
|
||||
{
|
||||
using (GZipStream gzip = new GZipStream(ms, CompressionMode.Compress, true))
|
||||
{
|
||||
gzip.Write(bytes, 0, bytes.Length);
|
||||
}
|
||||
|
||||
// Reset the stream to the beginning. It doesn't work otherwise!
|
||||
ms.Position = 0;
|
||||
byte[] compressed = new byte[ms.Length];
|
||||
ms.Read(compressed, 0, compressed.Length);
|
||||
return new MemoryStream(compressed);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user