Google Api
This commit is contained in:
parent
e307811d89
commit
83f9fc1bd8
123 changed files with 12961 additions and 60 deletions
|
|
@ -9,6 +9,7 @@ using Microsoft.Data.Entity;
|
||||||
using Yavsc.Models;
|
using Yavsc.Models;
|
||||||
using Yavsc.Models.Workflow;
|
using Yavsc.Models.Workflow;
|
||||||
|
|
||||||
|
|
||||||
namespace Yavsc.Controllers
|
namespace Yavsc.Controllers
|
||||||
{
|
{
|
||||||
[Produces("application/json")]
|
[Produces("application/json")]
|
||||||
|
|
|
||||||
|
|
@ -120,6 +120,8 @@ namespace Yavsc.Controllers
|
||||||
}
|
}
|
||||||
public IActionResult Todo()
|
public IActionResult Todo()
|
||||||
{
|
{
|
||||||
|
User.GetUserId();
|
||||||
|
|
||||||
return View();
|
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
Loading…
Add table
Add a link
Reference in a new issue