-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathClienTransaction.cs
More file actions
320 lines (261 loc) · 11.4 KB
/
ClienTransaction.cs
File metadata and controls
320 lines (261 loc) · 11.4 KB
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
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
using System.Security.Cryptography;
using System.Text;
using System.Text.RegularExpressions;
using HtmlAgilityPack;
namespace XClientTransaction;
public class ClientTransaction
{
public static readonly int AdditionalRandomNumber = 3;
public static readonly string DefaultKeyword = "obfiowerehiring";
private static readonly Regex OnDemandFileRegex =
new(@"['""]ondemand\.s['""]:\s*['""]([\w]+)['""]", RegexOptions.Compiled);
private static readonly Regex IndicesRegex = new(@"\(\w\[(\d{1,2})\],\s*16\)", RegexOptions.Compiled);
private static HttpClient? _httpClient;
private readonly HtmlDocument _homePage;
private string? _animationKey;
private List<int>? _defaultKeyBytesIndices;
private int _defaultRowIndex;
private string? _key;
private List<byte>? _keyBytes;
private ClientTransaction(HtmlDocument homePage)
{
_homePage = homePage;
}
public static async Task<ClientTransaction> CreateAsync(HttpClient client)
{
_httpClient = client;
var page = await HandleXMigrationAsync();
var tx = new ClientTransaction(page);
await tx.InitAsync();
return tx;
}
private async Task InitAsync()
{
var (rowIndex, keyIndices) = await GetIndicesAsync();
_defaultRowIndex = rowIndex;
_defaultKeyBytesIndices = keyIndices;
_key = GetKey();
_keyBytes = GetKeyBytes(_key);
_animationKey = GetAnimationKey();
}
public static async Task<HtmlDocument> HandleXMigrationAsync()
{
if (_httpClient == null)
throw new InvalidOperationException("HttpClient is not initialized");
const string homeUrl = "https://x.com";
_httpClient.DefaultRequestHeaders.TryAddWithoutValidation("User-Agent",
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/133.0.0.0 Safari/537.36");
var resp = await _httpClient.GetAsync(homeUrl);
var html = await resp.Content.ReadAsStringAsync();
var doc = new HtmlDocument();
doc.LoadHtml(html);
// Migration regex
var migrationRegex = new Regex(@"(https?:\/\/(?:www\.)?(?:twitter|x)\.com(?:\/x)?\/migrate[\/?]tok=[A-Za-z0-9%\-_]+)", RegexOptions.Compiled);
// Check meta refresh tag
var metaRefresh = doc.DocumentNode.SelectSingleNode("//meta[@http-equiv='refresh']");
Match? migMatch = null;
if (metaRefresh != null)
{
var metaContent = metaRefresh.OuterHtml;
migMatch = migrationRegex.Match(metaContent);
}
migMatch ??= migrationRegex.Match(html);
if (migMatch.Success)
{
resp = await _httpClient.GetAsync(migMatch.Groups[1].Value);
html = await resp.Content.ReadAsStringAsync();
doc = new HtmlDocument();
doc.LoadHtml(html);
}
// Check for form-based migration
var form = doc.DocumentNode.SelectSingleNode("//form[@name='f']")
?? doc.DocumentNode.SelectSingleNode("//form[@action='https://x.com/x/migrate']");
if (form != null)
{
var actionUrl = form.GetAttributeValue("action", "https://x.com/x/migrate");
var method = form.GetAttributeValue("method", "POST").ToUpperInvariant();
var inputs = form.SelectNodes(".//input");
var data = new Dictionary<string, string>();
if (inputs != null)
{
foreach (var input in inputs)
{
var name = input.GetAttributeValue("name", null);
var val = input.GetAttributeValue("value", "");
if (!string.IsNullOrEmpty(name))
data[name] = val;
}
}
if (method == "GET")
{
var query = await new FormUrlEncodedContent(data).ReadAsStringAsync();
var urlWithParams = $"{actionUrl}?{query}";
resp = await _httpClient.GetAsync(urlWithParams);
}
else
{
var content = new FormUrlEncodedContent(data);
resp = await _httpClient.PostAsync(actionUrl, content);
}
html = await resp.Content.ReadAsStringAsync();
doc = new HtmlDocument();
doc.LoadHtml(html);
}
return doc;
}
private string GetHomePageHtml()
{
return _homePage.DocumentNode.OuterHtml;
}
public async Task<(int, List<int>)> GetIndicesAsync()
{
if (_httpClient == null)
throw new InvalidOperationException("HttpClient is not initialized");
var html = GetHomePageHtml();
var match = OnDemandFileRegex.Match(html);
if (!match.Success || string.IsNullOrEmpty(match.Groups[1].Value))
throw new InvalidOperationException("Couldn't get on-demand file hash");
var hash = match.Groups[1].Value;
var url = $"https://abs.twimg.com/responsive-web/client-web/ondemand.s.{hash}a.js";
var resp = await _httpClient.GetAsync(url);
resp.EnsureSuccessStatusCode();
var text = await resp.Content.ReadAsStringAsync();
var indices = new List<int>();
var matches = IndicesRegex.Matches(text);
foreach (Match m in matches)
indices.Add(int.Parse(m.Groups[1].Value));
if (indices.Count < 2)
throw new InvalidOperationException("Couldn't get KEY_BYTE indices");
return (indices[0], indices.GetRange(1, indices.Count - 1));
}
private string GetKey()
{
var meta = _homePage.DocumentNode.SelectSingleNode("//meta[@name='twitter-site-verification']");
if (meta == null)
throw new InvalidOperationException("Couldn't get key from the page source");
var content = meta.GetAttributeValue("content", "");
if (string.IsNullOrEmpty(content))
throw new InvalidOperationException("Couldn't get key from the page source");
return content;
}
private static List<byte> GetKeyBytes(string key)
{
return Convert.FromBase64String(key).ToList();
}
private List<HtmlNode> GetFrames()
{
return _homePage.DocumentNode
.SelectNodes("//*[starts-with(@id, 'loading-x-anim')]")
?.ToList()
?? new List<HtmlNode>();
}
private List<List<int>> Get2dArray()
{
if (_keyBytes == null)
throw new InvalidOperationException("Key bytes not initialized");
var frames = GetFrames();
var idx = _keyBytes[5] % 4;
if (idx < 0 || idx >= frames.Count)
throw new ArgumentOutOfRangeException(nameof(idx), $"Frame index {idx} is out of range.");
var el = frames[idx];
var g = el.ChildNodes
.Where(n => n.NodeType == HtmlNodeType.Element)
.FirstOrDefault();
if (g == null)
throw new InvalidOperationException("Couldn't find <g> element as a child.");
// Get the second child of <g>
var gChildren = g.ChildNodes
.Where(n => n.NodeType == HtmlNodeType.Element)
.ToList();
if (gChildren.Count < 2)
throw new InvalidOperationException("Not enough children in <g> element.");
var pathEl = gChildren[1]; // Second element
var d = pathEl.GetAttributeValue("d", null);
if (string.IsNullOrEmpty(d))
throw new InvalidOperationException("Couldn't find path 'd' attribute");
var rest = d[9..]; // Substring(9)
var segments = rest.Split('C');
var result = new List<List<int>>();
foreach (var item in segments)
{
var numberStrings = Regex.Replace(item, @"[^\d]+", " ")
.Trim()
.Split(new[] { ' ' }, StringSplitOptions.RemoveEmptyEntries);
var numbers = numberStrings.Select(int.Parse).ToList();
result.Add(numbers);
}
return result;
}
private static double Solve(double value, double minVal, double maxVal, bool rounding)
{
var res = value * (maxVal - minVal) / 255 + minVal;
return rounding ? Math.Floor(res) : Math.Round(res, 2);
}
private static string Animate(List<int> frames, double targetTime)
{
var fromColor = frames.GetRange(0, 3).Select(v => (double)v).Concat(new double[] { 1 }).ToArray();
var toColor = frames.GetRange(3, 3).Select(v => (double)v).Concat(new double[] { 1 }).ToArray();
var fromRot = new double[] { 0 };
var toRot = new[] { Solve(frames[6], 60, 360, true) };
var curves = frames.Skip(7).Select((v, i) => Solve(v, Utils.IsOdd(i), 1, false)).ToArray();
var cubic = new Cubic(curves);
var f = cubic.GetValue(targetTime);
var color = Utils.Interpolate(fromColor, toColor, f).Select(v => Math.Max(0, Math.Min(255, v))).ToArray();
var rot = Utils.Interpolate(fromRot, toRot, f);
var matrix = Utils.ConvertRotationToMatrix(rot[0]);
var hexArr = new List<string>();
foreach (var v in color.Take(color.Length - 1))
hexArr.Add(((int)Math.Round(v)).ToString("x"));
foreach (var val in matrix)
{
var rv = Math.Abs(Math.Round(val, 2));
var hx = Utils.FloatToHex(rv);
if (hx.StartsWith('.'))
hexArr.Add(("0" + hx).ToLower());
else if (!string.IsNullOrEmpty(hx))
hexArr.Add(hx.ToLower());
else
hexArr.Add("0");
}
hexArr.Add("0");
hexArr.Add("0");
return string.Join("", hexArr).Replace(".", "").Replace("-", "");
}
private string GetAnimationKey()
{
if (_keyBytes == null || _defaultKeyBytesIndices == null)
throw new InvalidOperationException("Key bytes or indices not initialized");
const int total = 4096;
var rowIndex = _keyBytes[_defaultRowIndex] % 16;
double frameTime = _defaultKeyBytesIndices.Select(i => _keyBytes[i] % 16).Aggregate(1, (a, b) => a * b);
frameTime = Math.Round(frameTime / 10) * 10;
var grid = Get2dArray();
var row = grid[rowIndex];
var t = frameTime / total;
return Animate(row, t);
}
public string GenerateTransactionId(string method, string path, int? timeNow = null)
{
if (_keyBytes == null || _animationKey == null)
throw new InvalidOperationException("Client transaction not properly initialized");
var now = timeNow ?? (int)(DateTimeOffset.UtcNow.ToUnixTimeSeconds() - 1682924400);
var timeBytes = new byte[4];
for (var i = 0; i < 4; i++)
timeBytes[i] = (byte)((now >> (i * 8)) & 0xff);
var hashInput = $"{method}!{path}!{now}{DefaultKeyword}{_animationKey}";
byte[] hashBytes;
using (var sha = SHA256.Create())
{
hashBytes = sha.ComputeHash(Encoding.UTF8.GetBytes(hashInput));
}
var rnd = Random.Shared.Next(0, 256);
var arr = new List<byte>();
arr.AddRange(_keyBytes);
arr.AddRange(timeBytes);
arr.AddRange(hashBytes.Take(16));
arr.Add((byte)AdditionalRandomNumber);
var obfuscated = new List<byte> { (byte)rnd };
obfuscated.AddRange(arr.Select(x => (byte)(x ^ rnd)));
return Convert.ToBase64String(obfuscated.ToArray()).TrimEnd('=');
}
}