From cc36c930eccfc5587da747224242a3354bd346eb Mon Sep 17 00:00:00 2001 From: Rostislav Date: Mon, 21 Jan 2019 05:40:17 +0500 Subject: [PATCH] refactor as vknet extension --- .gitignore | 337 ++++++++++++++++++ Example/Example.csproj | 25 ++ Example/Program.cs | 58 +++ Example/nlog.config | 14 + VkNet.TokenMagic.sln | 31 ++ VkNet.TokenMagic/Extensions.cs | 62 ++++ .../Models/Google/GoogleCredentials.cs | 16 + .../Models/Google/ProtobufField.cs | 14 + .../Services/BrowserWithAndroidToken.cs | 184 ++++++++++ .../Services/RestClientWithUserAgent.cs | 105 ++++++ .../Token/Google/AndroidHttpClient.cs | 44 +++ .../Token/Google/GoogleSecurityHttpClient.cs | 104 ++++++ .../Services/Token/Google/MTalkTcpClient.cs | 88 +++++ .../Services/Token/Google/ProtobufParser.cs | 84 +++++ .../Token/Google/RandomAppIdProvider.cs | 28 ++ .../Services/Token/Google/ReceiptReceiver.cs | 26 ++ .../Services/Token/Google/VarInt.cs | 53 +++ VkNet.TokenMagic/VkNet.TokenMagic.csproj | 12 + 18 files changed, 1285 insertions(+) create mode 100644 .gitignore create mode 100644 Example/Example.csproj create mode 100644 Example/Program.cs create mode 100644 Example/nlog.config create mode 100644 VkNet.TokenMagic.sln create mode 100644 VkNet.TokenMagic/Extensions.cs create mode 100644 VkNet.TokenMagic/Models/Google/GoogleCredentials.cs create mode 100644 VkNet.TokenMagic/Models/Google/ProtobufField.cs create mode 100644 VkNet.TokenMagic/Services/BrowserWithAndroidToken.cs create mode 100644 VkNet.TokenMagic/Services/RestClientWithUserAgent.cs create mode 100644 VkNet.TokenMagic/Services/Token/Google/AndroidHttpClient.cs create mode 100644 VkNet.TokenMagic/Services/Token/Google/GoogleSecurityHttpClient.cs create mode 100644 VkNet.TokenMagic/Services/Token/Google/MTalkTcpClient.cs create mode 100644 VkNet.TokenMagic/Services/Token/Google/ProtobufParser.cs create mode 100644 VkNet.TokenMagic/Services/Token/Google/RandomAppIdProvider.cs create mode 100644 VkNet.TokenMagic/Services/Token/Google/ReceiptReceiver.cs create mode 100644 VkNet.TokenMagic/Services/Token/Google/VarInt.cs create mode 100644 VkNet.TokenMagic/VkNet.TokenMagic.csproj diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..c893523 --- /dev/null +++ b/.gitignore @@ -0,0 +1,337 @@ +## Ignore Visual Studio temporary files, build results, and +## files generated by popular Visual Studio add-ons. +## +## Get latest from https://github.com/github/gitignore/blob/master/VisualStudio.gitignore + +# User-specific files +*.rsuser +*.suo +*.user +*.userosscache +*.sln.docstates + +# User-specific files (MonoDevelop/Xamarin Studio) +*.userprefs + +# Build results +[Dd]ebug/ +[Dd]ebugPublic/ +[Rr]elease/ +[Rr]eleases/ +x64/ +x86/ +bld/ +[Bb]in/ +[Oo]bj/ +[Ll]og/ + +# Visual Studio 2015/2017 cache/options directory +.vs/ +# Uncomment if you have tasks that create the project's static files in wwwroot +#wwwroot/ + +# Visual Studio 2017 auto generated files +Generated\ Files/ + +# MSTest test Results +[Tt]est[Rr]esult*/ +[Bb]uild[Ll]og.* + +# NUNIT +*.VisualState.xml +TestResult.xml + +# Build Results of an ATL Project +[Dd]ebugPS/ +[Rr]eleasePS/ +dlldata.c + +# Benchmark Results +BenchmarkDotNet.Artifacts/ + +# .NET Core +project.lock.json +project.fragment.lock.json +artifacts/ + +# StyleCop +StyleCopReport.xml + +# Files built by Visual Studio +*_i.c +*_p.c +*_h.h +*.ilk +*.meta +*.obj +*.iobj +*.pch +*.pdb +*.ipdb +*.pgc +*.pgd +*.rsp +*.sbr +*.tlb +*.tli +*.tlh +*.tmp +*.tmp_proj +*_wpftmp.csproj +*.log +*.vspscc +*.vssscc +.builds +*.pidb +*.svclog +*.scc + +# Chutzpah Test files +_Chutzpah* + +# Visual C++ cache files +ipch/ +*.aps +*.ncb +*.opendb +*.opensdf +*.sdf +*.cachefile +*.VC.db +*.VC.VC.opendb + +# Visual Studio profiler +*.psess +*.vsp +*.vspx +*.sap + +# Visual Studio Trace Files +*.e2e + +# TFS 2012 Local Workspace +$tf/ + +# Guidance Automation Toolkit +*.gpState + +# ReSharper is a .NET coding add-in +_ReSharper*/ +*.[Rr]e[Ss]harper +*.DotSettings.user + +# JustCode is a .NET coding add-in +.JustCode + +# TeamCity is a build add-in +_TeamCity* + +# DotCover is a Code Coverage Tool +*.dotCover + +# AxoCover is a Code Coverage Tool +.axoCover/* +!.axoCover/settings.json + +# Visual Studio code coverage results +*.coverage +*.coveragexml + +# NCrunch +_NCrunch_* +.*crunch*.local.xml +nCrunchTemp_* + +# MightyMoose +*.mm.* +AutoTest.Net/ + +# Web workbench (sass) +.sass-cache/ + +# Installshield output folder +[Ee]xpress/ + +# DocProject is a documentation generator add-in +DocProject/buildhelp/ +DocProject/Help/*.HxT +DocProject/Help/*.HxC +DocProject/Help/*.hhc +DocProject/Help/*.hhk +DocProject/Help/*.hhp +DocProject/Help/Html2 +DocProject/Help/html + +# Click-Once directory +publish/ + +# Publish Web Output +*.[Pp]ublish.xml +*.azurePubxml +# Note: Comment the next line if you want to checkin your web deploy settings, +# but database connection strings (with potential passwords) will be unencrypted +*.pubxml +*.publishproj + +# Microsoft Azure Web App publish settings. Comment the next line if you want to +# checkin your Azure Web App publish settings, but sensitive information contained +# in these scripts will be unencrypted +PublishScripts/ + +# NuGet Packages +*.nupkg +# The packages folder can be ignored because of Package Restore +**/[Pp]ackages/* +# except build/, which is used as an MSBuild target. +!**/[Pp]ackages/build/ +# Uncomment if necessary however generally it will be regenerated when needed +#!**/[Pp]ackages/repositories.config +# NuGet v3's project.json files produces more ignorable files +*.nuget.props +*.nuget.targets + +# Microsoft Azure Build Output +csx/ +*.build.csdef + +# Microsoft Azure Emulator +ecf/ +rcf/ + +# Windows Store app package directories and files +AppPackages/ +BundleArtifacts/ +Package.StoreAssociation.xml +_pkginfo.txt +*.appx + +# Visual Studio cache files +# files ending in .cache can be ignored +*.[Cc]ache +# but keep track of directories ending in .cache +!*.[Cc]ache/ + +# Others +ClientBin/ +~$* +*~ +*.dbmdl +*.dbproj.schemaview +*.jfm +*.pfx +*.publishsettings +orleans.codegen.cs + +# Including strong name files can present a security risk +# (https://github.com/github/gitignore/pull/2483#issue-259490424) +#*.snk + +# Since there are multiple workflows, uncomment next line to ignore bower_components +# (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) +#bower_components/ + +# RIA/Silverlight projects +Generated_Code/ + +# Backup & report files from converting an old project file +# to a newer Visual Studio version. Backup files are not needed, +# because we have git ;-) +_UpgradeReport_Files/ +Backup*/ +UpgradeLog*.XML +UpgradeLog*.htm +ServiceFabricBackup/ +*.rptproj.bak + +# SQL Server files +*.mdf +*.ldf +*.ndf + +# Business Intelligence projects +*.rdl.data +*.bim.layout +*.bim_*.settings +*.rptproj.rsuser + +# Microsoft Fakes +FakesAssemblies/ + +# GhostDoc plugin setting file +*.GhostDoc.xml + +# Node.js Tools for Visual Studio +.ntvs_analysis.dat +node_modules/ + +# Visual Studio 6 build log +*.plg + +# Visual Studio 6 workspace options file +*.opt + +# Visual Studio 6 auto-generated workspace file (contains which files were open etc.) +*.vbw + +# Visual Studio LightSwitch build output +**/*.HTMLClient/GeneratedArtifacts +**/*.DesktopClient/GeneratedArtifacts +**/*.DesktopClient/ModelManifest.xml +**/*.Server/GeneratedArtifacts +**/*.Server/ModelManifest.xml +_Pvt_Extensions + +# Paket dependency manager +.paket/paket.exe +paket-files/ + +# FAKE - F# Make +.fake/ + +# JetBrains Rider +.idea/ +*.sln.iml + +# CodeRush personal settings +.cr/personal + +# Python Tools for Visual Studio (PTVS) +__pycache__/ +*.pyc + +# Cake - Uncomment if you are using it +# tools/** +# !tools/packages.config + +# Tabs Studio +*.tss + +# Telerik's JustMock configuration file +*.jmconfig + +# BizTalk build output +*.btp.cs +*.btm.cs +*.odx.cs +*.xsd.cs + +# OpenCover UI analysis results +OpenCover/ + +# Azure Stream Analytics local run output +ASALocalRun/ + +# MSBuild Binary and Structured Log +*.binlog + +# NVidia Nsight GPU debugger configuration file +*.nvuser + +# MFractors (Xamarin productivity tool) working folder +.mfractor/ + +# Local History for Visual Studio +.localhistory/ + +**/FileCache.*.json +**logs/ \ No newline at end of file diff --git a/Example/Example.csproj b/Example/Example.csproj new file mode 100644 index 0000000..2d567ec --- /dev/null +++ b/Example/Example.csproj @@ -0,0 +1,25 @@ + + + + Exe + netcoreapp2.1 + + + + + + + + + Always + + + + + + + + + + + diff --git a/Example/Program.cs b/Example/Program.cs new file mode 100644 index 0000000..8826585 --- /dev/null +++ b/Example/Program.cs @@ -0,0 +1,58 @@ +using System; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; +using VkNet; +using VkNet.Enums.Filters; +using VkNet.Model; +using VkNet.Model.RequestParams; +using VkNet.NLog.Extensions.Logging; +using VkNet.NLog.Extensions.Logging.Extensions; +using VkNet.TokenMagic; + +namespace Example +{ + class Program + { + private static void Main(string[] args) + { + var services = new ServiceCollection(); + services.AddVkTokenMagic(); + services.AddSingleton(); + services.AddSingleton(typeof(ILogger<>), typeof(Logger<>)); + services.AddLogging(builder => + { + builder.ClearProviders(); + builder.SetMinimumLevel(LogLevel.Trace); + builder.AddNLog(new NLogProviderOptions + { + CaptureMessageProperties = true, + CaptureMessageTemplates = true + }); + }); + NLog.LogManager.LoadConfiguration("nlog.config"); + + var vkNet = new VkApi(services); + vkNet.Authorize(new ApiAuthParams + { + Login = "LOGIN", + Password = "PASSWORD", + Settings = Settings.Audio + + }); + + var audios = vkNet.Audio.Get(new AudioGetParams + { + Count = 10 + }); + + foreach (var audio in audios) + { + Console.WriteLine($"{audio.Artist} - {audio.Title} {audio.Url}"); + } + Console.ReadLine(); + NLog.LogManager.Shutdown(); + } + } + + +} diff --git a/Example/nlog.config b/Example/nlog.config new file mode 100644 index 0000000..c222de1 --- /dev/null +++ b/Example/nlog.config @@ -0,0 +1,14 @@ + + + + + + + + + + + + + \ No newline at end of file diff --git a/VkNet.TokenMagic.sln b/VkNet.TokenMagic.sln new file mode 100644 index 0000000..bb91c73 --- /dev/null +++ b/VkNet.TokenMagic.sln @@ -0,0 +1,31 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio 15 +VisualStudioVersion = 15.0.28307.168 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "VkNet.TokenMagic", "VkNet.TokenMagic\VkNet.TokenMagic.csproj", "{EB2F6E3D-128C-42D4-8DD9-71FEB7A86650}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Example", "Example\Example.csproj", "{C2D199CE-608E-47C6-BF63-207F9E508768}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {EB2F6E3D-128C-42D4-8DD9-71FEB7A86650}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {EB2F6E3D-128C-42D4-8DD9-71FEB7A86650}.Debug|Any CPU.Build.0 = Debug|Any CPU + {EB2F6E3D-128C-42D4-8DD9-71FEB7A86650}.Release|Any CPU.ActiveCfg = Release|Any CPU + {EB2F6E3D-128C-42D4-8DD9-71FEB7A86650}.Release|Any CPU.Build.0 = Release|Any CPU + {C2D199CE-608E-47C6-BF63-207F9E508768}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {C2D199CE-608E-47C6-BF63-207F9E508768}.Debug|Any CPU.Build.0 = Debug|Any CPU + {C2D199CE-608E-47C6-BF63-207F9E508768}.Release|Any CPU.ActiveCfg = Release|Any CPU + {C2D199CE-608E-47C6-BF63-207F9E508768}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {6B73C46D-325F-496A-8E8B-405E063619A7} + EndGlobalSection +EndGlobal diff --git a/VkNet.TokenMagic/Extensions.cs b/VkNet.TokenMagic/Extensions.cs new file mode 100644 index 0000000..c3a3863 --- /dev/null +++ b/VkNet.TokenMagic/Extensions.cs @@ -0,0 +1,62 @@ +using System; +using System.Net; +using System.Net.Http; +using System.Net.Security; +using System.Security.Cryptography.X509Certificates; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.DependencyInjection.Extensions; +using Microsoft.Extensions.Logging; +using VkNet.Abstractions.Utils; +using VkNet.TokenMagic.Services; +using VkNet.TokenMagic.Services.Token.Google; +using VkNet.Utils; + +namespace VkNet.TokenMagic +{ + public static class Extensions + { + public static IServiceCollection AddTokenMagic(this IServiceCollection services) + { + services.TryAddSingleton(); + services.TryAddSingleton(); + services.TryAddSingleton(); + services.TryAddSingleton(); + services.TryAddSingleton(); + + services.TryAddSingleton(); // vknet needs user-agent too + + services.AddHttpClient().ConfigurePrimaryHttpMessageHandler(provider => MaybeProxyHttpHandler(provider)).SetHandlerLifetime(TimeSpan.FromMinutes(1)); + services.AddHttpClient().ConfigurePrimaryHttpMessageHandler(provider => MaybeProxyHttpHandler(provider, true)).SetHandlerLifetime(TimeSpan.FromMinutes(1)); + services.AddHttpClient().ConfigurePrimaryHttpMessageHandler(provider => MaybeProxyHttpHandler(provider)).SetHandlerLifetime(TimeSpan.FromMinutes(10)); + + services.TryAddSingleton(); + + return services; + } + + public static HttpClientHandler MaybeProxyHttpHandler(IServiceProvider provider, bool ignoreSsl=false) + { + var proxy = provider.GetService(); + var logger = provider.GetService>(); + var useProxyCondition = proxy != null; + if (useProxyCondition) + { + logger?.LogDebug($"Use Proxy: {proxy}"); + } + + Func certCallback = null; + if (ignoreSsl) + { + certCallback = (message, certificate2, arg3, arg4) => true; + logger?.LogDebug($"Ignoring ssl"); + } + + return new HttpClientHandler + { + Proxy = proxy, + UseProxy = useProxyCondition, + ServerCertificateCustomValidationCallback = certCallback + }; + } + } +} diff --git a/VkNet.TokenMagic/Models/Google/GoogleCredentials.cs b/VkNet.TokenMagic/Models/Google/GoogleCredentials.cs new file mode 100644 index 0000000..bdb6527 --- /dev/null +++ b/VkNet.TokenMagic/Models/Google/GoogleCredentials.cs @@ -0,0 +1,16 @@ +using System.Collections.Generic; + +namespace VkNet.TokenMagic.Models.Google +{ + public class GoogleCredentials + { + public long Id { get; set; } + public long Token { get; set; } + public List RawId { get; set; } + + public override string ToString() + { + return $"{nameof(GoogleCredentials)}(id {Id}; token {Token})"; + } + } +} \ No newline at end of file diff --git a/VkNet.TokenMagic/Models/Google/ProtobufField.cs b/VkNet.TokenMagic/Models/Google/ProtobufField.cs new file mode 100644 index 0000000..da6e597 --- /dev/null +++ b/VkNet.TokenMagic/Models/Google/ProtobufField.cs @@ -0,0 +1,14 @@ +namespace VkNet.TokenMagic.Models.Google +{ + public class ProtobufField + { + public ProtobufField(int value) + { + Type = value & 0x7; // last three bits, 0b00000111 + FieldNumber = value >> 3; + } + + public int Type { get; } + public int FieldNumber { get; } + } +} \ No newline at end of file diff --git a/VkNet.TokenMagic/Services/BrowserWithAndroidToken.cs b/VkNet.TokenMagic/Services/BrowserWithAndroidToken.cs new file mode 100644 index 0000000..3cf5e69 --- /dev/null +++ b/VkNet.TokenMagic/Services/BrowserWithAndroidToken.cs @@ -0,0 +1,184 @@ +using System; +using System.Collections.Generic; +using System.Net; +using Microsoft.Extensions.Logging; +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; +using Newtonsoft.Json.Serialization; +using VkNet.Abstractions.Core; +using VkNet.Abstractions.Utils; +using VkNet.Enums.SafetyEnums; +using VkNet.Exception; +using VkNet.Model; +using VkNet.TokenMagic.Services.Token.Google; +using VkNet.Utils; + +namespace VkNet.TokenMagic.Services +{ + public class BrowserWithAndroidToken : IBrowser + { + public BrowserWithAndroidToken(IVkApiVersionManager versionManager, IRestClient restClient, ReceiptReceiver receiptReceiver, ILogger logger) + { + _versionManager = versionManager; + _restClient = restClient; + _receiptReceiver = receiptReceiver; + _logger = logger; + } + + public IWebProxy Proxy { get; set; } + + public AuthorizationResult Authorize() + { + var authResult = BaseAuth(); + var receipt = _receiptReceiver.GetReceipt().ConfigureAwait(false).GetAwaiter().GetResult(); + if (receipt == null) + { + throw new VkApiException("receipt is null"); + } + + var newToken = RefreshToken(authResult.AccessToken, receipt); + + return new AuthorizationResult + { + AccessToken = newToken, + ExpiresIn = authResult.ExpiresIn, + UserId = authResult.UserId + }; + } + + public void SetAuthParams(IApiAuthParams authParams) + { + _apiAuthParams = authParams; + } + + private AuthorizationResult BaseAuth(string code = null) + { + if (string.IsNullOrEmpty(code)) + _logger?.LogDebug("1. Авторизация."); + + var response = Invoke("https://oauth.vk.com/token", + new VkParameters + { + {"grant_type", "password"}, + {"client_id", "2685278"}, + {"client_secret", "lxhD8OD7dMsqtXIm5IUY"}, + {"2fa_supported", true}, + {"username", $"{_apiAuthParams.Login}"}, + {"password", $"{_apiAuthParams.Password}"}, + {"code", code}, + {"scope", $"{_apiAuthParams.Settings}"}, + {"v", _versionManager.Version} + }); + + var json = JObject.Parse(response); + + var error = json["error"]; + + if (error == null) + return json.ToObject(DefaultJsonSerializer); + + switch (error.ToString()) + { + case "need_validation": + _logger?.LogDebug("1.1 Требуется код двухфакторной аутентификаци."); + + if (_apiAuthParams.TwoFactorAuthorization == null) + throw new ArgumentNullException(nameof(_apiAuthParams.TwoFactorAuthorization)); + + var result = _apiAuthParams.TwoFactorAuthorization.BeginInvoke(null, null); + result.AsyncWaitHandle.WaitOne(); + var authCode = _apiAuthParams.TwoFactorAuthorization.EndInvoke(result); + + return BaseAuth(authCode); + case "invalid_request": + case "invalid_client": + var errorDescription = json["error_description"].ToString(); + throw new VkApiAuthorizationException(errorDescription, _apiAuthParams.Login, + _apiAuthParams.Password); + case "need_captcha": + var sid = json["captcha_sid"].Value(); + var imgUrl = json["captcha_img"].ToString(); + throw new CaptchaNeededException(sid, imgUrl); + default: + throw new VkApiException($"Неизвестная ошибка.{Environment.NewLine}{response}"); + } + } + + private string Invoke(string url, VkParameters parameters) + { + var response = _restClient.PostAsync(new Uri(url), parameters) + .ConfigureAwait(false) + .GetAwaiter() + .GetResult(); + + var answer = response.Value ?? response.Message; + + return answer; + } + + private string RefreshToken(string oldToken, string receipt) + { + _logger?.LogDebug("2. Обновление токена."); + + var parameters = new List> + { + new KeyValuePair("access_token", oldToken), + new KeyValuePair("receipt", receipt), + new KeyValuePair("v", _versionManager.Version) + }; + var httpResponse = _restClient.GetAsync(new Uri("https://api.vk.com/method/auth.refreshToken"), parameters) + .ConfigureAwait(false) + .GetAwaiter() + .GetResult(); + var response = httpResponse.Value ?? httpResponse.Message; + var jObject = JObject.Parse(response); + var rawResponse = jObject["response"]; + return rawResponse["token"].ToString(); + } + + #region Private Fields + + private IApiAuthParams _apiAuthParams; + + private readonly IVkApiVersionManager _versionManager; + + private readonly IRestClient _restClient; + private readonly ReceiptReceiver _receiptReceiver; + + private readonly ILogger _logger; + + private JsonSerializer DefaultJsonSerializer => new JsonSerializer + { + ContractResolver = new DefaultContractResolver + { + NamingStrategy = new SnakeCaseNamingStrategy() + } + }; + + #endregion + + #region Not Implemented + + public Uri CreateAuthorizeUrl(ulong clientId, ulong scope, Display display, string state) + { + throw new NotImplementedException(); + } + + public AuthorizationResult Validate(string validateUrl, string phoneNumber) + { + throw new NotImplementedException(); + } + + public string GetJson(string url, IEnumerable> parameters) + { + throw new NotImplementedException(); + } + + public AuthorizationResult Validate(string validateUrl) + { + throw new NotImplementedException(); + } + + #endregion + } +} \ No newline at end of file diff --git a/VkNet.TokenMagic/Services/RestClientWithUserAgent.cs b/VkNet.TokenMagic/Services/RestClientWithUserAgent.cs new file mode 100644 index 0000000..65b6189 --- /dev/null +++ b/VkNet.TokenMagic/Services/RestClientWithUserAgent.cs @@ -0,0 +1,105 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Net; +using System.Net.Http; +using System.Threading.Tasks; +using Microsoft.Extensions.Logging; +using Newtonsoft.Json; +using VkNet.Abstractions.Utils; +using VkNet.Utils; + +namespace VkNet.TokenMagic.Services +{ + /// + public class RestClientWithUserAgent : IRestClient + { + // why is this in the interface? + public IWebProxy Proxy { get => throw new NotImplementedException(); set => throw new NotImplementedException(); } + + /// + /// The log + /// + private readonly ILogger _logger; + + private readonly HttpClient _httpClient; + + protected readonly string UserAgent; + + protected TimeSpan TimeoutSeconds; + + /// + public RestClientWithUserAgent(ILogger logger, HttpClient httpClient) + :this(logger, httpClient, DefaultUserAgent) + { + } + + /// + protected RestClientWithUserAgent(ILogger logger, HttpClient httpClient, string userAgent) + { + _logger = logger; + _httpClient = httpClient; + this.UserAgent = userAgent; + _httpClient.DefaultRequestHeaders.UserAgent.ParseAdd(UserAgent); + + } + + /// + public TimeSpan Timeout + { + get => TimeoutSeconds == TimeSpan.Zero ? TimeSpan.FromSeconds(300) : TimeoutSeconds; + set => TimeoutSeconds = value; + } + + /// + public Task> GetAsync(Uri uri, IEnumerable> parameters) + { + var queries = parameters + .Where(parameter => !string.IsNullOrWhiteSpace(parameter.Value)) + .Select(parameter => $"{parameter.Key.ToLowerInvariant()}={parameter.Value}"); + + var url = new UriBuilder(uri) + { + Query = string.Join("&", queries) + }; + + _logger?.LogDebug($"GET request: {url.Uri}"); + + var request = new HttpRequestMessage(HttpMethod.Get, url.Uri); + + return CallAsync(httpClient => httpClient.SendAsync(request, HttpCompletionOption.ResponseHeadersRead)); + } + + /// + public Task> PostAsync(Uri uri, IEnumerable> parameters) + { + if (_logger != null) + { + var json = JsonConvert.SerializeObject(parameters); + _logger.LogDebug($"POST request: {uri}{Environment.NewLine}{Utilities.PrettyPrintJson(json)}"); + } + + var content = new FormUrlEncodedContent(parameters); + + var request = new HttpRequestMessage(HttpMethod.Post, uri) { Content = content }; + + return CallAsync(httpClient => httpClient.SendAsync(request, HttpCompletionOption.ResponseHeadersRead)); + } + + protected async Task> CallAsync(Func> method) + { + var response = await method(_httpClient).ConfigureAwait(false); + + var content = await response.Content.ReadAsStringAsync().ConfigureAwait(false); + + _logger?.LogDebug($"Response:{Environment.NewLine}{Utilities.PrettyPrintJson(content)}"); + var url = response.RequestMessage.RequestUri.ToString(); + + return response.IsSuccessStatusCode + ? HttpResponse.Success(response.StatusCode, content, url) + : HttpResponse.Fail(response.StatusCode, content, url); + } + + protected const string DefaultUserAgent = "KateMobileAndroid/51.2 lite-443 (Android 4.4.2; SDK 19; x86; unknown Android SDK built for x86; en)"; + } +} diff --git a/VkNet.TokenMagic/Services/Token/Google/AndroidHttpClient.cs b/VkNet.TokenMagic/Services/Token/Google/AndroidHttpClient.cs new file mode 100644 index 0000000..7b9ebaa --- /dev/null +++ b/VkNet.TokenMagic/Services/Token/Google/AndroidHttpClient.cs @@ -0,0 +1,44 @@ +using System; +using System.Net.Http; +using System.Net.Http.Headers; +using System.Threading.Tasks; +using Microsoft.Extensions.Logging; + +namespace VkNet.TokenMagic.Services.Token.Google +{ + public class AndroidHttpClient + { + protected readonly HttpClient HttpClient; + private readonly ILogger _logger; + private readonly string _appId; + + public AndroidHttpClient(HttpClient httpClient, RandomAppIdProvider appIdProvider, ILogger logger) + { + HttpClient = httpClient; + _appId = appIdProvider.AppId; + _logger = logger; + } + + public async Task CheckIn() + { + var request = new HttpRequestMessage(HttpMethod.Post, Url) + { + Method = HttpMethod.Post + }; + request.Headers.Add("Expect", ""); + request.Content = new ByteArrayContent(queryMessage); + request.Content.Headers.ContentType = MediaTypeHeaderValue.Parse("application/x-protobuffer"); + _logger?.LogDebug($"{nameof(CheckIn)}"); + var response = await HttpClient.SendAsync(request).ConfigureAwait(false); + response.EnsureSuccessStatusCode(); + return await response.Content.ReadAsByteArrayAsync().ConfigureAwait(false); + } + + private static readonly byte[] queryMessage = { + 0x10, 0x00, 0x1a, 0x2a, 0x31, 0x2d, 0x39, 0x32, 0x39, 0x61, 0x30, 0x64, 0x63, 0x61, 0x30, 0x65, 0x65, 0x65, 0x35, 0x35, 0x35, 0x31, 0x33, 0x32, 0x38, 0x30, 0x31, 0x37, 0x31, 0x61, 0x38, 0x35, 0x38, 0x35, 0x64, 0x61, 0x37, 0x64, 0x63, 0x64, 0x33, 0x37, 0x30, 0x30, 0x66, 0x38, 0x22, 0xe3, 0x01, 0x0a, 0xbf, 0x01, 0x0a, 0x45, 0x67, 0x65, 0x6e, 0x65, 0x72, 0x69, 0x63, 0x5f, 0x78, 0x38, 0x36, 0x2f, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x5f, 0x73, 0x64, 0x6b, 0x5f, 0x78, 0x38, 0x36, 0x2f, 0x67, 0x65, 0x6e, 0x65, 0x72, 0x69, 0x63, 0x5f, 0x78, 0x38, 0x36, 0x3a, 0x34, 0x2e, 0x34, 0x2e, 0x32, 0x2f, 0x4b, 0x4b, 0x2f, 0x33, 0x30, 0x37, 0x39, 0x31, 0x38, 0x33, 0x3a, 0x65, 0x6e, 0x67, 0x2f, 0x74, 0x65, 0x73, 0x74, 0x2d, 0x6b, 0x65, 0x79, 0x73, 0x12, 0x06, 0x72, 0x61, 0x6e, 0x63, 0x68, 0x75, 0x1a, 0x0b, 0x67, 0x65, 0x6e, 0x65, 0x72, 0x69, 0x63, 0x5f, 0x78, 0x38, 0x36, 0x2a, 0x07, 0x75, 0x6e, 0x6b, 0x6e, 0x6f, 0x77, 0x6e, 0x32, 0x0e, 0x61, 0x6e, 0x64, 0x72, 0x6f, 0x69, 0x64, 0x2d, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x40, 0x85, 0xb5, 0x86, 0x06, 0x4a, 0x0b, 0x67, 0x65, 0x6e, 0x65, 0x72, 0x69, 0x63, 0x5f, 0x78, 0x38, 0x36, 0x50, 0x13, 0x5a, 0x19, 0x41, 0x6e, 0x64, 0x72, 0x6f, 0x69, 0x64, 0x20, 0x53, 0x44, 0x4b, 0x20, 0x62, 0x75, 0x69, 0x6c, 0x74, 0x20, 0x66, 0x6f, 0x72, 0x20, 0x78, 0x38, 0x36, 0x62, 0x07, 0x75, 0x6e, 0x6b, 0x6e, 0x6f, 0x77, 0x6e, 0x6a, 0x0e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x5f, 0x73, 0x64, 0x6b, 0x5f, 0x78, 0x38, 0x36, 0x70, 0x00, 0x10, 0x00, 0x32, 0x06, 0x33, 0x31, 0x30, 0x32, 0x36, 0x30, 0x3a, 0x06, 0x33, 0x31, 0x30, 0x32, 0x36, 0x30, 0x42, 0x0b, 0x6d, 0x6f, 0x62, 0x69, 0x6c, 0x65, 0x3a, 0x4c, 0x54, 0x45, 0x3a, 0x48, 0x00, 0x32, 0x05, 0x65, 0x6e, 0x5f, 0x55, 0x53, 0x38, 0xf0, 0xb4, 0xdf, 0xa6, 0xb9, 0x9a, 0xb8, 0x83, 0x8e, 0x01, 0x52, 0x0f, 0x33, 0x35, 0x38, 0x32, 0x34, 0x30, 0x30, 0x35, 0x31, 0x31, 0x31, 0x31, 0x31, 0x31, 0x30, 0x5a, 0x00, 0x62, 0x10, 0x41, 0x6d, 0x65, 0x72, 0x69, 0x63, 0x61, 0x2f, 0x4e, 0x65, 0x77, 0x5f, 0x59, 0x6f, 0x72, 0x6b, 0x70, 0x03, 0x7a, 0x1c, 0x37, 0x31, 0x51, 0x36, 0x52, 0x6e, 0x32, 0x44, 0x44, 0x5a, 0x6c, 0x31, 0x7a, 0x50, 0x44, 0x56, 0x61, 0x61, 0x65, 0x45, 0x48, 0x49, 0x74, 0x64, 0x2b, 0x59, 0x67, 0x3d, 0xa0, 0x01, 0x00, 0xb0, 0x01, 0x00 + }; + + private static readonly Uri Url = new Uri("https://android.clients.google.com/checkin"); + + } +} diff --git a/VkNet.TokenMagic/Services/Token/Google/GoogleSecurityHttpClient.cs b/VkNet.TokenMagic/Services/Token/Google/GoogleSecurityHttpClient.cs new file mode 100644 index 0000000..3f15c19 --- /dev/null +++ b/VkNet.TokenMagic/Services/Token/Google/GoogleSecurityHttpClient.cs @@ -0,0 +1,104 @@ +using System; +using System.Collections.Generic; +using System.Net.Http; +using System.Threading.Tasks; +using Microsoft.Extensions.Logging; +using VkNet.TokenMagic.Models.Google; + +namespace VkNet.TokenMagic.Services.Token.Google +{ + public class GoogleSecurityHttpClient { + + protected readonly HttpClient HttpClient; + private readonly string _appId; + private readonly ILogger _logger; + + public GoogleSecurityHttpClient(HttpClient httpClient, RandomAppIdProvider appIdProvider, ILogger logger) + { + HttpClient = httpClient; + HttpClient.DefaultRequestHeaders.UserAgent.ParseAdd(GcmUserAgent); + _appId = appIdProvider.AppId; + _logger = logger; + } + + public async Task GetReceipt(GoogleCredentials credentials) + { + + await RequestReceipt1(credentials).ConfigureAwait(false); + return await RequestReceipt2(credentials).ConfigureAwait(false); + } + + private async Task RequestReceipt1(GoogleCredentials credentials) + { + var request = new HttpRequestMessage(HttpMethod.Post, Url) + { + Method = HttpMethod.Post + }; + request.Headers.Add("Authorization", $"AidLogin {credentials.Id}:{credentials.Token}"); + request.Content = new FormUrlEncodedContent(GetRequestParams(credentials)); + + _logger?.LogDebug($"{nameof(RequestReceipt1)}"); + var response = await HttpClient.SendAsync(request).ConfigureAwait(false); + response.EnsureSuccessStatusCode(); + } + + private async Task RequestReceipt2(GoogleCredentials credentials) + { + var request = new HttpRequestMessage(HttpMethod.Post, Url) + { + Method = HttpMethod.Post + }; + request.Headers.Add("Authorization", $"AidLogin {credentials.Id}:{credentials.Token}"); + var requestParams = GetRequestParams(credentials); + requestParams["X-scope"] = $"id{string.Empty}"; // id is always empty here? + requestParams["X-kid"] = "|ID|2|"; + requestParams["X-X-kid"] = "|ID|2|"; + request.Content = new FormUrlEncodedContent(requestParams); + _logger?.LogDebug($"{nameof(RequestReceipt2)}"); + var response = await HttpClient.SendAsync(request).ConfigureAwait(false); + response.EnsureSuccessStatusCode(); + + var content = await response.Content.ReadAsStringAsync().ConfigureAwait(false); + var result = content.Split(new[] {"|ID|2|:"}, StringSplitOptions.None)[1]; + if (result == "PHONE_REGISTRATION_ERROR") + { + throw new InvalidOperationException($"{nameof(RequestReceipt2)} bad response: {result}\n{content}"); + } + + return result; + } + + protected Dictionary GetRequestParams(GoogleCredentials credentials) + { + return new Dictionary + { + {"X-scope", "GCM"}, + {"X-osv", "23"}, + {"X-subtype", "54740537194"}, + {"X-app_ver", "443"}, + {"X-kid", "|ID|1|"}, + {"X-appid", _appId}, + {"X-gmsv", "13283005"}, + {"X-cliv", "iid-10084000"}, + {"X-app_ver_name", "51.2 lite"}, + {"X-X-kid", "|ID|1|"}, + {"X-subscription", "54740537194"}, + {"X-X-subscription", "54740537194"}, + {"X-X-subtype", "54740537194"}, + {"app", "com.perm.kate_new_6"}, + {"sender", "54740537194"}, + {"device", credentials.Id.ToString()}, + {"cert", "966882ba564c2619d55d0a9afd4327a38c327456"}, + {"app_ver", "443"}, + {"info", "g57d5w1C4CcRUO6eTSP7b7VoT8yTYhY"}, + {"gcm_ver", "13283005"}, + {"plat", "0"}, + {"X-messenger2", "1"} + }; + } + + protected const string GcmUserAgent = "Android-GCM/1.5 (generic_x86 KK)"; + + protected static Uri Url = new Uri("https://android.clients.google.com/c2dm/register3"); + } +} \ No newline at end of file diff --git a/VkNet.TokenMagic/Services/Token/Google/MTalkTcpClient.cs b/VkNet.TokenMagic/Services/Token/Google/MTalkTcpClient.cs new file mode 100644 index 0000000..3614caf --- /dev/null +++ b/VkNet.TokenMagic/Services/Token/Google/MTalkTcpClient.cs @@ -0,0 +1,88 @@ +using System; +using System.Linq; +using System.Net.Security; +using System.Net.Sockets; +using System.Text; +using Microsoft.Extensions.Logging; +using VkNet.TokenMagic.Models.Google; + +namespace VkNet.TokenMagic.Services.Token.Google +{ + public class MTalkTcpClient + { + private readonly ILogger _logger; + + public MTalkTcpClient(ILogger logger) + { + _logger = logger; + } + + public void SendRequest(GoogleCredentials googleCredentials) + { + var request = BuildMTalkRequest(googleCredentials); + _logger?.LogDebug($"{nameof(SendRequest)}"); + using (var tcpClient = new TcpClient(Host, Port)) + { + using (var sslStream = new SslStream(tcpClient.GetStream(), false, (sender, certificate, chain, errors) => true, null)) + { + sslStream.AuthenticateAsClient(Host); + sslStream.Write(request); + sslStream.Flush(); + sslStream.ReadByte(); + var responseCode = sslStream.ReadByte(); + if (responseCode != SuccessCode) + { + throw new InvalidOperationException($"MTalk expected response code [{SuccessCode}], got [{responseCode}]"); + } + } + } + } + + private byte[] BuildMTalkRequest(GoogleCredentials googleCredentials) + { + var idStringBytes = Encoding.ASCII.GetBytes(googleCredentials.Id.ToString()); + var idLen = VarInt.Write(idStringBytes.Length).ToList(); + + var tokenStringByes = Encoding.ASCII.GetBytes(googleCredentials.Token.ToString()); + var tokenLen = VarInt.Write(tokenStringByes.Length).ToList(); + + var hexId = "android-" + BitConverter.ToString(googleCredentials.RawId.ToArray()).Replace("-", string.Empty).ToLowerInvariant(); + var hexIdBytes = Encoding.ASCII.GetBytes(hexId); + var hexIdLen = VarInt.Write(hexIdBytes.Length); + + var body = message1 + .Concat(idLen) + .Concat(idStringBytes) + .Concat(message2) + .Concat(idLen) + .Concat(idStringBytes) + .Concat(message3) + .Concat(tokenLen) + .Concat(tokenStringByes) + .Concat(message4) + .Concat(hexIdLen) + .Concat(hexIdBytes) + .Concat(message5) + .ToList(); + + var bodyLen = VarInt.Write(body.Count); + return message6 + .Concat(bodyLen) + .Concat(body) + .ToArray(); + } + + + + private byte[] message1 = { 0x0a, 0x0a, 0x61, 0x6e, 0x64, 0x72, 0x6f, 0x69, 0x64, 0x2d, 0x31, 0x39, 0x12, 0x0f, 0x6d, 0x63, 0x73, 0x2e, 0x61, 0x6e, 0x64, 0x72, 0x6f, 0x69, 0x64, 0x2e, 0x63, 0x6f, 0x6d, 0x1a }; + private byte[] message2 = { 0x22 }; + private byte[] message3 = { 0x2a }; + private byte[] message4 = { 0x32 }; + private byte[] message5 = { 0x42, 0x0b, 0x0a, 0x06, 0x6e, 0x65, 0x77, 0x5f, 0x76, 0x63, 0x12, 0x01, 0x31, 0x60, 0x00, 0x70, 0x01, 0x80, 0x01, 0x02, 0x88, 0x01, 0x01 }; + private byte[] message6 = { 0x29, 0x02 }; + + private const int SuccessCode = 3; + private const string Host = "mtalk.google.com"; + private const int Port = 5228; + } +} diff --git a/VkNet.TokenMagic/Services/Token/Google/ProtobufParser.cs b/VkNet.TokenMagic/Services/Token/Google/ProtobufParser.cs new file mode 100644 index 0000000..816b0ce --- /dev/null +++ b/VkNet.TokenMagic/Services/Token/Google/ProtobufParser.cs @@ -0,0 +1,84 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using VkNet.TokenMagic.Models.Google; + +namespace VkNet.TokenMagic.Services.Token.Google +{ + public class ProtobufParser + { + private const int IdFieldNumber = 7; + private const int TokenFieldNumber = 8; + + private readonly byte[] data; + private int offset; + + public ProtobufParser(byte[] data) + { + this.data = data; + this.offset = 0; + } + + public GoogleCredentials Parse() + { + List rawId = null; + long? id = null; + long? token = null; + while (offset < data.Length) + { + var (value, length) = VarInt.Read(data, offset); + offset += length; + var field = new ProtobufField(value); + switch (field.Type) + { + case 0: + var (_, i) = VarInt.Read(data, offset); + offset += i; + break; + case 1 when field.FieldNumber == IdFieldNumber: + rawId = data.Skip(offset).Take(8).ToList(); + id = BitConverter.ToInt64(data, offset); + offset += 8; + break; + case 1 when field.FieldNumber == TokenFieldNumber: + token = BitConverter.ToInt64(data, offset); + offset += 8; + break; + case 1: + offset += 8; + break; + case 2: + var (skip, j) = VarInt.Read(data, offset); + offset += skip + j; + break; + default: + throw new InvalidOperationException($"{nameof(ProtobufParser)} unexpected code [{field.Type}]"); + } + } + + if (offset == data.Length && id == null && token == null) + { + throw new InvalidOperationException($"{nameof(ProtobufParser)} reached end of data, id and token not found"); + } + + if (id == null) + { + throw new InvalidOperationException($"{nameof(ProtobufParser)} id not found"); + } + + if (token == null) + { + throw new InvalidOperationException($"{nameof(ProtobufParser)} token not found"); + } + + return new GoogleCredentials() + { + Id = id.Value, + Token = token.Value, + RawId = rawId + }; + } + + + } +} \ No newline at end of file diff --git a/VkNet.TokenMagic/Services/Token/Google/RandomAppIdProvider.cs b/VkNet.TokenMagic/Services/Token/Google/RandomAppIdProvider.cs new file mode 100644 index 0000000..e4dbec5 --- /dev/null +++ b/VkNet.TokenMagic/Services/Token/Google/RandomAppIdProvider.cs @@ -0,0 +1,28 @@ +using System; +using System.Text; + +namespace VkNet.TokenMagic.Services.Token.Google +{ + public class RandomAppIdProvider + { + public RandomAppIdProvider() + { + AppId = GenerateRandomString(11); + } + + public string AppId { get; } + + private string GenerateRandomString(int length) + { + var sb = new StringBuilder(length); + for (var i = 0; i < length; i++) + { + sb.Append(Alphabet[_random.Next(Alphabet.Length)]); + } + return sb.ToString(); + } + + private readonly Random _random = new Random(); + private const string Alphabet = "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ_-"; + } +} \ No newline at end of file diff --git a/VkNet.TokenMagic/Services/Token/Google/ReceiptReceiver.cs b/VkNet.TokenMagic/Services/Token/Google/ReceiptReceiver.cs new file mode 100644 index 0000000..c43704d --- /dev/null +++ b/VkNet.TokenMagic/Services/Token/Google/ReceiptReceiver.cs @@ -0,0 +1,26 @@ +using System.Threading.Tasks; + +namespace VkNet.TokenMagic.Services.Token.Google +{ + public class ReceiptReceiver + { + private readonly MTalkTcpClient _mTalkTcpClient; + private readonly AndroidHttpClient _androidHttpClient; + private readonly GoogleSecurityHttpClient _googleSecurityHttpClient; + + public ReceiptReceiver(MTalkTcpClient mTalkTcpClient, AndroidHttpClient androidHttpClient, GoogleSecurityHttpClient googleSecurityHttpClient) + { + _mTalkTcpClient = mTalkTcpClient; + _androidHttpClient = androidHttpClient; + _googleSecurityHttpClient = googleSecurityHttpClient; + } + + public async Task GetReceipt() + { + var protobuf = await _androidHttpClient.CheckIn().ConfigureAwait(false); + var googleCredentials = new ProtobufParser(protobuf).Parse(); + _mTalkTcpClient.SendRequest(googleCredentials); + return await _googleSecurityHttpClient.GetReceipt(googleCredentials).ConfigureAwait(false); + } + } +} diff --git a/VkNet.TokenMagic/Services/Token/Google/VarInt.cs b/VkNet.TokenMagic/Services/Token/Google/VarInt.cs new file mode 100644 index 0000000..85456ac --- /dev/null +++ b/VkNet.TokenMagic/Services/Token/Google/VarInt.cs @@ -0,0 +1,53 @@ +using System; +using System.Collections.Generic; + +namespace VkNet.TokenMagic.Services.Token.Google +{ + public static class VarInt + { + public static (int value, int length) Read(byte[] data, int offset) + { + var i = 0; + var result = 0; + while (i + offset < data.Length) + { + var current = data[i + offset]; + if ((current & 0x80) != 0) + { + result |= (current ^ 0x80) << (i * 7); + i++; + } + else + { + result |= current << (i * 7); + i++; + break; + } + } + + if (i + offset == data.Length) + { + throw new InvalidOperationException($"{nameof(VarInt)} failed to read varint"); + } + + return (result, i); + } + + public static IEnumerable Write(int value) + { + while (value != 0) + { + var current = value & 0x7F; + value >>= 7; + if (value != 0) + { + yield return (byte)(current | 0x80); + } + else + { + yield return (byte)current; + } + } + } + } +} \ No newline at end of file diff --git a/VkNet.TokenMagic/VkNet.TokenMagic.csproj b/VkNet.TokenMagic/VkNet.TokenMagic.csproj new file mode 100644 index 0000000..689c3ec --- /dev/null +++ b/VkNet.TokenMagic/VkNet.TokenMagic.csproj @@ -0,0 +1,12 @@ + + + + netstandard2.0 + + + + + + + +