diff --git a/Yavsc/ApiControllers/ActivityApiController.cs b/Yavsc/ApiControllers/ActivityApiController.cs index 0b6394d4..fea1771c 100644 --- a/Yavsc/ApiControllers/ActivityApiController.cs +++ b/Yavsc/ApiControllers/ActivityApiController.cs @@ -9,6 +9,7 @@ using Microsoft.Data.Entity; using Yavsc.Models; using Yavsc.Models.Workflow; + namespace Yavsc.Controllers { [Produces("application/json")] diff --git a/Yavsc/Controllers/HomeController.cs b/Yavsc/Controllers/HomeController.cs index 84692ad1..42b8ff65 100644 --- a/Yavsc/Controllers/HomeController.cs +++ b/Yavsc/Controllers/HomeController.cs @@ -120,6 +120,8 @@ namespace Yavsc.Controllers } public IActionResult Todo() { + User.GetUserId(); + return View(); } diff --git a/Yavsc/GoogleApiSupport/Google.Apis.Auth/GoogleJsonWebSignature.cs b/Yavsc/GoogleApiSupport/Google.Apis.Auth/GoogleJsonWebSignature.cs new file mode 100644 index 00000000..b1921499 --- /dev/null +++ b/Yavsc/GoogleApiSupport/Google.Apis.Auth/GoogleJsonWebSignature.cs @@ -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 +{ + /// + /// Google JSON Web Signature as specified in https://developers.google.com/accounts/docs/OAuth2ServiceAccount. + /// + 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 ValidJwtIssuers = new[] + { + "https://accounts.google.com", + "accounts.google.com" + }; + + private static readonly DateTime UnixEpoch = new DateTime(1970, 1, 1, 0, 0, 0, DateTimeKind.Utc); + + /// + /// Validates a Google-issued Json Web Token (JWT). + /// With throw a if the passed value is not valid JWT signed by Google. + /// + /// + /// Follows the procedure to + /// validate a JWT ID token. + /// + /// Google certificates are cached, and refreshed once per hour. This can be overridden by setting + /// to true. + /// + /// The JWT to validate. + /// Optional. The to use for JWT expiration verification. Defaults to the system clock. + /// Optional. If true forces new certificates to be downloaded from Google. Defaults to false. + /// The JWT payload, if the JWT is valid. Throws an otherwise. + /// Thrown when passed a JWT that is not a valid JWT signed by Google. + public static Task ValidateAsync(string jwt, IClock clock = null, bool forceGoogleCertRefresh = false) => + ValidateInternalAsync(jwt, clock ?? SystemClock.Default, forceGoogleCertRefresh, null); + + // internal for testing + internal static async Task 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
(Base64UrlToString(parts[0])); + Payload payload = NewtonsoftJsonSerializer.Instance.Deserialize(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 _certCache; + + // internal for testing + internal static async Task> 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 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(); + + /// + /// The header as specified in https://developers.google.com/accounts/docs/OAuth2ServiceAccount#formingheader. + /// + public class Header : JsonWebSignature.Header + { + } + + /// + /// The payload as specified in + /// https://developers.google.com/accounts/docs/OAuth2ServiceAccount#formingclaimset. + /// + public class Payload : JsonWebSignature.Payload + { + /// + /// a space-delimited list of the permissions the application requests or null. + /// + [Newtonsoft.Json.JsonPropertyAttribute("scope")] + public string Scope { get; set; } + + /// + /// The email address of the user for which the application is requesting delegated access. + /// + [Newtonsoft.Json.JsonPropertyAttribute("prn")] + public string Prn { get; set; } + } + } +} diff --git a/Yavsc/GoogleApiSupport/Google.Apis.Auth/InvalidJwtException.cs b/Yavsc/GoogleApiSupport/Google.Apis.Auth/InvalidJwtException.cs new file mode 100644 index 00000000..ef5879c2 --- /dev/null +++ b/Yavsc/GoogleApiSupport/Google.Apis.Auth/InvalidJwtException.cs @@ -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 +{ + /// + /// An exception that is thrown when a Json Web Token (JWT) is invalid. + /// + public class InvalidJwtException : Exception + { + /// + /// Initializes a new InvalidJwtException instanc e with the specified error message. + /// + /// The error message that explains why the JWT was invalid. + public InvalidJwtException(string message) : base(message) { } + } +} diff --git a/Yavsc/GoogleApiSupport/Google.Apis.Auth/JsonWebSignature.cs b/Yavsc/GoogleApiSupport/Google.Apis.Auth/JsonWebSignature.cs new file mode 100644 index 00000000..61e7fd2d --- /dev/null +++ b/Yavsc/GoogleApiSupport/Google.Apis.Auth/JsonWebSignature.cs @@ -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 +{ + /// + /// JSON Web Signature (JWS) implementation as specified in + /// http://tools.ietf.org/html/draft-ietf-jose-json-web-signature-11. + /// + public class JsonWebSignature + { + // TODO(peleyal): Implement some verify method: + // http://tools.ietf.org/html/draft-ietf-oauth-json-web-token-08#section-7 + + /// + /// Header as specified in http://tools.ietf.org/html/draft-ietf-jose-json-web-signature-11#section-4.1. + /// + public class Header : JsonWebToken.Header + { + /// + /// Gets or set the algorithm header parameter that identifies the cryptographic algorithm used to secure + /// the JWS or null. + /// + [Newtonsoft.Json.JsonPropertyAttribute("alg")] + public string Algorithm { get; set; } + + /// + /// 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 null. + /// + [Newtonsoft.Json.JsonPropertyAttribute("jku")] + public string JwkUrl { get; set; } + + /// + /// 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 null. + /// + [Newtonsoft.Json.JsonPropertyAttribute("jwk")] + public string Jwk { get; set; } + + /// + /// 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 null. + /// + [Newtonsoft.Json.JsonPropertyAttribute("kid")] + public string KeyId { get; set; } + + /// + /// 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 + /// null. + /// + [Newtonsoft.Json.JsonPropertyAttribute("x5u")] + public string X509Url { get; set; } + + /// + /// 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 null. + /// + [Newtonsoft.Json.JsonPropertyAttribute("x5t")] + public string X509Thumbprint { get; set; } + + /// + /// 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 null. + /// + [Newtonsoft.Json.JsonPropertyAttribute("x5c")] + public string X509Certificate { get; set; } + + /// + /// 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 null. + /// + [Newtonsoft.Json.JsonPropertyAttribute("crit")] + public IList critical { get; set; } + } + + /// JWS Payload. + public class Payload : JsonWebToken.Payload + { + } + } +} diff --git a/Yavsc/GoogleApiSupport/Google.Apis.Auth/JsonWebToken.cs b/Yavsc/GoogleApiSupport/Google.Apis.Auth/JsonWebToken.cs new file mode 100644 index 00000000..64b1a8f5 --- /dev/null +++ b/Yavsc/GoogleApiSupport/Google.Apis.Auth/JsonWebToken.cs @@ -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 +{ + /// + /// JSON Web Token (JWT) implementation as specified in + /// http://tools.ietf.org/html/draft-ietf-oauth-json-web-token-08. + /// + public class JsonWebToken + { + /// + /// JWT Header as specified in http://tools.ietf.org/html/draft-ietf-oauth-json-web-token-08#section-5. + /// + public class Header + { + /// + /// Gets or sets type header parameter used to declare the type of this object or null. + /// + [Newtonsoft.Json.JsonPropertyAttribute("typ")] + public string Type { get; set; } + + /// + /// Gets or sets content type header parameter used to declare structural information about the JWT or + /// null. + /// + [Newtonsoft.Json.JsonPropertyAttribute("cty")] + public string ContentType { get; set; } + } + + /// + /// JWT Payload as specified in http://tools.ietf.org/html/draft-ietf-oauth-json-web-token-08#section-4.1. + /// + public class Payload + { + /// + /// Gets or sets issuer claim that identifies the principal that issued the JWT or null. + /// + [Newtonsoft.Json.JsonPropertyAttribute("iss")] + public string Issuer { get; set; } + + /// + /// Gets or sets subject claim identifying the principal that is the subject of the JWT or null. + /// + [Newtonsoft.Json.JsonPropertyAttribute("sub")] + public string Subject { get; set; } + + /// + /// Gets or sets audience claim that identifies the audience that the JWT is intended for (should either be + /// a string or list) or null. + /// + [Newtonsoft.Json.JsonPropertyAttribute("aud")] + public object Audience { get; set; } + + /// + /// 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 null. + /// + [Newtonsoft.Json.JsonPropertyAttribute("exp")] + public long? ExpirationTimeSeconds { get; set; } + + /// + /// Gets or sets not before claim that identifies the time (in seconds) before which the token MUST NOT be + /// accepted for processing or null. + /// + [Newtonsoft.Json.JsonPropertyAttribute("nbf")] + public long? NotBeforeTimeSeconds { get; set; } + + /// + /// Gets or sets issued at claim that identifies the time (in seconds) at which the JWT was issued or + /// null. + /// + [Newtonsoft.Json.JsonPropertyAttribute("iat")] + public long? IssuedAtTimeSeconds { get; set; } + + /// + /// Gets or sets JWT ID claim that provides a unique identifier for the JWT or null. + /// + [Newtonsoft.Json.JsonPropertyAttribute("jti")] + public string JwtId { get; set; } + + /// + /// Gets or sets type claim that is used to declare a type for the contents of this JWT Claims Set or + /// null. + /// + [Newtonsoft.Json.JsonPropertyAttribute("typ")] + public string Type { get; set; } + + /// Gets the audience property as a list. + [Newtonsoft.Json.JsonIgnoreAttribute] + public IEnumerable AudienceAsList + { + get + { + var asList = Audience as List; + if (asList != null) + { + return asList; + } + var list = new List(); + var asString = Audience as string; + if (asString != null) + { + list.Add(asString); + } + + return list; + } + } + } + } +} \ No newline at end of file diff --git a/Yavsc/GoogleApiSupport/Google.Apis.Auth/OAuth2/AuthorizationCodeInstalledApp.cs b/Yavsc/GoogleApiSupport/Google.Apis.Auth/OAuth2/AuthorizationCodeInstalledApp.cs new file mode 100644 index 00000000..cab74391 --- /dev/null +++ b/Yavsc/GoogleApiSupport/Google.Apis.Auth/OAuth2/AuthorizationCodeInstalledApp.cs @@ -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 +{ + /// + /// Thread-safe OAuth 2.0 authorization code flow for an installed application that persists end-user credentials. + /// + /// + /// Incremental authorization (https://developers.google.com/+/web/api/rest/oauth) is currently not supported + /// for Installed Apps. + /// + public class AuthorizationCodeInstalledApp : IAuthorizationCodeInstalledApp + { + private static readonly ILogger Logger = ApplicationContext.Logger.ForType(); + + private readonly IAuthorizationCodeFlow flow; + private readonly ICodeReceiver codeReceiver; + + /// + /// Constructs a new authorization code installed application with the given flow and code receiver. + /// + public AuthorizationCodeInstalledApp(IAuthorizationCodeFlow flow, ICodeReceiver codeReceiver) + { + this.flow = flow; + this.codeReceiver = codeReceiver; + } + + #region IAuthorizationCodeInstalledApp Members + + /// Gets the authorization code flow. + public IAuthorizationCodeFlow Flow + { + get { return flow; } + } + + /// Gets the code receiver which is responsible for receiving the authorization code. + public ICodeReceiver CodeReceiver + { + get { return codeReceiver; } + } + + /// + public async Task 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); + } + + /// + /// Determines the need for retrieval of a new authorization code, based on the given token and the + /// authorization code flow. + /// + 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 + } +} \ No newline at end of file diff --git a/Yavsc/GoogleApiSupport/Google.Apis.Auth/OAuth2/BearerToken.cs b/Yavsc/GoogleApiSupport/Google.Apis.Auth/OAuth2/BearerToken.cs new file mode 100644 index 00000000..5507188d --- /dev/null +++ b/Yavsc/GoogleApiSupport/Google.Apis.Auth/OAuth2/BearerToken.cs @@ -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 +{ + /// + /// OAuth 2.0 helper for accessing protected resources using the Bearer token as specified in + /// http://tools.ietf.org/html/rfc6750. + /// + public class BearerToken + { + /// + /// 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. + /// + public class AuthorizationHeaderAccessMethod : IAccessMethod + { + const string Schema = "Bearer"; + + /// + public void Intercept(HttpRequestMessage request, string accessToken) + { + request.Headers.Authorization = new AuthenticationHeaderValue(Schema, accessToken); + } + + /// + public string GetAccessToken(HttpRequestMessage request) + { + if (request.Headers.Authorization != null && request.Headers.Authorization.Scheme == Schema) + { + return request.Headers.Authorization.Parameter; + } + return null; + } + } + + /// + /// Thread-safe OAuth 2.0 method for accessing protected resources using an access_token query parameter + /// as specified in http://tools.ietf.org/html/rfc6750#section-2.3. + /// + public class QueryParameterAccessMethod : IAccessMethod + { + const string AccessTokenKey = "access_token"; + + /// + 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))); + } + + /// + 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; + } + } + } +} diff --git a/Yavsc/GoogleApiSupport/Google.Apis.Auth/OAuth2/ClientSecrets.cs b/Yavsc/GoogleApiSupport/Google.Apis.Auth/OAuth2/ClientSecrets.cs new file mode 100644 index 00000000..f3afa5b2 --- /dev/null +++ b/Yavsc/GoogleApiSupport/Google.Apis.Auth/OAuth2/ClientSecrets.cs @@ -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 +{ + /// Client credential details for installed and web applications. + public sealed class ClientSecrets + { + /// Gets or sets the client identifier. + [Newtonsoft.Json.JsonProperty("client_id")] + public string ClientId { get; set; } + + /// Gets or sets the client Secret. + [Newtonsoft.Json.JsonProperty("client_secret")] + public string ClientSecret { get; set; } + } +} diff --git a/Yavsc/GoogleApiSupport/Google.Apis.Auth/OAuth2/ComputeCredential.cs b/Yavsc/GoogleApiSupport/Google.Apis.Auth/OAuth2/ComputeCredential.cs new file mode 100644 index 00000000..30728348 --- /dev/null +++ b/Yavsc/GoogleApiSupport/Google.Apis.Auth/OAuth2/ComputeCredential.cs @@ -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 +{ + /// + /// 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. + /// + /// More details about Compute Engine authentication is available at: + /// https://cloud.google.com/compute/docs/authentication. + /// + /// + public class ComputeCredential : ServiceCredential + { + /// The metadata server url. + public const string MetadataServerUrl = "http://metadata.google.internal"; + + /// Caches result from first call to IsRunningOnComputeEngine + private readonly static Lazy> isRunningOnComputeEngineCached = new Lazy>( + () => IsRunningOnComputeEngineNoCache()); + + /// + /// Experimentally, 200ms was found to be 99.9999% reliable. + /// This is a conservative timeout to minimize hanging on some troublesome network. + /// + private const int MetadataServerPingTimeoutInMilliseconds = 1000; + + /// The Metadata flavor header name. + private const string MetadataFlavor = "Metadata-Flavor"; + + /// The Metadata header response indicating Google. + 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."; + + /// + /// An initializer class for the Compute credential. It uses + /// as the token server URL. + /// + new public class Initializer : ServiceCredential.Initializer + { + /// Constructs a new initializer using the default compute token URL. + public Initializer() + : this(GoogleAuthConsts.ComputeTokenUrl) {} + + /// Constructs a new initializer using the given token URL. + public Initializer(string tokenUrl) + : base(tokenUrl) {} + } + + /// Constructs a new Compute credential instance. + public ComputeCredential() : this(new Initializer()) { } + + /// Constructs a new Compute credential instance. + public ComputeCredential(Initializer initializer) : base(initializer) { } + + #region ServiceCredential overrides + + /// + public override async Task 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 + + /// + /// 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. + /// + public static Task IsRunningOnComputeEngine() + { + return isRunningOnComputeEngineCached.Value; + } + + private static async Task 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 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; + } + } + } +} diff --git a/Yavsc/GoogleApiSupport/Google.Apis.Auth/OAuth2/DefaultCredentialProvider.cs b/Yavsc/GoogleApiSupport/Google.Apis.Auth/OAuth2/DefaultCredentialProvider.cs new file mode 100644 index 00000000..3240a36d --- /dev/null +++ b/Yavsc/GoogleApiSupport/Google.Apis.Auth/OAuth2/DefaultCredentialProvider.cs @@ -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. + + /// + /// 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. + /// + internal class DefaultCredentialProvider + { + private static readonly ILogger Logger = ApplicationContext.Logger.ForType(); + + /// + /// Environment variable override which stores the default application credentials file path. + /// + public const string CredentialEnvironmentVariable = "GOOGLE_APPLICATION_CREDENTIALS"; + + /// Well known file which stores the default application credentials. + private const string WellKnownCredentialsFile = "application_default_credentials.json"; + + /// Environment variable which contains the Application Data settings. + private const string AppdataEnvironmentVariable = "APPDATA"; + + /// Environment variable which contains the location of home directory on UNIX systems. + private const string HomeEnvironmentVariable = "HOME"; + + /// GCloud configuration directory in Windows, relative to %APPDATA%. + private const string CloudSDKConfigDirectoryWindows = "gcloud"; + + /// Help link to the application default credentials feature. + private const string HelpPermalink = + "https://developers.google.com/accounts/docs/application-default-credentials"; + + /// GCloud configuration directory on Linux/Mac, relative to $HOME. + private static readonly string CloudSDKConfigDirectoryUnix = Path.Combine(".config", "gcloud"); + + /// Caches result from first call to GetApplicationDefaultCredentialAsync + private readonly Lazy> cachedCredentialTask; + + /// Constructs a new default credential provider. + public DefaultCredentialProvider() + { + cachedCredentialTask = new Lazy>(CreateDefaultCredentialAsync); + } + + /// + /// Returns the Application Default Credentials. Subsequent invocations return cached value from + /// first invocation. + /// See for details. + /// + public Task GetDefaultCredentialAsync() + { + return cachedCredentialTask.Value; + } + + /// Creates a new default credential. + private async Task 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)); + } + + /// Creates a default credential from a JSON file. + private GoogleCredential CreateDefaultCredentialFromFile(string credentialPath) + { + Logger.Debug("Loading Credential from file {0}", credentialPath); + + using (Stream stream = GetStream(credentialPath)) + { + return CreateDefaultCredentialFromStream(stream); + } + } + + /// Creates a default credential from a stream that contains JSON credential data. + internal GoogleCredential CreateDefaultCredentialFromStream(Stream stream) + { + JsonCredentialParameters credentialParameters; + try + { + credentialParameters = NewtonsoftJsonSerializer.Instance.Deserialize(stream); + } + catch (Exception e) + { + throw new InvalidOperationException("Error deserializing JSON credential data.", e); + } + return CreateDefaultCredentialFromParameters(credentialParameters); + } + + /// Creates a default credential from a string that contains JSON credential data. + internal GoogleCredential CreateDefaultCredentialFromJson(string json) + { + JsonCredentialParameters credentialParameters; + try + { + credentialParameters = NewtonsoftJsonSerializer.Instance.Deserialize(json); + } + catch (Exception e) + { + throw new InvalidOperationException("Error deserializing JSON credential data.", e); + } + return CreateDefaultCredentialFromParameters(credentialParameters); + } + + + /// Creates a default credential from JSON data. + 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)); + } + } + + /// Creates a user credential from JSON data. + 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); + } + + /// Creates a from JSON data. + 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)); + } + + /// + /// Returns platform-specific well known credential file path. This file is created by + /// gcloud auth login + /// + 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); + } + + /// + /// Gets the environment variable. + /// This method is protected so it could be overriden for testing purposes only. + /// + protected virtual string GetEnvironmentVariable(string variableName) + { + return Environment.GetEnvironmentVariable(variableName); + } + + /// + /// Opens file as a stream. + /// This method is protected so it could be overriden for testing purposes only. + /// + protected virtual Stream GetStream(string filePath) + { + return new FileStream(filePath, FileMode.Open, FileAccess.Read); + } + } +} diff --git a/Yavsc/GoogleApiSupport/Google.Apis.Auth/OAuth2/Flows/AuthorizationCodeFlow.cs b/Yavsc/GoogleApiSupport/Google.Apis.Auth/OAuth2/Flows/AuthorizationCodeFlow.cs new file mode 100644 index 00000000..b2cef8b7 --- /dev/null +++ b/Yavsc/GoogleApiSupport/Google.Apis.Auth/OAuth2/Flows/AuthorizationCodeFlow.cs @@ -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 +{ + /// + /// Thread-safe OAuth 2.0 authorization code flow that manages and persists end-user credentials. + /// + /// 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. + /// + /// + public class AuthorizationCodeFlow : IAuthorizationCodeFlow + { + private static readonly ILogger Logger = ApplicationContext.Logger.ForType(); + + #region Initializer + + /// An initializer class for the authorization code flow. + public class Initializer + { + /// + /// Gets or sets the method for presenting the access token to the resource server. + /// The default value is + /// . + /// + public IAccessMethod AccessMethod { get; set; } + + /// Gets the token server URL. + public string TokenServerUrl { get; private set; } + + /// Gets or sets the authorization server URL. + public string AuthorizationServerUrl { get; private set; } + + /// Gets or sets the client secrets which includes the client identifier and its secret. + public ClientSecrets ClientSecrets { get; set; } + + /// + /// Gets or sets the client secrets stream which contains the client identifier and its secret. + /// + /// The AuthorizationCodeFlow constructor is responsible for disposing the stream. + public Stream ClientSecretsStream { get; set; } + + /// Gets or sets the data store used to store the token response. + public IDataStore DataStore { get; set; } + + /// + /// Gets or sets the scopes which indicate the API access your application is requesting. + /// + public IEnumerable Scopes { get; set; } + + /// + /// Gets or sets the factory for creating instance. + /// + public IHttpClientFactory HttpClientFactory { get; set; } + + /// + /// Get or sets the exponential back-off policy. Default value is UnsuccessfulResponse503, which + /// means that exponential back-off is used on 503 abnormal HTTP responses. + /// If the value is set to None, no exponential back-off policy is used, and it's up to user to + /// configure the in an + /// to set a specific back-off + /// implementation (using ). + /// + public ExponentialBackOffPolicy DefaultExponentialBackOffPolicy { get; set; } + + /// + /// 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 . + /// + public IClock Clock { get; set; } + + /// Constructs a new initializer. + /// Authorization server URL + /// Token server URL + public Initializer(string authorizationServerUrl, string tokenServerUrl) + { + AuthorizationServerUrl = authorizationServerUrl; + TokenServerUrl = tokenServerUrl; + + Scopes = new List(); + 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 scopes; + private readonly ConfigurableHttpClient httpClient; + private readonly IClock clock; + + #endregion + + /// Gets the token server URL. + public string TokenServerUrl { get { return tokenServerUrl; } } + + /// Gets the authorization code server URL. + public string AuthorizationServerUrl { get { return authorizationServerUrl; } } + + /// Gets the client secrets which includes the client identifier and its secret. + public ClientSecrets ClientSecrets { get { return clientSecrets; } } + + /// Gets the data store used to store the credentials. + public IDataStore DataStore { get { return dataStore; } } + + /// Gets the scopes which indicate the API access your application is requesting. + public IEnumerable Scopes { get { return scopes; } } + + /// Gets the HTTP client used to make authentication requests to the server. + public ConfigurableHttpClient HttpClient { get { return httpClient; } } + + /// Constructs a new flow using the initializer's properties. + 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 + + /// + public IAccessMethod AccessMethod { get { return accessMethod; } } + + /// + public IClock Clock { get { return clock; } } + + /// + public async Task LoadTokenAsync(string userId, CancellationToken taskCancellationToken) + { + taskCancellationToken.ThrowIfCancellationRequested(); + if (DataStore == null) + { + return null; + } + return await DataStore.GetAsync(userId).ConfigureAwait(false); + } + + /// + public async Task DeleteTokenAsync(string userId, CancellationToken taskCancellationToken) + { + taskCancellationToken.ThrowIfCancellationRequested(); + if (DataStore != null) + { + await DataStore.DeleteAsync(userId).ConfigureAwait(false); + } + } + + /// + public virtual AuthorizationCodeRequestUrl CreateAuthorizationCodeRequest(string redirectUri) + { + return new AuthorizationCodeRequestUrl(new Uri(AuthorizationServerUrl)) + { + ClientId = ClientSecrets.ClientId, + Scope = string.Join(" ", Scopes), + RedirectUri = redirectUri + }; + } + + /// + public async Task 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; + } + + /// + public async Task 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; + } + + /// + public virtual Task RevokeTokenAsync(string userId, string token, CancellationToken taskCancellationToken) + { + throw new NotImplementedException("The OAuth 2.0 protocol does not support token revocation."); + } + + /// + public virtual bool ShouldForceTokenRetrieval() { return false; } + + #endregion + + /// Stores the token in the . + /// User identifier. + /// Token to store. + /// Cancellation token to cancel operation. + private async Task StoreTokenAsync(string userId, TokenResponse token, CancellationToken taskCancellationToken) + { + taskCancellationToken.ThrowIfCancellationRequested(); + if (DataStore != null) + { + await DataStore.StoreAsync(userId, token).ConfigureAwait(false); + } + } + + /// Retrieve a new token from the server using the specified request. + /// User identifier. + /// Token request. + /// Cancellation token to cancel operation. + /// Token response with the new access token. + [VisibleForTestOnly] + public async Task 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; + } + } + + /// + public void Dispose() + { + if (HttpClient != null) + { + HttpClient.Dispose(); + } + } + } +} diff --git a/Yavsc/GoogleApiSupport/Google.Apis.Auth/OAuth2/Flows/GoogleAuthorizationCodeFlow.cs b/Yavsc/GoogleApiSupport/Google.Apis.Auth/OAuth2/Flows/GoogleAuthorizationCodeFlow.cs new file mode 100644 index 00000000..b6d9489f --- /dev/null +++ b/Yavsc/GoogleApiSupport/Google.Apis.Auth/OAuth2/Flows/GoogleAuthorizationCodeFlow.cs @@ -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 +{ + /// + /// Google specific authorization code flow which inherits from . + /// + public class GoogleAuthorizationCodeFlow : AuthorizationCodeFlow + { + private readonly string revokeTokenUrl; + + /// Gets the token revocation URL. + public string RevokeTokenUrl { get { return revokeTokenUrl; } } + + /// Gets or sets the include granted scopes indicator. + /// Do not use, use instead. + public readonly bool? includeGrantedScopes; + + /// Gets or sets the include granted scopes indicator. + public bool? IncludeGrantedScopes { get { return includeGrantedScopes; } } + + private readonly IEnumerable> userDefinedQueryParams; + + /// Gets the user defined query parameters. + public IEnumerable> UserDefinedQueryParams + { + get { return userDefinedQueryParams; } + } + + /// Constructs a new Google authorization code flow. + public GoogleAuthorizationCodeFlow(Initializer initializer) + : base(initializer) + { + revokeTokenUrl = initializer.RevokeTokenUrl; + includeGrantedScopes = initializer.IncludeGrantedScopes; + userDefinedQueryParams = initializer.UserDefinedQueryParams; + } + + /// + 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 + }; + } + + /// + 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(content); + throw new TokenResponseException(error, response.StatusCode); + } + + await DeleteTokenAsync(userId, taskCancellationToken); + } + + /// + public override bool ShouldForceTokenRetrieval() + { + return IncludeGrantedScopes.HasValue && IncludeGrantedScopes.Value; + } + + /// An initializer class for Google authorization code flow. + public new class Initializer : AuthorizationCodeFlow.Initializer + { + /// Gets or sets the token revocation URL. + public string RevokeTokenUrl { get; set; } + + /// + /// Gets or sets the optional indicator for including granted scopes for incremental authorization. + /// + public bool? IncludeGrantedScopes { get; set; } + + /// Gets or sets the optional user defined query parameters. + public IEnumerable> UserDefinedQueryParams { get; set; } + + /// + /// Constructs a new initializer. Sets Authorization server URL to + /// , and Token server URL to + /// . + /// + public Initializer() : this( + GoogleAuthConsts.OidcAuthorizationUrl, GoogleAuthConsts.OidcTokenUrl, GoogleAuthConsts.RevokeTokenUrl) + { + } + + /// Constructs a new initializer. + /// Authorization server URL + /// Token server URL + /// Revocation server URL + /// + /// This is mainly for internal testing at Google, where we occasionally need + /// to use alternative oauth endpoints. This is not for general use. + /// + protected Initializer(string authorizationServerUrl, string tokenServerUrl, string revokeTokenUrl) + : base(authorizationServerUrl, tokenServerUrl) + { + RevokeTokenUrl = revokeTokenUrl; + } + } + } +} diff --git a/Yavsc/GoogleApiSupport/Google.Apis.Auth/OAuth2/Flows/IAuthorizationCodeFlow.cs b/Yavsc/GoogleApiSupport/Google.Apis.Auth/OAuth2/Flows/IAuthorizationCodeFlow.cs new file mode 100644 index 00000000..0948ffd1 --- /dev/null +++ b/Yavsc/GoogleApiSupport/Google.Apis.Auth/OAuth2/Flows/IAuthorizationCodeFlow.cs @@ -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 +{ + /// OAuth 2.0 authorization code flow that manages and persists end-user credentials. + public interface IAuthorizationCodeFlow : IDisposable + { + /// Gets the method for presenting the access token to the resource server. + IAccessMethod AccessMethod { get; } + + /// Gets the clock. + IClock Clock { get; } + + /// Gets the data store used to store the credentials. + IDataStore DataStore { get; } + + /// + /// Asynchronously loads the user's token using the flow's + /// . + /// + /// User identifier + /// Cancellation token to cancel operation + /// Token response + Task LoadTokenAsync(string userId, CancellationToken taskCancellationToken); + + /// + /// Asynchronously deletes the user's token using the flow's + /// . + /// + /// User identifier. + /// Cancellation token to cancel operation. + Task DeleteTokenAsync(string userId, CancellationToken taskCancellationToken); + + /// Creates an authorization code request with the specified redirect URI. + AuthorizationCodeRequestUrl CreateAuthorizationCodeRequest(string redirectUri); + + /// Asynchronously exchanges code with a token. + /// User identifier. + /// Authorization code received from the authorization server. + /// Redirect URI which is used in the token request. + /// Cancellation token to cancel operation. + /// Token response which contains the access token. + Task ExchangeCodeForTokenAsync(string userId, string code, string redirectUri, + CancellationToken taskCancellationToken); + + /// Asynchronously refreshes an access token using a refresh token. + /// User identifier. + /// Refresh token which is used to get a new access token. + /// Cancellation token to cancel operation. + /// Token response which contains the access token and the input refresh token. + Task RefreshTokenAsync(string userId, string refreshToken, + CancellationToken taskCancellationToken); + + /// + /// 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. + /// + /// 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. + /// + /// User identifier. + /// Access token to be revoked. + /// Cancellation token to cancel operation. + /// true if the token was revoked successfully. + Task RevokeTokenAsync(string userId, string token, CancellationToken taskCancellationToken); + + /// + /// Indicates if a new token needs to be retrieved and stored regardless of normal circumstances. + /// + bool ShouldForceTokenRetrieval(); + } +} diff --git a/Yavsc/GoogleApiSupport/Google.Apis.Auth/OAuth2/GoogleAuthConsts.cs b/Yavsc/GoogleApiSupport/Google.Apis.Auth/OAuth2/GoogleAuthConsts.cs new file mode 100644 index 00000000..b6211668 --- /dev/null +++ b/Yavsc/GoogleApiSupport/Google.Apis.Auth/OAuth2/GoogleAuthConsts.cs @@ -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 +{ + /// + /// Google OAuth2 constants. + /// Canonical source for these URLs is: https://accounts.google.com/.well-known/openid-configuration + /// + public static class GoogleAuthConsts + { + /// The authorization code server URL. + public const string AuthorizationUrl = "https://accounts.google.com/o/oauth2/auth"; + + /// The OpenID Connect authorization code server URL. + /// + /// Use of this is not 100% compatible with using + /// , so they are two distinct URLs. + /// Internally within this library only this more up-to-date is used. + /// + public const string OidcAuthorizationUrl = "https://accounts.google.com/o/oauth2/v2/auth"; + + /// The approval URL (used in the Windows solution as a callback). + public const string ApprovalUrl = "https://accounts.google.com/o/oauth2/approval"; + + /// The authorization token server URL. + public const string TokenUrl = "https://accounts.google.com/o/oauth2/token"; + + /// The OpenID Connect authorization token server URL. + /// + /// Use of this is not 100% compatible with using + /// , so they are two distinct URLs. + /// Internally within this library only this more up-to-date is used. + /// + public const string OidcTokenUrl = "https://www.googleapis.com/oauth2/v4/token"; + + /// The Compute Engine authorization token server URL + public const string ComputeTokenUrl = + "http://metadata/computeMetadata/v1/instance/service-accounts/default/token"; + + /// The path to the Google revocation endpoint. + public const string RevokeTokenUrl = "https://accounts.google.com/o/oauth2/revoke"; + + /// The OpenID Connect Json Web Key Set (jwks) URL. + public const string JsonWebKeySetUrl = "https://www.googleapis.com/oauth2/v3/certs"; + + /// Installed application redirect URI. + public const string InstalledAppRedirectUri = "urn:ietf:wg:oauth:2.0:oob"; + + /// Installed application localhost redirect URI. + public const string LocalhostRedirectUri = "http://localhost"; + } +} \ No newline at end of file diff --git a/Yavsc/GoogleApiSupport/Google.Apis.Auth/OAuth2/GoogleClientSecrets.cs b/Yavsc/GoogleApiSupport/Google.Apis.Auth/OAuth2/GoogleClientSecrets.cs new file mode 100644 index 00000000..98e28587 --- /dev/null +++ b/Yavsc/GoogleApiSupport/Google.Apis.Auth/OAuth2/GoogleClientSecrets.cs @@ -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 +{ + /// + /// OAuth 2.0 client secrets model as specified in https://cloud.google.com/console/. + /// + public sealed class GoogleClientSecrets + { + /// Gets or sets the details for installed applications. + [Newtonsoft.Json.JsonProperty("installed")] + private ClientSecrets Installed { get; set; } + + /// Gets or sets the details for web applications. + [Newtonsoft.Json.JsonProperty("web")] + private ClientSecrets Web { get; set; } + + /// Gets the client secrets which contains the client identifier and client secret. + 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; + } + } + + /// Loads the Google client secret from the input stream. + public static GoogleClientSecrets Load(Stream stream) + { + return NewtonsoftJsonSerializer.Instance.Deserialize(stream); + } + } +} diff --git a/Yavsc/GoogleApiSupport/Google.Apis.Auth/OAuth2/GoogleCredential.cs b/Yavsc/GoogleApiSupport/Google.Apis.Auth/OAuth2/GoogleCredential.cs new file mode 100644 index 00000000..41a2c4d1 --- /dev/null +++ b/Yavsc/GoogleApiSupport/Google.Apis.Auth/OAuth2/GoogleCredential.cs @@ -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 +{ + /// + /// Credential for authorizing calls using OAuth 2.0. + /// It is a convenience wrapper that allows handling of different types of + /// credentials (like , + /// or ) in a unified way. + /// + /// See for the credential retrieval logic. + /// + /// + public class GoogleCredential : ICredential + { + /// Provider implements the logic for creating the application default credential. + private static DefaultCredentialProvider defaultCredentialProvider = new DefaultCredentialProvider(); + + /// The underlying credential being wrapped by this object. + protected readonly ICredential credential; + + /// Creates a new GoogleCredential. + internal GoogleCredential(ICredential credential) + { + this.credential = credential; + } + + /// + /// Returns the Application Default Credentials which are ambient credentials that identify and authorize + /// the whole application. + /// The ambient credentials are determined as following order: + /// + /// + /// + /// 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 + /// Google Developers Console 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. + /// + /// + /// + /// + /// If you have installed the Google Cloud SDK on your machine and have run the command + /// GCloud Auth Login, your identity can + /// be used as a proxy to test code calling APIs from that machine. + /// + /// + /// + /// + /// If you are running in Google Compute Engine production, the built-in service account associated with the + /// virtual machine instance will be used. + /// + /// + /// + /// + /// If all previous steps have failed, InvalidOperationException is thrown. + /// + /// + /// + /// + /// A task which completes with the application default credentials. + public static Task GetApplicationDefaultAsync() + { + return defaultCredentialProvider.GetDefaultCredentialAsync(); + } + + /// + /// Synchronously returns the Application Default Credentials which are ambient credentials that identify and authorize + /// the whole application. See for details on application default credentials. + /// This method will block until the credentials are available (or an exception is thrown). + /// It is highly preferable to call where possible. + /// + /// The application default credentials. + public static GoogleCredential GetApplicationDefault() => Task.Run(() => GetApplicationDefaultAsync()).Result; + + /// + /// Loads credential from stream containing JSON credential data. + /// + /// 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. + /// + /// + public static GoogleCredential FromStream(Stream stream) + { + return defaultCredentialProvider.CreateDefaultCredentialFromStream(stream); + } + + /// + /// Loads credential from a string containing JSON credential data. + /// + /// 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. + /// + /// + public static GoogleCredential FromJson(string json) + { + return defaultCredentialProvider.CreateDefaultCredentialFromJson(json); + } + + /// + /// Returns true only if this credential type has no scopes by default and requires + /// a call to before use. + /// + /// 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. + /// + /// + /// + /// + /// has scopes built-in. Nothing additional is required. + /// + /// + /// + /// + /// has scopes built-in, as they were obtained during the consent + /// screen. Nothing additional is required. + /// + /// + /// + /// does not have scopes built-in by default. Caller should + /// invoke to add scopes to the credential. + /// + /// + /// + /// + public virtual bool IsCreateScopedRequired + { + get { return false; } + } + + /// + /// If the credential supports scopes, creates a copy with the specified scopes. Otherwise, it returns the same + /// instance. + /// + public virtual GoogleCredential CreateScoped(IEnumerable scopes) + { + return this; + } + + /// + /// If the credential supports scopes, creates a copy with the specified scopes. Otherwise, it returns the same + /// instance. + /// + public GoogleCredential CreateScoped(params string[] scopes) + { + return CreateScoped((IEnumerable) scopes); + } + + void IConfigurableHttpClientInitializer.Initialize(ConfigurableHttpClient httpClient) + { + credential.Initialize(httpClient); + } + + Task ITokenAccess.GetAccessTokenForRequestAsync(string authUri, CancellationToken cancellationToken) + { + return credential.GetAccessTokenForRequestAsync(authUri, cancellationToken); + } + + /// + /// Gets the underlying credential instance being wrapped. + /// + public ICredential UnderlyingCredential => credential; + + /// Creates a GoogleCredential wrapping a . + internal static GoogleCredential FromCredential(ServiceAccountCredential credential) + { + return new ServiceAccountGoogleCredential(credential); + } + + /// + /// Wraps ServiceAccountCredential as GoogleCredential. + /// We need this subclass because wrapping ServiceAccountCredential (unlike other wrapped credential + /// types) requires special handling for IsCreateScopedRequired and CreateScoped members. + /// + 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 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)); + } + } + } +} diff --git a/Yavsc/GoogleApiSupport/Google.Apis.Auth/OAuth2/GoogleWebAuthorizationBroker.cs b/Yavsc/GoogleApiSupport/Google.Apis.Auth/OAuth2/GoogleWebAuthorizationBroker.cs new file mode 100644 index 00000000..ff8c655b --- /dev/null +++ b/Yavsc/GoogleApiSupport/Google.Apis.Auth/OAuth2/GoogleWebAuthorizationBroker.cs @@ -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 +{ + /// A helper utility to manage the authorization code flow. + public class GoogleWebAuthorizationBroker + { + // It's unforunate this is a public field. But it cannot be changed due to backward compatibility. + /// The folder which is used by the . + /// + /// The reason that this is not 'private const' is that a user can change it and store the credentials in a + /// different location. + /// + public static string Folder = "Google.Apis.Auth"; + + /// Asynchronously authorizes the specified user. + /// + /// In case no data store is specified, will be used by + /// default. + /// + /// The client secrets. + /// + /// The scopes which indicate the Google API access your application is requesting. + /// + /// The user to authorize. + /// Cancellation token to cancel an operation. + /// The data store, if not specified a file data store will be used. + /// The code receiver, if not specified a local server code receiver will be used. + /// User credential. + public static async Task AuthorizeAsync(ClientSecrets clientSecrets, + IEnumerable 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); + } + + /// Asynchronously authorizes the specified user. + /// + /// In case no data store is specified, will be used by + /// default. + /// + /// + /// The client secrets stream. The authorization code flow constructor is responsible for disposing the stream. + /// + /// + /// The scopes which indicate the Google API access your application is requesting. + /// + /// The user to authorize. + /// Cancellation token to cancel an operation. + /// The data store, if not specified a file data store will be used. + /// The code receiver, if not specified a local server code receiver will be used. + /// User credential. + public static async Task AuthorizeAsync(Stream clientSecretsStream, + IEnumerable 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); + } + + /// + /// Asynchronously reauthorizes the user. This method should be called if the users want to authorize after + /// they revoked the token. + /// + /// The current user credential. Its will be + /// updated. + /// Cancellation token to cancel an operation. + /// The code receiver, if not specified a local server code receiver will be used. + 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; + } + + /// The core logic for asynchronously authorizing the specified user. + /// The authorization code initializer. + /// + /// The scopes which indicate the Google API access your application is requesting. + /// + /// The user to authorize. + /// Cancellation token to cancel an operation. + /// The data store, if not specified a file data store will be used. + /// The code receiver, if not specified a local server code receiver will be used. + /// User credential. + public static async Task AuthorizeAsync( + GoogleAuthorizationCodeFlow.Initializer initializer, IEnumerable 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); + } + } +} diff --git a/Yavsc/GoogleApiSupport/Google.Apis.Auth/OAuth2/IAccessMethod.cs b/Yavsc/GoogleApiSupport/Google.Apis.Auth/OAuth2/IAccessMethod.cs new file mode 100644 index 00000000..abc72d09 --- /dev/null +++ b/Yavsc/GoogleApiSupport/Google.Apis.Auth/OAuth2/IAccessMethod.cs @@ -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 +{ + /// + /// Method of presenting the access token to the resource server as specified in + /// http://tools.ietf.org/html/rfc6749#section-7 + /// + public interface IAccessMethod + { + /// + /// Intercepts a HTTP request right before the HTTP request executes by providing the access token. + /// + void Intercept(HttpRequestMessage request, string accessToken); + + /// + /// Retrieves the original access token in the HTTP request, as provided in the + /// method. + /// + string GetAccessToken(HttpRequestMessage request); + } +} diff --git a/Yavsc/GoogleApiSupport/Google.Apis.Auth/OAuth2/IAuthorizationCodeInstalledApp.cs b/Yavsc/GoogleApiSupport/Google.Apis.Auth/OAuth2/IAuthorizationCodeInstalledApp.cs new file mode 100644 index 00000000..71109ab0 --- /dev/null +++ b/Yavsc/GoogleApiSupport/Google.Apis.Auth/OAuth2/IAuthorizationCodeInstalledApp.cs @@ -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 +{ + /// + /// Authorization code flow for an installed application that persists end-user credentials. + /// + public interface IAuthorizationCodeInstalledApp + { + /// Gets the authorization code flow. + IAuthorizationCodeFlow Flow { get; } + + /// Gets the code receiver. + ICodeReceiver CodeReceiver { get; } + + /// Asynchronously authorizes the installed application to access user's protected data. + /// User identifier + /// Cancellation token to cancel an operation + /// The user's credential + Task AuthorizeAsync(string userId, CancellationToken taskCancellationToken); + } +} diff --git a/Yavsc/GoogleApiSupport/Google.Apis.Auth/OAuth2/ICodeReceiver.cs b/Yavsc/GoogleApiSupport/Google.Apis.Auth/OAuth2/ICodeReceiver.cs new file mode 100644 index 00000000..ff43617e --- /dev/null +++ b/Yavsc/GoogleApiSupport/Google.Apis.Auth/OAuth2/ICodeReceiver.cs @@ -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 +{ + /// OAuth 2.0 verification code receiver. + public interface ICodeReceiver + { + /// Gets the redirected URI. + string RedirectUri { get; } + + /// Receives the authorization code. + /// The authorization code request URL + /// Cancellation token + /// The authorization code response + Task ReceiveCodeAsync(AuthorizationCodeRequestUrl url, + CancellationToken taskCancellationToken); + } +} \ No newline at end of file diff --git a/Yavsc/GoogleApiSupport/Google.Apis.Auth/OAuth2/ICredential.cs b/Yavsc/GoogleApiSupport/Google.Apis.Auth/OAuth2/ICredential.cs new file mode 100644 index 00000000..a97a434f --- /dev/null +++ b/Yavsc/GoogleApiSupport/Google.Apis.Auth/OAuth2/ICredential.cs @@ -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 +{ + /// + /// 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 + /// to be able to hook to http requests. + /// More details are available in the specific implementations. + /// + public interface ICredential : IConfigurableHttpClientInitializer, ITokenAccess + { + } +} diff --git a/Yavsc/GoogleApiSupport/Google.Apis.Auth/OAuth2/ITokenAccess.cs b/Yavsc/GoogleApiSupport/Google.Apis.Auth/OAuth2/ITokenAccess.cs new file mode 100644 index 00000000..28bda836 --- /dev/null +++ b/Yavsc/GoogleApiSupport/Google.Apis.Auth/OAuth2/ITokenAccess.cs @@ -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 +{ + /// + /// Allows direct retrieval of access tokens to authenticate requests. + /// This is necessary for workflows where you don't want to use + /// to access the API. + /// (e.g. gRPC that implemenents the entire HTTP2 stack internally). + /// + public interface ITokenAccess + { + /// + /// Gets an access token to authorize a request. + /// Implementations should handle automatic refreshes of the token + /// if they are supported. + /// The might be required by some credential types + /// (e.g. the JWT access token) while other credential types + /// migth just ignore it. + /// + /// The URI the returned token will grant access to. + /// The cancellation token. + /// The access token. + Task GetAccessTokenForRequestAsync(string authUri = null, CancellationToken cancellationToken = default(CancellationToken)); + } +} diff --git a/Yavsc/GoogleApiSupport/Google.Apis.Auth/OAuth2/JsonCredentialParameters.cs b/Yavsc/GoogleApiSupport/Google.Apis.Auth/OAuth2/JsonCredentialParameters.cs new file mode 100644 index 00000000..797a2fe5 --- /dev/null +++ b/Yavsc/GoogleApiSupport/Google.Apis.Auth/OAuth2/JsonCredentialParameters.cs @@ -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 +{ + /// + /// Holder for credential parameters read from JSON credential file. + /// Fields are union of parameters for all supported credential types. + /// + public class JsonCredentialParameters + { + /// + /// UserCredential is created by the GCloud SDK tool when the user runs + /// GCloud Auth Login. + /// + public const string AuthorizedUserCredentialType = "authorized_user"; + + /// + /// ServiceAccountCredential is downloaded by the user from + /// Google Developers Console. + /// + public const string ServiceAccountCredentialType = "service_account"; + + /// Type of the credential. + [Newtonsoft.Json.JsonProperty("type")] + public string Type { get; set; } + + /// + /// Client Id associated with UserCredential created by + /// GCloud Auth Login. + /// + [Newtonsoft.Json.JsonProperty("client_id")] + public string ClientId { get; set; } + + /// + /// Client Secret associated with UserCredential created by + /// GCloud Auth Login. + /// + [Newtonsoft.Json.JsonProperty("client_secret")] + public string ClientSecret { get; set; } + + /// + /// Client Email associated with ServiceAccountCredential obtained from + /// Google Developers Console + /// + [Newtonsoft.Json.JsonProperty("client_email")] + public string ClientEmail { get; set; } + + /// + /// Private Key associated with ServiceAccountCredential obtained from + /// Google Developers Console. + /// + [Newtonsoft.Json.JsonProperty("private_key")] + public string PrivateKey { get; set; } + + /// + /// Refresh Token associated with UserCredential created by + /// GCloud Auth Login. + /// + [Newtonsoft.Json.JsonProperty("refresh_token")] + public string RefreshToken { get; set; } + } +} diff --git a/Yavsc/GoogleApiSupport/Google.Apis.Auth/OAuth2/LocalServerCodeReceiver.cs b/Yavsc/GoogleApiSupport/Google.Apis.Auth/OAuth2/LocalServerCodeReceiver.cs new file mode 100644 index 00000000..9164adea --- /dev/null +++ b/Yavsc/GoogleApiSupport/Google.Apis.Auth/OAuth2/LocalServerCodeReceiver.cs @@ -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 +{ + /// + /// 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. + /// + public class LocalServerCodeReceiver : ICodeReceiver + { + private static readonly ILogger Logger = ApplicationContext.Logger.ForType(); + + /// The call back request path. + internal const string LoopbackCallbackPath = "/authorize/"; + + /// The call back format. Expects one port parameter. + internal static readonly string LoopbackCallback = $"http://{IPAddress.Loopback}:{{0}}{LoopbackCallbackPath}"; + + /// Close HTML tag to return the browser so it will close itself. + internal const string ClosePageResponse = +@" + OAuth 2.0 Authentication Token Received + + Received verification code. You may now close this window. + + +"; + + // Not required in NET45, but present for testing. + /// + /// 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. + /// + 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(); + + 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> 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> GetQueryParamsFromClientAsync(TcpClient client, CancellationToken cancellationToken) + { + var stream = client.GetStream(); + + var buffer = new byte[NetworkReadBufferSize]; + int bufferOfs = 0; + int bufferSize = 0; + Func> 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 ReadRequestLine(Func> 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 ValidateAndGetRequestParams(string requestLine) + { + var requestLineParts = requestLine.Split(' '); + if (requestLineParts.Length != 3) + { + throw new ServerException("Request line ill-formatted. Should be ' 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(); + } + 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> 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; + /// + public string RedirectUri + { + get + { + if (!string.IsNullOrEmpty(redirectUri)) + { + return redirectUri; + } + + return redirectUri = string.Format(LoopbackCallback, GetRandomUnusedPort()); + } + } + + /// + public async Task 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); + } + } + + /// Returns a random, unused port. + 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 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 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 + } +} diff --git a/Yavsc/GoogleApiSupport/Google.Apis.Auth/OAuth2/Pkcs8.cs b/Yavsc/GoogleApiSupport/Google.Apis.Auth/OAuth2/Pkcs8.cs new file mode 100644 index 00000000..e66ff104 --- /dev/null +++ b/Yavsc/GoogleApiSupport/Google.Apis.Auth/OAuth2/Pkcs8.cs @@ -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 + + /// + /// An incomplete ASN.1 decoder, only implements what's required + /// to decode a Service Credential. + /// + 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 result = new List(); + 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 sequence = new List(); + 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; + } + + } +} diff --git a/Yavsc/GoogleApiSupport/Google.Apis.Auth/OAuth2/PromptCodeReceiver.cs b/Yavsc/GoogleApiSupport/Google.Apis.Auth/OAuth2/PromptCodeReceiver.cs new file mode 100644 index 00000000..29dc582e --- /dev/null +++ b/Yavsc/GoogleApiSupport/Google.Apis.Auth/OAuth2/PromptCodeReceiver.cs @@ -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 +{ + /// OAuth 2.0 verification code receiver that reads the authorization code from the user input. + public class PromptCodeReceiver : ICodeReceiver + { + private static readonly ILogger Logger = ApplicationContext.Logger.ForType(); + + /// + public string RedirectUri + { + get { return GoogleAuthConsts.InstalledAppRedirectUri; } + } + + /// + public Task 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 }); + } + } +} diff --git a/Yavsc/GoogleApiSupport/Google.Apis.Auth/OAuth2/Requests/AuthorizationCodeRequestUrl.cs b/Yavsc/GoogleApiSupport/Google.Apis.Auth/OAuth2/Requests/AuthorizationCodeRequestUrl.cs new file mode 100644 index 00000000..b80a53da --- /dev/null +++ b/Yavsc/GoogleApiSupport/Google.Apis.Auth/OAuth2/Requests/AuthorizationCodeRequestUrl.cs @@ -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 +{ + /// + /// 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. + /// + public class AuthorizationCodeRequestUrl : AuthorizationRequestUrl + { + /// + /// Constructs a new authorization code request with the specified URI and sets response_type to code. + /// + public AuthorizationCodeRequestUrl(Uri authorizationServerUrl) + : base(authorizationServerUrl) + { + ResponseType = "code"; + } + + /// Creates a which is used to request the authorization code. + public Uri Build() + { + var builder = new RequestBuilder() + { + BaseUri = AuthorizationServerUrl + }; + ParameterUtils.InitParameters(builder, this); + return builder.BuildUri(); + } + } +} diff --git a/Yavsc/GoogleApiSupport/Google.Apis.Auth/OAuth2/Requests/AuthorizationCodeTokenRequest.cs b/Yavsc/GoogleApiSupport/Google.Apis.Auth/OAuth2/Requests/AuthorizationCodeTokenRequest.cs new file mode 100644 index 00000000..93bc5405 --- /dev/null +++ b/Yavsc/GoogleApiSupport/Google.Apis.Auth/OAuth2/Requests/AuthorizationCodeTokenRequest.cs @@ -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 +{ + /// + /// 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. + /// + public class AuthorizationCodeTokenRequest : TokenRequest + { + /// Gets or sets the authorization code received from the authorization server. + [Google.Apis.Util.RequestParameterAttribute("code")] + public string Code { get; set; } + + /// + /// Gets or sets the redirect URI parameter matching the redirect URI parameter in the authorization request. + /// + [Google.Apis.Util.RequestParameterAttribute("redirect_uri")] + public string RedirectUri { get; set; } + + /// + /// Constructs a new authorization code token request and sets grant_type to authorization_code. + /// + public AuthorizationCodeTokenRequest() + { + GrantType = "authorization_code"; + } + } +} diff --git a/Yavsc/GoogleApiSupport/Google.Apis.Auth/OAuth2/Requests/AuthorizationRequestUrl.cs b/Yavsc/GoogleApiSupport/Google.Apis.Auth/OAuth2/Requests/AuthorizationRequestUrl.cs new file mode 100644 index 00000000..712797f1 --- /dev/null +++ b/Yavsc/GoogleApiSupport/Google.Apis.Auth/OAuth2/Requests/AuthorizationRequestUrl.cs @@ -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 +{ + /// + /// 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. + /// + public class AuthorizationRequestUrl + { + /// + /// Gets or sets the response type which must be code for requesting an authorization code or + /// token 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 + /// + [Google.Apis.Util.RequestParameterAttribute("response_type", Google.Apis.Util.RequestParameterType.Query)] + public string ResponseType { get; set; } + + /// Gets or sets the client identifier. + [Google.Apis.Util.RequestParameterAttribute("client_id", Google.Apis.Util.RequestParameterType.Query)] + public string ClientId { get; set; } + + /// + /// 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 null for none. + /// + [Google.Apis.Util.RequestParameterAttribute("redirect_uri", Google.Apis.Util.RequestParameterType.Query)] + public string RedirectUri { get; set; } + + /// + /// Gets or sets space-separated list of scopes, as specified in http://tools.ietf.org/html/rfc6749#section-3.3 + /// or null for none. + /// + [Google.Apis.Util.RequestParameterAttribute("scope", Google.Apis.Util.RequestParameterType.Query)] + public string Scope { get; set; } + + /// + /// 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 null for none. + /// + [Google.Apis.Util.RequestParameterAttribute("state", Google.Apis.Util.RequestParameterType.Query)] + public string State { get; set; } + + private readonly Uri authorizationServerUrl; + /// Gets the authorization server URI. + public Uri AuthorizationServerUrl + { + get { return authorizationServerUrl; } + } + + /// Constructs a new authorization request with the specified URI. + /// Authorization server URI + public AuthorizationRequestUrl(Uri authorizationServerUrl) + { + this.authorizationServerUrl = authorizationServerUrl; + } + } +} \ No newline at end of file diff --git a/Yavsc/GoogleApiSupport/Google.Apis.Auth/OAuth2/Requests/GoogleAssertionTokenRequest.cs b/Yavsc/GoogleApiSupport/Google.Apis.Auth/OAuth2/Requests/GoogleAssertionTokenRequest.cs new file mode 100644 index 00000000..57154e6d --- /dev/null +++ b/Yavsc/GoogleApiSupport/Google.Apis.Auth/OAuth2/Requests/GoogleAssertionTokenRequest.cs @@ -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 +{ + /// + /// Service account assertion token request as specified in + /// https://developers.google.com/accounts/docs/OAuth2ServiceAccount#makingrequest. + /// + public class GoogleAssertionTokenRequest : TokenRequest + { + /// Gets or sets the JWT (including signature). + [Google.Apis.Util.RequestParameterAttribute("assertion")] + public string Assertion { get; set; } + + /// + /// Constructs a new refresh code token request and sets grant_type to + /// urn:ietf:params:oauth:grant-type:jwt-bearer. + /// + public GoogleAssertionTokenRequest() + { + GrantType = "urn:ietf:params:oauth:grant-type:jwt-bearer"; + } + } +} diff --git a/Yavsc/GoogleApiSupport/Google.Apis.Auth/OAuth2/Requests/GoogleAuthorizationCodeRequestUrl.cs b/Yavsc/GoogleApiSupport/Google.Apis.Auth/OAuth2/Requests/GoogleAuthorizationCodeRequestUrl.cs new file mode 100644 index 00000000..85829d29 --- /dev/null +++ b/Yavsc/GoogleApiSupport/Google.Apis.Auth/OAuth2/Requests/GoogleAuthorizationCodeRequestUrl.cs @@ -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 +{ + /// + /// 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. + /// + public class GoogleAuthorizationCodeRequestUrl : AuthorizationCodeRequestUrl + { + /// + /// Gets or sets the access type. Set online to request on-line access or offline to request + /// off-line access or null for the default behavior. The default value is offline. + /// + [Google.Apis.Util.RequestParameterAttribute("access_type", Google.Apis.Util.RequestParameterType.Query)] + public string AccessType { get; set; } + + /// + /// Gets or sets prompt for consent behavior auto to request auto-approval orforce to force the + /// approval UI to show, or null for the default behavior. + /// + [Google.Apis.Util.RequestParameterAttribute("approval_prompt", Google.Apis.Util.RequestParameterType.Query)] + public string ApprovalPrompt { get; set; } + + /// + /// Gets or sets the login hint. Sets email address or sub identifier. + /// 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. + /// + [Google.Apis.Util.RequestParameterAttribute("login_hint", Google.Apis.Util.RequestParameterType.Query)] + public string LoginHint { get; set; } + + /// + /// 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. + /// + /// Currently unsupported for installed apps. + [Google.Apis.Util.RequestParameterAttribute("include_granted_scopes", + Google.Apis.Util.RequestParameterType.Query)] + public string IncludeGrantedScopes { get; set; } + + /// + /// 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. + /// + /// + /// The name of this parameter is used only for the constructor and will not end up in the resultant query + /// string. + /// + [Google.Apis.Util.RequestParameterAttribute("user_defined_query_params", + Google.Apis.Util.RequestParameterType.UserDefinedQueries)] + public IEnumerable> UserDefinedQueryParams { get; set; } + + /// + /// Constructs a new authorization code request with the given authorization server URL. This constructor sets + /// the to offline. + /// + public GoogleAuthorizationCodeRequestUrl(Uri authorizationServerUrl) + : base(authorizationServerUrl) + { + AccessType = "offline"; + } + } +} \ No newline at end of file diff --git a/Yavsc/GoogleApiSupport/Google.Apis.Auth/OAuth2/Requests/GoogleRevokeTokenRequest.cs b/Yavsc/GoogleApiSupport/Google.Apis.Auth/OAuth2/Requests/GoogleRevokeTokenRequest.cs new file mode 100644 index 00000000..a6be5cae --- /dev/null +++ b/Yavsc/GoogleApiSupport/Google.Apis.Auth/OAuth2/Requests/GoogleRevokeTokenRequest.cs @@ -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 +{ + /// + /// Google OAuth 2.0 request to revoke an access token as specified in + /// https://developers.google.com/accounts/docs/OAuth2WebServer#tokenrevoke. + /// + class GoogleRevokeTokenRequest + { + private readonly Uri revokeTokenUrl; + /// Gets the URI for token revocation. + public Uri RevokeTokenUrl + { + get { return revokeTokenUrl; } + } + + /// Gets or sets the token to revoke. + [Google.Apis.Util.RequestParameterAttribute("token")] + public string Token { get; set; } + + public GoogleRevokeTokenRequest(Uri revokeTokenUrl) + { + this.revokeTokenUrl = revokeTokenUrl; + } + + /// Creates a which is used to request the authorization code. + public Uri Build() + { + var builder = new RequestBuilder() + { + BaseUri = revokeTokenUrl + }; + ParameterUtils.InitParameters(builder, this); + return builder.BuildUri(); + } + } +} diff --git a/Yavsc/GoogleApiSupport/Google.Apis.Auth/OAuth2/Requests/RefreshTokenRequest.cs b/Yavsc/GoogleApiSupport/Google.Apis.Auth/OAuth2/Requests/RefreshTokenRequest.cs new file mode 100644 index 00000000..64564064 --- /dev/null +++ b/Yavsc/GoogleApiSupport/Google.Apis.Auth/OAuth2/Requests/RefreshTokenRequest.cs @@ -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 +{ + /// + /// OAuth 2.0 request to refresh an access token using a refresh token as specified in + /// http://tools.ietf.org/html/rfc6749#section-6. + /// + public class RefreshTokenRequest : TokenRequest + { + /// Gets or sets the Refresh token issued to the client. + [Google.Apis.Util.RequestParameterAttribute("refresh_token")] + public string RefreshToken { get; set; } + + /// + /// Constructs a new refresh code token request and sets grant_type to refresh_token. + /// + public RefreshTokenRequest() + { + GrantType = "refresh_token"; + } + } +} diff --git a/Yavsc/GoogleApiSupport/Google.Apis.Auth/OAuth2/Requests/TokenRequest.cs b/Yavsc/GoogleApiSupport/Google.Apis.Auth/OAuth2/Requests/TokenRequest.cs new file mode 100644 index 00000000..06cff8ca --- /dev/null +++ b/Yavsc/GoogleApiSupport/Google.Apis.Auth/OAuth2/Requests/TokenRequest.cs @@ -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 +{ + /// + /// OAuth 2.0 request for an access token as specified in http://tools.ietf.org/html/rfc6749#section-4. + /// + public class TokenRequest + { + /// + /// Gets or sets space-separated list of scopes as specified in http://tools.ietf.org/html/rfc6749#section-3.3. + /// + [Google.Apis.Util.RequestParameterAttribute("scope")] + public string Scope { get; set; } + + /// + /// Gets or sets the Grant type. Sets authorization_code or password or client_credentials + /// or refresh_token or absolute URI of the extension grant type. + /// + [Google.Apis.Util.RequestParameterAttribute("grant_type")] + public string GrantType { get; set; } + + /// Gets or sets the client Identifier. + [Google.Apis.Util.RequestParameterAttribute("client_id")] + public string ClientId { get; set; } + + /// Gets or sets the client Secret. + [Google.Apis.Util.RequestParameterAttribute("client_secret")] + public string ClientSecret { get; set; } + } +} diff --git a/Yavsc/GoogleApiSupport/Google.Apis.Auth/OAuth2/Requests/TokenRequestExtenstions.cs b/Yavsc/GoogleApiSupport/Google.Apis.Auth/OAuth2/Requests/TokenRequestExtenstions.cs new file mode 100644 index 00000000..80cc36d0 --- /dev/null +++ b/Yavsc/GoogleApiSupport/Google.Apis.Auth/OAuth2/Requests/TokenRequestExtenstions.cs @@ -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 +{ + /// Extension methods to . + public static class TokenRequestExtenstions + { + /// + /// Executes the token request in order to receive a + /// . In case the token server returns an + /// error, a is thrown. + /// + /// The token request. + /// The HTTP client used to create an HTTP request. + /// The token server URL. + /// Cancellation token to cancel operation. + /// + /// The clock which is used to set the + /// property. + /// + /// Token response with the new access token. + public static async Task 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(content); + throw new TokenResponseException(error, response.StatusCode); + } + + // Gets the token and sets its issued time. + var newToken = NewtonsoftJsonSerializer.Instance.Deserialize(content); + newToken.IssuedUtc = clock.UtcNow; + return newToken; + } + } +} diff --git a/Yavsc/GoogleApiSupport/Google.Apis.Auth/OAuth2/Responses/AuthorizationCodeResponseUrl.cs b/Yavsc/GoogleApiSupport/Google.Apis.Auth/OAuth2/Responses/AuthorizationCodeResponseUrl.cs new file mode 100644 index 00000000..883653fa --- /dev/null +++ b/Yavsc/GoogleApiSupport/Google.Apis.Auth/OAuth2/Responses/AuthorizationCodeResponseUrl.cs @@ -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 +{ + /// + /// 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. + /// + /// Check that is not null or empty to verify the end-user granted authorization. + /// + /// + public class AuthorizationCodeResponseUrl + { + /// Gets or sets the authorization code generated by the authorization server. + public string Code { get; set; } + + /// + /// Gets or sets the state parameter matching the state parameter in the authorization request. + /// + public string State { get; set; } + + /// + /// 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. + /// + public string Error { get; set; } + + /// + /// Gets or sets the human-readable text which provides additional information used to assist the client + /// developer in understanding the error occurred. + /// + public string ErrorDescription { get; set; } + + /// + /// Gets or sets the URI identifying a human-readable web page with provides information about the error. + /// + public string ErrorUri { get; set; } + + /// Constructs a new authorization code response URL from the specified dictionary. + public AuthorizationCodeResponseUrl(IDictionary queryString) + { + InitFromDictionary(queryString); + } + + #region Constructs + + /// Constructs a new authorization code response URL from the specified query string. + public AuthorizationCodeResponseUrl(string query) + { + var pairs = query.Split('&'); + var queryString = new Dictionary(); + foreach (var pair in pairs) + { + var keyValue = pair.Split('='); + queryString[keyValue[0]] = keyValue[1]; + } + + InitFromDictionary(queryString); + } + + /// Initializes this instance from the input dictionary. + private void InitFromDictionary(IDictionary queryString) + { + //TODO(peleyal): improve the following code and make it a utility + IDictionary> setters = new Dictionary>(); + 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 setter; + foreach (var pair in queryString) + { + if (setters.TryGetValue(pair.Key, out setter)) + { + setter(pair.Value); + } + } + } + + /// Constructs a new empty authorization code response URL. + public AuthorizationCodeResponseUrl() + { + } + + #endregion + } +} diff --git a/Yavsc/GoogleApiSupport/Google.Apis.Auth/OAuth2/Responses/TokenErrorResponse.cs b/Yavsc/GoogleApiSupport/Google.Apis.Auth/OAuth2/Responses/TokenErrorResponse.cs new file mode 100644 index 00000000..e3110d78 --- /dev/null +++ b/Yavsc/GoogleApiSupport/Google.Apis.Auth/OAuth2/Responses/TokenErrorResponse.cs @@ -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 +{ + /// + /// OAuth 2.0 model for a unsuccessful access token response as specified in + /// http://tools.ietf.org/html/rfc6749#section-5.2. + /// + public class TokenErrorResponse + { + /// + /// 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. + /// + [Newtonsoft.Json.JsonProperty("error")] + public string Error { get; set; } + + /// + /// Gets or sets a human-readable text which provides additional information used to assist the client + /// developer in understanding the error occurred. + /// + [Newtonsoft.Json.JsonProperty("error_description")] + public string ErrorDescription { get; set; } + + /// + /// Gets or sets the URI identifying a human-readable web page with provides information about the error. + /// + [Newtonsoft.Json.JsonProperty("error_uri")] + public string ErrorUri { get; set; } + + /// + public override string ToString() + { + return string.Format("Error:\"{0}\", Description:\"{1}\", Uri:\"{2}\"", Error, ErrorDescription, ErrorUri); + } + + /// Constructs a new empty token error response. + public TokenErrorResponse() + { + } + + /// Constructs a new token error response from the given authorization code response. + public TokenErrorResponse(AuthorizationCodeResponseUrl authorizationCode) + { + Error = authorizationCode.Error; + ErrorDescription = authorizationCode.ErrorDescription; + ErrorUri = authorizationCode.ErrorUri; + } + } +} diff --git a/Yavsc/GoogleApiSupport/Google.Apis.Auth/OAuth2/Responses/TokenResponse.cs b/Yavsc/GoogleApiSupport/Google.Apis.Auth/OAuth2/Responses/TokenResponse.cs new file mode 100644 index 00000000..4799922d --- /dev/null +++ b/Yavsc/GoogleApiSupport/Google.Apis.Auth/OAuth2/Responses/TokenResponse.cs @@ -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 +{ + /// + /// OAuth 2.0 model for a successful access token response as specified in + /// http://tools.ietf.org/html/rfc6749#section-5.1. + /// + public class TokenResponse + { + private const int TokenExpiryTimeWindowSeconds = 60 * 5; // Refresh token 5 minutes before it expires. + + /// Gets or sets the access token issued by the authorization server. + [Newtonsoft.Json.JsonPropertyAttribute("access_token")] + public string AccessToken { get; set; } + + /// + /// Gets or sets the token type as specified in http://tools.ietf.org/html/rfc6749#section-7.1. + /// + [Newtonsoft.Json.JsonPropertyAttribute("token_type")] + public string TokenType { get; set; } + + /// Gets or sets the lifetime in seconds of the access token. + [Newtonsoft.Json.JsonPropertyAttribute("expires_in")] + public Nullable ExpiresInSeconds { get; set; } + + /// + /// 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. + /// + [Newtonsoft.Json.JsonPropertyAttribute("refresh_token")] + public string RefreshToken { get; set; } + + /// + /// Gets or sets the scope of the access token as specified in http://tools.ietf.org/html/rfc6749#section-3.3. + /// + [Newtonsoft.Json.JsonPropertyAttribute("scope")] + public string Scope { get; set; } + + /// + /// 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 + /// + [Newtonsoft.Json.JsonPropertyAttribute("id_token")] + public string IdToken { get; set; } + + /// + /// 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). + /// + [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(); } + } + + /// + /// The date and time that this token was issued, expressed in UTC. + /// + /// + /// This should be set by the CLIENT after the token was received from the server. + /// + [Newtonsoft.Json.JsonPropertyAttribute(Order = 2)] + public DateTime IssuedUtc { get; set; } + + /// + /// Returns true if the token is expired or it's going to be expired in the next minute. + /// + public bool IsExpired(IClock clock) + { + if (AccessToken == null || !ExpiresInSeconds.HasValue) + { + return true; + } + + return IssuedUtc.AddSeconds(ExpiresInSeconds.Value - TokenExpiryTimeWindowSeconds) <= clock.UtcNow; + } + + /// + /// Asynchronously parses a instance from the specified . + /// + /// The http response from which to parse the token. + /// The clock used to set the value of the token. + /// The logger used to output messages incase of error. + /// + /// The response was not successful or there is an error parsing the response into valid instance. + /// + /// + /// A task containing the parsed form the response message. + /// + public static async Task 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(content); + throw new TokenResponseException(error, response.StatusCode); + } + + // Gets the token and sets its issued time. + typeName = nameof(TokenResponse); + var newToken = NewtonsoftJsonSerializer.Instance.Deserialize(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); + } + } + } +} diff --git a/Yavsc/GoogleApiSupport/Google.Apis.Auth/OAuth2/Responses/TokenResponseException.cs b/Yavsc/GoogleApiSupport/Google.Apis.Auth/OAuth2/Responses/TokenResponseException.cs new file mode 100644 index 00000000..9e56f746 --- /dev/null +++ b/Yavsc/GoogleApiSupport/Google.Apis.Auth/OAuth2/Responses/TokenResponseException.cs @@ -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 +{ + /// + /// Token response exception which is thrown in case of receiving a token error when an authorization code or an + /// access token is expected. + /// + public class TokenResponseException : Exception + { + /// The error information. + public TokenErrorResponse Error { get; } + + /// HTTP status code of error, or null if unknown. + public HttpStatusCode? StatusCode { get; } + + /// Constructs a new token response exception from the given error. + public TokenResponseException(TokenErrorResponse error) + : this(error, null) { } + + /// Constructs a new token response exception from the given error nad optional HTTP status code. + public TokenResponseException(TokenErrorResponse error, HttpStatusCode? statusCode) + : base(error.ToString()) + { + Error = error; + StatusCode = statusCode; + } + } +} diff --git a/Yavsc/GoogleApiSupport/Google.Apis.Auth/OAuth2/ServiceAccountCredential.cs b/Yavsc/GoogleApiSupport/Google.Apis.Auth/OAuth2/ServiceAccountCredential.cs new file mode 100644 index 00000000..537734b1 --- /dev/null +++ b/Yavsc/GoogleApiSupport/Google.Apis.Auth/OAuth2/ServiceAccountCredential.cs @@ -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 +{ + /// + /// 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. + /// + /// Take a look in https://developers.google.com/accounts/docs/OAuth2ServiceAccount for more details. + /// + /// + /// 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 for explanation when JWT access token + /// is used and when regular OAuth2 token is used. + /// + /// + public class ServiceAccountCredential : ServiceCredential + { + private const string Sha256Oid = "2.16.840.1.101.3.4.2.1"; + /// An initializer class for the service account credential. + new public class Initializer : ServiceCredential.Initializer + { + /// Gets the service account ID (typically an e-mail address). + public string Id { get; private set; } + + /// + /// Gets or sets the email address of the user the application is trying to impersonate in the service + /// account flow or null. + /// + public string User { get; set; } + + /// Gets the scopes which indicate API access your application is requesting. + public IEnumerable Scopes { get; set; } + + /// + /// Gets or sets the key which is used to sign the request, as specified in + /// https://developers.google.com/accounts/docs/OAuth2ServiceAccount#computingsignature. + /// + public RsaKey Key { get; set; } + + /// Constructs a new initializer using the given id. + public Initializer(string id) + : this(id, GoogleAuthConsts.OidcTokenUrl) { } + + /// Constructs a new initializer using the given id and the token server URL. + public Initializer(string id, string tokenServerUrl) : base(tokenServerUrl) + { + Id = id; + Scopes = new List(); + } + + /// Extracts the from the given PKCS8 private key. + public Initializer FromPrivateKey(string privateKey) + { + RSAParameters rsaParameters = Pkcs8.DecodeRsaParameters(privateKey); + Key = (RsaKey)RSA.Create(); + Key.ImportParameters(rsaParameters); + return this; + } + + /// Extracts a from the given certificate. + 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; + } + } + + /// Unix epoch as a DateTime + 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 scopes; + private readonly RsaKey key; + + /// Gets the service account ID (typically an e-mail address). + public string Id { get { return id; } } + + /// + /// Gets the email address of the user the application is trying to impersonate in the service account flow + /// or null. + /// + public string User { get { return user; } } + + /// Gets the service account scopes. + public IEnumerable Scopes { get { return scopes; } } + + /// + /// Gets the key which is used to sign the request, as specified in + /// https://developers.google.com/accounts/docs/OAuth2ServiceAccount#computingsignature. + /// + public RsaKey Key { get { return key; } } + + /// true if this credential has any scopes associated with it. + internal bool HasScopes { get { return scopes != null && scopes.Any(); } } + + /// Constructs a new service account credential using the given initializer. + public ServiceAccountCredential(Initializer initializer) : base(initializer) + { + id = initializer.Id.ThrowIfNullOrEmpty("initializer.Id"); + user = initializer.User; + scopes = initializer.Scopes; + key = initializer.Key.ThrowIfNull("initializer.Key"); + } + + /// + /// Creates a new instance from JSON credential data. + /// + /// The stream from which to read the JSON key data for a service account. Must not be null. + /// + /// The does not contain valid JSON service account key data. + /// + /// The credentials parsed from the service account key data. + 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; + } + + /// + /// Requests a new token as specified in + /// https://developers.google.com/accounts/docs/OAuth2ServiceAccount#makingrequest. + /// + /// Cancellation token to cancel operation. + /// true if a new token was received successfully. + public override async Task 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; + } + + /// + /// Gets an access token to authorize a request. + /// If is set and this credential has no scopes associated + /// with it, a locally signed JWT access token for given + /// 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. + /// + /// The URI the returned token will grant access to. + /// The cancellation token. + /// The access token. + public override async Task 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); + } + + /// + /// 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. + /// The URI for which the access token will be valid. + /// + 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); + } + + /// + /// Signs JWT token using the private key and returns the serialized assertion. + /// + /// the JWT payload to sign. + 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(); + } + + /// + /// Creates a base64 encoded signature for the SHA-256 hash of the specified data. + /// + /// The data to hash and sign. Must not be null. + /// The base-64 encoded signature. + 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); + } + } + + /// + /// Creates a serialized header as specified in + /// https://developers.google.com/accounts/docs/OAuth2ServiceAccount#formingheader. + /// + private static string CreateSerializedHeader() + { + var header = new GoogleJsonWebSignature.Header() + { + Algorithm = "RS256", + Type = "JWT" + }; + + return NewtonsoftJsonSerializer.Instance.Serialize(header); + } + + /// + /// Creates a claim set as specified in + /// https://developers.google.com/accounts/docs/OAuth2ServiceAccount#formingclaimset. + /// + 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) + }; + } + + /// Encodes the provided UTF8 string into an URL safe base64 string. + /// Value to encode. + /// The URL safe base64 string. + private string UrlSafeBase64Encode(string value) + { + return UrlSafeBase64Encode(Encoding.UTF8.GetBytes(value)); + } + + /// Encodes the byte array into an URL safe base64 string. + /// Byte array to encode. + /// The URL safe base64 string. + private string UrlSafeBase64Encode(byte[] bytes) + { + return UrlSafeEncode(Convert.ToBase64String(bytes)); + } + + /// Encodes the base64 string into an URL safe string. + /// The base64 string to make URL safe. + /// The URL safe base64 string. + private string UrlSafeEncode(string base64Value) + { + return base64Value.Replace("=", String.Empty).Replace('+', '-').Replace('/', '_'); + } + } +} diff --git a/Yavsc/GoogleApiSupport/Google.Apis.Auth/OAuth2/ServiceCredential.cs b/Yavsc/GoogleApiSupport/Google.Apis.Auth/OAuth2/ServiceCredential.cs new file mode 100644 index 00000000..6cd4583e --- /dev/null +++ b/Yavsc/GoogleApiSupport/Google.Apis.Auth/OAuth2/ServiceCredential.cs @@ -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 +{ + /// + /// 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. + /// + /// ServiceAccountCredential inherits from this class in order to support Service Account. More + /// details available at: https://developers.google.com/accounts/docs/OAuth2ServiceAccount. + /// 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. + /// + /// + public abstract class ServiceCredential : ICredential, IHttpExecuteInterceptor, IHttpUnsuccessfulResponseHandler + { + /// Logger for this class + protected static readonly ILogger Logger = ApplicationContext.Logger.ForType(); + + /// An initializer class for the service credential. + public class Initializer + { + /// Gets the token server URL. + public string TokenServerUrl { get; private set; } + + /// + /// Gets or sets the clock used to refresh the token when it expires. The default value is + /// . + /// + public IClock Clock { get; set; } + + /// + /// Gets or sets the method for presenting the access token to the resource server. + /// The default value is . + /// + public IAccessMethod AccessMethod { get; set; } + + /// + /// Gets or sets the factory for creating a instance. + /// + public IHttpClientFactory HttpClientFactory { get; set; } + + /// + /// Get or sets the exponential back-off policy. Default value is UnsuccessfulResponse503, which + /// means that exponential back-off is used on 503 abnormal HTTP responses. + /// If the value is set to None, no exponential back-off policy is used, and it's up to the user to + /// configure the in an + /// to set a specific back-off + /// implementation (using ). + /// + public ExponentialBackOffPolicy DefaultExponentialBackOffPolicy { get; set; } + + /// Constructs a new initializer using the given token server URL. + 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 + + /// Gets the token server URL. + public string TokenServerUrl { get { return tokenServerUrl; } } + + /// Gets the clock used to refresh the token if it expires. + public IClock Clock { get { return clock; } } + + /// Gets the method for presenting the access token to the resource server. + public IAccessMethod AccessMethod { get { return accessMethod; } } + + /// Gets the HTTP client used to make authentication requests to the server. + public ConfigurableHttpClient HttpClient { get { return httpClient; } } + + private TokenResponse token; + private object lockObject = new object(); + + /// Gets the token response which contains the access token. + public TokenResponse Token + { + get + { + lock (lockObject) + { + return token; + } + } + protected set + { + lock (lockObject) + { + token = value; + } + } + } + + /// Constructs a new service account credential using the given initializer. + 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 + + /// + public void Initialize(ConfigurableHttpClient httpClient) + { + httpClient.MessageHandler.AddExecuteInterceptor(this); + httpClient.MessageHandler.AddUnsuccessfulResponseHandler(this); + } + + #endregion + + #region IHttpExecuteInterceptor implementation + + /// + 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 + + /// + /// Decorates unsuccessful responses, returns true if the response gets modified. + /// See IHttpUnsuccessfulResponseHandler for more information. + /// + public async Task 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 + + /// + /// Gets an access token to authorize a request. If the existing token has expired, try to refresh it first. + /// + /// + public virtual async Task 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 + + /// Requests a new token. + /// Cancellation token to cancel operation. + /// true if a new token was received successfully. + public abstract Task RequestAccessTokenAsync(CancellationToken taskCancellationToken); + } +} diff --git a/Yavsc/GoogleApiSupport/Google.Apis.Auth/OAuth2/UserCredential.cs b/Yavsc/GoogleApiSupport/Google.Apis.Auth/OAuth2/UserCredential.cs new file mode 100644 index 00000000..c80d4155 --- /dev/null +++ b/Yavsc/GoogleApiSupport/Google.Apis.Auth/OAuth2/UserCredential.cs @@ -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 +{ + /// + /// 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. + /// + public class UserCredential : ICredential, IHttpExecuteInterceptor, IHttpUnsuccessfulResponseHandler + { + /// Logger for this class. + protected static readonly ILogger Logger = ApplicationContext.Logger.ForType(); + + private TokenResponse token; + private object lockObject = new object(); + + /// Gets or sets the token response which contains the access token. + public TokenResponse Token + { + get + { + lock (lockObject) + { + return token; + } + } + set + { + lock (lockObject) + { + token = value; + } + } + } + + /// Gets the authorization code flow. + public IAuthorizationCodeFlow Flow + { + get { return flow; } + } + + /// Gets the user identity. + public string UserId + { + get { return userId; } + } + + private readonly IAuthorizationCodeFlow flow; + private readonly string userId; + + /// Constructs a new credential instance. + /// Authorization code flow. + /// User identifier. + /// An initial token for the user. + public UserCredential(IAuthorizationCodeFlow flow, string userId, TokenResponse token) + { + this.flow = flow; + this.userId = userId; + this.token = token; + } + + #region IHttpExecuteInterceptor + + /// + /// 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 . + /// + 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 + + /// + public async Task 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 + + /// + public void Initialize(ConfigurableHttpClient httpClient) + { + httpClient.MessageHandler.AddExecuteInterceptor(this); + httpClient.MessageHandler.AddUnsuccessfulResponseHandler(this); + } + + #endregion + + #region ITokenAccess implementation + + /// + public virtual async Task 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 + + /// + /// Refreshes the token by calling to + /// . + /// Then it updates the with the new token instance. + /// + /// Cancellation token to cancel an operation. + /// true if the token was refreshed. + public async Task 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; + } + + /// + /// Asynchronously revokes the token by calling + /// . + /// + /// Cancellation token to cancel an operation. + /// true if the token was revoked successfully. + public async Task 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; + } + } +} diff --git a/Yavsc/GoogleApiSupport/Google.Apis.Auth/OAuth2/Web/AuthWebUtility.cs b/Yavsc/GoogleApiSupport/Google.Apis.Auth/OAuth2/Web/AuthWebUtility.cs new file mode 100644 index 00000000..8d442d79 --- /dev/null +++ b/Yavsc/GoogleApiSupport/Google.Apis.Auth/OAuth2/Web/AuthWebUtility.cs @@ -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 +{ + /// Auth Utility methods for web development. + public class AuthWebUtility + { + /// Extracts the redirect URI from the state OAuth2 parameter. + /// + /// If the data store is not null, 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. + /// + /// The data store which contains the original state parameter. + /// User identifier. + /// + /// The authorization state parameter which we got back from the authorization server. + /// + /// Redirect URI to the address which initializes the authorization code flow. + public static async Task ExtracRedirectFromState(IDataStore dataStore, string userId, string state) + { + var oauthState = state; + if (dataStore != null) + { + var userKey = AuthorizationCodeWebApp.StateKey + userId; + var expectedState = await dataStore.GetAsync(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(userKey).ConfigureAwait(false); + oauthState = oauthState.Substring(0, oauthState.Length - AuthorizationCodeWebApp.StateRandomLength); + } + + return oauthState; + } + } +} diff --git a/Yavsc/GoogleApiSupport/Google.Apis.Auth/OAuth2/Web/AuthorizationCodeWebApp.cs b/Yavsc/GoogleApiSupport/Google.Apis.Auth/OAuth2/Web/AuthorizationCodeWebApp.cs new file mode 100644 index 00000000..c03dc0ed --- /dev/null +++ b/Yavsc/GoogleApiSupport/Google.Apis.Auth/OAuth2/Web/AuthorizationCodeWebApp.cs @@ -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 +{ + /// + /// Thread safe OAuth 2.0 authorization code flow for a web application that persists end-user credentials. + /// + public class AuthorizationCodeWebApp + { + /// + /// 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. + /// + public const string StateKey = "oauth_"; + + /// The length of the random number which will be added to the end of the state parameter. + public const int StateRandomLength = 8; + + /// + /// 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. + /// + public class AuthResult + { + /// + /// Gets or sets the user's credentials or null in case the end user needs to authorize. + /// + public UserCredential Credential { get; set; } + + /// + /// Gets or sets the redirect URI to for the user to authorize against the authorization server or + /// null in case the was loaded from the data + /// store. + /// + public string RedirectUri { get; set; } + } + + private readonly IAuthorizationCodeFlow flow; + private readonly string redirectUri; + private readonly string state; + + /// Gets the authorization code flow. + public IAuthorizationCodeFlow Flow + { + get { return flow; } + } + + /// Gets the OAuth2 callback redirect URI. + public string RedirectUri + { + get { return redirectUri; } + } + + /// Gets the state which is used to navigate back to the page that started the OAuth flow. + public string State + { + get { return state; } + } + + /// + /// Constructs a new authorization code installed application with the given flow and code receiver. + /// + 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; + } + + /// Asynchronously authorizes the web application to access user's protected data. + /// User identifier + /// Cancellation token to cancel an operation + /// + /// Auth result object which contains the user's credential or redirect URI for the authorization server + /// + public async Task 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) }; + } + + /// + /// Determines the need for retrieval of a new authorization code, based on the given token and the + /// authorization code flow. + /// + 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)); + } + } +} diff --git a/Yavsc/GoogleApiSupport/Google.Apis.Core/ApplicationContext.cs b/Yavsc/GoogleApiSupport/Google.Apis.Core/ApplicationContext.cs new file mode 100644 index 00000000..d316446b --- /dev/null +++ b/Yavsc/GoogleApiSupport/Google.Apis.Core/ApplicationContext.cs @@ -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 +{ + /// Defines the context in which this library runs. It allows setting up custom loggers. + public static class ApplicationContext + { + private static ILogger logger; + + // For testing + internal static void Reset() => logger = null; + + /// Returns the logger used within this application context. + /// It creates a if no logger was registered previously + public static ILogger Logger + { + get + { + // Register the default null-logger if no other one was set. + return logger ?? (logger = new NullLogger()); + } + } + + /// Registers a logger with this application context. + /// Thrown if a logger was already registered. + 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; + } + } +} diff --git a/Yavsc/GoogleApiSupport/Google.Apis.Core/Discovery/DiscoveryVersion.cs b/Yavsc/GoogleApiSupport/Google.Apis.Core/Discovery/DiscoveryVersion.cs new file mode 100644 index 00000000..d9fcdb55 --- /dev/null +++ b/Yavsc/GoogleApiSupport/Google.Apis.Core/Discovery/DiscoveryVersion.cs @@ -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 +{ + /// An enumeration of all supported discovery versions. + public enum DiscoveryVersion + { + /// Discovery version 1.0. + Version_1_0, + } +} \ No newline at end of file diff --git a/Yavsc/GoogleApiSupport/Google.Apis.Core/Discovery/Features.cs b/Yavsc/GoogleApiSupport/Google.Apis.Core/Discovery/Features.cs new file mode 100644 index 00000000..887a8e68 --- /dev/null +++ b/Yavsc/GoogleApiSupport/Google.Apis.Core/Discovery/Features.cs @@ -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 +{ + /// + /// Specifies a list of features which can be defined within the discovery document of a service. + /// + public enum Features + { + /// + /// If this feature is specified, then the data of a response is encapsulated within a "data" resource. + /// + [StringValue("dataWrapper")] + LegacyDataResponse, + } +} diff --git a/Yavsc/GoogleApiSupport/Google.Apis.Core/Discovery/IParameter.cs b/Yavsc/GoogleApiSupport/Google.Apis.Core/Discovery/IParameter.cs new file mode 100644 index 00000000..7688f4f6 --- /dev/null +++ b/Yavsc/GoogleApiSupport/Google.Apis.Core/Discovery/IParameter.cs @@ -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 +{ + /// Represents a parameter for a method. + public interface IParameter + { + /// Gets the name of the parameter. + string Name { get; } + + /// Gets the pattern that this parameter must follow. + string Pattern { get; } + + /// Gets an indication whether this parameter is optional or required. + bool IsRequired { get; } + + /// Gets the default value of this parameter. + string DefaultValue { get; } + + /// Gets the type of the parameter. + string ParameterType { get; } + } +} \ No newline at end of file diff --git a/Yavsc/GoogleApiSupport/Google.Apis.Core/Discovery/Parameter.cs b/Yavsc/GoogleApiSupport/Google.Apis.Core/Discovery/Parameter.cs new file mode 100644 index 00000000..7806b9fc --- /dev/null +++ b/Yavsc/GoogleApiSupport/Google.Apis.Core/Discovery/Parameter.cs @@ -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 +{ + /// Represents a method's parameter. + public class Parameter : IParameter + { + /// + public string Name { get; set; } + + /// + public string Pattern { get; set; } + + /// + public bool IsRequired { get; set; } + + /// + public string ParameterType { get; set; } + + /// + public string DefaultValue { get; set; } + } +} diff --git a/Yavsc/GoogleApiSupport/Google.Apis.Core/GoogleApiException.cs b/Yavsc/GoogleApiSupport/Google.Apis.Core/GoogleApiException.cs new file mode 100644 index 00000000..5efc2fde --- /dev/null +++ b/Yavsc/GoogleApiSupport/Google.Apis.Core/GoogleApiException.cs @@ -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 +{ + /// Represents an exception thrown by an API Service. + public class GoogleApiException : Exception + { + private readonly string serviceName; + + /// Gets the service name which related to this exception. + public string ServiceName + { + get { return serviceName; } + } + + /// Creates an API Service exception. + public GoogleApiException(string serviceName, string message, Exception inner) + : base(message, inner) + { + serviceName.ThrowIfNull("serviceName"); + this.serviceName = serviceName; + } + + /// Creates an API Service exception. + public GoogleApiException(string serviceName, string message) : this(serviceName, message, null) { } + + /// The Error which was returned from the server, or null if unavailable. + public RequestError Error { get; set; } + + /// The HTTP status code which was returned along with this error, or 0 if unavailable. + public HttpStatusCode HttpStatusCode { get; set; } + + /// + /// Returns a summary of this exception. + /// + /// A summary of this exception. + public override string ToString() + { + return string.Format("The service {1} has thrown an exception: {0}", base.ToString(), serviceName); + } + } +} diff --git a/Yavsc/GoogleApiSupport/Google.Apis.Core/Http/BackOffHandler.cs b/Yavsc/GoogleApiSupport/Google.Apis.Core/Http/BackOffHandler.cs new file mode 100644 index 00000000..dc12abfa --- /dev/null +++ b/Yavsc/GoogleApiSupport/Google.Apis.Core/Http/BackOffHandler.cs @@ -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 +{ + /// + /// A thread-safe back-off handler which handles an abnormal HTTP response or an exception with + /// . + /// + public class BackOffHandler : IHttpUnsuccessfulResponseHandler, IHttpExceptionHandler + { + private static readonly ILogger Logger = ApplicationContext.Logger.ForType(); + + /// An initializer class to initialize a back-off handler. + public class Initializer + { + /// Gets the back-off policy used by this back-off handler. + public IBackOff BackOff { get; private set; } + + /// + /// 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 false to both HandleExceptionAsync and + /// HandleResponseAsync. Default value is 16 seconds per a retry request. + /// + public TimeSpan MaxTimeSpan { get; set; } + + /// + /// Gets or sets a delegate function which indicates whether this back-off handler should handle an + /// abnormal HTTP response. The default is . + /// + public Func HandleUnsuccessfulResponseFunc { get; set; } + + /// + /// Gets or sets a delegate function which indicates whether this back-off handler should handle an + /// exception. The default is . + /// + public Func HandleExceptionFunc { get; set; } + + /// Default function which handles server errors (503). + public static readonly Func DefaultHandleUnsuccessfulResponseFunc = + (r) => (int)r.StatusCode == 503; + + /// + /// Default function which handles exception which aren't + /// or + /// . Those exceptions represent a task or an operation + /// which was canceled and shouldn't be retried. + /// + public static readonly Func DefaultHandleExceptionFunc = + (ex) => !(ex is TaskCanceledException || ex is OperationCanceledException); + + /// Constructs a new initializer by the given back-off. + public Initializer(IBackOff backOff) + { + BackOff = backOff; + HandleExceptionFunc = DefaultHandleExceptionFunc; + HandleUnsuccessfulResponseFunc = DefaultHandleUnsuccessfulResponseFunc; + MaxTimeSpan = TimeSpan.FromSeconds(16); + } + } + + /// Gets the back-off policy used by this back-off handler. + public IBackOff BackOff { get; private set; } + + /// + /// Gets the maximum time span to wait. If the back-off instance returns a greater time span, the handle method + /// returns false. Default value is 16 seconds per a retry request. + /// + public TimeSpan MaxTimeSpan { get; private set; } + + /// + /// Gets a delegate function which indicates whether this back-off handler should handle an abnormal HTTP + /// response. The default is . + /// + public Func HandleUnsuccessfulResponseFunc { get; private set; } + + /// + /// Gets a delegate function which indicates whether this back-off handler should handle an exception. The + /// default is . + /// + public Func HandleExceptionFunc { get; private set; } + + /// Constructs a new back-off handler with the given back-off. + /// The back-off policy. + public BackOffHandler(IBackOff backOff) + : this(new Initializer(backOff)) + { + } + + /// Constructs a new back-off handler with the given initializer. + public BackOffHandler(Initializer initializer) + { + BackOff = initializer.BackOff; + MaxTimeSpan = initializer.MaxTimeSpan; + HandleExceptionFunc = initializer.HandleExceptionFunc; + HandleUnsuccessfulResponseFunc = initializer.HandleUnsuccessfulResponseFunc; + } + + #region IHttpUnsuccessfulResponseHandler + + /// + public virtual async Task 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 + + /// + public virtual async Task 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 + + /// + /// 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 false. Otherwise the current thread + /// will block for x milliseconds (x is defined by the instance), and this handler + /// returns true. + /// + private async Task 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; + } + + /// Waits the given time span. Overriding this method is recommended for mocking purposes. + /// TimeSpan to wait (and block the current thread). + /// The cancellation token in case the user wants to cancel the operation in + /// the middle. + protected virtual async Task Wait(TimeSpan ts, CancellationToken cancellationToken) + { + await Task.Delay(ts, cancellationToken).ConfigureAwait(false); + } + } +} diff --git a/Yavsc/GoogleApiSupport/Google.Apis.Core/Http/ConfigurableHttpClient.cs b/Yavsc/GoogleApiSupport/Google.Apis.Core/Http/ConfigurableHttpClient.cs new file mode 100644 index 00000000..aaae764e --- /dev/null +++ b/Yavsc/GoogleApiSupport/Google.Apis.Core/Http/ConfigurableHttpClient.cs @@ -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 +{ + /// + /// Configurable HTTP client inherits from and contains a reference to + /// . + /// + public class ConfigurableHttpClient : HttpClient + { + /// Gets the configurable message handler. + public ConfigurableMessageHandler MessageHandler { get; private set; } + + /// Constructs a new HTTP client. + public ConfigurableHttpClient(ConfigurableMessageHandler handler) + : base(handler) + { + MessageHandler = handler; + DefaultRequestHeaders.ExpectContinue = false; + } + } +} diff --git a/Yavsc/GoogleApiSupport/Google.Apis.Core/Http/ConfigurableMessageHandler.cs b/Yavsc/GoogleApiSupport/Google.Apis.Core/Http/ConfigurableMessageHandler.cs new file mode 100644 index 00000000..fd1eef48 --- /dev/null +++ b/Yavsc/GoogleApiSupport/Google.Apis.Core/Http/ConfigurableMessageHandler.cs @@ -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 +{ + /// + /// A message handler which contains the main logic of our HTTP requests. It contains a list of + /// s for handling abnormal responses, a list of + /// s for handling exception in a request and a list of + /// 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. + /// + public class ConfigurableMessageHandler : DelegatingHandler + { + /// The class logger. + private static readonly ILogger Logger = ApplicationContext.Logger.ForType(); + + /// Maximum allowed number of tries. + [VisibleForTestOnly] + public const int MaxAllowedNumTries = 20; + + /// The current API version of this client library. + private static readonly string ApiVersion = Google.Apis.Util.Utilities.GetLibraryVersion(); + + /// The User-Agent suffix header which contains the . + 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 + + /// A list of . + private readonly IList unsuccessfulResponseHandlers = + new List(); + + /// A list of . + private readonly IList exceptionHandlers = + new List(); + + /// A list of . + private readonly IList executeInterceptors = + new List(); + + /// + /// Gets a list of s. + /// + /// Since version 1.10, and + /// were added in order to keep this class thread-safe. + /// More information is available on + /// #592. + /// + /// + [Obsolete("Use AddUnsuccessfulResponseHandler or RemoveUnsuccessfulResponseHandler instead.")] + public IList UnsuccessfulResponseHandlers + { + get { return unsuccessfulResponseHandlers; } + } + + /// Adds the specified handler to the list of unsuccessful response handlers. + public void AddUnsuccessfulResponseHandler(IHttpUnsuccessfulResponseHandler handler) + { + lock (unsuccessfulResponseHandlersLock) + { + unsuccessfulResponseHandlers.Add(handler); + } + } + + /// Removes the specified handler from the list of unsuccessful response handlers. + public void RemoveUnsuccessfulResponseHandler(IHttpUnsuccessfulResponseHandler handler) + { + lock (unsuccessfulResponseHandlersLock) + { + unsuccessfulResponseHandlers.Remove(handler); + } + } + + /// + /// Gets a list of s. + /// + /// Since version 1.10, and were added + /// in order to keep this class thread-safe. More information is available on + /// #592. + /// + /// + [Obsolete("Use AddExceptionHandler or RemoveExceptionHandler instead.")] + public IList ExceptionHandlers + { + get { return exceptionHandlers; } + } + + /// Adds the specified handler to the list of exception handlers. + public void AddExceptionHandler(IHttpExceptionHandler handler) + { + lock (exceptionHandlersLock) + { + exceptionHandlers.Add(handler); + } + } + + /// Removes the specified handler from the list of exception handlers. + public void RemoveExceptionHandler(IHttpExceptionHandler handler) + { + lock (exceptionHandlersLock) + { + exceptionHandlers.Remove(handler); + } + } + + /// + /// Gets a list of s. + /// + /// Since version 1.10, and were + /// added in order to keep this class thread-safe. More information is available on + /// #592. + /// + /// + [Obsolete("Use AddExecuteInterceptor or RemoveExecuteInterceptor instead.")] + public IList ExecuteInterceptors + { + get { return executeInterceptors; } + } + + /// Adds the specified interceptor to the list of execute interceptors. + public void AddExecuteInterceptor(IHttpExecuteInterceptor interceptor) + { + lock (executeInterceptorsLock) + { + executeInterceptors.Add(interceptor); + } + } + + /// Removes the specified interceptor from the list of execute interceptors. + public void RemoveExecuteInterceptor(IHttpExecuteInterceptor interceptor) + { + lock (executeInterceptorsLock) + { + executeInterceptors.Remove(interceptor); + } + } + + #endregion + + private int _loggingRequestId = 0; + + private ILogger _instanceLogger = Logger; + + /// + /// For testing only. + /// This defaults to the static , but can be overridden for fine-grain testing. + /// + internal ILogger InstanceLogger + { + get { return _instanceLogger; } + set { _instanceLogger = value.ForType(); } + } + + /// Number of tries. Default is 3. + private int numTries = 3; + + /// + /// Gets or sets the number of tries that will be allowed to execute. Retries occur as a result of either + /// or which handles the + /// abnormal HTTP response or exception before being terminated. + /// Set 1 for not retrying requests. The default value is 3. + /// + /// The number of allowed redirects (3xx) is defined by . This property defines + /// only the allowed tries for >=400 responses, or when an exception is thrown. For example if you set + /// to 1 and 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. + /// + /// + public int NumTries + { + get { return numTries; } + set + { + if (value > MaxAllowedNumTries || value < 1) + { + throw new ArgumentOutOfRangeException("NumTries"); + } + numTries = value; + } + } + + /// Number of redirects allowed. Default is 10. + private int numRedirects = 10; + + /// + /// Gets or sets the number of redirects that will be allowed to execute. The default value is 10. + /// See for more information. + /// + public int NumRedirects + { + get { return numRedirects; } + set + { + if (value > MaxAllowedNumTries || value < 1) + { + throw new ArgumentOutOfRangeException("NumRedirects"); + } + numRedirects = value; + } + } + + /// + /// Gets or sets whether the handler should follow a redirect when a redirect response is received. Default + /// value is true. + /// + public bool FollowRedirect { get; set; } + + /// Gets or sets whether logging is enabled. Default value is true. + public bool IsLoggingEnabled { get; set; } + + /// + /// Specifies the type(s) of request/response events to log. + /// + [Flags] + public enum LogEventType + { + /// + /// Log no request/response information. + /// + None = 0, + + /// + /// Log the request URI. + /// + RequestUri = 1, + + /// + /// Log the request headers. + /// + RequestHeaders = 2, + + /// + /// 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. + /// + RequestBody = 4, + + /// + /// Log the response status. + /// + ResponseStatus = 8, + + /// + /// Log the response headers. + /// + ResponseHeaders = 16, + + /// + /// 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. + /// + ResponseBody = 32, + + /// + /// Log abnormal response messages. + /// + ResponseAbnormal = 64, + } + + /// + /// The request/response types to log. + /// + public LogEventType LogEvents { get; set; } + + /// Gets or sets the application name which will be used on the User-Agent header. + public string ApplicationName { get; set; } + + /// Constructs a new configurable message handler. + 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>>()) + .Concat(headers2 ?? Enumerable.Empty>>()).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)); + } + + /// + /// The main logic of sending a request to the server. This send method adds the User-Agent header to a request + /// with and the library version. It also calls interceptors before each attempt, + /// and unsuccessful response handler or exception handlers when abnormal response or exception occurred. + /// + protected override async Task 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 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 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 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; + } + + /// + /// 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 303 the method on the request is changed to a GET as per the RFC2616 + /// specification. On a redirect, it also removes the Authorization and all If-* request headers. + /// + /// Whether this method changed the request and handled redirect successfully. + 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; + } + } +} diff --git a/Yavsc/GoogleApiSupport/Google.Apis.Core/Http/ExponentialBackOffInitializer.cs b/Yavsc/GoogleApiSupport/Google.Apis.Core/Http/ExponentialBackOffInitializer.cs new file mode 100644 index 00000000..2f2c5953 --- /dev/null +++ b/Yavsc/GoogleApiSupport/Google.Apis.Core/Http/ExponentialBackOffInitializer.cs @@ -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 +{ + /// + /// Indicates if exponential back-off is used automatically on exceptions in a service requests and \ or when 503 + /// responses is returned form the server. + /// + [Flags] + public enum ExponentialBackOffPolicy + { + /// Exponential back-off is disabled. + None = 0, + /// Exponential back-off is enabled only for exceptions. + Exception = 1, + /// Exponential back-off is enabled only for 503 HTTP Status code. + UnsuccessfulResponse503 = 2 + } + + /// + /// An initializer which adds exponential back-off as exception handler and \ or unsuccessful response handler by + /// the given . + /// + public class ExponentialBackOffInitializer : IConfigurableHttpClientInitializer + { + /// Gets or sets the used back-off policy. + private ExponentialBackOffPolicy Policy { get; set; } + + /// Gets or sets the back-off handler creation function. + private Func CreateBackOff { get; set; } + + /// + /// Constructs a new back-off initializer with the given policy and back-off handler create function. + /// + public ExponentialBackOffInitializer(ExponentialBackOffPolicy policy, Func createBackOff) + { + Policy = policy; + CreateBackOff = createBackOff; + } + + /// + 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); + } + } + } + +} diff --git a/Yavsc/GoogleApiSupport/Google.Apis.Core/Http/HttpClientFactory.cs b/Yavsc/GoogleApiSupport/Google.Apis.Core/Http/HttpClientFactory.cs new file mode 100644 index 00000000..ac2448eb --- /dev/null +++ b/Yavsc/GoogleApiSupport/Google.Apis.Core/Http/HttpClientFactory.cs @@ -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 +{ + /// The default implementation of the HTTP client factory. + public class HttpClientFactory : IHttpClientFactory + { + /// The class logger. + private static readonly ILogger Logger = ApplicationContext.Logger.ForType(); + + /// + 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; + } + + /// Creates a HTTP message handler. Override this method to mock a message handler. + 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; + } + } +} diff --git a/Yavsc/GoogleApiSupport/Google.Apis.Core/Http/HttpConsts.cs b/Yavsc/GoogleApiSupport/Google.Apis.Core/Http/HttpConsts.cs new file mode 100644 index 00000000..c69ee820 --- /dev/null +++ b/Yavsc/GoogleApiSupport/Google.Apis.Core/Http/HttpConsts.cs @@ -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 +{ + /// HTTP constants. + public static class HttpConsts + { + /// Http GET request + public const string Get = "GET"; + + /// Http DELETE request + public const string Delete = "DELETE"; + + /// Http PUT request + public const string Put = "PUT"; + + /// Http POST request + public const string Post = "POST"; + + /// Http PATCH request + public const string Patch = "PATCH"; + } +} diff --git a/Yavsc/GoogleApiSupport/Google.Apis.Core/Http/HttpExtenstions.cs b/Yavsc/GoogleApiSupport/Google.Apis.Core/Http/HttpExtenstions.cs new file mode 100644 index 00000000..579d8e12 --- /dev/null +++ b/Yavsc/GoogleApiSupport/Google.Apis.Core/Http/HttpExtenstions.cs @@ -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 +{ + /// + /// Extension methods to and + /// . + /// + public static class HttpExtenstions + { + /// Returns true if the response contains one of the redirect status codes. + 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; + } + } + + /// A Google.Apis utility method for setting an empty HTTP content. + public static HttpContent SetEmptyContent(this HttpRequestMessage request) + { + request.Content = new ByteArrayContent(new byte[0]); + request.Content.Headers.ContentLength = 0; + return request.Content; + } + } +} diff --git a/Yavsc/GoogleApiSupport/Google.Apis.Core/Http/IConfigurableHttpClientInitializer.cs b/Yavsc/GoogleApiSupport/Google.Apis.Core/Http/IConfigurableHttpClientInitializer.cs new file mode 100644 index 00000000..3882a05d --- /dev/null +++ b/Yavsc/GoogleApiSupport/Google.Apis.Core/Http/IConfigurableHttpClientInitializer.cs @@ -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 +{ + /// + /// 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 s, + /// s and s. + /// + public interface IConfigurableHttpClientInitializer + { + /// Initializes a HTTP client after it was created. + void Initialize(ConfigurableHttpClient httpClient); + } +} diff --git a/Yavsc/GoogleApiSupport/Google.Apis.Core/Http/IHttpClientFactory.cs b/Yavsc/GoogleApiSupport/Google.Apis.Core/Http/IHttpClientFactory.cs new file mode 100644 index 00000000..32021b17 --- /dev/null +++ b/Yavsc/GoogleApiSupport/Google.Apis.Core/Http/IHttpClientFactory.cs @@ -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 +{ + /// Arguments for creating a HTTP client. + public class CreateHttpClientArgs + { + /// Gets or sets whether GZip is enabled. + public bool GZipEnabled { get; set; } + + /// Gets or sets the application name that is sent in the User-Agent header. + public string ApplicationName { get; set; } + + /// Gets a list of initializers to initialize the HTTP client instance. + public IList Initializers { get; private set; } + + /// Constructs a new argument instance. + public CreateHttpClientArgs() + { + Initializers = new List(); + } + } + + /// + /// HTTP client factory creates configurable HTTP clients. A unique HTTP client should be created for each service. + /// + public interface IHttpClientFactory + { + /// Creates a new configurable HTTP client. + ConfigurableHttpClient CreateHttpClient(CreateHttpClientArgs args); + } +} diff --git a/Yavsc/GoogleApiSupport/Google.Apis.Core/Http/IHttpExceptionHandler.cs b/Yavsc/GoogleApiSupport/Google.Apis.Core/Http/IHttpExceptionHandler.cs new file mode 100644 index 00000000..2ad9cf11 --- /dev/null +++ b/Yavsc/GoogleApiSupport/Google.Apis.Core/Http/IHttpExceptionHandler.cs @@ -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 +{ + /// Argument class to . + public class HandleExceptionArgs + { + /// Gets or sets the sent request. + public HttpRequestMessage Request { get; set; } + + /// Gets or sets the exception which occurred during sending the request. + public Exception Exception { get; set; } + + /// Gets or sets the total number of tries to send the request. + public int TotalTries { get; set; } + + /// Gets or sets the current failed try. + public int CurrentFailedTry { get; set; } + + /// Gets an indication whether a retry will occur if the handler returns true. + public bool SupportsRetry + { + get { return TotalTries - CurrentFailedTry > 0; } + } + + /// Gets or sets the request's cancellation token. + public CancellationToken CancellationToken { get; set; } + } + + /// Exception handler is invoked when an exception is thrown during a HTTP request. + public interface IHttpExceptionHandler + { + /// + /// 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 true. + /// + /// + /// Handle exception argument which properties such as the request, exception, current failed try. + /// + /// Whether this handler has made a change that requires the request to be resent. + Task HandleExceptionAsync(HandleExceptionArgs args); + } +} diff --git a/Yavsc/GoogleApiSupport/Google.Apis.Core/Http/IHttpExecuteInterceptor.cs b/Yavsc/GoogleApiSupport/Google.Apis.Core/Http/IHttpExecuteInterceptor.cs new file mode 100644 index 00000000..3ffcbaa4 --- /dev/null +++ b/Yavsc/GoogleApiSupport/Google.Apis.Core/Http/IHttpExecuteInterceptor.cs @@ -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 +{ + /// + /// HTTP request execute interceptor to intercept a before it has + /// been sent. Sample usage is attaching "Authorization" header to a request. + /// + public interface IHttpExecuteInterceptor + { + /// + /// Invoked before the request is being sent. + /// + /// The HTTP request message. + /// Cancellation token to cancel the operation. + Task InterceptAsync(HttpRequestMessage request, CancellationToken cancellationToken); + } +} diff --git a/Yavsc/GoogleApiSupport/Google.Apis.Core/Http/IHttpUnsuccessfulResponseHandler.cs b/Yavsc/GoogleApiSupport/Google.Apis.Core/Http/IHttpUnsuccessfulResponseHandler.cs new file mode 100644 index 00000000..3c0b9a11 --- /dev/null +++ b/Yavsc/GoogleApiSupport/Google.Apis.Core/Http/IHttpUnsuccessfulResponseHandler.cs @@ -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 +{ + /// Argument class to . + public class HandleUnsuccessfulResponseArgs + { + /// Gets or sets the sent request. + public HttpRequestMessage Request { get; set; } + + /// Gets or sets the abnormal response. + public HttpResponseMessage Response { get; set; } + + /// Gets or sets the total number of tries to send the request. + public int TotalTries { get; set; } + + /// Gets or sets the current failed try. + public int CurrentFailedTry { get; set; } + + /// Gets an indication whether a retry will occur if the handler returns true. + public bool SupportsRetry + { + get { return TotalTries - CurrentFailedTry > 0; } + } + + /// Gets or sets the request's cancellation token. + public CancellationToken CancellationToken { get; set; } + } + + /// + /// Unsuccessful response handler which is invoked when an abnormal HTTP response is returned when sending a HTTP + /// request. + /// + public interface IHttpUnsuccessfulResponseHandler + { + /// + /// 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 true. + /// + /// + /// Handle response argument which contains properties such as the request, response, current failed try. + /// + /// Whether this handler has made a change that requires the request to be resent. + Task HandleResponseAsync(HandleUnsuccessfulResponseArgs args); + } +} diff --git a/Yavsc/GoogleApiSupport/Google.Apis.Core/Http/MaxUrlLengthInterceptor.cs b/Yavsc/GoogleApiSupport/Google.Apis.Core/Http/MaxUrlLengthInterceptor.cs new file mode 100644 index 00000000..324ba399 --- /dev/null +++ b/Yavsc/GoogleApiSupport/Google.Apis.Core/Http/MaxUrlLengthInterceptor.cs @@ -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 +{ + /// + /// Intercepts HTTP GET requests with a URLs longer than a specified maximum number of characters. + /// The interceptor will change such requests as follows: + /// + /// The request's method will be changed to POST + /// A X-HTTP-Method-Override header will be added with the value GET + /// Any query parameters from the URI will be moved into the body of the request. + /// If query parameters are moved, the content type is set to application/x-www-form-urlencoded + /// + /// + [VisibleForTestOnly] + public class MaxUrlLengthInterceptor : IHttpExecuteInterceptor + { + private readonly uint maxUrlLength; + + ///Constructs a new Max URL length interceptor with the given max length. + public MaxUrlLengthInterceptor(uint maxUrlLength) + { + this.maxUrlLength = maxUrlLength; + } + + /// + 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); + } + } +} diff --git a/Yavsc/GoogleApiSupport/Google.Apis.Core/ISerializer.cs b/Yavsc/GoogleApiSupport/Google.Apis.Core/ISerializer.cs new file mode 100644 index 00000000..ad296e1f --- /dev/null +++ b/Yavsc/GoogleApiSupport/Google.Apis.Core/ISerializer.cs @@ -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 +{ + /// Serialization interface that supports serialize and deserialize methods. + public interface ISerializer + { + /// Gets the application format this serializer supports (e.g. "json", "xml", etc.). + string Format { get; } + + /// Serializes the specified object into a Stream. + void Serialize(object obj, Stream target); + + /// Serializes the specified object into a string. + string Serialize(object obj); + + /// Deserializes the string into an object. + T Deserialize(string input); + + /// Deserializes the string into an object. + object Deserialize(string input, Type type); + + /// Deserializes the stream into an object. + T Deserialize(Stream input); + } +} diff --git a/Yavsc/GoogleApiSupport/Google.Apis.Core/Json/IJsonSerializer.cs b/Yavsc/GoogleApiSupport/Google.Apis.Core/Json/IJsonSerializer.cs new file mode 100644 index 00000000..83e0cf0a --- /dev/null +++ b/Yavsc/GoogleApiSupport/Google.Apis.Core/Json/IJsonSerializer.cs @@ -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 +{ + /// Represents a JSON serializer. + public interface IJsonSerializer : ISerializer + { + } +} diff --git a/Yavsc/GoogleApiSupport/Google.Apis.Core/Json/JsonExplicitNull.cs b/Yavsc/GoogleApiSupport/Google.Apis.Core/Json/JsonExplicitNull.cs new file mode 100644 index 00000000..22ca2940 --- /dev/null +++ b/Yavsc/GoogleApiSupport/Google.Apis.Core/Json/JsonExplicitNull.cs @@ -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 +{ + /// + /// Provides values which are explicitly expressed as null when converted to JSON. + /// + public static class JsonExplicitNull + { + /// + /// Get an that is explicitly expressed as null when converted to JSON. + /// + /// An that is explicitly expressed as null when converted to JSON. + public static IList ForIList() => ExplicitNullList.Instance; + + [JsonExplicitNull] + private sealed class ExplicitNullList : IList + { + public static ExplicitNullList Instance = new ExplicitNullList(); + + 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 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(); + } + } + } +} diff --git a/Yavsc/GoogleApiSupport/Google.Apis.Core/Json/JsonExplicitNullAttribute.cs b/Yavsc/GoogleApiSupport/Google.Apis.Core/Json/JsonExplicitNullAttribute.cs new file mode 100644 index 00000000..6a4da6a7 --- /dev/null +++ b/Yavsc/GoogleApiSupport/Google.Apis.Core/Json/JsonExplicitNullAttribute.cs @@ -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 +{ + /// + /// All values of a type with this attribute are represented as a literal null in JSON. + /// + [AttributeUsage(AttributeTargets.Class)] + public class JsonExplicitNullAttribute : Attribute { } +} diff --git a/Yavsc/GoogleApiSupport/Google.Apis.Core/Json/NewtonsoftJsonSerializer.cs b/Yavsc/GoogleApiSupport/Google.Apis.Core/Json/NewtonsoftJsonSerializer.cs new file mode 100644 index 00000000..9ef36e9c --- /dev/null +++ b/Yavsc/GoogleApiSupport/Google.Apis.Core/Json/NewtonsoftJsonSerializer.cs @@ -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 +{ + /// + /// A JSON converter which honers RFC 3339 and the serialized date is accepted by Google services. + /// + public class RFC3339DateTimeConverter : JsonConverter + { + /// + public override bool CanRead => false; + + /// + public override object ReadJson(Newtonsoft.Json.JsonReader reader, Type objectType, object existingValue, + JsonSerializer serializer) + { + throw new NotImplementedException("Unnecessary because CanRead is false."); + } + + /// + public override bool CanConvert(Type objectType) => + // Convert DateTime only. + objectType == typeof(DateTime) || objectType == typeof(Nullable); + + /// + public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer) + { + if (value != null) + { + DateTime date = (DateTime)value; + serializer.Serialize(writer, Utilities.ConvertToRFC3339(date)); + } + } + } + + /// + /// A JSON converter to write null literals into JSON when explicitly requested. + /// + public class ExplicitNullConverter : JsonConverter + { + /// + public override bool CanRead => false; + + /// + public override bool CanConvert(Type objectType) => objectType.GetTypeInfo().GetCustomAttributes(typeof(JsonExplicitNullAttribute), false).Any(); + + /// + public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer) + { + throw new NotImplementedException("Unnecessary because CanRead is false."); + } + + /// + public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer) => writer.WriteNull(); + } + + /// Class for serialization and deserialization of JSON documents using the Newtonsoft Library. + public class NewtonsoftJsonSerializer : IJsonSerializer + { + private readonly JsonSerializerSettings settings; + private readonly JsonSerializer serializer; + + /// The default instance of the Newtonsoft JSON Serializer, with default settings. + public static NewtonsoftJsonSerializer Instance { get; } = new NewtonsoftJsonSerializer(); + + /// + /// Constructs a new instance with the default serialization settings, equivalent to . + /// + public NewtonsoftJsonSerializer() : this(CreateDefaultSettings()) + { + } + + /// + /// Constructs a new instance with the given settings. + /// + /// The settings to apply when serializing and deserializing. Must not be null. + public NewtonsoftJsonSerializer(JsonSerializerSettings settings) + { + Utilities.ThrowIfNull(settings, nameof(settings)); + this.settings = settings; + serializer = JsonSerializer.Create(settings); + } + + /// + /// Creates a new instance of with the same behavior + /// as the ones used in . This method is expected to be used to construct + /// settings which are then passed to . + /// + /// A new set of default settings. + public static JsonSerializerSettings CreateDefaultSettings() => + new JsonSerializerSettings + { + NullValueHandling = NullValueHandling.Ignore, + MetadataPropertyHandling = MetadataPropertyHandling.Ignore, + Converters = { new RFC3339DateTimeConverter(), new ExplicitNullConverter() } + }; + + /// + public string Format => "json"; + + /// + public void Serialize(object obj, Stream target) + { + using (var writer = new StreamWriter(target)) + { + if (obj == null) + { + obj = string.Empty; + } + serializer.Serialize(writer, obj); + } + } + + /// + public string Serialize(object obj) + { + using (TextWriter tw = new StringWriter()) + { + if (obj == null) + { + obj = string.Empty; + } + serializer.Serialize(tw, obj); + return tw.ToString(); + } + } + + /// + public T Deserialize(string input) + { + if (string.IsNullOrEmpty(input)) + { + return default(T); + } + return JsonConvert.DeserializeObject(input, settings); + } + + /// + public object Deserialize(string input, Type type) + { + if (string.IsNullOrEmpty(input)) + { + return null; + } + return JsonConvert.DeserializeObject(input, type, settings); + } + + /// + public T Deserialize(Stream input) + { + // Convert the JSON document into an object. + using (StreamReader streamReader = new StreamReader(input)) + { + return (T)serializer.Deserialize(streamReader, typeof(T)); + } + } + } +} diff --git a/Yavsc/GoogleApiSupport/Google.Apis.Core/Logging/BaseLogger.cs b/Yavsc/GoogleApiSupport/Google.Apis.Core/Logging/BaseLogger.cs new file mode 100644 index 00000000..e3123e1c --- /dev/null +++ b/Yavsc/GoogleApiSupport/Google.Apis.Core/Logging/BaseLogger.cs @@ -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 +{ + /// + /// An abstract base logger, upon which real loggers may be built. + /// + 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"; + + /// + /// Construct a . + /// + /// Logging will be enabled at this level and all higher levels. + /// The to use to timestamp log entries. + /// The type from which entries are being logged. May be null. + 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; + + /// + /// The being used to timestamp log entries. + /// + public IClock Clock { get; } + + /// + /// The type from which entries are being logged. May be null. + /// + public Type LoggerForType { get; } + + /// + /// Logging is enabled at this level and all higher levels. + /// + public LogLevel MinimumLogLevel { get; } + + /// + /// Is Debug level logging enabled? + /// + public bool IsDebugEnabled { get; } + + /// + /// Is info level logging enabled? + /// + public bool IsInfoEnabled { get; } + + /// + /// Is warning level logging enabled? + /// + public bool IsWarningEnabled { get; } + + /// + /// Is error level logging enabled? + /// + public bool IsErrorEnabled { get; } + + /// + /// Build a new logger of the derived concrete type, for use to log from the specified type. + /// + /// The type from which entries are being logged. + /// A new instance, logging from the specified type. + protected abstract ILogger BuildNewLogger(Type type); + + /// + public ILogger ForType() => ForType(typeof(T)); + + /// + public ILogger ForType(Type type) => type == LoggerForType ? this : BuildNewLogger(type); + + /// + /// Perform the actual logging. + /// + /// The of this log entry. + /// The fully formatted log message, ready for logging. + 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}"; + } + + /// + public void Debug(string message, params object[] formatArgs) + { + if (IsDebugEnabled) + { + Log(LogLevel.Debug, FormatLogEntry("D", message, formatArgs)); + } + } + + /// + public void Info(string message, params object[] formatArgs) + { + if (IsInfoEnabled) + { + Log(LogLevel.Info, FormatLogEntry("I", message, formatArgs)); + } + } + + /// + public void Warning(string message, params object[] formatArgs) + { + if (IsWarningEnabled) + { + Log(LogLevel.Warning, FormatLogEntry("W", message, formatArgs)); + } + } + + /// + public void Error(Exception exception, string message, params object[] formatArgs) + { + if (IsErrorEnabled) + { + Log(LogLevel.Error, $"{FormatLogEntry("E", message, formatArgs)} {exception}"); + } + } + + /// + public void Error(string message, params object[] formatArgs) + { + if (IsErrorEnabled) + { + Log(LogLevel.Error, FormatLogEntry("E", message, formatArgs)); + } + } + + } +} diff --git a/Yavsc/GoogleApiSupport/Google.Apis.Core/Logging/ConsoleLogger.cs b/Yavsc/GoogleApiSupport/Google.Apis.Core/Logging/ConsoleLogger.cs new file mode 100644 index 00000000..9161edca --- /dev/null +++ b/Yavsc/GoogleApiSupport/Google.Apis.Core/Logging/ConsoleLogger.cs @@ -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 +{ + /// + /// A logger than logs to StdError or StdOut. + /// + public sealed class ConsoleLogger : BaseLogger, ILogger + { + /// + /// Construct a . + /// + /// Logging will be enabled at this level and all higher levels. + /// true to log to StdOut, defaults to logging to StdError. + /// Optional ; will use the system clock if null. + 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; + } + + /// + /// false to log to StdError; true to log to StdOut. + /// + public bool LogToStdOut { get; } + + /// + protected override ILogger BuildNewLogger(Type type) => new ConsoleLogger(MinimumLogLevel, LogToStdOut, Clock, type); + + /// + protected override void Log(LogLevel logLevel, string formattedMessage) + { + (LogToStdOut ? Console.Out : Console.Error).WriteLine(formattedMessage); + } + } +} diff --git a/Yavsc/GoogleApiSupport/Google.Apis.Core/Logging/ILogger.cs b/Yavsc/GoogleApiSupport/Google.Apis.Core/Logging/ILogger.cs new file mode 100644 index 00000000..dbdbbd2d --- /dev/null +++ b/Yavsc/GoogleApiSupport/Google.Apis.Core/Logging/ILogger.cs @@ -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 +{ + /// Describes a logging interface which is used for outputting messages. + public interface ILogger + { + /// Gets an indication whether debug output is logged or not. + bool IsDebugEnabled { get; } + + /// Returns a logger which will be associated with the specified type. + /// Type to which this logger belongs. + /// A type-associated logger. + ILogger ForType(Type type); + + /// Returns a logger which will be associated with the specified type. + /// A type-associated logger. + ILogger ForType(); + + /// Logs a debug message. + /// The message to log. + /// String.Format arguments (if applicable). + void Debug(string message, params object[] formatArgs); + + /// Logs an info message. + /// The message to log. + /// String.Format arguments (if applicable). + void Info(string message, params object[] formatArgs); + + /// Logs a warning. + /// The message to log. + /// String.Format arguments (if applicable). + void Warning(string message, params object[] formatArgs); + + /// Logs an error message resulting from an exception. + /// + /// The message to log. + /// String.Format arguments (if applicable). + void Error(Exception exception, string message, params object[] formatArgs); + + /// Logs an error message. + /// The message to log. + /// String.Format arguments (if applicable). + void Error(string message, params object[] formatArgs); + } +} diff --git a/Yavsc/GoogleApiSupport/Google.Apis.Core/Logging/LogLevel.cs b/Yavsc/GoogleApiSupport/Google.Apis.Core/Logging/LogLevel.cs new file mode 100644 index 00000000..d73c8b16 --- /dev/null +++ b/Yavsc/GoogleApiSupport/Google.Apis.Core/Logging/LogLevel.cs @@ -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 +{ + /// + /// The supported logging levels. + /// + public enum LogLevel + { + /// + /// A value lower than all logging levels. + /// + All = 0, + + /// + /// Debug logging. + /// + Debug = 100, + + /// + /// Info logging. + /// + Info = 200, + + /// + /// Warning logging. + /// + Warning = 300, + + /// + /// Error logging. + /// + Error = 400, + + /// + /// A value higher than all logging levels. + /// + None = 1000, + } +} diff --git a/Yavsc/GoogleApiSupport/Google.Apis.Core/Logging/MemoryLogger.cs b/Yavsc/GoogleApiSupport/Google.Apis.Core/Logging/MemoryLogger.cs new file mode 100644 index 00000000..7cdebc03 --- /dev/null +++ b/Yavsc/GoogleApiSupport/Google.Apis.Core/Logging/MemoryLogger.cs @@ -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 +{ + /// + /// A logger than logs to an in-memory buffer. + /// Generally for use during tests. + /// + public sealed class MemoryLogger : BaseLogger, ILogger + { + /// + /// Construct a . + /// + /// Logging will be enabled at this level and all higher levels. + /// The maximum number of log entries. Further log entries will be silently discarded. + /// Optional ; will use the system clock if null. + public MemoryLogger(LogLevel minimumLogLevel, int maximumEntryCount = 1000, IClock clock = null) : + this(minimumLogLevel, maximumEntryCount, clock, new List(), null) { } + + private MemoryLogger(LogLevel minimumLogLevel, int maximumEntryCount, IClock clock, List logEntries, Type forType) : base(minimumLogLevel, clock, forType) + { + _logEntries = logEntries; + LogEntries = new ReadOnlyCollection(_logEntries); + _maximumEntryCount = maximumEntryCount; + } + + private readonly int _maximumEntryCount; + + // This list is shared between all derived MemoryLogger instances + private readonly List _logEntries; + + /// + /// The list of log entries. + /// + public IList LogEntries { get; } + + /// + protected override ILogger BuildNewLogger(Type type) => new MemoryLogger(MinimumLogLevel, _maximumEntryCount, Clock, _logEntries, type); + + /// + protected override void Log(LogLevel logLevel, string formattedMessage) + { + lock (_logEntries) + { + if (_logEntries.Count < _maximumEntryCount) + { + _logEntries.Add(formattedMessage); + } + } + } + + } +} diff --git a/Yavsc/GoogleApiSupport/Google.Apis.Core/Logging/NullLogger.cs b/Yavsc/GoogleApiSupport/Google.Apis.Core/Logging/NullLogger.cs new file mode 100644 index 00000000..79ba3847 --- /dev/null +++ b/Yavsc/GoogleApiSupport/Google.Apis.Core/Logging/NullLogger.cs @@ -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 +{ + /// + /// Represents a NullLogger which does not do any logging. + /// + public class NullLogger : ILogger + { + /// + public bool IsDebugEnabled + { + get { return false; } + } + + /// + public ILogger ForType(Type type) + { + return new NullLogger(); + } + + /// + public ILogger ForType() + { + return new NullLogger(); + } + + /// + public void Info(string message, params object[] formatArgs) {} + + /// + public void Warning(string message, params object[] formatArgs) {} + + /// + public void Debug(string message, params object[] formatArgs) {} + + /// + public void Error(Exception exception, string message, params object[] formatArgs) {} + + /// + public void Error(string message, params object[] formatArgs) {} + } +} \ No newline at end of file diff --git a/Yavsc/GoogleApiSupport/Google.Apis.Core/Requests/Parameters/ParameterCollection.cs b/Yavsc/GoogleApiSupport/Google.Apis.Core/Requests/Parameters/ParameterCollection.cs new file mode 100644 index 00000000..c3c0d85e --- /dev/null +++ b/Yavsc/GoogleApiSupport/Google.Apis.Core/Requests/Parameters/ParameterCollection.cs @@ -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 +{ + /// A collection of parameters (key value pairs). May contain duplicate keys. + public class ParameterCollection : List> + { + /// Constructs a new parameter collection. + public ParameterCollection() : base() { } + + /// Constructs a new parameter collection from the given collection. + public ParameterCollection(IEnumerable> collection) : base(collection) { } + + /// Adds a single parameter to this collection. + public void Add(string key, string value) + { + Add(new KeyValuePair(key, value)); + } + + /// Returns true if this parameter is set within the collection. + public bool ContainsKey(string key) + { + key.ThrowIfNullOrEmpty("key"); + string value; + return TryGetValue(key, out value); + } + + /// + /// 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. + /// + public bool TryGetValue(string key, out string value) + { + key.ThrowIfNullOrEmpty("key"); + + foreach (KeyValuePair 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; + } + + /// + /// Returns the value of the first matching key, or throws a KeyNotFoundException if the parameter is not + /// present within the collection. + /// + 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; + } + + /// + /// Returns all matches for the specified key. May return an empty enumeration if the key is not present. + /// + public IEnumerable GetAllMatches(string key) + { + key.ThrowIfNullOrEmpty("key"); + + foreach (KeyValuePair pair in this) + { + // Check if this pair matches the specified key name. + if (pair.Key.Equals(key)) + { + yield return pair.Value; + } + } + } + + /// + /// Returns all matches for the specified key. May return an empty enumeration if the key is not present. + /// + public IEnumerable this[string key] + { + get { return GetAllMatches(key); } + } + + /// + /// 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. + /// + 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; + } + + /// + /// 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. + /// + public static ParameterCollection FromDictionary(IDictionary dictionary) + { + var collection = new ParameterCollection(); + foreach (KeyValuePair 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; + } + } +} diff --git a/Yavsc/GoogleApiSupport/Google.Apis.Core/Requests/Parameters/ParameterUtils.cs b/Yavsc/GoogleApiSupport/Google.Apis.Core/Requests/Parameters/ParameterUtils.cs new file mode 100644 index 00000000..f65890b3 --- /dev/null +++ b/Yavsc/GoogleApiSupport/Google.Apis.Core/Requests/Parameters/ParameterUtils.cs @@ -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 +{ + /// + /// Utility class for iterating on properties in a request object. + /// + public static class ParameterUtils + { + private static readonly ILogger Logger = ApplicationContext.Logger.ForType(typeof(ParameterUtils)); + + /// + /// Creates a with all the specified parameters in + /// the input request. It uses reflection to iterate over all properties with + /// attribute. + /// + /// + /// A request object which contains properties with + /// attribute. Those properties will be serialized + /// to the returned . + /// + /// + /// A which contains the all the given object required + /// values. + /// + public static FormUrlEncodedContent CreateFormUrlEncodedContent(object request) + { + IList> list = new List>(); + IterateParameters(request, (type, name, value) => + { + list.Add(new KeyValuePair(name, value.ToString())); + }); + return new FormUrlEncodedContent(list); + } + + /// + /// Creates a parameter dictionary by using reflection to iterate over all properties with + /// attribute. + /// + /// + /// A request object which contains properties with + /// attribute. Those properties will be set + /// in the output dictionary. + /// + public static IDictionary CreateParameterDictionary(object request) + { + var dict = new Dictionary(); + IterateParameters(request, (type, name, value) => + { + dict.Add(name, value); + }); + return dict; + } + + /// + /// Sets query parameters in the given builder with all all properties with the + /// attribute. + /// + /// The request builder + /// + /// A request object which contains properties with + /// attribute. Those properties will be set in the + /// given request builder object + /// + public static void InitParameters(RequestBuilder builder, object request) + { + IterateParameters(request, (type, name, value) => + { + builder.AddParameter(type, name, value.ToString()); + }); + } + + /// + /// Iterates over all properties in the request + /// object and invokes the specified action for each of them. + /// + /// A request object + /// An action to invoke which gets the parameter type, name and its value + private static void IterateParameters(object request, Action 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>).IsAssignableFrom(value.GetType())) + { + foreach (var pair in (IEnumerable>)value) + { + action(RequestParameterType.Query, pair.Key, pair.Value); + } + } + else + { + Logger.Warning("Parameter marked with RequestParameterType.UserDefinedQueries attribute " + + "was not of type IEnumerable> and will be skipped."); + } + } + else + { + action(attribute.Type, name, value); + } + } + } + } + } +} diff --git a/Yavsc/GoogleApiSupport/Google.Apis.Core/Requests/Parameters/ParameterValidator.cs b/Yavsc/GoogleApiSupport/Google.Apis.Core/Requests/Parameters/ParameterValidator.cs new file mode 100644 index 00000000..78cad077 --- /dev/null +++ b/Yavsc/GoogleApiSupport/Google.Apis.Core/Requests/Parameters/ParameterValidator.cs @@ -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 +{ + /// Logic for validating a parameter. + public static class ParameterValidator + { + /// Validates a parameter value against the methods regex. + [VisibleForTestOnly] + public static bool ValidateRegex(IParameter param, string paramValue) + { + return string.IsNullOrEmpty(param.Pattern) || new Regex(param.Pattern).IsMatch(paramValue); + } + + /// Validates if a parameter is valid. + 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); + } + } +} \ No newline at end of file diff --git a/Yavsc/GoogleApiSupport/Google.Apis.Core/Requests/RequestBuilder.cs b/Yavsc/GoogleApiSupport/Google.Apis.Core/Requests/RequestBuilder.cs new file mode 100644 index 00000000..41005fb6 --- /dev/null +++ b/Yavsc/GoogleApiSupport/Google.Apis.Core/Requests/RequestBuilder.cs @@ -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 +{ + /// Utility class for building a URI using or a HTTP request using + /// from the query and path parameters of a REST call. + public class RequestBuilder + { + static RequestBuilder() + { + UriPatcher.PatchUriQuirks(); + } + + private static readonly ILogger Logger = ApplicationContext.Logger.ForType(); + + /// Pattern to get the groups that are part of the path. + private static Regex PathParametersPattern = new Regex(@"{[^{}]*}*"); + + /// Supported HTTP methods. + private static IEnumerable SupportedMethods = new List + { + HttpConsts.Get, HttpConsts.Post, HttpConsts.Put, HttpConsts.Delete, HttpConsts.Patch + }; + + /// + /// 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. + /// + private IDictionary> PathParameters { get; set; } + + /// + /// A dictionary containing the parameters which will apply to the query portion of this request. + /// + private List> QueryParameters { get; set; } + + /// The base URI for this request (usually applies to the service itself). + public Uri BaseUri { get; set; } + + /// + /// The path portion of this request. It's appended to the and the parameters are + /// substituted from the dictionary. + /// + public string Path { get; set; } + + /// The HTTP method used for this request. + private string method; + + /// The HTTP method used for this request (such as GET, PUT, POST, etc...). + /// The default Value is . + public string Method + { + get { return method; } + set + { + if (!SupportedMethods.Contains(value)) + throw new ArgumentOutOfRangeException("Method"); + method = value; + } + } + + /// Construct a new request builder. + /// TODO(peleyal): Consider using the Factory pattern here. + public RequestBuilder() + { + PathParameters = new Dictionary>(); + QueryParameters = new List>(); + Method = HttpConsts.Get; + } + + /// Constructs a Uri as defined by the parts of this request builder. + 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()); + } + + /// Operator list that can appear in the path argument. + private const string OPERATORS = "+#./;?&|!@="; + + /// + /// Builds the REST path string builder based on and the URI template spec + /// http://tools.ietf.org/html/rfc6570. + /// + /// + 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; + } + + /// Adds a parameter value. + /// Type of the parameter (must be 'Path' or 'Query'). + /// Parameter name. + /// Parameter value. + 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 { value }; + } + else + { + PathParameters[name].Add(value); + } + break; + case RequestParameterType.Query: + QueryParameters.Add(new KeyValuePair(name, value)); + break; + default: + throw new ArgumentOutOfRangeException("type"); + } + } + + /// Creates a new HTTP request message. + public HttpRequestMessage CreateRequest() + { + return new HttpRequestMessage(new HttpMethod(Method), BuildUri()); + } + } +} diff --git a/Yavsc/GoogleApiSupport/Google.Apis.Core/Requests/RequestError.cs b/Yavsc/GoogleApiSupport/Google.Apis.Core/Requests/RequestError.cs new file mode 100644 index 00000000..dca3f419 --- /dev/null +++ b/Yavsc/GoogleApiSupport/Google.Apis.Core/Requests/RequestError.cs @@ -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 +{ + /// + /// Collection of server errors + /// + public class RequestError + { + /// + /// Enumeration of known error codes which may occur during a request. + /// + public enum ErrorCodes + { + /// + /// 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. + /// + ETagConditionFailed = 412 + } + + /// + /// Contains a list of all errors + /// + public IList Errors { get; set; } + + /// + /// The error code returned + /// + public int Code { get; set; } + + /// + /// The error message returned + /// + public string Message { get; set; } + + /// + /// Returns a string summary of this error + /// + /// A string summary of this error + 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(); + } + } +} diff --git a/Yavsc/GoogleApiSupport/Google.Apis.Core/Requests/SingleError.cs b/Yavsc/GoogleApiSupport/Google.Apis.Core/Requests/SingleError.cs new file mode 100644 index 00000000..d1581055 --- /dev/null +++ b/Yavsc/GoogleApiSupport/Google.Apis.Core/Requests/SingleError.cs @@ -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 +{ + /// + /// A single server error + /// + public class SingleError + { + /// + /// The domain in which the error occured + /// + public string Domain { get; set; } + + /// + /// The reason the error was thrown + /// + public string Reason { get; set; } + + /// + /// The error message + /// + public string Message { get; set; } + + /// + /// Type of the location + /// + public string LocationType { get; set; } + + /// + /// Location where the error was thrown + /// + public string Location { get; set; } + + /// + /// Returns a string summary of this error + /// + /// A string summary of this error + public override string ToString() + { + return string.Format( + "Message[{0}] Location[{1} - {2}] Reason[{3}] Domain[{4}]", Message, Location, LocationType, Reason, + Domain); + } + } +} diff --git a/Yavsc/GoogleApiSupport/Google.Apis.Core/Testing/VisibleForTestOnly.cs b/Yavsc/GoogleApiSupport/Google.Apis.Core/Testing/VisibleForTestOnly.cs new file mode 100644 index 00000000..b45d2d68 --- /dev/null +++ b/Yavsc/GoogleApiSupport/Google.Apis.Core/Testing/VisibleForTestOnly.cs @@ -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 +{ + /// + /// 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 + /// [assembly: InternalsVisibleTo("Full.Name.Of.Testing.Assembly")] + /// + [AttributeUsage(AttributeTargets.Class | AttributeTargets.Method | AttributeTargets.Property | + AttributeTargets.Field)] + public class VisibleForTestOnly : Attribute { } +} \ No newline at end of file diff --git a/Yavsc/GoogleApiSupport/Google.Apis.Core/Util/ExponentialBackOff.cs b/Yavsc/GoogleApiSupport/Google.Apis.Core/Util/ExponentialBackOff.cs new file mode 100644 index 00000000..7160fdf6 --- /dev/null +++ b/Yavsc/GoogleApiSupport/Google.Apis.Core/Util/ExponentialBackOff.cs @@ -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 +{ + /// + /// Implementation of 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. + /// + public class ExponentialBackOff : IBackOff + { + /// The maximum allowed number of retries. + private const int MaxAllowedNumRetries = 20; + + private readonly TimeSpan deltaBackOff; + /// + /// Gets the delta time span used to generate a random milliseconds to add to the next back-off. + /// If the value is 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. + /// + public TimeSpan DeltaBackOff + { + get { return deltaBackOff; } + } + + private readonly int maxNumOfRetries; + /// Gets the maximum number of retries. Default value is 10. + public int MaxNumOfRetries + { + get { return maxNumOfRetries; } + } + + /// The random instance which generates a random number to add the to next back-off. + private Random random = new Random(); + + /// Constructs a new exponential back-off with default values. + public ExponentialBackOff() + : this(TimeSpan.FromMilliseconds(250)) + { + } + + /// Constructs a new exponential back-off with the given delta and maximum retries. + 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 + + /// + 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 + } +} diff --git a/Yavsc/GoogleApiSupport/Google.Apis.Core/Util/IBackOff.cs b/Yavsc/GoogleApiSupport/Google.Apis.Core/Util/IBackOff.cs new file mode 100644 index 00000000..32528c4b --- /dev/null +++ b/Yavsc/GoogleApiSupport/Google.Apis.Core/Util/IBackOff.cs @@ -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 +{ + /// Strategy interface to control back-off between retry attempts. + public interface IBackOff + { + /// + /// Gets the a time span to wait before next retry. If the current retry reached the maximum number of retries, + /// the returned value is . + /// + TimeSpan GetNextBackOff(int currentRetry); + + /// Gets the maximum number of retries. + int MaxNumOfRetries { get; } + } +} diff --git a/Yavsc/GoogleApiSupport/Google.Apis.Core/Util/IClock.cs b/Yavsc/GoogleApiSupport/Google.Apis.Core/Util/IClock.cs new file mode 100644 index 00000000..6848b0c1 --- /dev/null +++ b/Yavsc/GoogleApiSupport/Google.Apis.Core/Util/IClock.cs @@ -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 +{ + /// Clock wrapper for getting the current time. + public interface IClock + { + /// + /// Gets a object that is set to the current date and time on this computer, + /// expressed as the local time. + /// + [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; } + + /// + /// Gets a object that is set to the current date and time on this computer, + /// expressed as UTC time. + /// + DateTime UtcNow { get; } + } + + /// + /// A default clock implementation that wraps the + /// and properties. + /// + public class SystemClock : IClock + { + /// Constructs a new system clock. + protected SystemClock() { } + + /// The default instance. + public static readonly IClock Default = new SystemClock(); + + /// + public DateTime Now => DateTime.Now; + + /// + public DateTime UtcNow => DateTime.UtcNow; + } +} diff --git a/Yavsc/GoogleApiSupport/Google.Apis.Core/Util/Repeatable.cs b/Yavsc/GoogleApiSupport/Google.Apis.Core/Util/Repeatable.cs new file mode 100644 index 00000000..a8fcd460 --- /dev/null +++ b/Yavsc/GoogleApiSupport/Google.Apis.Core/Util/Repeatable.cs @@ -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 +{ + /// + /// Repeatable class which allows you to both pass a single element, as well as an array, as a parameter value. + /// + public class Repeatable : IEnumerable + { + private readonly IList values; + + /// Creates a repeatable value. + public Repeatable(IEnumerable enumeration) + { + values = new ReadOnlyCollection(new List(enumeration)); + } + + /// + public IEnumerator GetEnumerator() + { + return values.GetEnumerator(); + } + + IEnumerator IEnumerable.GetEnumerator() + { + return GetEnumerator(); + } + + /// Converts the single element into a repeatable. + public static implicit operator Repeatable(T elem) + { + if (elem == null) + { + return null; + } + + return new Repeatable(new[] { elem }); + } + + /// Converts a number of elements into a repeatable. + public static implicit operator Repeatable(T[] elem) + { + if (elem.Length == 0) + { + return null; + } + + return new Repeatable(elem); + } + + /// Converts a number of elements into a repeatable. + public static implicit operator Repeatable(List elem) + { + return new Repeatable(elem); + } + } +} diff --git a/Yavsc/GoogleApiSupport/Google.Apis.Core/Util/RequestParameterAttribute.cs b/Yavsc/GoogleApiSupport/Google.Apis.Core/Util/RequestParameterAttribute.cs new file mode 100644 index 00000000..b4c08668 --- /dev/null +++ b/Yavsc/GoogleApiSupport/Google.Apis.Core/Util/RequestParameterAttribute.cs @@ -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 +{ + /// + /// 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. + /// + [AttributeUsage(AttributeTargets.Property, AllowMultiple = false)] + public class RequestParameterAttribute : Attribute + { + private readonly string name; + private readonly RequestParameterType type; + + /// Gets the name of the parameter. + public string Name { get { return name; } } + + /// Gets the type of the parameter, Path or Query. + public RequestParameterType Type { get { return type; } } + + /// + /// Constructs a new property attribute to be a part of a REST URI. + /// This constructor uses as the parameter's type. + /// + /// + /// 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". + /// + public RequestParameterAttribute(string name) + : this(name, RequestParameterType.Query) + { + + } + + /// Constructs a new property attribute to be a part of a REST URI. + /// + /// 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". + /// + /// The type of the parameter, either Path, Query or UserDefinedQueries. + public RequestParameterAttribute(string name, RequestParameterType type) + { + this.name = name; + this.type = type; + } + } + + /// Describe the type of this parameter (Path, Query or UserDefinedQueries). + public enum RequestParameterType + { + /// A path parameter which is inserted into the path portion of the request URI. + Path, + + /// A query parameter which is inserted into the query portion of the request URI. + Query, + + /// + /// 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. + /// + UserDefinedQueries + } +} diff --git a/Yavsc/GoogleApiSupport/Google.Apis.Core/Util/StandardResponse.cs b/Yavsc/GoogleApiSupport/Google.Apis.Core/Util/StandardResponse.cs new file mode 100644 index 00000000..b1625b7c --- /dev/null +++ b/Yavsc/GoogleApiSupport/Google.Apis.Core/Util/StandardResponse.cs @@ -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 +{ + /// + /// 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. + /// + public sealed class StandardResponse + { + /// May be null if call failed. + [JsonProperty("data")] + public InnerType Data { get; set; } + + /// May be null if call succedded. + [JsonProperty("error")] + public RequestError Error { get; set; } + } +} \ No newline at end of file diff --git a/Yavsc/GoogleApiSupport/Google.Apis.Core/Util/Store/IDataStore.cs b/Yavsc/GoogleApiSupport/Google.Apis.Core/Util/Store/IDataStore.cs new file mode 100644 index 00000000..99b874ed --- /dev/null +++ b/Yavsc/GoogleApiSupport/Google.Apis.Core/Util/Store/IDataStore.cs @@ -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 +{ + /// + /// Stores and manages data objects, where the key is a string and the value is an object. + /// + /// null keys are not allowed. + /// + /// + public interface IDataStore + { + /// Asynchronously stores the given value for the given key (replacing any existing value). + /// The type to store in the data store. + /// The key. + /// The value to store. + Task StoreAsync(string key, T value); + + /// + /// 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. + /// + /// The type to delete from the data store. + /// The key to delete. + Task DeleteAsync(string key); + + /// Asynchronously returns the stored value for the given key or null if not found. + /// The type to retrieve from the data store. + /// The key to retrieve its value. + /// The stored object. + Task GetAsync(string key); + + /// Asynchronously clears all values in the data store. + Task ClearAsync(); + } +} \ No newline at end of file diff --git a/Yavsc/GoogleApiSupport/Google.Apis.Core/Util/StringValueAttribute.cs b/Yavsc/GoogleApiSupport/Google.Apis.Core/Util/StringValueAttribute.cs new file mode 100644 index 00000000..f2b49000 --- /dev/null +++ b/Yavsc/GoogleApiSupport/Google.Apis.Core/Util/StringValueAttribute.cs @@ -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 +{ + /// Defines an attribute containing a string representation of the member. + [AttributeUsage(AttributeTargets.Field, AllowMultiple = false)] + public class StringValueAttribute : Attribute + { + private readonly string text; + /// The text which belongs to this member. + public string Text { get { return text; } } + + /// Creates a new string value attribute with the specified text. + public StringValueAttribute(string text) + { + text.ThrowIfNull("text"); + this.text = text; + } + } +} diff --git a/Yavsc/GoogleApiSupport/Google.Apis.Core/Util/UriPatcher.cs b/Yavsc/GoogleApiSupport/Google.Apis.Core/Util/UriPatcher.cs new file mode 100644 index 00000000..a93dbd39 --- /dev/null +++ b/Yavsc/GoogleApiSupport/Google.Apis.Core/Util/UriPatcher.cs @@ -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 +{ + /// + /// Workarounds for some unfortunate behaviors in the .NET Framework's + /// implementation of System.Uri + /// + /// + /// 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. + /// + public static class UriPatcher + { + /// + /// Patch URI quirks in System.Uri. See class summary for details. + /// + 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 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); + } + } + } + } + } +} diff --git a/Yavsc/GoogleApiSupport/Google.Apis.Core/Util/Utilities.cs b/Yavsc/GoogleApiSupport/Google.Apis.Core/Util/Utilities.cs new file mode 100644 index 00000000..8a71268b --- /dev/null +++ b/Yavsc/GoogleApiSupport/Google.Apis.Core/Util/Utilities.cs @@ -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 +{ + /// A utility class which contains helper methods and extension methods. + public static class Utilities + { + /// Returns the version of the core library. + [VisibleForTestOnly] + public static string GetLibraryVersion() + { + return Regex.Match(typeof(Utilities).GetTypeInfo().Assembly.FullName, "Version=([\\d\\.]+)").Groups[1].ToString(); + } + + /// + /// A Google.Apis utility method for throwing an if the object is + /// null. + /// + public static T ThrowIfNull(this T obj, string paramName) + { + if (obj == null) + { + throw new ArgumentNullException(paramName); + } + + return obj; + } + + /// + /// A Google.Apis utility method for throwing an if the string is + /// null or empty. + /// + /// The original string. + public static string ThrowIfNullOrEmpty(this string str, string paramName) + { + if (string.IsNullOrEmpty(str)) + { + throw new ArgumentException("Parameter was empty", paramName); + } + return str; + } + + /// Returns true in case the enumerable is null or empty. + internal static bool IsNullOrEmpty(this IEnumerable coll) + { + return coll == null || coll.Count() == 0; + } + + /// + /// A Google.Apis utility method for returning the first matching custom attribute (or null) of the specified member. + /// + public static T GetCustomAttribute(this MemberInfo info) where T : Attribute + { + object[] results = info.GetCustomAttributes(typeof(T), false).ToArray(); + return results.Length == 0 ? null : (T)results[0]; + } + + /// Returns the defined string value of an Enum. + 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(); + 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"); + } + + /// + /// Returns the defined string value of an Enum. Use for test purposes or in other Google.Apis projects. + /// + public static string GetEnumStringValue(Enum value) + { + return value.GetStringValue(); + } + + /// + /// Tries to convert the specified object to a string. Uses custom type converters if available. + /// Returns null for a null object. + /// + [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(); + 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(); + } + + /// Converts the input date into a RFC3339 string (http://www.ietf.org/rfc/rfc3339.txt). + 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); + } + + /// + /// Parses the input string and returns if the input is a valid + /// representation of a date. Otherwise it returns null. + /// + public static DateTime? GetDateTimeFromString(string raw) + { + DateTime result; + if (!DateTime.TryParse(raw, out result)) + { + return null; + } + return result; + } + + /// Returns a string (by RFC3339) form the input instance. + public static string GetStringFromDateTime(DateTime? date) + { + if (!date.HasValue) + { + return null; + } + return ConvertToRFC3339(date.Value); + } + } +} diff --git a/Yavsc/GoogleApiSupport/Google.Apis/Download/IDownloadProgress.cs b/Yavsc/GoogleApiSupport/Google.Apis/Download/IDownloadProgress.cs new file mode 100644 index 00000000..33ac73a8 --- /dev/null +++ b/Yavsc/GoogleApiSupport/Google.Apis/Download/IDownloadProgress.cs @@ -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 +{ + /// Enum which represents the status of the current download. + public enum DownloadStatus + { + /// The download has not started. + NotStarted, + + /// Data is being downloaded. + Downloading, + + /// The download was completed successfully. + Completed, + + /// The download failed. + Failed + }; + + /// Reports download progress. + public interface IDownloadProgress + { + /// Gets the current status of the upload. + DownloadStatus Status { get; } + + /// Gets the number of bytes received from the server. + long BytesDownloaded { get; } + + /// Gets an exception if one occurred. + Exception Exception { get; } + } +} diff --git a/Yavsc/GoogleApiSupport/Google.Apis/Download/IMediaDownloader.cs b/Yavsc/GoogleApiSupport/Google.Apis/Download/IMediaDownloader.cs new file mode 100644 index 00000000..323c4f8a --- /dev/null +++ b/Yavsc/GoogleApiSupport/Google.Apis/Download/IMediaDownloader.cs @@ -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 +{ + /// Media download which uses download file part by part, by . + public interface IMediaDownloader + { + /// An event which notifies when the download status has been changed. + event Action ProgressChanged; + + /// Gets or sets the chunk size to download, it defines the size of each part. + int ChunkSize { get; set; } + + /// Downloads synchronously the given URL to the given stream. + IDownloadProgress Download(string url, Stream stream); + + /// Downloads asynchronously the given URL to the given stream. + Task DownloadAsync(string url, Stream stream); + + /// + /// Downloads asynchronously the given URL to the given stream. This download method supports a cancellation + /// token to cancel a request before it was completed. + /// + /// + /// In case the download fails will contain the exception that + /// cause the failure. The only exception which will be thrown is + /// which indicates that the task was canceled. + /// + Task DownloadAsync(string url, Stream stream, CancellationToken cancellationToken); + } +} diff --git a/Yavsc/GoogleApiSupport/Google.Apis/Download/MediaDownloader.cs b/Yavsc/GoogleApiSupport/Google.Apis/Download/MediaDownloader.cs new file mode 100644 index 00000000..4345df10 --- /dev/null +++ b/Yavsc/GoogleApiSupport/Google.Apis/Download/MediaDownloader.cs @@ -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 +{ + /// + /// A media downloader implementation which handles media downloads. + /// + public class MediaDownloader : IMediaDownloader + { + static MediaDownloader() + { + UriPatcher.PatchUriQuirks(); + } + + private static readonly ILogger Logger = ApplicationContext.Logger.ForType(); + + /// The service which this downloader belongs to. + private readonly IClientService service; + + private const int MB = 0x100000; + + /// Maximum chunk size. Default value is 10*MB. + public const int MaximumChunkSize = 10 * MB; + + private int chunkSize = MaximumChunkSize; + /// + /// Gets or sets the amount of data that will be downloaded before notifying the caller of + /// the download's progress. + /// Must not exceed . + /// Default value is . + /// + public int ChunkSize + { + get { return chunkSize; } + set + { + if (value > MaximumChunkSize) + { + throw new ArgumentOutOfRangeException("ChunkSize"); + } + chunkSize = value; + } + } + + /// + /// The range header for the request, if any. This can be used to download specific parts + /// of the requested media. + /// + public RangeHeaderValue Range { get; set; } + + #region Progress + + /// + /// 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. + /// + private class DownloadProgress : IDownloadProgress + { + /// Constructs a new progress instance. + /// The status of the download. + /// The number of bytes received so far. + public DownloadProgress(DownloadStatus status, long bytes) + { + Status = status; + BytesDownloaded = bytes; + } + + /// Constructs a new progress instance. + /// An exception which occurred during the download. + /// The number of bytes received before the exception occurred. + public DownloadProgress(Exception exception, long bytes) + { + Status = DownloadStatus.Failed; + BytesDownloaded = bytes; + Exception = exception; + } + + /// Gets or sets the status of the download. + public DownloadStatus Status { get; private set; } + + /// Gets or sets the amount of bytes that have been downloaded so far. + public long BytesDownloaded { get; private set; } + + /// Gets or sets the exception which occurred during the download or null. + public Exception Exception { get; private set; } + } + + /// + /// Updates the current progress and call the event to notify listeners. + /// + private void UpdateProgress(IDownloadProgress progress) + { + ProgressChanged?.Invoke(progress); + } + + #endregion + + /// Constructs a new downloader with the given client service. + public MediaDownloader(IClientService service) + { + this.service = service; + } + + /// + /// Gets or sets the callback for modifying requests made when downloading. + /// + public Action ModifyRequest { get; set; } + + #region IMediaDownloader Overrides + + /// + public event Action ProgressChanged; + + #region Download (sync and async) + + /// + public IDownloadProgress Download(string url, Stream stream) + { + return DownloadCoreAsync(url, stream, CancellationToken.None).Result; + } + + /// + public async Task DownloadAsync(string url, Stream stream) + { + return await DownloadAsync(url, stream, CancellationToken.None).ConfigureAwait(false); + } + + /// + public async Task DownloadAsync(string url, Stream stream, + CancellationToken cancellationToken) + { + return await DownloadCoreAsync(url, stream, cancellationToken).ConfigureAwait(false); + } + + #endregion + + #endregion + + /// + /// CountedBuffer bundles together a byte buffer and a count of valid bytes. + /// + private class CountedBuffer + { + public byte[] Data { get; set; } + + /// + /// How many bytes at the beginning of Data are valid. + /// + public int Count { get; private set; } + + public CountedBuffer(int size) + { + Data = new byte[size]; + Count = 0; + } + + /// + /// Returns true if the buffer contains no data. + /// + public bool IsEmpty { get { return Count == 0; } } + + /// + /// Read data from stream until the stream is empty or the buffer is full. + /// + /// Stream from which to read. + /// Cancellation token for the operation. + 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; + } + } + + /// + /// 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. + /// + /// The number of bytes to remove. + 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; + } + } + } + + /// + /// 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. + /// + /// The URL of the resource to download. + /// The download will download the resource into this stream. + /// A cancellation token to cancel this download in the middle. + /// A task with the download progress object. If an exception occurred during the download, its + /// property will contain the exception. + private async Task 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; + } + } + + /// + /// Called when a successful HTTP response is received, allowing subclasses to examine headers. + /// + /// + /// For unsuccessful responses, an appropriate exception is thrown immediately, without this method + /// being called. + /// + /// HTTP response received. + protected virtual void OnResponseReceived(HttpResponseMessage response) + { + // No-op + } + + /// + /// Called when an HTTP response is received, allowing subclasses to examine data before it's + /// written to the client stream. + /// + /// Byte array containing the data downloaded. + /// Length of data downloaded in this chunk, in bytes. + protected virtual void OnDataReceived(byte[] data, int length) + { + // No-op + } + + /// + /// Called when a download has completed, allowing subclasses to perform any final validation + /// or transformation. + /// + protected virtual void OnDownloadCompleted() + { + // No-op + } + } +} diff --git a/Yavsc/GoogleApiSupport/Google.Apis/ETagAction.cs b/Yavsc/GoogleApiSupport/Google.Apis/ETagAction.cs new file mode 100644 index 00000000..f893f372 --- /dev/null +++ b/Yavsc/GoogleApiSupport/Google.Apis/ETagAction.cs @@ -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 +{ + /// + /// Defines the behaviour/header used for sending an etag along with a request. + /// + public enum ETagAction + { + /// + /// The default etag behaviour will be determined by the type of the request. + /// + Default, + + /// + /// The ETag won't be added to the header of the request. + /// + Ignore, + + /// + /// 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. + /// + IfMatch, + + /// + /// 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. + /// + IfNoneMatch, + } +} diff --git a/Yavsc/GoogleApiSupport/Google.Apis/Media/MediaApiErrorHandling.cs b/Yavsc/GoogleApiSupport/Google.Apis/Media/MediaApiErrorHandling.cs new file mode 100644 index 00000000..7d42bcef --- /dev/null +++ b/Yavsc/GoogleApiSupport/Google.Apis/Media/MediaApiErrorHandling.cs @@ -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 +{ + /// + /// Common error handling code for the Media API. + /// + internal static class MediaApiErrorHandling + { + /// + /// 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. + /// + internal static Task ExceptionForResponseAsync( + IClientService service, + HttpResponseMessage response) + { + return ExceptionForResponseAsync(service.Serializer, service.Name, response); + } + + /// + /// 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. + /// + internal static async Task 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>(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 + }; + } + } +} diff --git a/Yavsc/GoogleApiSupport/Google.Apis/Requests/BatchRequest.cs b/Yavsc/GoogleApiSupport/Google.Apis/Requests/BatchRequest.cs new file mode 100644 index 00000000..e11fe5fd --- /dev/null +++ b/Yavsc/GoogleApiSupport/Google.Apis/Requests/BatchRequest.cs @@ -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 +{ + /// + /// A batch request which represents individual requests to Google servers. You should add a single service + /// request using the method and execute all individual requests using + /// . More information about the batch protocol is available in + /// https://developers.google.com/storage/docs/json_api/v1/how-tos/batch. + /// + /// Current implementation doesn't retry on unsuccessful individual response and doesn't support requests with + /// different access tokens (different users or scopes). + /// + /// + public sealed class BatchRequest + { + private const string DefaultBatchUrl = "https://www.googleapis.com/batch"; + private const int QueueLimit = 1000; + + private readonly IList allRequests = new List(); + + private readonly string batchUrl; + private readonly IClientService service; + + // For testing + internal string BatchUrl => batchUrl; + + /// A concrete type callback for an individual response. + /// The response type. + /// The content response or null if the request failed. + /// Error or null if the request succeeded. + /// The request index. + /// The HTTP individual response. + public delegate void OnResponse + (TResponse content, RequestError error, int index, HttpResponseMessage message) where TResponse : class; + + #region Inner Request + + /// This inner class represents an individual inner request. + private class InnerRequest + { + /// Gets or sets the client service request. + public IClientServiceRequest ClientRequest { get; set; } + + /// Gets or sets the response class type. + public Type ResponseType { get; set; } + + /// A callback method which will be called after an individual response was parsed. + /// The content response or null if the request failed. + /// Error or null if the request succeeded. + /// The request index. + /// The HTTP individual response. + 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; + } + } + } + + /// + /// This generic inner class represents an individual inner request with a generic response type. + /// + private class InnerRequest : InnerRequest + where TResponse : class + { + /// Gets or sets a concrete type callback for an individual response. + public OnResponse 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 + + /// + /// Constructs a new batch request using the given service. See + /// for more information. + /// + public BatchRequest(IClientService service) + : this(service, (service as BaseClientService)?.BatchUri ?? DefaultBatchUrl) { } + + /// + /// 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. + /// + public BatchRequest(IClientService service, string batchUrl) + { + this.batchUrl = batchUrl; + this.service = service; + } + + /// Gets the count of all queued requests. + public int Count + { + get { return allRequests.Count; } + } + + /// Queues an individual request. + /// The response's type. + /// The individual request. + /// A callback which will be called after a response was parsed. + public void Queue(IClientServiceRequest request, OnResponse callback) + where TResponse : class + { + if (Count > QueueLimit) + { + throw new InvalidOperationException("A batch request cannot contain more than 1000 single requests"); + } + + allRequests.Add(new InnerRequest + { + ClientRequest = request, + ResponseType = typeof(TResponse), + OnResponseCallback = callback, + }); + } + + /// Asynchronously executes the batch request. + public Task ExecuteAsync() + { + return ExecuteAsync(CancellationToken.None); + } + + /// Asynchronously executes the batch request. + /// Cancellation token to cancel operation. + 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); + } + } + + /// Parses the given string content to a HTTP response. + [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 headersDic = new Dictionary(); + 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; + } + + /// + /// Creates the batch outer request content which includes all the individual requests to Google servers. + /// + [VisibleForTestOnly] + internal async static Task CreateOuterRequestContent(IEnumerable 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; + + } + + /// Creates the individual server request. + [VisibleForTestOnly] + internal static async Task 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; + } + + /// + /// Creates a string representation that includes the request's headers and content based on the input HTTP + /// request message. + /// + [VisibleForTestOnly] + internal static async Task 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(); + } + } +} \ No newline at end of file diff --git a/Yavsc/GoogleApiSupport/Google.Apis/Requests/ClientServiceRequest.cs b/Yavsc/GoogleApiSupport/Google.Apis/Requests/ClientServiceRequest.cs new file mode 100644 index 00000000..a670b11f --- /dev/null +++ b/Yavsc/GoogleApiSupport/Google.Apis/Requests/ClientServiceRequest.cs @@ -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 +{ + /// + /// Represents an abstract, strongly typed request base class to make requests to a service. + /// Supports a strongly typed response. + /// + /// The type of the response object + public abstract class ClientServiceRequest : IClientServiceRequest + { + /// The class logger. + private static readonly ILogger Logger = ApplicationContext.Logger.ForType>(); + + /// The service on which this request will be executed. + private readonly IClientService service; + + /// Defines whether the E-Tag will be used in a specified way or be ignored. + public ETagAction ETagAction { get; set; } + + /// + /// Gets or sets the callback for modifying HTTP requests made by this service request. + /// + public Action ModifyRequest { get; set; } + + #region IClientServiceRequest Properties + + /// + public abstract string MethodName { get; } + + /// + public abstract string RestPath { get; } + + /// + public abstract string HttpMethod { get; } + + /// + public IDictionary RequestParameters { get; private set; } + + /// + public IClientService Service + { + get { return service; } + } + + #endregion + + /// Creates a new service request. + protected ClientServiceRequest(IClientService service) + { + this.service = service; + } + + /// + /// Initializes request's parameters. Inherited classes MUST override this method to add parameters to the + /// dictionary. + /// + protected virtual void InitParameters() + { + RequestParameters = new Dictionary(); + } + + #region Execution + + /// + 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; + } + } + + /// + 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; + } + } + + /// + public async Task ExecuteAsync() + { + return await ExecuteAsync(CancellationToken.None).ConfigureAwait(false); + } + + /// + public async Task ExecuteAsync(CancellationToken cancellationToken) + { + using (var response = await ExecuteUnparsedAsync(cancellationToken).ConfigureAwait(false)) + { + cancellationToken.ThrowIfCancellationRequested(); + return await ParseResponse(response).ConfigureAwait(false); + } + } + + /// + public async Task ExecuteAsStreamAsync() + { + return await ExecuteAsStreamAsync(CancellationToken.None).ConfigureAwait(false); + } + + /// + public async Task 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 + + /// Sync executes the request without parsing the result. + private async Task ExecuteUnparsedAsync(CancellationToken cancellationToken) + { + using (var request = CreateRequest()) + { + return await service.HttpClient.SendAsync(request, cancellationToken).ConfigureAwait(false); + } + } + + /// Parses the response and deserialize the content into the requested response object. + private async Task ParseResponse(HttpResponseMessage response) + { + if (response.IsSuccessStatusCode) + { + return await service.DeserializeResponse(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 + + /// + public HttpRequestMessage CreateRequest(Nullable 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; + } + + /// + /// Creates the which is used to generate a request. + /// + /// + /// A new builder instance which contains the HTTP method and the right Uri with its path and query parameters. + /// + 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; + } + + /// Generates the right URL for this request. + protected string GenerateRequestUri() + { + return CreateBuilder().BuildUri().ToString(); + } + + /// Returns the body of this request. + /// The body of this request. + protected virtual object GetBody() + { + return null; + } + + #region ETag + + /// + /// Adds the right ETag action (e.g. If-Match) header to the given HTTP request if the body contains ETag. + /// + 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); + } + } + } + + /// Returns the default ETagAction for a specific HTTP verb. + [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 + + /// Adds path and query parameters to the given requestBuilder. + 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 + } +} diff --git a/Yavsc/GoogleApiSupport/Google.Apis/Requests/HttpRequestMessageExtenstions.cs b/Yavsc/GoogleApiSupport/Google.Apis/Requests/HttpRequestMessageExtenstions.cs new file mode 100644 index 00000000..b8619b13 --- /dev/null +++ b/Yavsc/GoogleApiSupport/Google.Apis/Requests/HttpRequestMessageExtenstions.cs @@ -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 +{ + /// Extension methods to . + static class HttpRequestMessageExtenstions + { + /// + /// Sets the content of the request by the given body and the the required GZip configuration. + /// + /// The request. + /// The service. + /// The body of the future request. If null do nothing. + /// + /// Indicates if the content will be wrapped in a GZip stream, or a regular string stream will be used. + /// + 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; + } + + /// Creates a GZip content based on the given content. + /// Content to GZip. + /// GZiped HTTP content. + internal static HttpContent CreateZipContent(string content) + { + var stream = CreateGZipStream(content); + var sc = new StreamContent(stream); + sc.Headers.ContentEncoding.Add("gzip"); + return sc; + } + + /// Creates a GZip stream by the given serialized object. + 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); + } + } + } +} diff --git a/Yavsc/GoogleApiSupport/Google.Apis/Requests/IClientServiceRequest.cs b/Yavsc/GoogleApiSupport/Google.Apis/Requests/IClientServiceRequest.cs new file mode 100644 index 00000000..43c00957 --- /dev/null +++ b/Yavsc/GoogleApiSupport/Google.Apis/Requests/IClientServiceRequest.cs @@ -0,0 +1,80 @@ +/* +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.Threading; +using System.Threading.Tasks; + +using Google.Apis.Discovery; +using Google.Apis.Services; + +namespace Google.Apis.Requests +{ + /// A client service request which supports both sync and async execution to get the stream. + public interface IClientServiceRequest + { + /// Gets the name of the method to which this request belongs. + string MethodName { get; } + + /// Gets the rest path of this request. + string RestPath { get; } + + /// Gets the HTTP method of this request. + string HttpMethod { get; } + + /// Gets the parameters information for this specific request. + IDictionary RequestParameters { get; } + + /// Gets the service which is related to this request. + IClientService Service { get; } + + /// Creates a HTTP request message with all path and query parameters, ETag, etc. + /// + /// If null use the service default GZip behavior. Otherwise indicates if GZip is enabled or disabled. + /// + HttpRequestMessage CreateRequest(Nullable overrideGZipEnabled = null); + + /// Executes the request asynchronously and returns the result stream. + Task ExecuteAsStreamAsync(); + + /// Executes the request asynchronously and returns the result stream. + /// A cancellation token to cancel operation. + Task ExecuteAsStreamAsync(CancellationToken cancellationToken); + + /// Executes the request and returns the result stream. + Stream ExecuteAsStream(); + } + + /// + /// A client service request which inherits from and represents a specific + /// service request with the given response type. It supports both sync and async execution to get the response. + /// + public interface IClientServiceRequest : IClientServiceRequest + { + /// Executes the request asynchronously and returns the result object. + Task ExecuteAsync(); + + /// Executes the request asynchronously and returns the result object. + /// A cancellation token to cancel operation. + Task ExecuteAsync(CancellationToken cancellationToken); + + /// Executes the request and returns the result object. + TResponse Execute(); + } +} diff --git a/Yavsc/GoogleApiSupport/Google.Apis/Requests/IDirectResponseSchema.cs b/Yavsc/GoogleApiSupport/Google.Apis/Requests/IDirectResponseSchema.cs new file mode 100644 index 00000000..4d9bb499 --- /dev/null +++ b/Yavsc/GoogleApiSupport/Google.Apis/Requests/IDirectResponseSchema.cs @@ -0,0 +1,35 @@ +/* +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 +{ + /// + /// Interface containing additional response-properties which will be added to every schema type which is + /// a direct response to a request. + /// + public interface IDirectResponseSchema + { + /// + /// The e-tag of this response. + /// + /// + /// Will be set by the service deserialization method, + /// or the by json response parser if implemented on service. + /// + string ETag { get; set; } + } +} diff --git a/Yavsc/GoogleApiSupport/Google.Apis/Requests/PageStreamer.cs b/Yavsc/GoogleApiSupport/Google.Apis/Requests/PageStreamer.cs new file mode 100644 index 00000000..0b62a6dc --- /dev/null +++ b/Yavsc/GoogleApiSupport/Google.Apis/Requests/PageStreamer.cs @@ -0,0 +1,159 @@ +/* +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.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; + +namespace Google.Apis.Requests +{ + // TODO(jskeet): Make sure one of our samples uses this. + + /// + /// A page streamer is a helper to provide both synchronous and asynchronous page streaming + /// of a listable or queryable resource. + /// + /// + /// + /// The expected usage pattern is to create a single paginator for a resource collection, + /// and then use the instance methods to obtain paginated results. + /// + /// + /// + /// To construct a page streamer to return snippets from the YouTube v3 Data API, you might use code + /// such as the following. The pattern for other APIs would be very similar, with the request.PageToken, + /// response.NextPageToken and response.Items properties potentially having different names. Constructing + /// the page streamer doesn't require any service references or authentication, so it's completely safe to perform this + /// in a type initializer. + /// ( + /// (request, token) => request.PageToken = token, + /// response => response.NextPageToken, + /// response => response.Items); + /// ]]> + /// + /// The type of resource being paginated + /// The type of request used to fetch pages + /// The type of response obtained when fetching pages + /// The type of the "next page token", which must be a reference type; + /// a null reference for a token indicates the end of a stream of pages. + public sealed class PageStreamer + where TToken : class + where TRequest : IClientServiceRequest + { + // Simple way of avoiding NullReferenceException if the response extractor returns null. + private static readonly TResource[] emptyResources = new TResource[0]; + + private readonly Action requestModifier; + private readonly Func tokenExtractor; + private readonly Func> resourceExtractor; + + /// + /// Creates a paginator for later use. + /// + /// Action to modify a request to include the specified page token. + /// Must not be null. + /// Function to extract the next page token from a response. + /// Must not be null. + /// Function to extract a sequence of resources from a response. + /// Must not be null, although it can return null if it is passed a response which contains no + /// resources. + public PageStreamer( + Action requestModifier, + Func tokenExtractor, + Func> resourceExtractor) + { + if (requestModifier == null) + { + throw new ArgumentNullException("requestProvider"); + } + if (tokenExtractor == null) + { + throw new ArgumentNullException("tokenExtractor"); + } + if (resourceExtractor == null) + { + throw new ArgumentNullException("resourceExtractor"); + } + this.requestModifier = requestModifier; + this.tokenExtractor = tokenExtractor; + this.resourceExtractor = resourceExtractor; + } + + /// + /// Lazily fetches resources a page at a time. + /// + /// The initial request to send. If this contains a page token, + /// that token is maintained. This will be modified with new page tokens over time, and should not + /// be changed by the caller. (The caller should clone the request if they want an independent object + /// to use in other calls or to modify.) Must not be null. + /// A sequence of resources, which are fetched a page at a time. Must not be null. + public IEnumerable Fetch(TRequest request) + { + if (request == null) + { + throw new ArgumentNullException("request"); + } + TToken token; + do + { + TResponse response = request.Execute(); + token = tokenExtractor(response); + requestModifier(request, token); + foreach (var item in resourceExtractor(response) ?? emptyResources) + { + yield return item; + } + } while (token != null); + } + + /// + /// Asynchronously (but eagerly) fetches a complete set of resources, potentially making multiple requests. + /// + /// The initial request to send. If this contains a page token, + /// that token is maintained. This will be modified with new page tokens over time, and should not + /// be changed by the caller. (The caller should clone the request if they want an independent object + /// to use in other calls or to modify.) Must not be null. + /// A sequence of resources, which are fetched asynchronously and a page at a time. + /// + /// A task whose result (when complete) is the complete set of results fetched starting with the given + /// request, and continuing to make further requests until a response has no "next page" token. + public async Task> FetchAllAsync( + TRequest request, + CancellationToken cancellationToken) + { + if (request == null) + { + throw new ArgumentNullException("request"); + } + var results = new List(); + TToken token; + do + { + cancellationToken.ThrowIfCancellationRequested(); + TResponse response = await request.ExecuteAsync(cancellationToken).ConfigureAwait(false); + token = tokenExtractor(response); + requestModifier(request, token); + results.AddRange(resourceExtractor(response) ?? emptyResources); + } while (token != null); + return results; + } + } +} diff --git a/Yavsc/GoogleApiSupport/Google.Apis/Services/BaseClientService.cs b/Yavsc/GoogleApiSupport/Google.Apis/Services/BaseClientService.cs new file mode 100644 index 00000000..87e51857 --- /dev/null +++ b/Yavsc/GoogleApiSupport/Google.Apis/Services/BaseClientService.cs @@ -0,0 +1,353 @@ +/* +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.Tasks; + +using Newtonsoft.Json; + +using Google.Apis.Discovery; +using Google.Apis.Http; +using Google.Apis.Json; +using Google.Apis.Logging; +using Google.Apis.Requests; +using Google.Apis.Util; +using Google.Apis.Testing; + +namespace Google.Apis.Services +{ + /// + /// A base class for a client service which provides common mechanism for all services, like + /// serialization and GZip support. It should be safe to use a single service instance to make server requests + /// concurrently from multiple threads. + /// This class adds a special to the + /// execute interceptor list, which uses the given + /// Authenticator. It calls to its applying authentication method, and injects the "Authorization" header in the + /// request. + /// If the given Authenticator implements , this + /// class adds the Authenticator to the 's unsuccessful + /// response handler list. + /// + public abstract class BaseClientService : IClientService + { + /// The class logger. + private static readonly ILogger Logger = ApplicationContext.Logger.ForType(); + + /// The default maximum allowed length of a URL string for GET requests. + [VisibleForTestOnly] + public const uint DefaultMaxUrlLength = 2048; + + #region Initializer + + /// An initializer class for the client service. + public class Initializer + { + /// + /// Gets or sets the factory for creating instance. If this + /// property is not set the service uses a new instance. + /// + public IHttpClientFactory HttpClientFactory { get; set; } + + /// + /// Gets or sets a HTTP client initializer which is able to customize properties on + /// and + /// . + /// + public IConfigurableHttpClientInitializer HttpClientInitializer { get; set; } + + /// + /// Get or sets the exponential back-off policy used by the service. Default value is + /// UnsuccessfulResponse503, which means that exponential back-off is used on 503 abnormal HTTP + /// response. + /// If the value is set to None, no exponential back-off policy is used, and it's up to the user to + /// configure the in an + /// to set a specific back-off + /// implementation (using ). + /// + public ExponentialBackOffPolicy DefaultExponentialBackOffPolicy { get; set; } + + /// Gets or sets whether this service supports GZip. Default value is true. + public bool GZipEnabled { get; set; } + + /// + /// Gets or sets the serializer. Default value is . + /// + public ISerializer Serializer { get; set; } + + /// Gets or sets the API Key. Default value is null. + public string ApiKey { get; set; } + + /// + /// Gets or sets Application name to be used in the User-Agent header. Default value is null. + /// + public string ApplicationName { get; set; } + + /// + /// Maximum allowed length of a URL string for GET requests. Default value is 2048. If the value is + /// set to 0, requests will never be modified due to URL string length. + /// + public uint MaxUrlLength { get; set; } + + /// Constructs a new initializer with default values. + public Initializer() + { + GZipEnabled = true; + Serializer = new NewtonsoftJsonSerializer(); + DefaultExponentialBackOffPolicy = ExponentialBackOffPolicy.UnsuccessfulResponse503; + MaxUrlLength = DefaultMaxUrlLength; + } + + internal void Validate() + { + // TODO: Validate ApplicationName + } + } + + #endregion + + /// Constructs a new base client with the specified initializer. + protected BaseClientService(Initializer initializer) + { + initializer.Validate(); + // Set the right properties by the initializer's properties. + GZipEnabled = initializer.GZipEnabled; + Serializer = initializer.Serializer; + ApiKey = initializer.ApiKey; + ApplicationName = initializer.ApplicationName; + if (ApplicationName == null) + { + Logger.Warning("Application name is not set. Please set Initializer.ApplicationName property"); + } + HttpClientInitializer = initializer.HttpClientInitializer; + + // Create a HTTP client for this service. + HttpClient = CreateHttpClient(initializer); + } + + /// Returns true if this service contains the specified feature. + private bool HasFeature(Features feature) + { + return Features.Contains(Utilities.GetEnumStringValue(feature)); + } + + private ConfigurableHttpClient CreateHttpClient(Initializer initializer) + { + // If factory wasn't set use the default HTTP client factory. + var factory = initializer.HttpClientFactory ?? new HttpClientFactory(); + var args = new CreateHttpClientArgs + { + GZipEnabled = GZipEnabled, + ApplicationName = ApplicationName, + }; + + // Add the user's input initializer. + if (HttpClientInitializer != null) + { + args.Initializers.Add(HttpClientInitializer); + } + + // Add exponential back-off initializer if necessary. + if (initializer.DefaultExponentialBackOffPolicy != ExponentialBackOffPolicy.None) + { + args.Initializers.Add(new ExponentialBackOffInitializer(initializer.DefaultExponentialBackOffPolicy, + CreateBackOffHandler)); + } + + var httpClient = factory.CreateHttpClient(args); + if (initializer.MaxUrlLength > 0) + { + httpClient.MessageHandler.AddExecuteInterceptor(new MaxUrlLengthInterceptor(initializer.MaxUrlLength)); + } + return httpClient; + } + + /// + /// Creates the back-off handler with . + /// Overrides this method to change the default behavior of back-off handler (e.g. you can change the maximum + /// waited request's time span, or create a back-off handler with you own implementation of + /// ). + /// + protected virtual BackOffHandler CreateBackOffHandler() + { + // TODO(peleyal): consider return here interface and not the concrete class + return new BackOffHandler(new ExponentialBackOff()); + } + + #region IClientService Members + + /// + public ConfigurableHttpClient HttpClient { get; private set; } + + /// + public IConfigurableHttpClientInitializer HttpClientInitializer { get; private set; } + + /// + public bool GZipEnabled { get; private set; } + + /// + public string ApiKey { get; private set; } + + /// + public string ApplicationName { get; private set; } + + /// + public void SetRequestSerailizedContent(HttpRequestMessage request, object body) + { + request.SetRequestSerailizedContent(this, body, GZipEnabled); + } + + #region Serialization + + /// + public ISerializer Serializer { get; private set; } + + /// + public virtual string SerializeObject(object obj) + { + if (HasFeature(Discovery.Features.LegacyDataResponse)) + { + // Legacy path + var request = new StandardResponse { Data = obj }; + return Serializer.Serialize(request); + } + return Serializer.Serialize(obj); + } + + /// + public virtual async Task DeserializeResponse(HttpResponseMessage response) + { + var text = await response.Content.ReadAsStringAsync().ConfigureAwait(false); + + // If a string is request, don't parse the response. + if (Type.Equals(typeof(T), typeof(string))) + { + return (T)(object)text; + } + + // Check if there was an error returned. The error node is returned in both paths + // Deserialize the stream based upon the format of the stream. + if (HasFeature(Discovery.Features.LegacyDataResponse)) + { + // Legacy path (deprecated!) + StandardResponse sr = null; + try + { + sr = Serializer.Deserialize>(text); + } + catch (JsonReaderException ex) + { + throw new GoogleApiException(Name, + "Failed to parse response from server as json [" + text + "]", ex); + } + + if (sr.Error != null) + { + throw new GoogleApiException(Name, "Server error - " + sr.Error) + { + Error = sr.Error + }; + } + + if (sr.Data == null) + { + throw new GoogleApiException(Name, "The response could not be deserialized."); + } + return sr.Data; + } + + // New path: Deserialize the object directly. + T result = default(T); + try + { + result = Serializer.Deserialize(text); + } + catch (JsonReaderException ex) + { + throw new GoogleApiException(Name, "Failed to parse response from server as json [" + text + "]", ex); + } + + // TODO(peleyal): is this the right place to check ETag? it isn't part of deserialization! + // If this schema/object provides an error container, check it. + var eTag = response.Headers.ETag != null ? response.Headers.ETag.Tag : null; + if (result is IDirectResponseSchema && eTag != null) + { + (result as IDirectResponseSchema).ETag = eTag; + } + return result; + } + + /// + public virtual async Task DeserializeError(HttpResponseMessage response) + { + StandardResponse errorResponse = null; + try + { + var str = await response.Content.ReadAsStringAsync().ConfigureAwait(false); + errorResponse = Serializer.Deserialize>(str); + if (errorResponse.Error == null) + { + throw new GoogleApiException(Name, "error response is null"); + } + } + catch (Exception ex) + { + // exception will be thrown in case the response content is empty or it can't be deserialized to + // Standard response (which contains data and error properties) + throw new GoogleApiException(Name, + "An Error occurred, but the error response could not be deserialized", ex); + } + + return errorResponse.Error; + } + + #endregion + + #region Abstract Members + + /// + public abstract string Name { get; } + + /// + public abstract string BaseUri { get; } + + /// + public abstract string BasePath { get; } + + /// The URI used for batch operations. + public virtual string BatchUri { get { return null; } } + + /// The path used for batch operations. + public virtual string BatchPath { get { return null; } } + + /// + public abstract IList Features { get; } + + #endregion + + #endregion + + /// + public virtual void Dispose() + { + if (HttpClient != null) + { + HttpClient.Dispose(); + } + } + } +} diff --git a/Yavsc/GoogleApiSupport/Google.Apis/Services/IClientService.cs b/Yavsc/GoogleApiSupport/Google.Apis/Services/IClientService.cs new file mode 100644 index 00000000..6facfde6 --- /dev/null +++ b/Yavsc/GoogleApiSupport/Google.Apis/Services/IClientService.cs @@ -0,0 +1,91 @@ +/* +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.Generic; +using System.Net.Http; +using System.Threading.Tasks; + +using Google.Apis.Http; +using Google.Apis.Requests; + +namespace Google.Apis.Services +{ + /// + /// Client service contains all the necessary information a Google service requires. + /// Each concrete has a reference to a service for + /// important properties like API key, application name, base Uri, etc. + /// This service interface also contains serialization methods to serialize an object to stream and deserialize a + /// stream into an object. + /// + public interface IClientService : IDisposable + { + /// Gets the HTTP client which is used to create requests. + ConfigurableHttpClient HttpClient { get; } + + /// + /// Gets a HTTP client initializer which is able to custom properties on + /// and + /// . + /// + IConfigurableHttpClientInitializer HttpClientInitializer { get; } + + /// Gets the service name. + string Name { get; } + + /// Gets the BaseUri of the service. All request paths should be relative to this URI. + string BaseUri { get; } + + /// Gets the BasePath of the service. + string BasePath { get; } + + /// Gets the supported features by this service. + IList Features { get; } + + /// Gets or sets whether this service supports GZip. + bool GZipEnabled { get; } + + /// Gets the API-Key (DeveloperKey) which this service uses for all requests. + string ApiKey { get; } + + /// Gets the application name to be used in the User-Agent header. + string ApplicationName { get; } + + /// + /// Sets the content of the request by the given body and the this service's configuration. + /// First the body object is serialized by the Serializer and then, if GZip is enabled, the content will be + /// wrapped in a GZip stream, otherwise a regular string stream will be used. + /// + void SetRequestSerailizedContent(HttpRequestMessage request, object body); + + #region Serialization Methods + + /// Gets the Serializer used by this service. + ISerializer Serializer { get; } + + /// Serializes an object into a string representation. + string SerializeObject(object data); + + /// Deserializes a response into the specified object. + Task DeserializeResponse(HttpResponseMessage response); + + /// Deserializes an error response into a object. + /// If no error is found in the response. + Task DeserializeError(HttpResponseMessage response); + + #endregion + } +} diff --git a/Yavsc/GoogleApiSupport/Google.Apis/Upload/IUploadProgress.cs b/Yavsc/GoogleApiSupport/Google.Apis/Upload/IUploadProgress.cs new file mode 100644 index 00000000..773431d1 --- /dev/null +++ b/Yavsc/GoogleApiSupport/Google.Apis/Upload/IUploadProgress.cs @@ -0,0 +1,72 @@ +/* +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; + +namespace Google.Apis.Upload +{ + /// + /// Enum to communicate the status of an upload for progress reporting. + /// + public enum UploadStatus + { + /// + /// The upload has not started. + /// + NotStarted, + + /// + /// The upload is initializing. + /// + Starting, + + /// + /// Data is being uploaded. + /// + Uploading, + + /// + /// The upload completed successfully. + /// + Completed, + + /// + /// The upload failed. + /// + Failed + }; + + /// + /// Interface reporting upload progress. + /// + public interface IUploadProgress + { + /// + /// Gets the current status of the upload + /// + UploadStatus Status { get; } + + /// + /// Gets the approximate number of bytes sent to the server. + /// + long BytesSent { get; } + + /// + /// Gets an exception if one occurred. + /// + Exception Exception { get; } + } +} diff --git a/Yavsc/GoogleApiSupport/Google.Apis/Upload/IUploadSessionData.cs b/Yavsc/GoogleApiSupport/Google.Apis/Upload/IUploadSessionData.cs new file mode 100644 index 00000000..5202c46e --- /dev/null +++ b/Yavsc/GoogleApiSupport/Google.Apis/Upload/IUploadSessionData.cs @@ -0,0 +1,37 @@ +/* +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. +*/ + +namespace Google.Apis.Upload +{ + /// + /// Interface IUploadSessionData: Provides UploadUri for client to persist. Allows resuming an upload after a program restart for seekable ContentStreams. + /// + /// + /// Defines the data passed from the ResumeableUpload class upon initiation of an upload. + /// When the client application adds an event handler for the UploadSessionData event, the data + /// defined in this interface (currently the UploadURI) is passed as a parameter to the event handler procedure. + /// An event handler for the UploadSessionData event is only required if the application will support resuming the + /// upload after a program restart. + /// + public interface IUploadSessionData + { + /// + /// The resumable session URI (UploadUri) + /// + System.Uri UploadUri { get; } + } + +} diff --git a/Yavsc/GoogleApiSupport/Google.Apis/Upload/ResumableUpload.cs b/Yavsc/GoogleApiSupport/Google.Apis/Upload/ResumableUpload.cs new file mode 100644 index 00000000..dc9fb582 --- /dev/null +++ b/Yavsc/GoogleApiSupport/Google.Apis/Upload/ResumableUpload.cs @@ -0,0 +1,1051 @@ +/* +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; +using System.IO; +using System.Linq; +using System.Net; +using System.Net.Http; +using System.Threading; +using System.Threading.Tasks; + +using Google.Apis.Http; +using Google.Apis.Logging; +using Google.Apis.Media; +using Google.Apis.Requests; +using Google.Apis.Services; +using Google.Apis.Testing; +using Google.Apis.Util; + +namespace Google.Apis.Upload +{ + /// + /// Media upload which uses Google's resumable media upload protocol to upload data. + /// + /// + /// See: https://developers.google.com/drive/manage-uploads#resumable for more information on the protocol. + /// + public abstract class ResumableUpload + { + #region Constants + + /// The class logger. + private static readonly ILogger Logger = ApplicationContext.Logger.ForType(); + + private const int KB = 0x400; + private const int MB = 0x100000; + + /// Minimum chunk size (except the last one). Default value is 256*KB. + public const int MinimumChunkSize = 256 * KB; + + /// Default chunk size. Default value is 10*MB. + public const int DefaultChunkSize = 10 * MB; + + /// + /// Defines how many bytes are read from the input stream in each stream read action. + /// The read will continue until we read or we reached the end of the stream. + /// + internal int BufferSize = 4 * KB; + + /// Indicates the stream's size is unknown. + private const int UnknownSize = -1; + /// Content-Range header value for the body upload of zero length files. + private const string ZeroByteContentRangeHeader = "bytes */0"; + + #endregion // Constants + + #region Construction + + /// + /// Creates a instance. + /// + /// The data to be uploaded. Must not be null. + /// The options for the upload operation. May be null. + protected ResumableUpload(Stream contentStream, ResumableUploadOptions options) + { + contentStream.ThrowIfNull(nameof(contentStream)); + ContentStream = contentStream; + // Check if the stream length is known. + StreamLength = ContentStream.CanSeek ? ContentStream.Length : UnknownSize; + HttpClient = options?.ConfigurableHttpClient ?? new HttpClientFactory().CreateHttpClient(new CreateHttpClientArgs { ApplicationName = "ResumableUpload", GZipEnabled = true }); + Options = options; + } + + /// + /// Creates a instance for a resumable upload session which has already been initiated. + /// + /// + /// See https://cloud.google.com/storage/docs/json_api/v1/how-tos/resumable-upload#start-resumable for more information about initiating + /// resumable upload sessions and saving the session URI, or upload URI. + /// + /// The session URI of the resumable upload session. Must not be null. + /// The data to be uploaded. Must not be null. + /// The options for the upload operation. May be null. + /// The instance which can be used to upload the specified content. + public static ResumableUpload CreateFromUploadUri( + Uri uploadUri, + Stream contentStream, + ResumableUploadOptions options = null) + { + uploadUri.ThrowIfNull(nameof(uploadUri)); + return new InitiatedResumableUpload(uploadUri, contentStream, options); + } + + private sealed class InitiatedResumableUpload : ResumableUpload + { + private Uri _initiatedUploadUri; + + public InitiatedResumableUpload(Uri uploadUri, Stream contentStream, ResumableUploadOptions options) + : base(contentStream, options) + { + _initiatedUploadUri = uploadUri; + } + + public override Task InitiateSessionAsync(CancellationToken cancellationToken = default(CancellationToken)) + { + return Task.FromResult(_initiatedUploadUri); + } + } + + #endregion // Construction + + #region Properties + + + /// + /// Gets the options used to control the resumable upload. + /// + protected ResumableUploadOptions Options { get; } + + /// + /// Gets the HTTP client to use to make requests. + /// + internal ConfigurableHttpClient HttpClient { get; } + + /// Gets or sets the stream to upload. + public Stream ContentStream { get; } + + /// + /// Gets or sets the length of the steam. Will be if the media content length is + /// unknown. + /// + internal long StreamLength { get; set; } + + /// + /// Gets or sets the content of the last buffer request to the server or null. It is used when the media + /// content length is unknown, for resending it in case of server error. + /// Only used with a non-seekable stream. + /// + private byte[] LastMediaRequest { get; set; } + + /// + /// Gets or sets the last request length. + /// Only used with a non-seekable stream. + /// + private int LastMediaLength { get; set; } + + /// + /// Gets or sets the resumable session URI. + /// See https://developers.google.com/drive/manage-uploads#save-session-uri" for more details. + /// + private Uri UploadUri { get; set; } + + /// Gets or sets the amount of bytes the server had received so far. + private long BytesServerReceived { get; set; } + + /// Gets or sets the amount of bytes the client had sent so far. + private long BytesClientSent { get; set; } + + /// Change this value ONLY for testing purposes! + [VisibleForTestOnly] + protected int chunkSize = DefaultChunkSize; + + /// + /// Gets or sets the size of each chunk sent to the server. + /// Chunks (except the last chunk) must be a multiple of to be compatible with + /// Google upload servers. + /// + public int ChunkSize + { + get { return chunkSize; } + set + { + if (value < MinimumChunkSize) + { + throw new ArgumentOutOfRangeException("ChunkSize"); + } + chunkSize = value; + } + } + + #endregion // Properties + + #region Events + + /// Event called whenever the progress of the upload changes. + public event Action ProgressChanged; + + #endregion //Events + + #region Error handling (Exception and 5xx) + + /// + /// Callback class that is invoked on abnormal response or an exception. + /// This class changes the request to query the current status of the upload in order to find how many bytes + /// were successfully uploaded before the error occurred. + /// See https://developers.google.com/drive/manage-uploads#resume-upload for more details. + /// + class ServerErrorCallback : IHttpUnsuccessfulResponseHandler, IHttpExceptionHandler, IDisposable + { + private ResumableUpload Owner { get; set; } + + /// + /// Constructs a new callback and register it as unsuccessful response handler and exception handler on the + /// configurable message handler. + /// + public ServerErrorCallback(ResumableUpload resumable) + { + this.Owner = resumable; + Owner.HttpClient.MessageHandler.AddUnsuccessfulResponseHandler(this); + Owner.HttpClient.MessageHandler.AddExceptionHandler(this); + } + + public Task HandleResponseAsync(HandleUnsuccessfulResponseArgs args) + { + var result = false; + var statusCode = (int)args.Response.StatusCode; + // Handle the error if and only if all the following conditions occur: + // - there is going to be an actual retry + // - the message request is for media upload with the current Uri (remember that the message handler + // can be invoked from other threads \ messages, so we should call server error callback only if the + // request is in the current context). + // - we got a 5xx server error. + if (args.SupportsRetry && args.Request.RequestUri.Equals(Owner.UploadUri) && statusCode / 100 == 5) + { + result = OnServerError(args.Request); + } + + TaskCompletionSource tcs = new TaskCompletionSource(); + tcs.SetResult(result); + return tcs.Task; + } + + public Task HandleExceptionAsync(HandleExceptionArgs args) + { + var result = args.SupportsRetry && !args.CancellationToken.IsCancellationRequested && + args.Request.RequestUri.Equals(Owner.UploadUri) ? OnServerError(args.Request) : false; + + TaskCompletionSource tcs = new TaskCompletionSource(); + tcs.SetResult(result); + return tcs.Task; + } + + /// Changes the request in order to resume the interrupted upload. + private bool OnServerError(HttpRequestMessage request) + { + // Clear all headers and set Content-Range and Content-Length headers. + var range = String.Format("bytes */{0}", Owner.StreamLength < 0 ? "*" : Owner.StreamLength.ToString()); + request.Headers.Clear(); + request.Method = System.Net.Http.HttpMethod.Put; + request.SetEmptyContent().Headers.Add("Content-Range", range); + return true; + } + + public void Dispose() + { + Owner.HttpClient.MessageHandler.RemoveUnsuccessfulResponseHandler(this); + Owner.HttpClient.MessageHandler.RemoveExceptionHandler(this); + } + } + + #endregion + + #region Progress Monitoring + + /// Class that communicates the progress of resumable uploads to a container. + private class ResumableUploadProgress : IUploadProgress + { + /// + /// Create a ResumableUploadProgress instance. + /// + /// The status of the upload. + /// The number of bytes sent so far. + public ResumableUploadProgress(UploadStatus status, long bytesSent) + { + Status = status; + BytesSent = bytesSent; + } + + /// + /// Create a ResumableUploadProgress instance. + /// + /// An exception that occurred during the upload. + /// The number of bytes sent before this exception occurred. + public ResumableUploadProgress(Exception exception, long bytesSent) + { + Status = UploadStatus.Failed; + BytesSent = bytesSent; + Exception = exception; + } + + public UploadStatus Status { get; private set; } + public long BytesSent { get; private set; } + public Exception Exception { get; private set; } + } + + /// + /// Current state of progress of the upload. + /// + /// + private ResumableUploadProgress Progress { get; set; } + + /// + /// Updates the current progress and call the event to notify listeners. + /// + private void UpdateProgress(ResumableUploadProgress progress) + { + Progress = progress; + ProgressChanged?.Invoke(progress); + } + + /// + /// Get the current progress state. + /// + /// An IUploadProgress describing the current progress of the upload. + /// + public IUploadProgress GetProgress() + { + return Progress; + } + + #endregion + + #region UploadSessionData + /// + /// Event called when an UploadUri is created. + /// Not needed if the application program will not support resuming after a program restart. + /// + /// + /// Within the event, persist the UploadUri to storage. + /// It is strongly recommended that the full path filename (or other media identifier) is also stored so that it can be compared to the current open filename (media) upon restart. + /// + public event Action UploadSessionData; + /// + /// Data to be passed to the application program to allow resuming an upload after a program restart. + /// + private class ResumeableUploadSessionData : IUploadSessionData + { + /// + /// Create a ResumeableUploadSessionData instance to pass the UploadUri to the client. + /// + /// The resumable session URI. + public ResumeableUploadSessionData(Uri uploadUri) + { + UploadUri = uploadUri; + } + public Uri UploadUri { get; private set; } + } + /// + /// Send data (UploadUri) to application so it can store it to persistent storage. + /// + private void SendUploadSessionData(ResumeableUploadSessionData sessionData) + { + UploadSessionData?.Invoke(sessionData); + } + #endregion + + #region Upload Implementation + + /// + /// Uploads the content to the server. This method is synchronous and will block until the upload is completed. + /// + /// + /// In case the upload fails the will contain the exception that + /// cause the failure. + /// + public IUploadProgress Upload() + { + return UploadAsync(CancellationToken.None).Result; + } + + /// Uploads the content asynchronously to the server. + public Task UploadAsync() + { + return UploadAsync(CancellationToken.None); + } + + /// Uploads the content to the server using the given cancellation token. + /// + /// In case the upload fails will contain the exception that + /// cause the failure. The only exception which will be thrown is + /// which indicates that the task was canceled. + /// + /// A cancellation token to cancel operation. + public async Task UploadAsync(CancellationToken cancellationToken) + { + BytesServerReceived = 0; + UpdateProgress(new ResumableUploadProgress(UploadStatus.Starting, 0)); + + try + { + UploadUri = await InitiateSessionAsync(cancellationToken).ConfigureAwait(false); + if (ContentStream.CanSeek) + { + SendUploadSessionData(new ResumeableUploadSessionData(UploadUri)); + } + Logger.Debug("MediaUpload[{0}] - Start uploading...", UploadUri); + } + catch (Exception ex) + { + Logger.Error(ex, "MediaUpload - Exception occurred while initializing the upload"); + UpdateProgress(new ResumableUploadProgress(ex, BytesServerReceived)); + return Progress; + } + + return await UploadCoreAsync(cancellationToken).ConfigureAwait(false); + } + + /// + /// Resumes the upload from the last point it was interrupted. + /// Use when resuming and the program was not restarted. + /// + public IUploadProgress Resume() + { + return ResumeAsync(null, CancellationToken.None).Result; + } + /// + /// Resumes the upload from the last point it was interrupted. + /// Use when the program was restarted and you wish to resume the upload that was in progress when the program was halted. + /// Implemented only for ContentStreams where .CanSeek is True. + /// + /// + /// In your application's UploadSessionData Event Handler, store UploadUri.AbsoluteUri property value (resumable session URI string value) + /// to persistent storage for use with Resume() or ResumeAsync() upon a program restart. + /// It is strongly recommended that the FullPathFilename of the media file that is being uploaded is saved also so that a subsequent execution of the + /// program can compare the saved FullPathFilename value to the FullPathFilename of the media file that it has opened for uploading. + /// You do not need to seek to restart point in the ContentStream file. + /// + /// VideosResource.InsertMediaUpload UploadUri property value that was saved to persistent storage during a prior execution. + public IUploadProgress Resume(Uri uploadUri) + { + return ResumeAsync(uploadUri, CancellationToken.None).Result; + } + /// + /// Asynchronously resumes the upload from the last point it was interrupted. + /// + /// + /// You do not need to seek to restart point in the ContentStream file. + /// + public Task ResumeAsync() + { + return ResumeAsync(null, CancellationToken.None); + } + /// + /// Asynchronously resumes the upload from the last point it was interrupted. + /// Use when resuming and the program was not restarted. + /// + /// + /// You do not need to seek to restart point in the ContentStream file. + /// + /// A cancellation token to cancel the asynchronous operation. + public Task ResumeAsync(CancellationToken cancellationToken) + { + return ResumeAsync(null, cancellationToken); + } + /// + /// Asynchronously resumes the upload from the last point it was interrupted. + /// Use when resuming and the program was restarted. + /// Implemented only for ContentStreams where .CanSeek is True. + /// + /// + /// In your application's UploadSessionData Event Handler, store UploadUri.AbsoluteUri property value (resumable session URI string value) + /// to persistent storage for use with Resume() or ResumeAsync() upon a program restart. + /// It is strongly recommended that the FullPathFilename of the media file that is being uploaded is saved also so that a subsequent execution of the + /// program can compare the saved FullPathFilename value to the FullPathFilename of the media file that it has opened for uploading. + /// You do not need to seek to restart point in the ContentStream file. + /// + /// VideosResource.InsertMediaUpload UploadUri property value that was saved to persistent storage during a prior execution. + public Task ResumeAsync(Uri uploadUri) + { + return ResumeAsync(uploadUri, CancellationToken.None); + } + /// + /// Asynchronously resumes the upload from the last point it was interrupted. + /// Use when the program was restarted and you wish to resume the upload that was in progress when the program was halted. + /// Implemented only for ContentStreams where .CanSeek is True. + /// + /// + /// In your application's UploadSessionData Event Handler, store UploadUri.AbsoluteUri property value (resumable session URI string value) + /// to persistent storage for use with Resume() or ResumeAsync() upon a program restart. + /// It is strongly recommended that the FullPathFilename of the media file that is being uploaded is saved also so that a subsequent execution of the + /// program can compare the saved FullPathFilename value to the FullPathFilename of the media file that it has opened for uploading. + /// You do not need to seek to restart point in the ContentStream file. + /// + /// VideosResource.InsertMediaUpload UploadUri property value that was saved to persistent storage during a prior execution. + /// A cancellation token to cancel the asynchronous operation. + public async Task ResumeAsync(Uri uploadUri, CancellationToken cancellationToken) + { + // When called with uploadUri parameter of non-null value, the UploadUri is being + // provided upon a program restart to resume a previously interrupted upload. + if (uploadUri != null) + { + if (ContentStream.CanSeek) + { + Logger.Info("Resuming after program restart: UploadUri={0}", uploadUri); + UploadUri = uploadUri; + } + else + { + throw new NotImplementedException("Resume after program restart not allowed when ContentStream.CanSeek is false"); + } + } + if (UploadUri == null) + { + Logger.Info("There isn't any upload in progress, so starting to upload again"); + return await UploadAsync(cancellationToken).ConfigureAwait(false); + } + // The first "resuming" request is to query the server in which point the upload was interrupted. + var range = String.Format("bytes */{0}", StreamLength < 0 ? "*" : StreamLength.ToString()); + HttpRequestMessage request = new RequestBuilder() + { + BaseUri = UploadUri, + Method = HttpConsts.Put + }.CreateRequest(); + request.SetEmptyContent().Headers.Add("Content-Range", range); + + try + { + HttpResponseMessage response; + using (var callback = new ServerErrorCallback(this)) + { + response = await HttpClient.SendAsync(request, cancellationToken) + .ConfigureAwait(false); + } + + if (await HandleResponse(response).ConfigureAwait(false)) + { + // All the media was successfully upload. + UpdateProgress(new ResumableUploadProgress(UploadStatus.Completed, BytesServerReceived)); + return Progress; + } + } + catch (TaskCanceledException ex) + { + Logger.Error(ex, "MediaUpload[{0}] - Task was canceled", UploadUri); + UpdateProgress(new ResumableUploadProgress(ex, BytesServerReceived)); + throw ex; + } + catch (Exception ex) + { + Logger.Error(ex, "MediaUpload[{0}] - Exception occurred while resuming uploading media", UploadUri); + UpdateProgress(new ResumableUploadProgress(ex, BytesServerReceived)); + return Progress; + } + + // Continue to upload the media stream. + return await UploadCoreAsync(cancellationToken).ConfigureAwait(false); + } + + /// The core logic for uploading a stream. It is used by the upload and resume methods. + private async Task UploadCoreAsync(CancellationToken cancellationToken) + { + try + { + using (var callback = new ServerErrorCallback(this)) + { + while (!await SendNextChunkAsync(ContentStream, cancellationToken).ConfigureAwait(false)) + { + UpdateProgress(new ResumableUploadProgress(UploadStatus.Uploading, BytesServerReceived)); + } + UpdateProgress(new ResumableUploadProgress(UploadStatus.Completed, BytesServerReceived)); + } + } + catch (TaskCanceledException ex) + { + Logger.Error(ex, "MediaUpload[{0}] - Task was canceled", UploadUri); + UpdateProgress(new ResumableUploadProgress(ex, BytesServerReceived)); + throw ex; + } + catch (Exception ex) + { + Logger.Error(ex, "MediaUpload[{0}] - Exception occurred while uploading media", UploadUri); + UpdateProgress(new ResumableUploadProgress(ex, BytesServerReceived)); + } + + return Progress; + } + + /// + /// Initiates the resumable upload session and returns the session URI, or upload URI. + /// See https://developers.google.com/drive/manage-uploads#start-resumable and + /// https://cloud.google.com/storage/docs/json_api/v1/how-tos/resumable-upload#start-resumable for more information. + /// + /// The token to monitor for cancellation requests. + /// + /// The task containing the session URI to use for the resumable upload. + /// + public abstract Task InitiateSessionAsync(CancellationToken cancellationToken = default(CancellationToken)); + + /// + /// Process a response from the final upload chunk call. + /// + /// The response body from the final uploaded chunk. + protected virtual void ProcessResponse(HttpResponseMessage httpResponse) + { + } + + /// Uploads the next chunk of data to the server. + /// True if the entire media has been completely uploaded. + protected async Task SendNextChunkAsync(Stream stream, CancellationToken cancellationToken) + { + cancellationToken.ThrowIfCancellationRequested(); + + HttpRequestMessage request = new RequestBuilder() + { + BaseUri = UploadUri, + Method = HttpConsts.Put + }.CreateRequest(); + + // Prepare next chunk to send. + int contentLength = ContentStream.CanSeek + ? PrepareNextChunkKnownSize(request, stream, cancellationToken) + : PrepareNextChunkUnknownSize(request, stream, cancellationToken); + + BytesClientSent = BytesServerReceived + contentLength; + + Logger.Debug("MediaUpload[{0}] - Sending bytes={1}-{2}", UploadUri, BytesServerReceived, + BytesClientSent - 1); + + HttpResponseMessage response = await HttpClient.SendAsync(request, cancellationToken) + .ConfigureAwait(false); + return await HandleResponse(response).ConfigureAwait(false); + } + + /// Handles a media upload HTTP response. + /// True if the entire media has been completely uploaded. + private async Task HandleResponse(HttpResponseMessage response) + { + if (response.IsSuccessStatusCode) + { + MediaCompleted(response); + return true; + } + else if (response.StatusCode == (HttpStatusCode)308) + { + // The upload protocol uses 308 to indicate that there is more data expected from the server. + // If the server has received no bytes, it indicates this by not including + // a Range header in the response.. + var range = response.Headers.FirstOrDefault(x => x.Key == "Range").Value?.First(); + BytesServerReceived = GetNextByte(range); + Logger.Debug("MediaUpload[{0}] - {1} Bytes were sent successfully", UploadUri, BytesServerReceived); + return false; + } + throw await ExceptionForResponseAsync(response).ConfigureAwait(false); + } + + /// + /// Creates a instance using the error response from the server. + /// + /// The error response. + /// An exception which can be thrown by the caller. + protected Task ExceptionForResponseAsync(HttpResponseMessage response) + { + return MediaApiErrorHandling.ExceptionForResponseAsync(Options?.Serializer, Options?.ServiceName, response); + } + + /// A callback when the media was uploaded successfully. + private void MediaCompleted(HttpResponseMessage response) + { + Logger.Debug("MediaUpload[{0}] - media was uploaded successfully", UploadUri); + ProcessResponse(response); + BytesServerReceived = StreamLength; + + // Clear the last request byte array. + LastMediaRequest = null; + } + + /// Prepares the given request with the next chunk in case the steam length is unknown. + private int PrepareNextChunkUnknownSize(HttpRequestMessage request, Stream stream, + CancellationToken cancellationToken) + { + if (LastMediaRequest == null) + { + // Initialise state + // ChunkSize + 1 to give room for one extra byte for end-of-stream checking + LastMediaRequest = new byte[ChunkSize + 1]; + LastMediaLength = 0; + } + // Re-use any bytes the server hasn't received + int copyCount = (int)(BytesClientSent - BytesServerReceived) + + Math.Max(0, LastMediaLength - ChunkSize); + if (LastMediaLength != copyCount) + { + Buffer.BlockCopy(LastMediaRequest, LastMediaLength - copyCount, LastMediaRequest, 0, copyCount); + LastMediaLength = copyCount; + } + // Read any more required bytes from stream, to form the next chunk + while (LastMediaLength < ChunkSize + 1 && StreamLength == UnknownSize) + { + cancellationToken.ThrowIfCancellationRequested(); + int readSize = Math.Min(BufferSize, ChunkSize + 1 - LastMediaLength); + int len = stream.Read(LastMediaRequest, LastMediaLength, readSize); + LastMediaLength += len; + if (len == 0) + { + // Stream ended, so we know the length + StreamLength = BytesServerReceived + LastMediaLength; + } + } + // Set Content-Length and Content-Range. + int contentLength = Math.Min(ChunkSize, LastMediaLength); + var byteArrayContent = new ByteArrayContent(LastMediaRequest, 0, contentLength); + byteArrayContent.Headers.Add("Content-Range", GetContentRangeHeader(BytesServerReceived, contentLength)); + request.Content = byteArrayContent; + return contentLength; + } + + /// Prepares the given request with the next chunk in case the steam length is known. + private int PrepareNextChunkKnownSize(HttpRequestMessage request, Stream stream, + CancellationToken cancellationToken) + { + int chunkSize = (int)Math.Min(StreamLength - BytesServerReceived, (long)ChunkSize); + + // Stream length is known and it supports seek and position operations. + // We can change the stream position and read bytes from the last point. + byte[] buffer = new byte[Math.Min(chunkSize, BufferSize)]; + + // If the number of bytes received by the server isn't equal to the amount of bytes the client sent, we + // need to change the position of the input stream, otherwise we can continue from the current position. + if (stream.Position != BytesServerReceived) + { + stream.Position = BytesServerReceived; + } + + MemoryStream ms = new MemoryStream(chunkSize); + int bytesRead = 0; + while (true) + { + cancellationToken.ThrowIfCancellationRequested(); + + // Read from input stream and write to output stream. + // TODO(peleyal): write a utility similar to (.NET 4 Stream.CopyTo method). + int len = stream.Read(buffer, 0, (int)Math.Min(buffer.Length, chunkSize - bytesRead)); + if (len == 0) break; + ms.Write(buffer, 0, len); + bytesRead += len; + } + + // Set the stream position to beginning and wrap it with stream content. + ms.Position = 0; + request.Content = new StreamContent(ms); + request.Content.Headers.Add("Content-Range", GetContentRangeHeader(BytesServerReceived, chunkSize)); + + return chunkSize; + } + + /// Returns the next byte index need to be sent. + private long GetNextByte(string range) + { + return range == null ? 0 : long.Parse(range.Substring(range.IndexOf('-') + 1)) + 1; + } + + /// + /// Build a content range header of the form: "bytes X-Y/T" where: + /// + /// X is the first byte being sent. + /// Y is the last byte in the range being sent (inclusive). + /// T is the total number of bytes in the range or * for unknown size. + /// + /// + /// + /// See: RFC2616 HTTP/1.1, Section 14.16 Header Field Definitions, Content-Range + /// http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.16 + /// + /// Start of the chunk. + /// Size of the chunk being sent. + /// The content range header value. + private string GetContentRangeHeader(long chunkStart, long chunkSize) + { + string strLength = StreamLength < 0 ? "*" : StreamLength.ToString(); + + // If a file of length 0 is sent, one chunk needs to be sent with 0 size. + // This chunk cannot be specified with the standard (inclusive) range header. + // In this case, use * to indicate no bytes sent in the Content-Range header. + if (chunkStart == 0 && chunkSize == 0 && StreamLength == 0) + { + return ZeroByteContentRangeHeader; + } + else + { + long chunkEnd = chunkStart + chunkSize - 1; + return String.Format("bytes {0}-{1}/{2}", chunkStart, chunkEnd, strLength); + } + } + + #endregion Upload Implementation + } + + /// + /// Media upload which uses Google's resumable media upload protocol to upload data. + /// + /// + /// See: https://developers.google.com/drive/manage-uploads#resumable for more information on the protocol. + /// + /// + /// The type of the body of this request. Generally this should be the metadata related to the content to be + /// uploaded. Must be serializable to/from JSON. + /// + public class ResumableUpload : ResumableUpload + { + #region Constants + + /// Payload description headers, describing the content itself. + private const string PayloadContentTypeHeader = "X-Upload-Content-Type"; + + /// Payload description headers, describing the content itself. + private const string PayloadContentLengthHeader = "X-Upload-Content-Length"; + + /// Specify the type of this upload (this class supports resumable only). + private const string UploadType = "uploadType"; + + /// The uploadType parameter value for resumable uploads. + private const string Resumable = "resumable"; + + #endregion // Constants + + #region Construction + + /// + /// Create a resumable upload instance with the required parameters. + /// + /// The client service. + /// The path for this media upload method. + /// The HTTP method to start this upload. + /// The stream containing the content to upload. + /// Content type of the content to be uploaded. Some services + /// may allow this to be null; others require a content type to be specified and will + /// fail when the upload is started if the value is null. + /// + /// Caller is responsible for maintaining the open until the upload is + /// completed. + /// Caller is responsible for closing the . + /// + protected ResumableUpload(IClientService service, string path, string httpMethod, Stream contentStream, string contentType) + : base(contentStream, + new ResumableUploadOptions + { + HttpClient = service.HttpClient, + Serializer = service.Serializer, + ServiceName = service.Name + }) + { + service.ThrowIfNull(nameof(service)); + path.ThrowIfNull(nameof(path)); + httpMethod.ThrowIfNullOrEmpty(nameof(httpMethod)); + contentStream.ThrowIfNull(nameof(contentStream)); + + this.Service = service; + this.Path = path; + this.HttpMethod = httpMethod; + this.ContentType = contentType; + } + + #endregion // Construction + + #region Properties + + /// Gets or sets the service. + public IClientService Service { get; private set; } + + /// + /// Gets or sets the path of the method (combined with + /// ) to produce + /// absolute Uri. + /// + public string Path { get; private set; } + + /// Gets or sets the HTTP method of this upload (used to initialize the upload). + public string HttpMethod { get; private set; } + + /// Gets or sets the stream's Content-Type. + public string ContentType { get; private set; } + + /// Gets or sets the body of this request. + public TRequest Body { get; set; } + + #endregion // Properties + + #region Upload Implementation + + /// + public override async Task InitiateSessionAsync(CancellationToken cancellationToken = default(CancellationToken)) + { + HttpRequestMessage request = CreateInitializeRequest(); + Options?.ModifySessionInitiationRequest?.Invoke(request); + var response = await Service.HttpClient.SendAsync(request, cancellationToken).ConfigureAwait(false); + + if (!response.IsSuccessStatusCode) + { + throw await ExceptionForResponseAsync(response).ConfigureAwait(false); + } + return response.Headers.Location; + } + + /// Creates a request to initialize a request. + private HttpRequestMessage CreateInitializeRequest() + { + var builder = new RequestBuilder() + { + BaseUri = new Uri(Service.BaseUri), + Path = Path, + Method = HttpMethod, + }; + + // init parameters + builder.AddParameter(RequestParameterType.Query, "key", Service.ApiKey); + builder.AddParameter(RequestParameterType.Query, UploadType, Resumable); + SetAllPropertyValues(builder); + + HttpRequestMessage request = builder.CreateRequest(); + if (ContentType != null) + { + request.Headers.Add(PayloadContentTypeHeader, ContentType); + } + + // if the length is unknown at the time of this request, omit "X-Upload-Content-Length" header + if (ContentStream.CanSeek) + { + request.Headers.Add(PayloadContentLengthHeader, StreamLength.ToString()); + } + + Service.SetRequestSerailizedContent(request, Body); + return request; + } + + /// + /// Reflectively enumerate the properties of this object looking for all properties containing the + /// RequestParameterAttribute and copy their values into the request builder. + /// + private void SetAllPropertyValues(RequestBuilder requestBuilder) + { + Type myType = this.GetType(); + var properties = myType.GetProperties(); + + foreach (var property in properties) + { + var attribute = Utilities.GetCustomAttribute(property); + + if (attribute != null) + { + string name = attribute.Name ?? property.Name.ToLower(); + object value = property.GetValue(this, null); + if (value != null) + { + var valueAsEnumerable = value as IEnumerable; + if (!(value is string) && valueAsEnumerable != null) + { + foreach (var elem in valueAsEnumerable) + { + requestBuilder.AddParameter(attribute.Type, name, Utilities.ConvertToString(elem)); + } + } + else + { + // Otherwise just convert it to a string. + requestBuilder.AddParameter(attribute.Type, name, Utilities.ConvertToString(value)); + } + } + } + } + } + + #endregion Upload Implementation + } + + /// + /// Media upload which uses Google's resumable media upload protocol to upload data. + /// The version with two types contains both a request object and a response object. + /// + /// + /// See: https://developers.google.com/gdata/docs/resumable_upload for + /// information on the protocol. + /// + /// + /// The type of the body of this request. Generally this should be the metadata related + /// to the content to be uploaded. Must be serializable to/from JSON. + /// + /// + /// The type of the response body. + /// + public class ResumableUpload : ResumableUpload + { + #region Construction + + /// + /// Create a resumable upload instance with the required parameters. + /// + /// The client service. + /// The path for this media upload method. + /// The HTTP method to start this upload. + /// The stream containing the content to upload. + /// Content type of the content to be uploaded. + /// + /// The stream must support the "Length" property. + /// Caller is responsible for maintaining the open until the + /// upload is completed. + /// Caller is responsible for closing the . + /// + protected ResumableUpload(IClientService service, string path, string httpMethod, + Stream contentStream, string contentType) + : base(service, path, httpMethod, contentStream, contentType) { } + + #endregion // Construction + + #region Properties + + /// + /// The response body. + /// + /// + /// This property will be set during upload. The event + /// is triggered when this has been set. + /// + public TResponse ResponseBody { get; private set; } + + #endregion // Properties + + #region Events + + /// Event which is called when the response metadata is processed. + public event Action ResponseReceived; + + #endregion // Events + + #region Overrides + + /// Process the response body + protected override void ProcessResponse(HttpResponseMessage response) + { + base.ProcessResponse(response); + ResponseBody = Service.DeserializeResponse(response).Result; + + ResponseReceived?.Invoke(ResponseBody); + } + + #endregion // Overrides + } +} diff --git a/Yavsc/GoogleApiSupport/Google.Apis/Upload/ResumableUploadOptions.cs b/Yavsc/GoogleApiSupport/Google.Apis/Upload/ResumableUploadOptions.cs new file mode 100644 index 00000000..0ea7408e --- /dev/null +++ b/Yavsc/GoogleApiSupport/Google.Apis/Upload/ResumableUploadOptions.cs @@ -0,0 +1,59 @@ +// Copyright 2017 Google Inc. All Rights Reserved. +// +// 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; +using System; +using System.Net.Http; + +namespace Google.Apis.Upload +{ + /// + /// Options for operations. + /// + public sealed class ResumableUploadOptions + { + /// + /// Gets or sets the HTTP client to use when starting the upload sessions and uploading data. + /// + public HttpClient HttpClient { get; set; } + + /// + /// Gets or sets the callback for modifying the session initiation request. + /// See https://cloud.google.com/storage/docs/json_api/v1/how-tos/resumable-upload#start-resumable for more information. + /// + /// + /// Note: If these options are used with a created using , + /// this property will be ignored as the session has already been initiated. + /// + public Action ModifySessionInitiationRequest { get; set; } + + /// + /// Gets or sets the serializer to use when parsing error responses. + /// + public ISerializer Serializer { get; set; } + + /// + /// Gets or sets the name of the service performing the upload. + /// + /// + /// This will be used to set the in the event of an error. + /// + public string ServiceName { get; set; } + + /// + /// Gets the as a if it is an instance of one. + /// + internal ConfigurableHttpClient ConfigurableHttpClient => HttpClient as ConfigurableHttpClient; + } +} diff --git a/Yavsc/GoogleApiSupport/Google.Apis/Util/Store/FileDataStore.cs b/Yavsc/GoogleApiSupport/Google.Apis/Util/Store/FileDataStore.cs new file mode 100644 index 00000000..06fe1b44 --- /dev/null +++ b/Yavsc/GoogleApiSupport/Google.Apis/Util/Store/FileDataStore.cs @@ -0,0 +1,183 @@ +/* +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. +*/ + +// TODO: This does not support UWP Storage. + +using Google.Apis.Json; +using System; +using System.IO; +using System.Threading.Tasks; + +namespace Google.Apis.Util.Store +{ + /// + /// File data store that implements . This store creates a different file for each + /// combination of type and key. This file data store stores a JSON format of the specified object. + /// + public class FileDataStore : IDataStore + { + private const string XdgDataHomeSubdirectory = "google-filedatastore"; + private static readonly Task CompletedTask = Task.FromResult(0); + + readonly string folderPath; + /// Gets the full folder path. + public string FolderPath { get { return folderPath; } } + + /// + /// Constructs a new file data store. If fullPath is false the path will be used as relative to + /// Environment.SpecialFolder.ApplicationData" on Windows, or $HOME on Linux and MacOS, + /// otherwise the input folder will be treated as absolute. + /// The folder is created if it doesn't exist yet. + /// + /// Folder path. + /// + /// Defines whether the folder parameter is absolute or relative to + /// Environment.SpecialFolder.ApplicationData on Windows, or$HOME on Linux and MacOS. + /// + public FileDataStore(string folder, bool fullPath = false) + { + folderPath = fullPath + ? folder + : Path.Combine(GetHomeDirectory(), folder); + if (!Directory.Exists(folderPath)) + { + Directory.CreateDirectory(folderPath); + } + } + + private string GetHomeDirectory() + { + string appData = Environment.GetEnvironmentVariable("APPDATA"); + if (!string.IsNullOrEmpty(appData)) + { + // This is almost certainly windows. + // This path must be the same between the desktop FileDataStore and this netstandard FileDataStore. + return appData; + } + string home = Environment.GetEnvironmentVariable("HOME"); + if (!string.IsNullOrEmpty(home)) + { + // This is almost certainly Linux or MacOS. + // Follow the XDG Base Directory Specification: https://specifications.freedesktop.org/basedir-spec/latest/index.html + // Store data in subdirectory of $XDG_DATA_HOME if it exists, defaulting to $HOME/.local/share if not set. + string xdgDataHome = Environment.GetEnvironmentVariable("XDG_DATA_HOME"); + if (string.IsNullOrEmpty(xdgDataHome)) + { + xdgDataHome = Path.Combine(home, ".local", "share"); + } + return Path.Combine(xdgDataHome, XdgDataHomeSubdirectory); + } + throw new PlatformNotSupportedException("Relative FileDataStore paths not supported on this platform."); + } + + /// + /// Stores the given value for the given key. It creates a new file (named ) in + /// . + /// + /// The type to store in the data store. + /// The key. + /// The value to store in the data store. + public Task StoreAsync(string key, T value) + { + if (string.IsNullOrEmpty(key)) + { + throw new ArgumentException("Key MUST have a value"); + } + + var serialized = NewtonsoftJsonSerializer.Instance.Serialize(value); + var filePath = Path.Combine(folderPath, GenerateStoredKey(key, typeof(T))); + File.WriteAllText(filePath, serialized); + return CompletedTask; + } + + /// + /// Deletes the given key. It deletes the named file in + /// . + /// + /// The key to delete from the data store. + public Task DeleteAsync(string key) + { + if (string.IsNullOrEmpty(key)) + { + throw new ArgumentException("Key MUST have a value"); + } + + var filePath = Path.Combine(folderPath, GenerateStoredKey(key, typeof(T))); + if (File.Exists(filePath)) + { + File.Delete(filePath); + } + return CompletedTask; + } + + /// + /// Returns the stored value for the given key or null if the matching file ( + /// in doesn't exist. + /// + /// The type to retrieve. + /// The key to retrieve from the data store. + /// The stored object. + public Task GetAsync(string key) + { + if (string.IsNullOrEmpty(key)) + { + throw new ArgumentException("Key MUST have a value"); + } + + TaskCompletionSource tcs = new TaskCompletionSource(); + var filePath = Path.Combine(folderPath, GenerateStoredKey(key, typeof(T))); + if (File.Exists(filePath)) + { + try + { + var obj = File.ReadAllText(filePath); + tcs.SetResult(NewtonsoftJsonSerializer.Instance.Deserialize(obj)); + } + catch (Exception ex) + { + tcs.SetException(ex); + } + } + else + { + tcs.SetResult(default(T)); + } + return tcs.Task; + } + + /// + /// Clears all values in the data store. This method deletes all files in . + /// + public Task ClearAsync() + { + if (Directory.Exists(folderPath)) + { + Directory.Delete(folderPath, true); + Directory.CreateDirectory(folderPath); + } + + return CompletedTask; + } + + /// Creates a unique stored key based on the key and the class type. + /// The object key. + /// The type to store or retrieve. + public static string GenerateStoredKey(string key, Type t) + { + return string.Format("{0}-{1}", t.FullName, key); + } + } +} diff --git a/Yavsc/GoogleApiSupport/Google.Apis/Util/Store/NullDataStore.cs b/Yavsc/GoogleApiSupport/Google.Apis/Util/Store/NullDataStore.cs new file mode 100644 index 00000000..6fc052d7 --- /dev/null +++ b/Yavsc/GoogleApiSupport/Google.Apis/Util/Store/NullDataStore.cs @@ -0,0 +1,69 @@ +/* +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.Threading.Tasks; + +namespace Google.Apis.Util.Store +{ + /// + /// A null datastore. Nothing is stored, nothing is retrievable. + /// + public class NullDataStore : IDataStore + { + private static readonly Task s_completedTask = CompletedTask(); + + private static Task CompletedTask() + { + var tcs = new TaskCompletionSource(); + tcs.SetResult(default(T)); + return tcs.Task; + } + + /// + /// Construct a new null datastore, that stores nothing. + /// + public NullDataStore() + { + } + + /// + public Task ClearAsync() => s_completedTask; + + /// + public Task DeleteAsync(string key) => s_completedTask; + + /// + /// Asynchronously returns the stored value for the given key or null if not found. + /// This implementation of will always return a completed task + /// with a result of null. + /// + /// The type to retrieve from the data store. + /// The key to retrieve its value. + /// Always null. + public Task GetAsync(string key) => CompletedTask(); + + /// + /// Asynchronously stores the given value for the given key (replacing any existing value). + /// This implementation of does not store the value, + /// and will not return it in future calls to . + /// + /// The type to store in the data store. + /// The key. + /// The value. + /// A task that completes immediately. + public Task StoreAsync(string key, T value) => s_completedTask; + } +} diff --git a/Yavsc/Helpers/GoogleHelpers.cs b/Yavsc/Helpers/GoogleHelpers.cs index ba911642..ad45524b 100644 --- a/Yavsc/Helpers/GoogleHelpers.cs +++ b/Yavsc/Helpers/GoogleHelpers.cs @@ -33,13 +33,18 @@ using Microsoft.AspNet.Identity; namespace Yavsc.Helpers { - using Models.Auth; using Models.Google.Messaging; using Models.Messaging; using Models; using Interfaces.Workflow; using Yavsc.Models.Google; using Yavsc.Models.Calendar; + using Google.Apis.Auth.OAuth2; + using Google.Apis.Auth.OAuth2.Responses; + using Microsoft.Data.Entity; + using Google.Apis.Auth.OAuth2.Flows; + using Microsoft.AspNet.Identity.EntityFramework; + /// /// Google helpers. @@ -80,22 +85,40 @@ namespace Yavsc.Helpers throw new Exception ("Quelque chose s'est mal passé à l'envoi",ex); } } - public static async Task GetCredentialForGoogleApiAsync(this UserManager userManager, ApplicationDbContext context, string uid) + public static ServiceAccountCredential GetCredentialForApi(IEnumerable scopes) { - var user = await userManager.FindByIdAsync(uid); - var googleId = context.UserLogins.FirstOrDefault( - x => x.UserId == uid && x.LoginProvider == "Google" - ).ProviderKey; + var initializer = new ServiceAccountCredential.Initializer(Startup.GoogleSettings.Account.client_email); + initializer = initializer.FromPrivateKey(Startup.GoogleSettings.Account.private_key); + initializer.Scopes = scopes; + var credential = new ServiceAccountCredential(initializer); + return credential; + } + + public static async Task> GetGoogleUserLoginAsync( + this UserManager userManager, + ApplicationDbContext context, + string yavscUserId) + { + var user = await userManager.FindByIdAsync(yavscUserId); + var googleLogin = await context.UserLogins.FirstOrDefaultAsync( + x => x.UserId == yavscUserId && x.LoginProvider == "Google" + ); + return googleLogin; + } + public static UserCredential GetGoogleCredential(IdentityUserLogin googleUserLogin) + { + var googleId = googleUserLogin.ProviderKey; if (string.IsNullOrEmpty(googleId)) throw new InvalidOperationException("No Google login"); - var token = await context.GetTokensAsync(googleId); - return new UserCredential(uid, token); + TokenResponse resp = null; + var flow = new GoogleAuthorizationCodeFlow(new GoogleAuthorizationCodeFlow.Initializer()); + return new UserCredential(flow, googleId, resp); } static string evStatusDispo = "Dispo"; - public static async Task GetFreeTime (this ICalendarManager manager, string userId, string calId, DateTime startDate, DateTime endDate) + public static async Task GetFreeTime (this ICalendarManager manager, string calId, DateTime startDate, DateTime endDate) { - CalendarEventList evlist = await manager.GetCalendarAsync(calId, startDate, endDate, userId) ; + CalendarEventList evlist = await manager.GetCalendarAsync(calId, startDate, endDate) ; var result = evlist.items .Where( ev => ev.status == evStatusDispo @@ -129,6 +152,7 @@ namespace Yavsc.Helpers public static async Task GetJsonTokenAsync(string scope) { + var claimSet = CreateGoogleServiceClaimSet(scope, 3600); string jsonClaims = JsonConvert.SerializeObject(claimSet); string encClaims = Convert.ToBase64String(System.Text.Encoding.UTF8.GetBytes(jsonClaims)); diff --git a/Yavsc/Helpers/SimpleJsonPostMethod.cs b/Yavsc/Helpers/SimpleJsonPostMethod.cs index 5e9ceacc..ab17d3a7 100644 --- a/Yavsc/Helpers/SimpleJsonPostMethod.cs +++ b/Yavsc/Helpers/SimpleJsonPostMethod.cs @@ -59,16 +59,16 @@ namespace Yavsc.Helpers /// Invoke the specified query. /// /// Query. - public TAnswer Invoke(object query) + public async Task Invoke(object query) { - using (Stream streamQuery = request.GetRequestStream()) { + using (Stream streamQuery = await request.GetRequestStreamAsync()) { using (StreamWriter writer = new StreamWriter(streamQuery)) { writer.Write (JsonConvert.SerializeObject(query)); }} TAnswer ans = default (TAnswer); - using (WebResponse response = request.GetResponse ()) { - using (Stream responseStream = response.GetResponseStream ()) { + using (WebResponse response = await request.GetResponseAsync ()) { + using (Stream responseStream = response.GetResponseStream ()) { using (StreamReader rdr = new StreamReader (responseStream)) { ans = (TAnswer) JsonConvert.DeserializeObject (rdr.ReadToEnd ()); } diff --git a/Yavsc/Models/Google/Calendar/CalendarList.cs b/Yavsc/Models/Google/Calendar/CalendarList.cs index 12200ff4..5505f8b0 100644 --- a/Yavsc/Models/Google/Calendar/CalendarList.cs +++ b/Yavsc/Models/Google/Calendar/CalendarList.cs @@ -39,6 +39,8 @@ namespace Yavsc.Models.Google.Calendar /// Gets or sets the next sync token. /// /// The next sync token. + public string description { get; set; } + public string summpary { get; set; } public string nextSyncToken { get; set; } /// /// Gets or sets the items. diff --git a/Yavsc/Models/Google/Resource.cs b/Yavsc/Models/Google/Resource.cs index 2d323178..1f58edaf 100644 --- a/Yavsc/Models/Google/Resource.cs +++ b/Yavsc/Models/Google/Resource.cs @@ -31,6 +31,15 @@ namespace Yavsc.Models.Google public GDate start; public GDate end; public string recurence; + + public string description; + + public string summary; + + /// + /// Avaible <=> transparency == "transparent" + /// + public string transparency; } } diff --git a/Yavsc/Services/GoogleApis/CalendarManager.cs b/Yavsc/Services/GoogleApis/CalendarManager.cs index f05a4271..d55dc6a3 100644 --- a/Yavsc/Services/GoogleApis/CalendarManager.cs +++ b/Yavsc/Services/GoogleApis/CalendarManager.cs @@ -23,28 +23,27 @@ using System; using System.Net; using System.IO; using System.Web; -using Yavsc.Models.Auth; using Newtonsoft.Json; +using System.Threading.Tasks; +using Microsoft.AspNet.Identity; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.OptionsModel; +using Google.Apis.Auth.OAuth2; +using Google.Apis.Util.Store; namespace Yavsc.Models.Google.Calendar { - using System.Threading.Tasks; - using Microsoft.AspNet.Identity; - using Microsoft.Extensions.Logging; - using Microsoft.Extensions.OptionsModel; using Models.Google; using Yavsc.Helpers; using Yavsc.Models.Calendar; using Yavsc.ViewModels.Calendar; - - /// /// Google Calendar API client. /// public class CalendarManager : ICalendarManager { - // protected static string scopeCalendar = "https://www.googleapis.com/auth/calendar"; + protected static string scopeCalendar = "https://www.googleapis.com/auth/calendar"; private string _ApiKey; private readonly UserManager _userManager; @@ -82,26 +81,34 @@ namespace Yavsc.Models.Google.Calendar /// private string timeZone = "+01:00"; + private readonly IDataStore dataStore = new FileDataStore(GoogleWebAuthorizationBroker.Folder); + + + /// /// Gets the calendar list. /// /// The calendars. - /// Cred. + /// Yavsc user id public async Task GetCalendarsAsync (string userId) { - UserCredential creds = await _userManager.GetCredentialForGoogleApiAsync( - _dbContext, userId); - if (creds==null) - throw new InvalidOperationException("No credential"); + CalendarList res = null; - HttpWebRequest webreq = WebRequest.CreateHttp (getCalListUri); - webreq.Headers.Add (HttpRequestHeader.Authorization, creds.GetHeader()); + var login = await _userManager.GetGoogleUserLoginAsync(_dbContext,userId); + var token = await _dbContext.GetTokensAsync(login.ProviderKey); + if (token==null) + throw new InvalidOperationException("No Google token"); + + HttpWebRequest webreq = WebRequest.CreateHttp(getCalListUri); + webreq.Headers.Add("Authorization", "Bearer "+ token.AccessToken); webreq.Method = "GET"; webreq.ContentType = "application/http"; using (WebResponse resp = webreq.GetResponse ()) { using (Stream respstream = resp.GetResponseStream ()) { using (var rdr = new StreamReader(respstream)) { - res = JsonConvert.DeserializeObject(rdr.ReadToEnd()); + string json = rdr.ReadToEnd(); + _logger.LogInformation(">> Json calendar list : "+json); + res = JsonConvert.DeserializeObject(json); } } resp.Close (); @@ -119,12 +126,14 @@ namespace Yavsc.Models.Google.Calendar /// Mindate. /// Maxdate. /// credential string. - public async Task GetCalendarAsync (string calid, DateTime mindate, DateTime maxdate,string userId) + public async Task GetCalendarAsync (string calid, DateTime mindate, DateTime maxdate) { - UserCredential creds = await _userManager.GetCredentialForGoogleApiAsync( - _dbContext, userId); + // ServiceAccountCredential screds = new ServiceAccountCredential(init); + + var creds = GoogleHelpers.GetCredentialForApi(new string[]{scopeCalendar}); if (creds==null) throw new InvalidOperationException("No credential"); + if (string.IsNullOrWhiteSpace (calid)) throw new Exception ("the calendar identifier is not specified"); @@ -136,7 +145,7 @@ namespace Yavsc.Models.Google.Calendar HttpWebRequest webreq = WebRequest.CreateHttp (uri); - webreq.Headers.Add (HttpRequestHeader.Authorization, creds.GetHeader()); + webreq.Headers.Add (HttpRequestHeader.Authorization, "Bearer "+ await creds.GetAccessTokenForRequestAsync()); webreq.Method = "GET"; webreq.ContentType = "application/http"; CalendarEventList res = null; @@ -169,7 +178,7 @@ namespace Yavsc.Models.Google.Calendar string inputId, string calid, DateTime mindate, DateTime maxdate, string userId) { - var eventList = await GetCalendarAsync(calid, mindate, maxdate, userId); + var eventList = await GetCalendarAsync(calid, mindate, maxdate); return new DateTimeChooserViewModel { InputId = inputId, diff --git a/Yavsc/Services/GoogleApis/MapTracks.cs b/Yavsc/Services/GoogleApis/MapTracks.cs index 1bc7abd2..cc563103 100644 --- a/Yavsc/Services/GoogleApis/MapTracks.cs +++ b/Yavsc/Services/GoogleApis/MapTracks.cs @@ -18,6 +18,7 @@ // // You should have received a copy of the GNU Lesser General Public License // along with this program. If not, see . +using System.Threading.Tasks; using Yavsc.Helpers; using Yavsc.Models.Google; @@ -53,13 +54,13 @@ namespace Yavsc.GoogleApis /// /// The entity. /// Entities. - public static string [] CreateEntity( Entity[] entities ) { + public static async Task CreateEntity( Entity[] entities ) { string [] ans = null; using (SimpleJsonPostMethod wr = new SimpleJsonPostMethod (googleMapTracksPath + "entities/create")) { - ans = wr.Invoke (entities); + ans = await wr.Invoke (entities); } return ans; } @@ -69,13 +70,13 @@ namespace Yavsc.GoogleApis /// /// The entities. /// Eq. - static Entity[] ListEntities (EntityQuery eq) + static async Task ListEntities (EntityQuery eq) { Entity [] ans = null; using (SimpleJsonPostMethod wr = new SimpleJsonPostMethod (googleMapTracksPath + "entities/create")) { - ans = wr.Invoke (eq); + ans = await wr.Invoke (eq); } return ans; } diff --git a/Yavsc/Services/ICalendarManager.cs b/Yavsc/Services/ICalendarManager.cs index cf1f74ed..8e5c7ccc 100644 --- a/Yavsc/Services/ICalendarManager.cs +++ b/Yavsc/Services/ICalendarManager.cs @@ -34,6 +34,6 @@ namespace Yavsc.Models.Calendar /// public interface ICalendarManager { Task GetCalendarsAsync (string userId); - Task GetCalendarAsync (string calid, DateTime mindate, DateTime maxdate, string userId); + Task GetCalendarAsync (string calid, DateTime mindate, DateTime maxdate); } } diff --git a/Yavsc/Services/MessageServices.cs b/Yavsc/Services/MessageServices.cs index 7bc48b2c..3192effb 100755 --- a/Yavsc/Services/MessageServices.cs +++ b/Yavsc/Services/MessageServices.cs @@ -30,30 +30,18 @@ namespace Yavsc.Services /// public async Task NotifyBookQueryAsync(GoogleAuthSettings googleSettings, IEnumerable registrationIds, RdvQueryEvent ev) { - MessageWithPayloadResponse response = null; - await Task.Run(()=>{ - response = googleSettings.NotifyEvent(registrationIds, ev); - }); - return response; + return await googleSettings.NotifyEvent(registrationIds, ev); } public async Task NotifyEstimateAsync(GoogleAuthSettings googleSettings, IEnumerable registrationIds, EstimationEvent ev) { - MessageWithPayloadResponse response = null; - await Task.Run(()=>{ - response = googleSettings.NotifyEvent(registrationIds, ev); - }); - return response; + return await googleSettings.NotifyEvent(registrationIds, ev); } public async Task NotifyHairCutQueryAsync(GoogleAuthSettings googleSettings, IEnumerable registrationIds, HairCutQueryEvent ev) { - MessageWithPayloadResponse response = null; - await Task.Run(()=>{ - response = googleSettings.NotifyEvent(registrationIds, ev); - }); - return response; + return await googleSettings.NotifyEvent(registrationIds, ev); } public Task SendEmailAsync(SiteSettings siteSettings, SmtpSettings smtpSettings, string email, string subject, string message) diff --git a/Yavsc/ViewComponents/CalendarViewComponent.cs b/Yavsc/ViewComponents/CalendarViewComponent.cs index f015a547..6fff0e9c 100644 --- a/Yavsc/ViewComponents/CalendarViewComponent.cs +++ b/Yavsc/ViewComponents/CalendarViewComponent.cs @@ -29,14 +29,14 @@ Google.Apis Google.Apis.Core public async Task InvokeAsync ( string templateName, - string htmlFieldName, - string userId, string calId ) + string htmlFieldName, + string calId) { var minDate = DateTime.Now; var maxDate = minDate.AddDays(20); var cal = await _manager.GetCalendarAsync( - calId, minDate, maxDate, userId + calId, minDate, maxDate ); ViewData["Calendar"] = cal; diff --git a/Yavsc/Views/HairCutCommand/HairCut.cshtml b/Yavsc/Views/HairCutCommand/HairCut.cshtml index 7da4e16c..b245e98a 100644 --- a/Yavsc/Views/HairCutCommand/HairCut.cshtml +++ b/Yavsc/Views/HairCutCommand/HairCut.cshtml @@ -342,7 +342,6 @@ "Calendar", "Default", "EventDate", - Model.PerformerProfile.PerformerId, Model.PerformerProfile.Performer.DedicatedGoogleCalendar) diff --git a/Yavsc/Views/Manage/SetGoogleCalendar.cshtml b/Yavsc/Views/Manage/SetGoogleCalendar.cshtml index 1dc3077f..bc93a6ca 100644 --- a/Yavsc/Views/Manage/SetGoogleCalendar.cshtml +++ b/Yavsc/Views/Manage/SetGoogleCalendar.cshtml @@ -5,7 +5,7 @@ }
@{ var entryNum=0; } -@foreach (var calendar in ViewBag.Calendars.items) +@foreach (var calendar in ViewBag.Calendars?.items) { entryNum++; @if (calendar.accessRole=="owner") { diff --git a/Yavsc/Yavsc.csproj b/Yavsc/Yavsc.csproj index 865d91c4..00904ec9 100644 --- a/Yavsc/Yavsc.csproj +++ b/Yavsc/Yavsc.csproj @@ -1184,6 +1184,46 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +