refactor as vknet extension

This commit is contained in:
Rostislav 2019-01-21 05:40:17 +05:00
commit cc36c930ec
18 changed files with 1285 additions and 0 deletions

337
.gitignore vendored Normal file
View file

@ -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/

25
Example/Example.csproj Normal file
View file

@ -0,0 +1,25 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>netcoreapp2.1</TargetFramework>
</PropertyGroup>
<ItemGroup>
<None Remove="nlog.config" />
</ItemGroup>
<ItemGroup>
<EmbeddedResource Include="nlog.config">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</EmbeddedResource>
</ItemGroup>
<ItemGroup>
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="2.2.0" />
<PackageReference Include="Microsoft.Extensions.Http" Version="2.2.0" />
<PackageReference Include="VkNet" Version="1.41.0" />
<PackageReference Include="VkNet.NLog.Extensions.Logging" Version="1.3.1" />
</ItemGroup>
</Project>

58
Example/Program.cs Normal file
View file

@ -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<ILoggerFactory, LoggerFactory>();
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();
}
}
}

14
Example/nlog.config Normal file
View file

@ -0,0 +1,14 @@
<?xml version="1.0" encoding="utf-8" ?>
<nlog xmlns="http://www.nlog-project.org/schemas/NLog.xsd"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
<targets>
<target name="logfile" xsi:type="File" fileName="file.txt" layout="${longdate} ${callsite} ${level} ${message}" />
<target name="logconsole" xsi:type="Console" layout="${longdate} ${callsite} ${level} ${message}" />
</targets>
<rules>
<logger name="*" minlevel="Trace" writeTo="logconsole" />
<logger name="*" minlevel="Trace" writeTo="logfile" />
</rules>
</nlog>

31
VkNet.TokenMagic.sln Normal file
View file

@ -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

View file

@ -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<RandomAppIdProvider>();
services.TryAddSingleton<MTalkTcpClient>();
services.TryAddSingleton<AndroidHttpClient>();
services.TryAddSingleton<GoogleSecurityHttpClient>();
services.TryAddSingleton<ReceiptReceiver>();
services.TryAddSingleton<IRestClient, RestClientWithUserAgent>(); // vknet needs user-agent too
services.AddHttpClient<AndroidHttpClient>().ConfigurePrimaryHttpMessageHandler(provider => MaybeProxyHttpHandler(provider)).SetHandlerLifetime(TimeSpan.FromMinutes(1));
services.AddHttpClient<GoogleSecurityHttpClient>().ConfigurePrimaryHttpMessageHandler(provider => MaybeProxyHttpHandler(provider, true)).SetHandlerLifetime(TimeSpan.FromMinutes(1));
services.AddHttpClient<IRestClient, RestClientWithUserAgent>().ConfigurePrimaryHttpMessageHandler(provider => MaybeProxyHttpHandler(provider)).SetHandlerLifetime(TimeSpan.FromMinutes(10));
services.TryAddSingleton<IBrowser, BrowserWithAndroidToken>();
return services;
}
public static HttpClientHandler MaybeProxyHttpHandler(IServiceProvider provider, bool ignoreSsl=false)
{
var proxy = provider.GetService<IWebProxy>();
var logger = provider.GetService<ILogger<HttpClient>>();
var useProxyCondition = proxy != null;
if (useProxyCondition)
{
logger?.LogDebug($"Use Proxy: {proxy}");
}
Func<HttpRequestMessage, X509Certificate2, X509Chain, SslPolicyErrors, bool> certCallback = null;
if (ignoreSsl)
{
certCallback = (message, certificate2, arg3, arg4) => true;
logger?.LogDebug($"Ignoring ssl");
}
return new HttpClientHandler
{
Proxy = proxy,
UseProxy = useProxyCondition,
ServerCertificateCustomValidationCallback = certCallback
};
}
}
}

View file

@ -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<byte> RawId { get; set; }
public override string ToString()
{
return $"{nameof(GoogleCredentials)}(id {Id}; token {Token})";
}
}
}

View file

@ -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; }
}
}

View file

@ -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<BrowserWithAndroidToken> 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<AuthorizationResult>(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<long>();
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<KeyValuePair<string, string>>
{
new KeyValuePair<string, string>("access_token", oldToken),
new KeyValuePair<string, string>("receipt", receipt),
new KeyValuePair<string, string>("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<BrowserWithAndroidToken> _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<KeyValuePair<string, string>> parameters)
{
throw new NotImplementedException();
}
public AuthorizationResult Validate(string validateUrl)
{
throw new NotImplementedException();
}
#endregion
}
}

View file

@ -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
{
/// <inheritdoc />
public class RestClientWithUserAgent : IRestClient
{
// why is this in the interface?
public IWebProxy Proxy { get => throw new NotImplementedException(); set => throw new NotImplementedException(); }
/// <summary>
/// The log
/// </summary>
private readonly ILogger<RestClient> _logger;
private readonly HttpClient _httpClient;
protected readonly string UserAgent;
protected TimeSpan TimeoutSeconds;
/// <inheritdoc />
public RestClientWithUserAgent(ILogger<RestClient> logger, HttpClient httpClient)
:this(logger, httpClient, DefaultUserAgent)
{
}
/// <inheritdoc />
protected RestClientWithUserAgent(ILogger<RestClient> logger, HttpClient httpClient, string userAgent)
{
_logger = logger;
_httpClient = httpClient;
this.UserAgent = userAgent;
_httpClient.DefaultRequestHeaders.UserAgent.ParseAdd(UserAgent);
}
/// <inheritdoc />
public TimeSpan Timeout
{
get => TimeoutSeconds == TimeSpan.Zero ? TimeSpan.FromSeconds(300) : TimeoutSeconds;
set => TimeoutSeconds = value;
}
/// <inheritdoc />
public Task<HttpResponse<string>> GetAsync(Uri uri, IEnumerable<KeyValuePair<string, string>> 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));
}
/// <inheritdoc />
public Task<HttpResponse<string>> PostAsync(Uri uri, IEnumerable<KeyValuePair<string, string>> 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<HttpResponse<string>> CallAsync(Func<HttpClient, Task<HttpResponseMessage>> 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<string>.Success(response.StatusCode, content, url)
: HttpResponse<string>.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)";
}
}

View file

@ -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<AndroidHttpClient> _logger;
private readonly string _appId;
public AndroidHttpClient(HttpClient httpClient, RandomAppIdProvider appIdProvider, ILogger<AndroidHttpClient> logger)
{
HttpClient = httpClient;
_appId = appIdProvider.AppId;
_logger = logger;
}
public async Task<byte[]> 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");
}
}

View file

@ -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<GoogleSecurityHttpClient> _logger;
public GoogleSecurityHttpClient(HttpClient httpClient, RandomAppIdProvider appIdProvider, ILogger<GoogleSecurityHttpClient> logger)
{
HttpClient = httpClient;
HttpClient.DefaultRequestHeaders.UserAgent.ParseAdd(GcmUserAgent);
_appId = appIdProvider.AppId;
_logger = logger;
}
public async Task<string> 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<string> 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<string, string> GetRequestParams(GoogleCredentials credentials)
{
return new Dictionary<string, string>
{
{"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");
}
}

View file

@ -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<MTalkTcpClient> _logger;
public MTalkTcpClient(ILogger<MTalkTcpClient> 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;
}
}

View file

@ -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<byte> 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
};
}
}
}

View file

@ -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_-";
}
}

View file

@ -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<string> GetReceipt()
{
var protobuf = await _androidHttpClient.CheckIn().ConfigureAwait(false);
var googleCredentials = new ProtobufParser(protobuf).Parse();
_mTalkTcpClient.SendRequest(googleCredentials);
return await _googleSecurityHttpClient.GetReceipt(googleCredentials).ConfigureAwait(false);
}
}
}

View file

@ -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<byte> 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;
}
}
}
}
}

View file

@ -0,0 +1,12 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>netstandard2.0</TargetFramework>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.Extensions.Http" Version="2.2.0" />
<PackageReference Include="VkNet" Version="1.41.0" />
</ItemGroup>
</Project>