oz-request 1.0.0 → 1.0.1

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/README.md ADDED
@@ -0,0 +1,24 @@
1
+ # 转发 ozon 接口 o-request
2
+
3
+ ## 项目说明,跟目录有 index.js 和 index.mjs 只是为 npm 包能兼容 require 和 import 引入方式。代码是功能是一样的。
4
+
5
+ ## 使用步骤
6
+
7
+ 1、安装包:`npm install o-request`
8
+
9
+ 2、引入使用
10
+
11
+ ```
12
+ const createOzonClient = require('o-request');
13
+
14
+ const client = createOzonClient({
15
+ proxyList: ['http://user:pass@ip:port'] // 必填,代理ip
16
+ });
17
+
18
+ const result = await client({
19
+ url: 'https://seller.ozon.ru/app/api/...', // ozon的接口(必填)
20
+ reqParmas: { ... }, // 接口入参(必填)
21
+ originalCookie: '...', // ozon后台的cookie(必填)
22
+ headers: { 'X-Custom': 'value' } // 可自定义请求ozon接口时,请求头字段(非必填)
23
+ });
24
+ ```
package/index.mjs ADDED
@@ -0,0 +1,371 @@
1
+ // index.mjs
2
+ import http2 from 'http2';
3
+ import tls from 'tls';
4
+ import { CookieJar } from 'tough-cookie';
5
+ import { URL } from 'url';
6
+ import crypto from 'crypto';
7
+ import tough from 'tough-cookie';
8
+ import { HttpsProxyAgent } from 'https-proxy-agent';
9
+ import dns from 'dns';
10
+
11
+ // 全局环境设置(仅作用于当前模块)
12
+ process.env.NODE_TLS_REJECT_UNAUTHORIZED = '0';
13
+ dns.setDefaultResultOrder('ipv4first');
14
+
15
+ // ---------- 工具函数 ----------
16
+ const getRandomNum = (min, max) => {
17
+ min = Math.ceil(min);
18
+ max = Math.floor(max);
19
+ return Math.floor(Math.random() * (max - min + 1)) + min;
20
+ };
21
+
22
+ const stopFn = (s = 1) => {
23
+ return new Promise((resolve) => {
24
+ setTimeout(resolve, 1000 * s);
25
+ });
26
+ };
27
+
28
+ function getWindowsChromeUA() {
29
+ const versions = {
30
+ chrome: Math.floor(Math.random() * 20) + 130,
31
+ windows: ['10.0', '11.0'][Math.floor(Math.random() * 2)]
32
+ };
33
+ return `Mozilla/5.0 (Windows NT ${versions.windows}; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/${versions.chrome}.0.0.0 Safari/537.36`;
34
+ }
35
+
36
+ // ---------- Cookie 会话管理器 ----------
37
+ const pendingCreates = new Map();
38
+
39
+ async function getOrCreateJar(cookieStr) {
40
+ const key = cookieStr;
41
+ if (pendingCreates.has(key)) {
42
+ return pendingCreates.get(key);
43
+ }
44
+ const createPromise = (async () => {
45
+ const jar = new CookieJar();
46
+ await initSession(jar, cookieStr);
47
+ pendingCreates.delete(key);
48
+ return jar;
49
+ })();
50
+ pendingCreates.set(key, createPromise);
51
+ return createPromise;
52
+ }
53
+
54
+ async function initSession(jar, initialCookies) {
55
+ initialCookies = initialCookies.replaceAll('__Secure', 'mySecure');
56
+ for (const cookieStr of initialCookies.split('; ')) {
57
+ const cookie = tough.Cookie.parse(cookieStr);
58
+ if (cookie) {
59
+ await new Promise(resolve =>
60
+ jar.setCookie(cookie, 'https://seller.ozon.ru', resolve)
61
+ );
62
+ }
63
+ }
64
+ }
65
+
66
+ async function serializeCookies(jar) {
67
+ let cookies = await new Promise(resolve =>
68
+ jar.getCookies('https://seller.ozon.ru', (err, cookies) => {
69
+ if (err) return resolve([]);
70
+ const cookieMap = new Map();
71
+ cookies.forEach(cookie => {
72
+ cookieMap.set(cookie.key, cookie.cookieString());
73
+ });
74
+ resolve(Array.from(cookieMap.values()).join('; '));
75
+ })
76
+ );
77
+ cookies = cookies.replaceAll('mySecure', '__Secure');
78
+ return cookies;
79
+ }
80
+
81
+ // ---------- 工厂函数 ----------
82
+ export default function createOzonClient(options) {
83
+ const { proxyList, tlsFingerprint = null } = options;
84
+
85
+ if (!proxyList || !Array.isArray(proxyList) || proxyList.length === 0) {
86
+ throw new Error('proxyList must be a non-empty array of proxy URLs');
87
+ }
88
+
89
+ const DEFAULT_FINGERPRINT = {
90
+ cipherSuites: [
91
+ 'TLS_AES_128_GCM_SHA256',
92
+ 'TLS_AES_256_GCM_SHA384',
93
+ 'TLS_CHACHA20_POLY1305_SHA256',
94
+ 'ECDHE-ECDSA-AES128-GCM-SHA256',
95
+ 'ECDHE-RSA-AES128-GCM-SHA256',
96
+ 'ECDHE-ECDSA-AES256-GCM-SHA384',
97
+ 'ECDHE-RSA-AES256-GCM-SHA384',
98
+ 'ECDHE-ECDSA-CHACHA20-POLY1305',
99
+ 'ECDHE-RSA-CHACHA20-POLY1305',
100
+ 'ECDHE-RSA-AES128-SHA',
101
+ 'ECDHE-RSA-AES256-SHA',
102
+ 'AES128-GCM-SHA256',
103
+ 'AES256-GCM-SHA384',
104
+ 'AES128-SHA',
105
+ 'AES256-SHA'
106
+ ].join(':'),
107
+ ellipticCurves: ['X25519', 'secp256r1', 'secp384r1'],
108
+ signatureAlgorithms: [
109
+ 'ecdsa_secp256r1_sha256',
110
+ 'rsa_pss_rsae_sha256',
111
+ 'rsa_pkcs1_sha256',
112
+ 'ecdsa_secp384r1_sha384',
113
+ 'rsa_pss_rsae_sha384',
114
+ 'rsa_pkcs1_sha384',
115
+ 'rsa_pss_rsae_sha512',
116
+ 'rsa_pkcs1_sha512'
117
+ ]
118
+ };
119
+
120
+ const activeFingerprint = tlsFingerprint || DEFAULT_FINGERPRINT;
121
+
122
+ function createBrowserSocket(hostname) {
123
+ return tls.connect({
124
+ host: hostname,
125
+ port: 443,
126
+ ciphers: activeFingerprint.cipherSuites,
127
+ ALPNProtocols: ['h2'],
128
+ servername: hostname,
129
+ ecdhCurve: activeFingerprint.ellipticCurves.join(':'),
130
+ signatureAlgorithms: activeFingerprint.signatureAlgorithms.join(':'),
131
+ minVersion: 'TLSv1.2',
132
+ maxVersion: 'TLSv1.3'
133
+ });
134
+ }
135
+
136
+ async function cloudRequest({ url, data, count307, jar, customHeaders = {} }) {
137
+ const parsedUrl = new URL(url);
138
+ const hostname = parsedUrl.hostname;
139
+
140
+ try {
141
+ const socket = createBrowserSocket(hostname);
142
+ await new Promise((resolve, reject) => {
143
+ socket.once('secureConnect', resolve);
144
+ socket.once('error', reject);
145
+ });
146
+
147
+ const ipIndex = getRandomNum(0, proxyList.length - 1);
148
+ const proxyUrl = proxyList[ipIndex];
149
+ const agent = new HttpsProxyAgent(proxyUrl, {
150
+ rejectUnauthorized: false,
151
+ keepAlive: true,
152
+ lookup: (hostname, options, callback) => {
153
+ dns.resolve4(hostname, (err, addresses) => {
154
+ if (err) return callback(err);
155
+ callback(null, addresses[0], 4);
156
+ });
157
+ }
158
+ });
159
+
160
+ const client = http2.connect(`https://${hostname}`, {
161
+ createConnection: () => socket,
162
+ settings: {
163
+ headerTableSize: 65536,
164
+ enablePush: false,
165
+ initialWindowSize: 6291456,
166
+ maxFrameSize: 16384,
167
+ maxHeaderListSize: 262144
168
+ },
169
+ agent,
170
+ protocol: 'h2'
171
+ });
172
+
173
+ client.on('error', err => {
174
+ console.error('HTTP/2连接错误:', err);
175
+ client.destroy();
176
+ throw new Error('HTTP/2连接错误');
177
+ });
178
+
179
+ const cookiesStr = await serializeCookies(jar);
180
+
181
+ let sellerIdMatch =
182
+ cookiesStr.match(/sc_company_id=(\d+)/) ||
183
+ cookiesStr.match(/contentId=(\d+)/);
184
+ let sellerId = sellerIdMatch ? sellerIdMatch[1] : '';
185
+
186
+ const baseHeaders = {
187
+ ':method': 'POST',
188
+ ':path': parsedUrl.pathname + parsedUrl.search,
189
+ ':scheme': 'https',
190
+ ':authority': parsedUrl.hostname,
191
+ accept: 'application/json, text/plain, */*',
192
+ 'accept-language': 'ru-RU,ru;q=0.9,en-US;q=0.8,en;q=0.7',
193
+ 'content-type': 'application/json',
194
+ cookie: cookiesStr,
195
+ origin: 'https://seller.ozon.ru',
196
+ 'sec-ch-ua': '"Google Chrome";v="117", "Not;A=Brand";v="8", "Chromium";v="117"',
197
+ 'sec-ch-ua-mobile': '?0',
198
+ 'sec-ch-ua-platform': '"Windows"',
199
+ 'sec-fetch-dest': 'empty',
200
+ 'sec-fetch-mode': 'cors',
201
+ 'sec-fetch-site': 'same-origin',
202
+ Referer: 'https://seller.ozon.ru/app/products/create',
203
+ 'X-O3-App-Name': 'seller-ui',
204
+ 'X-O3-Company-Id': sellerId + '',
205
+ 'X-O3-Language': 'zh-Hans',
206
+ 'X-O3-Page-Type': 'products-other',
207
+ Origin: 'https://seller.ozon.ru',
208
+ 'x-forwarded-for': proxyUrl.split('@')[1].split(':')[0],
209
+ 'x-real-ip': proxyUrl.split('@')[1].split(':')[0],
210
+ via: '1.1 proxy-server'
211
+ };
212
+
213
+ const headers = { ...baseHeaders, ...customHeaders };
214
+ headers['x-b3-traceid'] = crypto.randomBytes(8).toString('hex');
215
+ headers['x-b3-spanid'] = crypto.randomBytes(4).toString('hex');
216
+
217
+ return new Promise((resolve, reject) => {
218
+ let responseData = Buffer.alloc(0);
219
+ let responseHeaders = {};
220
+
221
+ const req = client.request(headers);
222
+ const timeout = setTimeout(() => {
223
+ req.close();
224
+ client.close();
225
+ reject(new Error('请求超时'));
226
+ }, 30000);
227
+
228
+ req.on('response', headers => {
229
+ responseHeaders = headers;
230
+ if (headers['set-cookie']) {
231
+ const setCookies = Array.isArray(headers['set-cookie'])
232
+ ? headers['set-cookie']
233
+ : [headers['set-cookie']];
234
+ setCookies.forEach(cookie => {
235
+ const storedCookie = cookie.replace('__Secure', 'mySecure');
236
+ jar.setCookieSync(storedCookie, 'https://seller.ozon.ru');
237
+ });
238
+ }
239
+ });
240
+
241
+ req.on('data', chunk => {
242
+ responseData = Buffer.concat([responseData, chunk]);
243
+ });
244
+
245
+ req.on('end', async () => {
246
+ clearTimeout(timeout);
247
+ client.close();
248
+
249
+ if (responseHeaders[':status'] === 307) {
250
+ const redirectUrl = responseHeaders.location;
251
+ console.log('进入307');
252
+ count307++;
253
+ if (count307 >= 5) {
254
+ console.log('307上限,结束');
255
+ resolve('change_cookie');
256
+ } else {
257
+ resolve(
258
+ cloudRequest({
259
+ url: redirectUrl,
260
+ data,
261
+ count307,
262
+ jar,
263
+ customHeaders
264
+ })
265
+ );
266
+ }
267
+ } else {
268
+ if (responseHeaders['content-type']?.includes('text/html')) {
269
+ return resolve({ error_type: 'abt_data' });
270
+ }
271
+ try {
272
+ const res = JSON.parse(responseData.toString());
273
+ if (!res.items) {
274
+ console.log('业务错误,重试!');
275
+ resolve('change_cookie');
276
+ return;
277
+ }
278
+ const newCookie = await serializeCookies(jar);
279
+ res.newCookie = newCookie;
280
+ resolve(res);
281
+ } catch (e) {
282
+ console.log(e);
283
+ console.log('请求错误,成功捕获,进行重试');
284
+ resolve('change_cookie');
285
+ }
286
+ }
287
+ });
288
+
289
+ req.on('error', err => {
290
+ clearTimeout(timeout);
291
+ client.close();
292
+ console.error('请求错误:', err);
293
+ reject(err);
294
+ });
295
+
296
+ req.write(JSON.stringify(data));
297
+ req.end();
298
+ });
299
+ } catch (error) {
300
+ console.error('请求过程中发生错误:', error);
301
+ if (
302
+ error.message.includes('ECONNRESET') ||
303
+ error.message.includes('HTTP/2连接错误')
304
+ ) {
305
+ return 'change_cookie';
306
+ }
307
+ throw error;
308
+ }
309
+ }
310
+
311
+ async function fetchOzonDataCore({ url, reqParmas, originalCookie, headers = {} }) {
312
+ return new Promise(resolve => {
313
+ let tryTimes = 0;
314
+ const core = async () => {
315
+ let response;
316
+ try {
317
+ const jar = await getOrCreateJar(originalCookie);
318
+ response = await cloudRequest({
319
+ url,
320
+ data: reqParmas,
321
+ count307: 0,
322
+ jar,
323
+ customHeaders: headers
324
+ });
325
+ if (response.error_type == 'abt_data') {
326
+ console.log('机器人拦截');
327
+ tryTimes++;
328
+ if (tryTimes >= 5) {
329
+ resolve({ fail: true, err_msg: '机器人拦截' });
330
+ } else {
331
+ await stopFn(0.2);
332
+ core();
333
+ }
334
+ } else if (response == 'change_cookie') {
335
+ tryTimes++;
336
+ if (tryTimes >= 5) {
337
+ resolve({ fail: true, err_msg: '请切换cookie' });
338
+ return;
339
+ }
340
+ await stopFn(0.2);
341
+ core();
342
+ } else if (!response || response?.error) {
343
+ tryTimes++;
344
+ if (tryTimes >= 5) {
345
+ resolve({ fail: true, err_msg: '请切换cookie' });
346
+ return;
347
+ }
348
+ console.log('正常是cookie失效,重新获取新的');
349
+ await stopFn(0.2);
350
+ core();
351
+ } else {
352
+ resolve(response);
353
+ }
354
+ } catch (error) {
355
+ console.log('错误2: 重试');
356
+ console.log(error);
357
+ tryTimes++;
358
+ if (tryTimes >= 5) {
359
+ resolve({ fail: true, err_msg: '请求发生错误' });
360
+ return;
361
+ }
362
+ await stopFn(0.2);
363
+ core();
364
+ }
365
+ };
366
+ core();
367
+ });
368
+ }
369
+
370
+ return fetchOzonDataCore;
371
+ }
package/package.json CHANGED
@@ -1,9 +1,16 @@
1
1
  {
2
2
  "name": "oz-request",
3
- "version": "1.0.0",
3
+ "version": "1.0.1",
4
4
  "description": "oz-request",
5
5
  "main": "index.js",
6
- "keywords": [],
6
+ "module": "index.mjs",
7
+ "exports": {
8
+ "require": "./index.js",
9
+ "import": "./index.mjs"
10
+ },
11
+ "type": "commonjs",
12
+ "scripts": {},
13
+ "keywords": ["ozon", "http2", "tls", "fingerprint", "proxy"],
7
14
  "author": "Your Name",
8
15
  "license": "MIT",
9
16
  "dependencies": {
package/src/utils.js DELETED
@@ -1,160 +0,0 @@
1
- // 获取随机数(min 到 max之间的随机数,包含边界值)
2
- const getRandomNum = (min, max) => {
3
- min = Math.ceil(min);
4
- max = Math.floor(max);
5
- return Math.floor(Math.random() * (max - min + 1)) + min;
6
- };
7
-
8
- // 暂停函数
9
- const stopFn = (s = 1) => {
10
- return new Promise((resolve, reject) => {
11
- setTimeout(() => {
12
- resolve();
13
- }, 1000 * s);
14
- });
15
- };
16
-
17
- // 数组根据key,进行去重
18
- const arrObjUni = (array, key) => {
19
- const _set = [...new Set(array.map(e => e[key]))];
20
- let deArray = [];
21
- _set.map(item => {
22
- deArray.push(array[array.findIndex(val => val[key] === item)]);
23
- });
24
- return deArray;
25
- };
26
-
27
- //处理属性,优先取外部的值,然后封装为老数据格式
28
- const transformAttributes = (
29
- newAttrs,
30
- baseItem,
31
- { length, width, height, weight, barcode }
32
- ) => {
33
- const attrs = newAttrs.map(attr => {
34
- const { attribute_id, values = [] } = attr;
35
-
36
- let value = '';
37
- let collection = [];
38
- const dictionary_value_id = values[0]?.dictionary_value_id || '';
39
-
40
- // 多值 → collection
41
- if (
42
- values.length > 1 ||
43
- (dictionary_value_id !== '0' &&
44
- attribute_id !== '8229' &&
45
- attribute_id !== '85')
46
- ) {
47
- //8229特殊属性,不确定还有其他的没有
48
- collection = values.map(v => v.value).filter(Boolean);
49
- } else {
50
- // 单值 → value
51
- const v = values[0];
52
- const val = v.value || '';
53
- value = val;
54
- }
55
-
56
- return {
57
- key: attribute_id,
58
- value,
59
- collection,
60
- complex: [],
61
- complex_collection: []
62
- };
63
- });
64
-
65
- //变化完后去把内部值替换成外部的,没有就新建
66
- const outerMap = {
67
- 4180: {
68
- value: baseItem?.variant_name || '',
69
- type: 'value'
70
- },
71
- 8229: {
72
- value: baseItem?.description_type_name || '',
73
- type: 'value'
74
- },
75
- 4194: {
76
- value: baseItem?.main_image || '',
77
- type: 'value'
78
- },
79
- 4195: {
80
- value: baseItem?.secondary_images || [],
81
- type: 'arr'
82
- },
83
- 9024: {
84
- value: barcode || '',
85
- type: 'value'
86
- }
87
- };
88
-
89
- if (baseItem?.brand_name) {
90
- outerMap['85'] = {
91
- value: baseItem?.brand_name || '',
92
- type: 'value'
93
- };
94
- outerMap['31'] = {
95
- value: baseItem?.brand_name || '',
96
- type: 'value'
97
- };
98
- }
99
-
100
- //不知道会不会外部没有,里面有,保险起见!
101
- if (length && width && height && weight) {
102
- outerMap['9454'] = {
103
- value: length + '',
104
- type: 'value'
105
- };
106
- outerMap['9455'] = {
107
- value: width + '',
108
- type: 'value'
109
- };
110
- outerMap['9456'] = {
111
- value: height + '',
112
- type: 'value'
113
- };
114
- outerMap['4497'] = {
115
- value: weight + '',
116
- type: 'value'
117
- };
118
- }
119
- // 遍历attrs,根据key_id,替换value
120
- attrs.forEach(item => {
121
- const { key } = item;
122
- if (outerMap[key] && outerMap[key].type === 'value') {
123
- item.value = outerMap[key].value;
124
- outerMap[key].isReplace = true;
125
- } else if (outerMap[key] && outerMap[key].type === 'arr') {
126
- item.collection = outerMap[key].value;
127
- outerMap[key].isReplace = true;
128
- }
129
- });
130
-
131
- //对没没有替换的属性,添加到attrs中
132
- for (const [key, value] of Object.entries(outerMap)) {
133
- if (!value.isReplace) {
134
- const obj = {
135
- key: key,
136
- complex: [],
137
- complex_collection: []
138
- };
139
- value.type === 'value'
140
- ? (obj.value = value.value)
141
- : (obj.collection = value.value);
142
- attrs.push(obj);
143
- }
144
- }
145
- return attrs;
146
- };
147
-
148
- const getCompanyIdByCookie = cookie => {
149
- const match = cookie?.match(/sc_company_id=([^;]+)/);
150
- const companyId = match ? match[1] : null;
151
- return companyId;
152
- };
153
-
154
- export {
155
- stopFn,
156
- arrObjUni,
157
- getRandomNum,
158
- transformAttributes,
159
- getCompanyIdByCookie
160
- };
File without changes