@mattywhite/skyscanner-api 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +22 -0
- package/README.md +407 -0
- package/package.json +33 -0
- package/src/config.js +41 -0
- package/src/devicedata.json +123 -0
- package/src/errors.js +36 -0
- package/src/index.js +20 -0
- package/src/px.js +286 -0
- package/src/skyscanner.js +634 -0
- package/src/types.js +78 -0
package/src/px.js
ADDED
|
@@ -0,0 +1,286 @@
|
|
|
1
|
+
const crypto = require('crypto');
|
|
2
|
+
const axios = require('axios');
|
|
3
|
+
const config = require('./config');
|
|
4
|
+
const { PerimeterXError } = require('./errors');
|
|
5
|
+
const fs = require('fs');
|
|
6
|
+
|
|
7
|
+
class ParseAppc {
|
|
8
|
+
constructor(appc) {
|
|
9
|
+
if (appc.length < 10) {
|
|
10
|
+
throw new PerimeterXError("Cannot parse AppC challenge, must be at least 10");
|
|
11
|
+
}
|
|
12
|
+
this.timestamp = parseInt(appc[2]);
|
|
13
|
+
this.hash = appc[3];
|
|
14
|
+
this.f24f = parseInt(appc[4]);
|
|
15
|
+
this.f25g = parseInt(appc[5]);
|
|
16
|
+
this.f21c = parseInt(appc[6]);
|
|
17
|
+
this.f22d = parseInt(appc[7]);
|
|
18
|
+
this.f23e = parseInt(appc[8]);
|
|
19
|
+
this.f26h = parseInt(appc[9]);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
static a(i10, i11, i12, i13) {
|
|
23
|
+
const i14 = i13 % 10;
|
|
24
|
+
const i15 = i14 !== 0 ? i12 % i14 : i12 % 10;
|
|
25
|
+
const i16 = i10 * i10;
|
|
26
|
+
const i17 = i11 * i11;
|
|
27
|
+
|
|
28
|
+
switch (i15) {
|
|
29
|
+
case 0: return i16 + i11;
|
|
30
|
+
case 1: return i10 + i17;
|
|
31
|
+
case 2: return i16 * i11;
|
|
32
|
+
case 3: return i10 ^ i11;
|
|
33
|
+
case 4: return i10 - i17;
|
|
34
|
+
case 5:
|
|
35
|
+
const i18 = i10 + 783;
|
|
36
|
+
return (i18 * i18) + i17;
|
|
37
|
+
case 6: return (i10 ^ i11) + i11;
|
|
38
|
+
case 7: return i16 - i17;
|
|
39
|
+
case 8: return i10 * i11;
|
|
40
|
+
case 9: return (i11 * i10) - i10;
|
|
41
|
+
default: return -1;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
encode(string1) {
|
|
46
|
+
const a10 = ParseAppc.a(
|
|
47
|
+
ParseAppc.a(this.f21c, this.f22d, this.f24f, this.f26h),
|
|
48
|
+
this.f23e,
|
|
49
|
+
this.f25g,
|
|
50
|
+
this.f26h
|
|
51
|
+
);
|
|
52
|
+
|
|
53
|
+
let intValue = 0;
|
|
54
|
+
try {
|
|
55
|
+
const buffer = Buffer.from(string1, 'utf-8');
|
|
56
|
+
if (buffer.length >= 4) {
|
|
57
|
+
intValue = buffer.readInt32BE(0);
|
|
58
|
+
}
|
|
59
|
+
} catch (e) {
|
|
60
|
+
intValue = 0;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
return intValue ^ a10;
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
class PXSolver {
|
|
68
|
+
constructor(proxy = '', verify = true) {
|
|
69
|
+
this.proxy = proxy;
|
|
70
|
+
this.verify = verify;
|
|
71
|
+
this.deviceData = null;
|
|
72
|
+
|
|
73
|
+
this.headers = {
|
|
74
|
+
'Host': 'collector-pxrf8vapwa.perimeterx.net',
|
|
75
|
+
'User-Agent': 'PerimeterX Android SDK/3.4.4',
|
|
76
|
+
'Accept-Charset': 'UTF-8',
|
|
77
|
+
'Accept': '*/*',
|
|
78
|
+
'Content-Type': 'application/x-www-form-urlencoded; charset=utf-8',
|
|
79
|
+
'Connection': 'keep-alive',
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
getDeviceData() {
|
|
84
|
+
if (this.deviceData) {
|
|
85
|
+
return this.deviceData;
|
|
86
|
+
}
|
|
87
|
+
const data = fs.readFileSync(config.PX_DEVICE_DATA_PATH, 'utf-8');
|
|
88
|
+
this.deviceData = JSON.parse(data);
|
|
89
|
+
return this.deviceData;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
getFingerprint() {
|
|
93
|
+
const devices = this.getDeviceData();
|
|
94
|
+
return devices[Math.floor(Math.random() * devices.length)];
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
batteryPercentageToVoltage(percentage) {
|
|
98
|
+
if (percentage < 0 || percentage > 100) {
|
|
99
|
+
throw new Error("Percentage must be between 0 and 100.");
|
|
100
|
+
}
|
|
101
|
+
let voltage;
|
|
102
|
+
if (percentage <= 10) {
|
|
103
|
+
voltage = 3.0 + (percentage / 10) * 0.3;
|
|
104
|
+
} else if (percentage <= 70) {
|
|
105
|
+
voltage = 3.3 + ((percentage - 10) / 60) * 0.6;
|
|
106
|
+
} else {
|
|
107
|
+
voltage = 3.9 + ((percentage - 70) / 30) * 0.3;
|
|
108
|
+
}
|
|
109
|
+
return Math.round(voltage * 100) / 100;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
randomHex(length) {
|
|
113
|
+
return crypto.randomBytes(length).toString('hex');
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
generateUUID() {
|
|
117
|
+
return crypto.randomUUID();
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
async genPxAuthorization() {
|
|
121
|
+
const fingerprint = this.getFingerprint();
|
|
122
|
+
const [auth, uuid] = await this.genPx(fingerprint);
|
|
123
|
+
return [auth, uuid];
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
async genPx(fingerprint) {
|
|
127
|
+
const PX326 = this.generateUUID();
|
|
128
|
+
const PX327 = PX326.split("-")[0].toUpperCase();
|
|
129
|
+
const batteryPercentage = Math.floor(Math.random() * (90 - 15 + 1)) + 15;
|
|
130
|
+
const connType = Math.random() > 0.5 ? "WiFi" : "Mobile";
|
|
131
|
+
const batteryStatus = ["charging", "discharging", "not charging"][Math.floor(Math.random() * 3)];
|
|
132
|
+
|
|
133
|
+
const fingerprintData = [
|
|
134
|
+
{
|
|
135
|
+
"t": "PX315",
|
|
136
|
+
"d": {
|
|
137
|
+
"PX330": "new_session",
|
|
138
|
+
"PX1214": this.randomHex(8),
|
|
139
|
+
"PX91": fingerprint.height,
|
|
140
|
+
"PX92": fingerprint.width,
|
|
141
|
+
"PX21215": Math.floor(Math.random() * (255 - 150 + 1)) + 150,
|
|
142
|
+
"PX316": true,
|
|
143
|
+
"PX318": String(fingerprint.sdk_int),
|
|
144
|
+
"PX319": fingerprint.os_version,
|
|
145
|
+
"PX320": fingerprint.model,
|
|
146
|
+
"PX339": fingerprint.brand,
|
|
147
|
+
"PX321": fingerprint.build_device,
|
|
148
|
+
"PX323": Math.floor(Date.now() / 1000),
|
|
149
|
+
"PX322": "Android",
|
|
150
|
+
"PX337": true,
|
|
151
|
+
"PX336": true,
|
|
152
|
+
"PX335": true,
|
|
153
|
+
"PX334": true,
|
|
154
|
+
"PX333": true,
|
|
155
|
+
"PX331": true,
|
|
156
|
+
"PX332": true,
|
|
157
|
+
"PX421": "false",
|
|
158
|
+
"PX442": "false",
|
|
159
|
+
"PX21218": "[]",
|
|
160
|
+
"PX21217": "[]",
|
|
161
|
+
"PX21224": "true",
|
|
162
|
+
"PX21221": "true",
|
|
163
|
+
"PX317": connType,
|
|
164
|
+
"PX344": "Android",
|
|
165
|
+
"PX347": '["en_US"]',
|
|
166
|
+
"PX343": "Unknown",
|
|
167
|
+
"PX415": batteryPercentage,
|
|
168
|
+
"PX413": "unknown",
|
|
169
|
+
"PX416": batteryStatus !== "charging" ? "" : (Math.random() > 0.5 ? "USB" : "Wireless"),
|
|
170
|
+
"PX414": batteryStatus,
|
|
171
|
+
"PX419": "",
|
|
172
|
+
"PX418": Math.round((Math.random() * (35.0 - 25.0) + 25.0) * 10) / 10,
|
|
173
|
+
"PX420": this.batteryPercentageToVoltage(batteryPercentage),
|
|
174
|
+
"PX340": "v3.4.4",
|
|
175
|
+
"PX342": "7.146",
|
|
176
|
+
"PX341": '"Skyscanner"',
|
|
177
|
+
"PX348": "net.skyscanner.android.main",
|
|
178
|
+
"PX1159": false,
|
|
179
|
+
"PX345": 0,
|
|
180
|
+
"PX351": 0,
|
|
181
|
+
"PX326": PX326,
|
|
182
|
+
"PX327": PX327,
|
|
183
|
+
"PX328": crypto.createHash('sha1').update(`${fingerprint.model}${PX326}${PX327}`).digest('hex').toUpperCase(),
|
|
184
|
+
"PX1208": "[]",
|
|
185
|
+
"PX21219": "{}",
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
];
|
|
189
|
+
|
|
190
|
+
const payload = Buffer.from(JSON.stringify(fingerprintData)).toString('base64');
|
|
191
|
+
const UUID = this.generateUUID();
|
|
192
|
+
const data = `payload=${payload}&uuid=${UUID}&appId=PXrf8vapwA&tag=mobile&ftag=22`;
|
|
193
|
+
|
|
194
|
+
const axiosConfig = {
|
|
195
|
+
headers: this.headers,
|
|
196
|
+
httpsAgent: this.proxy ? new (require('https-proxy-agent').HttpsProxyAgent)(this.proxy) : undefined
|
|
197
|
+
};
|
|
198
|
+
|
|
199
|
+
let firstReq;
|
|
200
|
+
try {
|
|
201
|
+
firstReq = await axios.post(
|
|
202
|
+
"https://collector-pxrf8vapwa.perimeterx.net/api/v1/collector/mobile",
|
|
203
|
+
data,
|
|
204
|
+
axiosConfig
|
|
205
|
+
);
|
|
206
|
+
} catch (error) {
|
|
207
|
+
throw new PerimeterXError(`Error while posting first payload: ${error.message}`);
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
if (firstReq.status !== 200) {
|
|
211
|
+
throw new PerimeterXError(`Error while posting first payload, code ${firstReq.status} message: ${firstReq.data}`);
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
const responseData = firstReq.data.do;
|
|
215
|
+
|
|
216
|
+
let vid = null;
|
|
217
|
+
let sid = null;
|
|
218
|
+
let appc = null;
|
|
219
|
+
|
|
220
|
+
for (const row of responseData) {
|
|
221
|
+
const args = row.split("|");
|
|
222
|
+
if (vid && sid && appc) break;
|
|
223
|
+
|
|
224
|
+
if (args[0] === "sid") {
|
|
225
|
+
sid = args[1];
|
|
226
|
+
continue;
|
|
227
|
+
}
|
|
228
|
+
if (args[0] === "vid") {
|
|
229
|
+
vid = args[1];
|
|
230
|
+
continue;
|
|
231
|
+
}
|
|
232
|
+
if (args[0] === "appc" && args.length >= 10) {
|
|
233
|
+
appc = args;
|
|
234
|
+
continue;
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
if (!vid || !sid || !appc) {
|
|
239
|
+
throw new PerimeterXError(`Cannot find vid, sid or appc. Data: ${responseData}`);
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
const encodedAppc = new ParseAppc(appc);
|
|
243
|
+
fingerprintData[0].t = 'PX329';
|
|
244
|
+
const d = fingerprintData[0].d;
|
|
245
|
+
|
|
246
|
+
delete d.PX1208;
|
|
247
|
+
delete d.PX21219;
|
|
248
|
+
|
|
249
|
+
d.PX259 = encodedAppc.timestamp;
|
|
250
|
+
d.PX256 = encodedAppc.hash;
|
|
251
|
+
d.PX257 = String(encodedAppc.encode(fingerprint.model));
|
|
252
|
+
|
|
253
|
+
d.PX1208 = '[]';
|
|
254
|
+
d.PX21219 = '{}';
|
|
255
|
+
|
|
256
|
+
const payload2 = Buffer.from(JSON.stringify(fingerprintData)).toString('base64');
|
|
257
|
+
const data2 = `payload=${payload2}&uuid=${UUID}&appId=PXrf8vapwA&tag=mobile&ftag=22&sid=${sid}&vid=${vid}`;
|
|
258
|
+
|
|
259
|
+
let secondReq;
|
|
260
|
+
try {
|
|
261
|
+
secondReq = await axios.post(
|
|
262
|
+
"https://collector-pxrf8vapwa.perimeterx.net/api/v1/collector/mobile",
|
|
263
|
+
data2,
|
|
264
|
+
axiosConfig
|
|
265
|
+
);
|
|
266
|
+
} catch (error) {
|
|
267
|
+
throw new PerimeterXError(`Error while posting second payload: ${error.message}`);
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
if (secondReq.status !== 200) {
|
|
271
|
+
throw new PerimeterXError(`Error while posting second payload, code ${secondReq.status} message: ${secondReq.data}`);
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
const finalData = secondReq.data.do;
|
|
275
|
+
|
|
276
|
+
if (finalData.length !== 1 || finalData[0].split("|")[0] !== "bake") {
|
|
277
|
+
throw new PerimeterXError(`Error parsing PX response: ${finalData}`);
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
const args = finalData[0].split("|");
|
|
281
|
+
|
|
282
|
+
return [`3:${args[3]}`, UUID];
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
module.exports = PXSolver;
|