Skip to content
Merged
Show file tree
Hide file tree
Changes from 8 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 6 additions & 1 deletion MegaApiClient.Tests/TestWebClient.cs
Original file line number Diff line number Diff line change
Expand Up @@ -43,10 +43,15 @@ public int BufferSize
}

public string PostRequestJson(Uri url, string jsonData)
{
return PostRequestJson(url, jsonData, null);
}

public string PostRequestJson(Uri url, string jsonData, string hashcash)
{
return _policy.Execute(() =>
{
var result = s_webClient.PostRequestJson(url, jsonData);
var result = s_webClient.PostRequestJson(url, jsonData, hashcash);
OnCalled?.Invoke(CallType.PostRequestJson, url);

return result;
Expand Down
7 changes: 6 additions & 1 deletion MegaApiClient/Exceptions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -150,6 +150,11 @@ public enum ApiResultCode
/// <summary>
/// Login requires Two-Factor Authentication
/// </summary>
TwoFactorAuthenticationError = -26
TwoFactorAuthenticationError = -26,

/// <summary>
/// Hashcash calculation required
/// </summary>
HashcashRequired = -27,
}
}
1 change: 1 addition & 0 deletions MegaApiClient/Interface/IWebClient.cs
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ public interface IWebClient
int BufferSize { get; set; }

string PostRequestJson(Uri url, string jsonData);
string PostRequestJson(Uri url, string jsonData, string hashcash);

string PostRequestRaw(Uri url, Stream dataStream);

Expand Down
122 changes: 121 additions & 1 deletion MegaApiClient/MegaApiClient.cs
Original file line number Diff line number Diff line change
Expand Up @@ -1116,9 +1116,10 @@ private TResponse RequestCore<TResponse>(RequestBase request, byte[] key)
object jsonData = null;
var attempt = 0;
var apiCode = ApiResultCode.Ok;
string hashcash = null;
while (_options.ComputeApiRequestRetryWaitDelay(++attempt, out var retryDelay))
{
var dataResult = _webClient.PostRequestJson(uri, dataRequest);
var dataResult = _webClient.PostRequestJson(uri, dataRequest, hashcash);

if (string.IsNullOrEmpty(dataResult)
|| (jsonData = JsonConvert.DeserializeObject(dataResult)) == null
Expand All @@ -1136,6 +1137,16 @@ private TResponse RequestCore<TResponse>(RequestBase request, byte[] key)
ApiRequestFailed?.Invoke(this, new ApiRequestFailedEventArgs(uri, attempt, retryDelay, apiCode, dataResult));
}

if(apiCode == ApiResultCode.HashcashRequired)
{
var challenge = ((JArray)jsonData)[1].Value<string>();
hashcash = GenerateHashcashToken(challenge);
jsonData = null;
attempt = 0;
apiCode = ApiResultCode.Ok;
continue;
}

if (apiCode == ApiResultCode.RequestFailedRetry)
{
Wait(retryDelay);
Expand Down Expand Up @@ -1311,6 +1322,115 @@ private IEnumerable<int> ComputeChunksSizesToUpload(long[] chunksPositions, long
}
}

private static string GenerateHashcashToken(string challenge)
{
var parts = challenge.Split(':');
if (parts.Length < 4)
{
throw new ArgumentException("Invalid challenge format");
}

var version = int.Parse(parts[0]);
if (version != 1)
{
throw new ArgumentException("Hashcash challenge is not version 1");
}

var easiness = int.Parse(parts[1]);
var tokenStr = parts[3];

var baseVal = ((easiness & 63) << 1) + 1;
var shifts = (easiness >> 6) * 7 + 3;
var threshold = baseVal << shifts;

var token = D64(tokenStr);

var iterations = 262144;
var chunkSize = 48;
var prefixSize = 4;
var totalSize = prefixSize + iterations * chunkSize;

var buffer = new byte[totalSize];

for (var i = 0; i < iterations; i++)
{
for (var j = 0; j < chunkSize; j++)
{
buffer[prefixSize + i * chunkSize + j] = token[j];
}
}

using (var sha256 = SHA256.Create())
{
while (true)
{
var hash = sha256.ComputeHash(buffer);
var hashPrefix = BitConverter.ToUInt32(hash, 0);

if (BitConverter.IsLittleEndian)
{
hashPrefix = ReverseBytes(hashPrefix);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
hashPrefix = ReverseBytes(hashPrefix);
hashPrefix = BinaryPrimitives.ReverseEndianness(hashPrefix);

this part was failing for me, so I tried replaced it with the builtin function for reversing the endianess and it seems to work

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Have not implemented this yet because it requires installing System.Memory, but let me know if you prefer this @gpailler

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I prefer to not introduce a new dependency here.
@Bl4Cc4t Did you have a chance to investigate why the original code was failing?

@tr4wzified Maybe you can use the following instead

if (BitConverter.IsLittleEndian)
{
  Array.Reverse(hash);
}
      
var hashPrefix = BitConverter.ToUInt32(hash, 0);

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@Bl4Cc4t @tr4wzified ? I'm just waiting for your feedback in order to merge this PR and deploy a first version on MyGet.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'll check this out later today

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Changed it, looks like everything's working here

}

if (hashPrefix <= threshold)
{
var prefix = E64(SubArray(buffer, 0, 4));
return $"1:{tokenStr}:{prefix}";
}

var j = 0;
while (true)
{
buffer[j]++;
if (buffer[j++] != 0)
{
break;
}
}
}
}
}

private static string E64(byte[] buffer)
Comment thread
tr4wzified marked this conversation as resolved.
Outdated
{
return Convert.ToBase64String(buffer)
.Replace('+', '-')
.Replace('/', '_')
.TrimEnd('=');
}

private static byte[] D64(string s)
{
var padded = s
.Replace('-', '+')
.Replace('_', '/');
switch (padded.Length % 4)
{
case 2: padded += "=="; break;
case 3: padded += "="; break;
}
return Convert.FromBase64String(padded);
}

private static byte[] SubArray(byte[] data, int index, int length)
{
var result = new byte[length];
for (var i = 0; i < length; i++)
{
result[i] = data[index + i];
}
return result;
}

private static uint ReverseBytes(uint value)
{
return (value >> 24) |
((value & 0x00FF0000) >> 8) |
((value & 0x0000FF00) << 8) |
(value << 24);
}


#endregion

#region AuthInfos
Expand Down
13 changes: 6 additions & 7 deletions MegaApiClient/MegaApiClient.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -47,12 +47,6 @@
<Reference Include="System.Net.Http" />
</ItemGroup>

<ItemGroup Condition="$(TargetFramework.StartsWith('netstandard')) == 'true'">
<PackageReference Include="System.Net.Http">
<Version>4.3.4</Version>
</PackageReference>
</ItemGroup>

<ItemGroup Condition="$(TargetFramework) == 'netstandard1.3'">
<PackageReference Include="Microsoft.AspNetCore.WebUtilities" Version="1.1.2" />
</ItemGroup>
Expand Down Expand Up @@ -81,7 +75,12 @@
<IncludeAssets>runtime;build;native;contentfiles;analyzers</IncludeAssets>
</PackageReference>
</ItemGroup>

<ItemGroup Condition="$(TargetFramework.StartsWith('netstandard')) == 'true'">
<PackageReference Include="System.Net.Http">
<Version>4.3.4</Version>
</PackageReference>
</ItemGroup>

<!-- Deterministic Builds -->
<PropertyGroup Condition="'$(GITHUB_ACTIONS)' == 'true'">
<ContinuousIntegrationBuild>true</ContinuousIntegrationBuild>
Expand Down
38 changes: 36 additions & 2 deletions MegaApiClient/WebClient_HttpClient.cs
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,18 @@ public string PostRequestJson(Uri url, string jsonData)
{
using (var jsonStream = new MemoryStream(jsonData.ToBytes()))
{
using (var responseStream = PostRequest(url, jsonStream, "application/json"))
using (var responseStream = PostRequest(url, jsonStream, "application/json", null))
{
return StreamToString(responseStream);
}
}
}

public string PostRequestJson(Uri url, string jsonData, string hashcash)
{
using (var jsonStream = new MemoryStream(jsonData.ToBytes()))
{
using (var responseStream = PostRequest(url, jsonStream, "application/json", hashcash))
{
return StreamToString(responseStream);
}
Expand All @@ -76,7 +87,11 @@ public Stream GetRequestRaw(Uri url)
return _httpClient.GetStreamAsync(url).Result;
}

private Stream PostRequest(Uri url, Stream dataStream, string contentType)
private Stream PostRequest(Uri url, Stream dataStream, string contentType) {
return PostRequest(url, dataStream, contentType, null);
}

private Stream PostRequest(Uri url, Stream dataStream, string contentType, string hashcash)
{
using (var content = new StreamContent(dataStream, BufferSize))
{
Expand All @@ -87,6 +102,11 @@ private Stream PostRequest(Uri url, Stream dataStream, string contentType)
Content = content
};

if(hashcash != null)
{
requestMessage.Headers.Add("X-Hashcash", hashcash);
}

var response = _httpClient.SendAsync(requestMessage, HttpCompletionOption.ResponseHeadersRead).Result;
if (!response.IsSuccessStatusCode
&& response.StatusCode == HttpStatusCode.InternalServerError
Expand All @@ -95,6 +115,20 @@ private Stream PostRequest(Uri url, Stream dataStream, string contentType)
return new MemoryStream(Encoding.UTF8.GetBytes(((long)ApiResultCode.RequestFailedRetry).ToString()));
}

if(!response.IsSuccessStatusCode
&& response.StatusCode == HttpStatusCode.PaymentRequired
&& response.Headers.TryGetValues("X-Hashcash", out var hashcashValues))
{
foreach(var value in hashcashValues)
{
hashcash = value;
break;
}

var json = $"[{(long)ApiResultCode.HashcashRequired}, '{hashcash}']";
return new MemoryStream(Encoding.UTF8.GetBytes(json));
}

response.EnsureSuccessStatusCode();

return response.Content.ReadAsStreamAsync().Result;
Expand Down
16 changes: 15 additions & 1 deletion MegaApiClient/WebClient_HttpWebRequest.cs
Original file line number Diff line number Diff line change
Expand Up @@ -30,10 +30,15 @@ public WebClient(int responseTimeout = DefaultResponseTimeout, string userAgent
public int BufferSize { get; set; }

public string PostRequestJson(Uri url, string jsonData)
{
return PostRequestJson(url, jsonData, null);
}

public string PostRequestJson(Uri url, string jsonData, string hashcash)
{
using (var jsonStream = new MemoryStream(jsonData.ToBytes()))
{
using (var responseStream = PostRequest(url, jsonStream, "application/json"))
using (var responseStream = PostRequest(url, jsonStream, "application/json", hashcash))
{
return StreamToString(responseStream);
}
Expand Down Expand Up @@ -62,11 +67,20 @@ public Stream GetRequestRaw(Uri url)
}

private Stream PostRequest(Uri url, Stream dataStream, string contentType)
{
return PostRequest(url, dataStream, contentType, null);
}

private Stream PostRequest(Uri url, Stream dataStream, string contentType, string hashcash)
{
var request = CreateRequest(url);
request.ContentLength = dataStream.Length;
request.Method = "POST";
request.ContentType = contentType;
if(hashcash != null)
{
request.Headers.Add("X-Hashcash", hashcash);
}

using (var requestStream = request.GetRequestStream())
{
Expand Down
Loading