Webshare login working in CinemaWeb
All checks were successful
continuous-integration/drone/push Build is passing

This commit is contained in:
2024-12-06 17:09:22 +01:00
parent 1da58e2e0e
commit a0f6870493
6 changed files with 152 additions and 128 deletions

View File

@@ -25,7 +25,7 @@ public sealed class CinemaHost : IHostedService
private static Session? _webshare;
#pragma warning restore CS8618
private readonly ILogger<CinemaHost> _logger;
private string? _lastWebsharePasswordHash;
private string? _lastWebsharePassword;
private string? _lastWebshareToken;
/// <summary>
@@ -87,16 +87,16 @@ public sealed class CinemaHost : IHostedService
private void EnsureWebshareSession(CinemaPluginConfiguration config)
{
if (config.WebshareUser != _webshare?.UserName
|| config.WebsharePasswordHash != _lastWebsharePasswordHash
|| config.WebsharePassword != _lastWebsharePassword
|| config.WebshareToken != _lastWebshareToken)
{
// Re-create the Webshare session as the credentials have changed
if (config.WebshareUser != null && config.WebsharePasswordHash != null)
_webshare = new Session(config.WebshareUser, config.WebsharePasswordHash, config.WebshareToken);
if (config.WebshareUser != null && config.WebsharePassword != null)
_webshare = new Session(config.WebshareUser, config.WebsharePassword, config.WebshareToken);
else
_webshare = null;
_lastWebsharePasswordHash = config.WebsharePasswordHash;
_lastWebsharePassword = config.WebsharePassword;
_lastWebshareToken = config.WebshareToken;
}
}

View File

@@ -24,6 +24,6 @@ public class CinemaPluginConfiguration : BasePluginConfiguration
public bool HideMusicFolder { get; set; }
public string WebshareUser { get; set; }
public string WebsharePasswordHash { get; set; }
public string WebsharePassword { get; set; }
public string WebshareToken { get; set; }
}

View File

@@ -56,7 +56,6 @@
<div class="inputContainer">
<input is="emby-input" type="password" id="websharePassword" label="Webshare password" />
<input type="hidden" id="websharePasswordHash" />
</div>
</div>
<br />
@@ -118,10 +117,10 @@
bubbles: true,
cancelable: false
}));
var websharePasswordHash = document.querySelector('#websharePasswordHash');
if (config.WebsharePasswordHash && config.WebsharePasswordHash.length != 0)
websharePasswordHash.value = "";
websharePasswordHash.dispatchEvent(new Event('change', {
var websharePassword = document.querySelector('#websharePassword');
if (config.WebsharePassword && config.WebsharePassword.length != 0)
websharePassword.value = "";
websharePassword.dispatchEvent(new Event('change', {
bubbles: true,
cancelable: false
}));
@@ -145,7 +144,7 @@
config.HideMusicFolder = document.querySelector('#hideMusicFolder').checked;
config.WebshareUser = document.querySelector('#webshareUser').value;
config.WebsharePasswordHash = document.querySelector('#websharePasswordHash').value;
config.WebsharePassword = document.querySelector('#websharePassword').value;
ApiClient.updatePluginConfiguration(CinemaPluginConfig.uniquePluginId, config).then(Dashboard.processPluginConfigurationUpdateResult);
});

View File

@@ -1,8 +1,12 @@
using System.Security.Cryptography;
using System.Text;
using System.Xml;
namespace Cinema.Webshare;
static class ApiExtensions {
private static readonly BaseEncoding UnixMD5 = new BaseEncoding("./0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz", false);
public static Exception? GetExceptionIfStatusNotOK(this XmlReader r, out string? code)
{
string status = r.ReadElementContentAsString("status", "");
@@ -22,4 +26,112 @@ static class ApiExtensions {
if (e != null)
throw e;
}
public static string HashPassword(this string password, string salt) {
string crypt = MD5Crypt(password, 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;
}
}

View File

@@ -98,8 +98,6 @@ public static class LinkGenerator
string toHash = fileName + fileId;
byte[] password = SHA256.HashData(Encoding.UTF8.GetBytes(toHash));
string passwordS = Convert.ToHexString(password).ToLowerInvariant();
string crypt = Session.MD5Crypt(passwordS, salt);
byte[] result = SHA1.HashData(Encoding.ASCII.GetBytes(crypt));
return Convert.ToHexString(result).ToLowerInvariant();
return passwordS.HashPassword(salt);
}
}

View File

@@ -14,13 +14,12 @@ public sealed class Session
{
internal const string TokenKeyName = "wst";
private static readonly BaseEncoding UnixMD5 = new BaseEncoding("./0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz", false);
private static readonly Uri WebshareApiUri = LinkGenerator.WebshareApiUri;
private static readonly TimeSpan InitialLoginBackoff = new TimeSpan(4 * TimeSpan.TicksPerSecond);
private static readonly TimeSpan MaxLoginBackoff = new TimeSpan(8 * TimeSpan.TicksPerMinute);
private readonly string _userName;
private readonly string _passwordHash;
private readonly string _password;
private string? _token;
private bool? _isVip;
private int? _vipDaysLeft;
@@ -31,13 +30,13 @@ public sealed class Session
/// <summary>
/// Constructs a session from credentials and optionally a possibly live token.
/// </summary>
public Session(string userName, string passwordHash, string? token = null)
public Session(string userName, string password, string? token = null)
{
if (userName == null || passwordHash == null)
if (userName == null || password == null)
throw new ArgumentNullException();
this._userName = userName;
this._passwordHash = passwordHash;
this._password = password;
this._token = token;
this._nextLoginTrial = default;
this._nextLoginBackoff = InitialLoginBackoff;
@@ -84,21 +83,29 @@ public sealed class Session
string? token;
try
{
string? salt = await GetSaltAsync(_userName, cancel);
token = salt != null ? await GetTokenAsync(new CredentialCheck(_userName, salt), _passwordHash, cancel) : null;
string? salt;
if ((salt = await GetSaltAsync(_userName, cancel)) == null
|| (token = await GetTokenAsync(_userName, _password.HashPassword(salt), cancel)) == null)
{
// Login failed
token = null;
}
}
catch
{
token = null;
}
if (token != null) {
if (token != null)
{
_nextLoginBackoff = InitialLoginBackoff;
_token = token;
if (await RefreshUserDataAsync(cancel))
return true;
} else {
}
else
{
TimeSpan nextBackoff = new TimeSpan(2 * _nextLoginBackoff.Ticks);
if (nextBackoff > MaxLoginBackoff)
nextBackoff = MaxLoginBackoff;
@@ -141,8 +148,8 @@ public sealed class Session
if (userName.IsNull || password == null)
throw new ArgumentNullException();
string passwordHash = MD5Crypt(password, userName.Salt);
return null != await GetTokenAsync(userName, passwordHash, cancel);
string passwordHash = password.HashPassword(userName.Salt);
return null != await GetTokenAsync(userName.UserName, passwordHash, cancel);
}
/// <summary>
@@ -178,12 +185,12 @@ public sealed class Session
/// Gets the session token.
/// </summary>
/// <returns>Non-null if user name is valid. -or- Null otherwise.</returns>
private static async Task<string?> GetTokenAsync(CredentialCheck userName, string passwordHash, CancellationToken cancel)
private static async Task<string?> GetTokenAsync(string userName, string passwordHash, CancellationToken cancel)
{
// Example response: <?xml version="1.0" encoding="UTF-8"?><response><status>OK</status><token>sometokenvalue</token><app_version>30</app_version></response>
Uri tokenUri = new Uri(WebshareApiUri, "login/");
HttpResponseMessage tokenRes = await Program._http.PostAsync(tokenUri, new FormUrlEncodedContent(new[] {
new KeyValuePair<string, string>("username_or_email", userName.UserName),
new KeyValuePair<string, string>("username_or_email", userName),
new KeyValuePair<string, string>("password", passwordHash),
new KeyValuePair<string, string>("keep_logged_in", "1"),
}), cancel
@@ -230,114 +237,22 @@ public sealed class Session
return false;
// Calculate the download password
foreach (string skipName in new string[] { "id", "ident", "username", "email", "credits", "points", "files", "bytes",
"score_files", "score_bytes", "private_files", "private_bytes", "private_space", "tester", "role"})
tokenR.ReadElementContentAsString(skipName, "");
_isVip = tokenR.ReadElementContentAsString("vip", "") == "1";
if (int.TryParse(tokenR.ReadElementContentAsString("vip_days", ""), CultureInfo.InvariantCulture, out int vipDays))
_vipDaysLeft = vipDays;
foreach (string skipName in new string[] { "vip_hours", "vip_minutes", "vip_until", "terms_version", "email_verified", "wants_newsletters", "wants_https_download" })
tokenR.ReadElementContentAsString(skipName, "");
tokenR.ReadElementContentAsString("app_version", "");
tokenR.ReadEndElement(); // response
return true;
}
internal 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;
}
}