-
Notifications
You must be signed in to change notification settings - Fork 2
/
ClientRequester.cs
142 lines (129 loc) · 6.49 KB
/
ClientRequester.cs
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
using System.Security.Cryptography;
using System.Text;
namespace tbm.Crawler.Tieba;
public class ClientRequester
{
private readonly ILogger<ClientRequester> _logger;
private readonly IConfigurationSection _config;
private readonly IHttpClientFactory _httpFactory;
private readonly ClientRequesterTcs _requesterTcs;
private static readonly Random Rand = new();
public ClientRequester(
ILogger<ClientRequester> logger, IConfiguration config,
IHttpClientFactory httpFactory, ClientRequesterTcs requesterTcs)
{
(_logger, _httpFactory, _requesterTcs) = (logger, httpFactory, requesterTcs);
_config = config.GetSection("ClientRequester");
}
public Task<JsonElement> RequestJson
(string url, string clientVersion, Dictionary<string, string> postParam, CancellationToken stoppingToken = default) =>
Request(() => PostJson(url, postParam, clientVersion, stoppingToken), stream =>
{
stoppingToken.ThrowIfCancellationRequested();
using var doc = JsonDocument.Parse(stream);
return doc.RootElement.Clone();
});
public Task<TResponse> RequestProtoBuf<TRequest, TResponse>(
string url, string clientVersion,
TRequest requestParam, Action<TRequest, Common> commonParamSetter,
Func<TResponse> responseFactory, CancellationToken stoppingToken = default
)
where TRequest : IMessage<TRequest>
where TResponse : IMessage<TResponse> =>
Request(() => PostProtoBuf(url, clientVersion, requestParam, commonParamSetter, stoppingToken), stream =>
{
try
{
stoppingToken.ThrowIfCancellationRequested();
return new MessageParser<TResponse>(responseFactory).ParseFrom(stream);
}
catch (InvalidProtocolBufferException e)
{
_ = stream.Seek(0, SeekOrigin.Begin);
var stream2 = new MemoryStream((int)stream.Length);
stream.CopyTo(stream2);
// the invalid protoBuf bytes usually is just a plain html string
var responseBody = Encoding.UTF8.GetString(stream2.ToArray());
if (responseBody.Contains("为了保护您的账号安全和最佳的浏览体验,当前业务已经不支持IE8以下浏览器"))
throw new TiebaException(true, true);
throw new TiebaException($"Malformed protoBuf response from tieba. {responseBody}", e);
}
});
private static async Task<T> Request<T>(Func<Task<HttpResponseMessage>> requester, Func<Stream, T> responseConsumer)
{
try
{
using var response = await requester();
var stream = await response.EnsureSuccessStatusCode().Content.ReadAsStreamAsync();
return responseConsumer(stream);
}
catch (TaskCanceledException e) when (e.InnerException is TimeoutException)
{
throw new TiebaException("Tieba client request timeout.");
}
catch (HttpRequestException e)
{
if (e.StatusCode == null)
throw new TiebaException("Network error from tieba.", e);
throw new TiebaException($"HTTP {(int)e.StatusCode} from tieba.");
}
}
private Task<HttpResponseMessage> PostJson
(string url, Dictionary<string, string> postParam, string clientVersion, CancellationToken stoppingToken = default)
{
var postData = new Dictionary<string, string>
{
{"_client_id", $"wappc_{Rand.NextLong(1000000000000, 9999999999999)}_{Rand.Next(100, 999)}"},
{"_client_type", "2"},
{"_client_version", clientVersion}
}.Concat(postParam).ToList();
var sign = postData.Aggregate("", (acc, pair) =>
{
acc += pair.Key + '=' + pair.Value;
return acc;
}) + "tiebaclient!!!";
var signMd5 = BitConverter.ToString(MD5.HashData(Encoding.UTF8.GetBytes(sign))).Replace("-", "");
postData.Add(KeyValuePair.Create("sign", signMd5));
return Post(http => http.PostAsync(url, new FormUrlEncodedContent(postData), stoppingToken),
() => _logger.LogTrace("POST {} {}", url, postParam));
}
private Task<HttpResponseMessage> PostProtoBuf<TRequest>(
string url, string clientVersion,
TRequest requestParam, Action<TRequest, Common> commonParamSetter,
CancellationToken stoppingToken = default
)
where TRequest : IMessage<TRequest>
{
// https://github.com/Starry-OvO/aiotieba/issues/67#issuecomment-1376006123
// https://github.com/MoeNetwork/wmzz_post/blob/80aba25de46f5b2cb1a15aa2a69b527a7374ffa9/wmzz_post_setting.php#L64
commonParamSetter(requestParam, new() {ClientVersion = clientVersion, ClientType = 2});
// https://github.com/dotnet/runtime/issues/22996 http://test.greenbytes.de/tech/tc2231
var protoBufFile = new ByteArrayContent(requestParam.ToByteArray());
protoBufFile.Headers.Add("Content-Disposition", "form-data; name=\"data\"; filename=\"file\"");
var content = new MultipartFormDataContent {protoBufFile};
// https://stackoverflow.com/questions/30926645/httpcontent-boundary-double-quotes
var boundary = content.Headers.ContentType?.Parameters.First(header => header.Name == "boundary");
if (boundary != null) boundary.Value = boundary.Value?.Replace("\"", "");
var request = new HttpRequestMessage(HttpMethod.Post, url) {Content = content};
_ = request.Headers.UserAgent.TryParseAdd($"bdtb for Android {clientVersion}");
request.Headers.Add("x_bd_data_type", "protobuf");
request.Headers.Accept.ParseAdd("*/*");
request.Headers.Connection.Add("keep-alive");
return Post(http => http.SendAsync(request, stoppingToken),
() => _logger.LogTrace("POST {} {}", url, requestParam));
}
private Task<HttpResponseMessage> Post
(Func<HttpClient, Task<HttpResponseMessage>> responseTaskFactory, Action logTraceCallback)
{
var http = _httpFactory.CreateClient("tbClient");
_requesterTcs.Wait();
if (_config.GetValue("LogTrace", false)) logTraceCallback();
var ret = responseTaskFactory(http);
_ = ret.ContinueWith(task =>
{
if (task.IsCompletedSuccessfully && task.Result.IsSuccessStatusCode) _requesterTcs.Increase();
else _requesterTcs.Decrease();
}, TaskContinuationOptions.ExecuteSynchronously);
return ret;
}
}