Webshare login working in CinemaWeb
All checks were successful
continuous-integration/drone/push Build is passing
All checks were successful
continuous-integration/drone/push Build is passing
This commit is contained in:
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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; }
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user