diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..1ee5385 --- /dev/null +++ b/.gitignore @@ -0,0 +1,362 @@ +## 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 + +# Mono auto generated files +mono_crash.* + +# Build results +[Dd]ebug/ +[Dd]ebugPublic/ +[Rr]elease/ +[Rr]eleases/ +x64/ +x86/ +[Ww][Ii][Nn]32/ +[Aa][Rr][Mm]/ +[Aa][Rr][Mm]64/ +bld/ +[Bb]in/ +[Oo]bj/ +[Ll]og/ +[Ll]ogs/ + +# 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 +nunit-*.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/ + +# ASP.NET Scaffolding +ScaffoldingReadMe.txt + +# 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 + +# TeamCity is a build add-in +_TeamCity* + +# DotCover is a Code Coverage Tool +*.dotCover + +# AxoCover is a Code Coverage Tool +.axoCover/* +!.axoCover/settings.json + +# Coverlet is a free, cross platform Code Coverage Tool +coverage*.json +coverage*.xml +coverage*.info + +# 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 +# NuGet Symbol Packages +*.snupkg +# 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 +*.appxbundle +*.appxupload + +# 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 +*- [Bb]ackup.rdl +*- [Bb]ackup ([0-9]).rdl +*- [Bb]ackup ([0-9][0-9]).rdl + +# 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/ + +# 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/ + +# BeatPulse healthcheck temp database +healthchecksdb + +# Backup folder for Package Reference Convert tool in Visual Studio 2017 +MigrationBackup/ + +# Ionide (cross platform F# VS Code tools) working folder +.ionide/ + +# Fody - auto-generated XML schema +FodyWeavers.xsd diff --git a/ApiRequest.cs b/ApiRequest.cs new file mode 100644 index 0000000..9ea93f0 --- /dev/null +++ b/ApiRequest.cs @@ -0,0 +1,29 @@ +using System; +using System.Collections.Generic; +using System.ComponentModel.DataAnnotations; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace WVCore.Server +{ + class ChallengeRequest + { + public string PSSH { get; set; } + public string CertBase64 { get; set; } + } + + class KeyRequest + { + public string PSSH { get; set; } + public string ChallengeBase64 { get; set; } + public string LicenseBase64 { get; set; } + } + + internal class ApiRequest + { + public string PSSH { get; set; } + public Dictionary Headers { get; set; } + public string LicenseUrl { get; set; } + } +} diff --git a/ApiResponse.cs b/ApiResponse.cs new file mode 100644 index 0000000..eec4722 --- /dev/null +++ b/ApiResponse.cs @@ -0,0 +1,19 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace WVCore.Server +{ + class ChallengeResonse + { + public string ChallengeBase64 { get; set; } + } + + internal class ApiResponse + { + public string PSSH { get; set; } + public List Keys { get; set; } + } +} diff --git a/Directory.Build.props b/Directory.Build.props new file mode 100644 index 0000000..0b34136 --- /dev/null +++ b/Directory.Build.props @@ -0,0 +1,30 @@ + + + + Speed + true + false + true + Link + link + 7.0.0-* + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/HTTPUtil.cs b/HTTPUtil.cs new file mode 100644 index 0000000..88c83f1 --- /dev/null +++ b/HTTPUtil.cs @@ -0,0 +1,75 @@ +using NLog; +using System; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.Collections.Specialized; +using System.Diagnostics; +using System.IO; +using System.Linq; +using System.Net; +using System.Net.Cache; +using System.Net.Http; +using System.Net.Http.Headers; +using System.Security.Cryptography; +using System.Text; +using System.Text.Json; +using System.Text.RegularExpressions; +using System.Threading; +using System.Threading.Tasks; +using System.Web; + +namespace WVCore.Server +{ + public class HTTPUtil + { + static Logger logger = LogManager.GetCurrentClassLogger(); + + public static readonly HttpClient AppHttpClient = new(new HttpClientHandler + { + AllowAutoRedirect = true, + AutomaticDecompression = DecompressionMethods.All, + ServerCertificateCustomValidationCallback = (sender, cert, chain, sslPolicyErrors) => true + }) + { + Timeout = TimeSpan.FromMinutes(5) + }; + + public static async Task PostDataAsync(string URL, Dictionary headers, byte[] postData) + { + logger.Debug($"Post to: {URL}"); + logger.Debug($"Post data: {Util.BytesToHex(postData, " ")}"); + ByteArrayContent content = new ByteArrayContent(postData); + if (headers.TryGetValue("Content-Type", out var contentType)) + { + content.Headers.ContentType = MediaTypeHeaderValue.Parse(contentType); + } + HttpResponseMessage response = await PostAsync(URL, headers, content); + byte[] bytes = await response.Content.ReadAsByteArrayAsync(); + logger.Debug($"Recv data: {Util.BytesToHex(bytes, " ")}"); + return bytes; + } + + private static async Task PostAsync(string URL, Dictionary headers, HttpContent content) + { + HttpRequestMessage request = new HttpRequestMessage() + { + RequestUri = new Uri(URL), + Method = HttpMethod.Post, + Content = content + }; + + if (headers != null) + foreach (KeyValuePair header in headers) + request.Headers.TryAddWithoutValidation(header.Key, header.Value); + + logger.Debug(request.Headers.ToString()); + + return await SendAsync(request); + } + + static async Task SendAsync(HttpRequestMessage request) + { + return await AppHttpClient.SendAsync(request); + } + } +} diff --git a/Program.cs b/Program.cs new file mode 100644 index 0000000..9348373 --- /dev/null +++ b/Program.cs @@ -0,0 +1,91 @@ +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Http; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.DependencyInjection; +using NLog.Config; +using NLog.Targets; +using NLog; +using ProtoBuf; + +namespace WVCore.Server +{ + internal class Program + { + static FileInfo? ClientIdFile = null; + static FileInfo? PrivateKeyFile = null; + + static Program() + { + if (!File.Exists("device_client_id_blob")) + throw new FileNotFoundException("找不到device_client_id_blob文件"); + if (!File.Exists("device_private_key")) + throw new FileNotFoundException("找不到device_private_key文件"); + + ClientIdFile = new FileInfo("device_client_id_blob"); + PrivateKeyFile = new FileInfo("device_private_key"); + } + + static WVApi GetWVApi() + { + return new WVApi(ClientIdFile, PrivateKeyFile); + } + + /** + * Test PSSH: AAAAp3Bzc2gAAAAA7e+LqXnWSs6jyCfc1R0h7QAAAIcSEFF0U4YtQlb9i61PWEIgBNcSEPCTfpp3yFXwptQ4ZMXZ82USEE1LDKJawVjwucGYPFF+4rUSEJAqBRprNlaurBkm/A9dkjISECZHD0KW1F0Eqbq7RC4WmAAaDXdpZGV2aW5lX3Rlc3QiFnNoYWthX2NlYzViZmY1ZGM0MGRkYzlI49yVmwY= + * Server: https://cwip-shaka-proxy.appspot.com/no_auth + */ + static void Main(string[] args) + { + InitLog(); + var logger = LogManager.GetCurrentClassLogger(); + logger.Debug("Log Inited."); + + var builder = WebApplication.CreateBuilder(args); + builder.Logging.ClearProviders(); + + builder.Services.AddCors(p => + { + p.AddDefaultPolicy(builder => builder + .AllowAnyOrigin() + .AllowAnyHeader() + .AllowAnyMethod()); + }); + + logger.Info("The app started!"); + logger.Info("Listening: http://0.0.0.0:18888"); + + var app = builder.Build(); + app.UseCors(); + app.Urls.Add("http://0.0.0.0:18888"); + app.MapGet("/wvapi", () => "Please Use POST!"); + app.MapPost("/wvapi", async (HttpRequest request) => await RequestHandler.HandleCommon(request, GetWVApi())); + app.MapPost("/getchallenge", async (HttpRequest request) => await RequestHandler.HandleChallenge(request, GetWVApi())); + app.MapPost("/getkeys", async (HttpRequest request) => await RequestHandler.HandleKeys(request, GetWVApi())); + app.Run(); + } + + static void InitLog() + { + var config = new LoggingConfiguration(); + var consoleTarget = new ColoredConsoleTarget(); + config.AddTarget("console", consoleTarget); + var fileTarget = new FileTarget(); + config.AddTarget("file", fileTarget); + + consoleTarget.Layout = "${longdate} [${level:uppercase=true}] - ${message} ${exception:format=tostring}"; + fileTarget.Layout = "${longdate} [${level:uppercase=true}] [${logger}] - ${message} ${exception:format=tostring}"; + fileTarget.FileName = "${basedir}/logs/WVCore.${shortdate}.log"; + + var rule1 = new LoggingRule("*", NLog.LogLevel.Info, consoleTarget); + config.LoggingRules.Add(rule1); + + if (!Environment.GetCommandLineArgs().Any(s => s.Contains("nolog"))) + { + var rule2 = new LoggingRule("*", NLog.LogLevel.Debug, fileTarget); + config.LoggingRules.Add(rule2); + } + + LogManager.Configuration = config; + } + } +} \ No newline at end of file diff --git a/README.md b/README.md index 4e3da39..222b571 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,99 @@ # WVCore.Server -Tiny Server. Example of WVCore Api. +Tiny Server. Example of [WVCore](https://github.com/nilaoda/WVCore) Api. + +# Api + +## `/wvapi` + +METHOD: POST + +REQ: +```json +{ + "PSSH":"PSSH", + "Headers":{ + "User-Agent":"IOS" + }, + "LicenseUrl":"https://auth" +} +``` + +RESP: +```json +{ + "pssh":"PSSH", + "keys":[ + "kid:key", + "kid:key" + ] +} +``` + +## `/getchallenge` + +METHOD: POST + +REQ: +```json +{ + "PSSH":"PSSH", + "CertBase64":"CertBase64" +} +``` + +RESP: +```json +{ + "challengeBase64":"challengeBase64" +} +``` + +## `/getkeys` + +METHOD: POST + +REQ: +```json +{ + "PSSH":"PSSH", + "ChallengeBase64":"ChallengeBase64", + "LicenseBase64":"LicenseBase64" +} +``` + +RESP: +```json +{ + "pssh":"PSSH", + "keys":[ + "kid:key", + "kid:key" + ] +} +``` + +# JS Example + +```js +let body = { + "PSSH": "AAAAp3Bzc2gAAAAA7e+LqXnWSs6jyCfc1R0h7QAAAIcSEFF0U4YtQlb9i61PWEIgBNcSEPCTfpp3yFXwptQ4ZMXZ82USEE1LDKJawVjwucGYPFF+4rUSEJAqBRprNlaurBkm/A9dkjISECZHD0KW1F0Eqbq7RC4WmAAaDXdpZGV2aW5lX3Rlc3QiFnNoYWthX2NlYzViZmY1ZGM0MGRkYzlI49yVmwY=", + "Headers": { + "User-Agent": "Test" + }, + "LicenseUrl": "https://cwip-shaka-proxy.appspot.com/no_auth" +} +let json = await fetch("http://127.0.0.1:18888/wvapi", { + body: JSON.stringify(body), + headers: { + "Content-Type": "application/json" + }, + method: "POST" +}).then(resp => resp.json()); +console.log(json.keys); +``` + +# More + +https://github.com/nilaoda/Blog/discussions/58 + + diff --git a/RequestHandler.cs b/RequestHandler.cs new file mode 100644 index 0000000..1f22499 --- /dev/null +++ b/RequestHandler.cs @@ -0,0 +1,73 @@ +using Microsoft.AspNetCore.Http; +using NLog; + +namespace WVCore.Server +{ + internal class RequestHandler + { + static Logger logger = LogManager.GetCurrentClassLogger(); + + public static async Task HandleCommon(HttpRequest request, WVApi wvApi) + { + try + { + var apiReqeust = await request.ReadFromJsonAsync(); + logger.Info(request.Path + " ==> " + Util.ConvertToJson(apiReqeust)); + var keys = await WVUtil.GetKeysAsync(apiReqeust.PSSH, apiReqeust.LicenseUrl, apiReqeust.Headers, wvApi); + var apiResponse = new ApiResponse() + { + PSSH = apiReqeust.PSSH, + Keys = keys + }; + logger.Info(request.Path + " <== " + Util.ConvertToJson(apiResponse)); + return Results.Ok(apiResponse); + } + catch (Exception) + { + return Results.Problem("Error"); + } + } + + public static async Task HandleChallenge(HttpRequest request, WVApi wvApi) + { + try + { + var challengeReqeust = await request.ReadFromJsonAsync(); + logger.Info(request.Path + " ==> " + Util.ConvertToJson(challengeReqeust)); + var challenge = wvApi.GetChallenge(challengeReqeust.PSSH, challengeReqeust.CertBase64, false, false); + var challengeResponse = new ChallengeResonse() + { + ChallengeBase64 = Convert.ToBase64String(challenge) + }; + logger.Info(request.Path + " <== " + Util.ConvertToJson(challengeResponse)); + return Results.Ok(challengeResponse); + } + catch (Exception) + { + return Results.Problem("Error"); + } + } + + public static async Task HandleKeys(HttpRequest request, WVApi wvApi) + { + try + { + var keyReqeust = await request.ReadFromJsonAsync(); + logger.Info(request.Path + " ==> " + Util.ConvertToJson(keyReqeust)); + var pssh = wvApi.ProvideLicense(keyReqeust.LicenseBase64, keyReqeust.ChallengeBase64); + var keys = wvApi.GetKeys().Select(k => k.ToString()).ToList(); + var apiResponse = new ApiResponse() + { + PSSH = keyReqeust.PSSH, + Keys = keys + }; + logger.Info(request.Path + " <== " + Util.ConvertToJson(apiResponse)); + return Results.Ok(apiResponse); + } + catch (Exception) + { + return Results.Problem("Error"); + } + } + } +} diff --git a/Util.cs b/Util.cs new file mode 100644 index 0000000..1d23ca2 --- /dev/null +++ b/Util.cs @@ -0,0 +1,30 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Text.Json; +using System.Text.Json.Serialization; +using System.Threading.Tasks; + +namespace WVCore.Server +{ + internal class Util + { + public static string ConvertToJson(object o) + { + var options = new JsonSerializerOptions + { + Encoder = System.Text.Encodings.Web.JavaScriptEncoder.UnsafeRelaxedJsonEscaping, + WriteIndented = false, + DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull, + Converters = { new JsonStringEnumConverter() } + }; + return JsonSerializer.Serialize(o, options); + } + + public static string BytesToHex(byte[] data, string split = "") + { + return BitConverter.ToString(data).Replace("-", split); + } + } +} diff --git a/WVCore.Server.csproj b/WVCore.Server.csproj new file mode 100644 index 0000000..d83fb1d --- /dev/null +++ b/WVCore.Server.csproj @@ -0,0 +1,22 @@ + + + + Exe + net6.0 + enable + enable + + + + + + + + + + + + + + + diff --git a/WVUtil.cs b/WVUtil.cs new file mode 100644 index 0000000..fbd9cb6 --- /dev/null +++ b/WVUtil.cs @@ -0,0 +1,40 @@ +using NLog; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using WVCore.Widevine; + +namespace WVCore.Server +{ + internal class WVUtil + { + static Logger logger = LogManager.GetCurrentClassLogger(); + + public static async Task> GetKeysAsync(string pssh, string licenseUrl, Dictionary? headers, WVApi cdm) + { + var keyStrings = new List(); + if (headers == null) + headers = new Dictionary(); + + logger.Debug("get cert..."); + var resp1 = await HTTPUtil.PostDataAsync(licenseUrl, headers, new byte[] { 0x08, 0x04 }); + var certDataB64 = Convert.ToBase64String(resp1); + logger.Debug("get challenge..."); + var challenge = cdm.GetChallenge(pssh, certDataB64, false, false); + logger.Debug("get license..."); + var resp2 = await HTTPUtil.PostDataAsync(licenseUrl, headers, challenge); + var licenseB64 = Convert.ToBase64String(resp2); + //license传递给cdm + cdm.ProvideLicense(licenseB64); + logger.Debug("get keys..."); + List keys = cdm.GetKeys(); + foreach (var k in keys) + { + keyStrings.Add(k.ToString()); + } + return keyStrings; + } + } +} diff --git a/rd.xml b/rd.xml new file mode 100644 index 0000000..7de6519 --- /dev/null +++ b/rd.xml @@ -0,0 +1,53 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file