All checks were successful
continuous-integration/drone/push Build is passing
227 lines
8.0 KiB
C#
227 lines
8.0 KiB
C#
using System;
|
|
using System.Diagnostics.CodeAnalysis;
|
|
using System.Security.Cryptography;
|
|
using System.Text;
|
|
using System.Xml;
|
|
|
|
namespace Cinema.Webshare;
|
|
|
|
public class LinkGenerator
|
|
{
|
|
private static readonly BaseEncoding UnixMD5 = new BaseEncoding("./0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz", false);
|
|
private static readonly Uri WebshareApiUri = new Uri("https://webshare.cz/api/");
|
|
|
|
/// <summary>
|
|
/// Generates a download link for the given Webshare download.
|
|
/// </summary>
|
|
/// <param name="fileId">Webshare file identifier.</param>
|
|
/// <param name="fileName">Webshare file name.</param>
|
|
/// <param name="cancel">Asynchronous cancellation.</param>
|
|
/// <returns>Time-limited download link.</returns>
|
|
public async static Task<Uri> GenerateDownloadLinkAsync(string fileId, string fileName, CancellationToken cancel)
|
|
{
|
|
// Obtain the download password salt
|
|
// Example response: <?xml version="1.0" encoding="UTF-8"?><response><status>OK</status><salt>UX8y8Fpa</salt><app_version>30</app_version></response>
|
|
Uri saltUri = new Uri(WebshareApiUri, "file_password_salt/");
|
|
HttpResponseMessage saltRes = await Program._http.PostAsync(saltUri, new FormUrlEncodedContent(new[] { new KeyValuePair<string, string>("ident", fileId) }), cancel);
|
|
if (!saltRes.IsSuccessStatusCode)
|
|
throw new IOException("Failed to get password salt from Webshare API.");
|
|
|
|
XmlReader saltR = XmlReader.Create(saltRes.Content.ReadAsStream());
|
|
saltR.ReadStartElement("response");
|
|
Exception? e = GetExceptionIfStatusNotOK(saltR, out string? code);
|
|
string password;
|
|
if (e != null)
|
|
{
|
|
if (code == "FILE_PASSWORD_SALT_FATAL_2")
|
|
{
|
|
// No password is set
|
|
password = "";
|
|
}
|
|
else
|
|
throw e;
|
|
}
|
|
else
|
|
{
|
|
// Calculate the download password
|
|
string salt = saltR.ReadElementContentAsString("salt", "");
|
|
saltR.ReadElementContentAsString("app_version", "");
|
|
saltR.ReadEndElement(); // response
|
|
|
|
password = CalculatePassword(fileId, fileName, salt);
|
|
}
|
|
|
|
// Obtain the download link
|
|
// Example response: <?xml version="1.0" encoding="UTF-8"?><response><status>OK</status><link>https://free.19.dl.wsfiles.cz/9209/...</link><app_version>30</app_version></response>
|
|
Uri linkUri = new Uri(WebshareApiUri, "file_link/");
|
|
HttpResponseMessage linkRes = await Program._http.PostAsync(linkUri, new FormUrlEncodedContent(new[] {
|
|
new KeyValuePair<string, string>("ident", fileId),
|
|
new KeyValuePair<string, string>("download_type", "video_stream"),
|
|
new KeyValuePair<string, string>("device_uuid", Guid.NewGuid().ToString("X")),
|
|
new KeyValuePair<string, string>("device_res_x", "3840"),
|
|
new KeyValuePair<string, string>("device_res_y", "2160"),
|
|
new KeyValuePair<string, string>("password", password),
|
|
}), cancel
|
|
);
|
|
if (!linkRes.IsSuccessStatusCode)
|
|
throw new IOException("Failed to get download link from Webshare API.");
|
|
|
|
XmlReader linkR = XmlReader.Create(linkRes.Content.ReadAsStream());
|
|
linkR.ReadStartElement("response");
|
|
ThrowIfStatusNotOK(linkR);
|
|
string link = linkR.ReadElementContentAsString("link", "");
|
|
linkR.ReadElementContentAsString("app_version", "");
|
|
linkR.ReadEndElement(); // response
|
|
|
|
return new Uri(link);
|
|
}
|
|
|
|
private static Exception? GetExceptionIfStatusNotOK(XmlReader r, out string? code)
|
|
{
|
|
string status = r.ReadElementContentAsString("status", "");
|
|
if (status == "OK") {
|
|
code = null;
|
|
return null;
|
|
}
|
|
|
|
code = r.ReadElementContentAsString("code", "");
|
|
string message = r.ReadElementContentAsString("message", "");
|
|
return new IOException(code + ": " + message);
|
|
}
|
|
|
|
private static void ThrowIfStatusNotOK(XmlReader r)
|
|
{
|
|
Exception? e = GetExceptionIfStatusNotOK(r, out _);
|
|
if (e != null)
|
|
throw e;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Calculates the Webshare download password from file identifier, name and salt.
|
|
/// </summary>
|
|
/// <param name="fileId">Webshare file identifier.</param>
|
|
/// <param name="fileName">Webshare file name.</param>
|
|
/// <param name="salt">Download salt obtained from https://webshare.cz/api/file_password_salt/.</param>
|
|
/// <returns>Password to use to obtain a download link using https://webshare.cz/api/file_link/.</returns>
|
|
private static string CalculatePassword(string fileId, string fileName, string salt)
|
|
{
|
|
if (fileId == null || fileName == null || salt == null)
|
|
throw new ArgumentNullException();
|
|
|
|
// Example
|
|
// name = "bIaFGdkvKiiTBo2_S3E"
|
|
// id = "12kRBqAYQS2"
|
|
// password = sha256(name + id) = "1e98873b57b7660cba7267c191f2d1fb7d198d062eb2fe0686502883042c4105"
|
|
// crypt = md5crypt(password, salt: "UX8y8Fpa") = "$1$UX8y8Fpa$stY.bUYGBsmAlc6IIxnHU0"
|
|
// result = sha1(crypt) = "8ceff8e5abf9aa375a8a5b05871b6cd6fb7fd185")
|
|
string toHash = fileName + fileId;
|
|
byte[] password = SHA256.HashData(Encoding.UTF8.GetBytes(toHash));
|
|
string passwordS = Convert.ToHexString(password).ToLowerInvariant();
|
|
string crypt = MD5Crypt(passwordS, salt);
|
|
byte[] result = SHA1.HashData(Encoding.ASCII.GetBytes(crypt));
|
|
return Convert.ToHexString(result).ToLowerInvariant();
|
|
}
|
|
|
|
private static string MD5Crypt(string password, string salt)
|
|
{
|
|
string prefixString = "$1$";
|
|
byte[] prefixBytes = Encoding.ASCII.GetBytes(prefixString);
|
|
byte[] saltBytes = Encoding.ASCII.GetBytes(salt);
|
|
|
|
byte[] key = Encoding.ASCII.GetBytes(password);
|
|
byte[] truncatedSalt = TruncateAndCopy(saltBytes, 8);
|
|
byte[] crypt = Crypt(key, truncatedSalt, prefixBytes, MD5.Create());
|
|
|
|
string result = prefixString
|
|
+ salt + '$'
|
|
+ UnixMD5.GetString(crypt);
|
|
|
|
return result.TrimEnd('=');
|
|
}
|
|
|
|
private static byte[] Crypt(byte[] key, byte[] salt, byte[] prefix, HashAlgorithm A)
|
|
{
|
|
byte[] H = null, I = null;
|
|
|
|
A.Initialize();
|
|
AddToDigest(A, key);
|
|
AddToDigest(A, salt);
|
|
AddToDigest(A, key);
|
|
FinishDigest(A);
|
|
|
|
I = (byte[])A.Hash.Clone();
|
|
|
|
A.Initialize();
|
|
AddToDigest(A, key);
|
|
AddToDigest(A, prefix);
|
|
AddToDigest(A, salt);
|
|
|
|
AddToDigestRolling(A, I, 0, I.Length, key.Length);
|
|
|
|
int length = key.Length;
|
|
for (int i = 0; i < 31 && length != 0; i++)
|
|
{
|
|
AddToDigest(A, new[] { (length & (1 << i)) != 0 ? (byte)0 : key[0] });
|
|
length &= ~(1 << i);
|
|
}
|
|
FinishDigest(A);
|
|
|
|
H = (byte[])A.Hash.Clone();
|
|
|
|
for (int i = 0; i < 1000; i++)
|
|
{
|
|
A.Initialize();
|
|
if ((i & 1) != 0) { AddToDigest(A, key); }
|
|
if ((i & 1) == 0) { AddToDigest(A, H); }
|
|
if ((i % 3) != 0) { AddToDigest(A, salt); }
|
|
if ((i % 7) != 0) { AddToDigest(A, key); }
|
|
if ((i & 1) != 0) { AddToDigest(A, H); }
|
|
if ((i & 1) == 0) { AddToDigest(A, key); }
|
|
FinishDigest(A);
|
|
|
|
Array.Copy(A.Hash, H, H.Length);
|
|
}
|
|
|
|
byte[] crypt = new byte[H.Length];
|
|
int[] permutation = new[] { 11, 4, 10, 5, 3, 9, 15, 2, 8, 14, 1, 7, 13, 0, 6, 12 };
|
|
Array.Reverse(permutation);
|
|
for (int i = 0; i < crypt.Length; i++)
|
|
{
|
|
crypt[i] = H[permutation[i]];
|
|
}
|
|
|
|
return crypt;
|
|
}
|
|
|
|
private static void AddToDigest(HashAlgorithm algorithm, byte[] buffer)
|
|
{
|
|
AddToDigest(algorithm, buffer, 0, buffer.Length);
|
|
}
|
|
|
|
private static void AddToDigest(HashAlgorithm algorithm, byte[] buffer, int offset, int count)
|
|
{
|
|
algorithm.TransformBlock(buffer, offset, count, buffer, offset);
|
|
}
|
|
|
|
private static void AddToDigestRolling(HashAlgorithm algorithm, byte[] buffer, int offset, int inputCount, int outputCount)
|
|
{
|
|
int count;
|
|
for (count = 0; count < outputCount; count += inputCount)
|
|
{
|
|
AddToDigest(algorithm, buffer, offset, Math.Min(outputCount - count, inputCount));
|
|
}
|
|
}
|
|
|
|
private static void FinishDigest(HashAlgorithm algorithm)
|
|
{
|
|
algorithm.TransformFinalBlock(new byte[0], 0, 0);
|
|
}
|
|
|
|
private static byte[] TruncateAndCopy(byte[] buffer, int maxLength)
|
|
{
|
|
byte[] truncatedBuffer = new byte[Math.Min(buffer.Length, maxLength)];
|
|
Array.Copy(buffer, truncatedBuffer, truncatedBuffer.Length);
|
|
return truncatedBuffer;
|
|
}
|
|
}
|