urllib 2.37.3 → 3.0.0-alpha.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/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "urllib",
3
- "version": "2.37.3",
4
- "description": "Help in opening URLs (mostly HTTP) in a complex world — basic and digest authentication, redirections, cookies and more.",
3
+ "version": "3.0.0-alpha.0",
4
+ "description": "Help in opening URLs (mostly HTTP) in a complex world — basic and digest authentication, redirections, cookies and more. Base undici fetch API.",
5
5
  "keywords": [
6
6
  "urllib",
7
7
  "http",
@@ -9,81 +9,83 @@
9
9
  "curl",
10
10
  "wget",
11
11
  "request",
12
- "https"
12
+ "https",
13
+ "undici",
14
+ "fetch"
13
15
  ],
14
- "author": "fengmk2 <fengmk2@gmail.com> (https://fengmk2.com)",
16
+ "author": "fengmk2 <fengmk2@gmail.com> (https://github.com/fengmk2)",
15
17
  "homepage": "https://github.com/node-modules/urllib",
16
- "main": "lib/index.js",
17
- "types": "lib/index.d.ts",
18
+ "type": "module",
19
+ "exports": {
20
+ ".": {
21
+ "import": {
22
+ "types": "./src/esm/index.d.ts",
23
+ "default": "./src/esm/index.js"
24
+ },
25
+ "require": {
26
+ "types": "./src/cjs/index.d.ts",
27
+ "default": "./src/cjs/index.js"
28
+ }
29
+ }
30
+ },
31
+ "types": "./src/esm/index.d.ts",
32
+ "main": "./src/cjs/index.js",
18
33
  "files": [
19
- "lib"
34
+ "src"
20
35
  ],
21
36
  "repository": {
22
37
  "type": "git",
23
38
  "url": "git://github.com/node-modules/urllib.git"
24
39
  },
25
40
  "scripts": {
26
- "test-local": "mocha -t 30000 -r intelli-espower-loader test/*.test.js",
27
- "test": "npm run lint && npm run test-local",
28
- "test-cov": "istanbul cover node_modules/mocha/bin/_mocha -- -t 30000 -r intelli-espower-loader test/*.test.js",
29
- "ci": "npm run lint && npm run test-cov",
30
- "lint": "jshint .",
31
- "autod": "autod -w --prefix '^' -t test -e examples",
32
- "contributor": "git-contributor"
41
+ "lint": "eslint src --ext .ts",
42
+ "build": "npm run build:dist",
43
+ "build:cjs": "tsc -p ./tsconfig.build.cjs.json",
44
+ "build:esm": "tsc -p ./tsconfig.build.esm.json && node ./scripts/esm_import_fix.js",
45
+ "build:dist": "tsc --version && npm run build:cjs && npm run build:esm",
46
+ "build:cjs:test": "cd test/cjs && rm -rf node_modules && npm link ../.. && node index.js",
47
+ "build:esm:test": "cd test/esm && rm -rf node_modules && npm link ../.. && node index.js",
48
+ "build:test": "npm run build && npm run build:cjs:test && npm run build:esm:test",
49
+ "test": "tsc --version && jest --coverage",
50
+ "ci": "npm run lint && npm run test && npm run build:test",
51
+ "contributor": "git-contributor",
52
+ "prepack": "npm run build && rm -rf src/*.tsbuildinfo"
33
53
  },
34
54
  "dependencies": {
35
- "any-promise": "^1.3.0",
36
55
  "content-type": "^1.0.2",
37
- "debug": "^2.6.9",
38
56
  "default-user-agent": "^1.0.0",
39
57
  "digest-header": "^0.0.1",
40
- "ee-first": "~1.1.1",
41
- "formstream": "^1.1.0",
42
58
  "humanize-ms": "^1.2.0",
43
59
  "iconv-lite": "^0.4.15",
44
60
  "ip": "^1.1.5",
45
- "proxy-agent": "^4.0.1",
46
- "pump": "^3.0.0",
47
- "qs": "^6.4.0",
48
- "statuses": "^1.3.1",
49
- "utility": "^1.16.1"
61
+ "mime-types": "^2.1.35",
62
+ "tslib": "^2.4.0",
63
+ "undici": "^5.4.0"
50
64
  },
51
65
  "devDependencies": {
52
- "@types/mocha": "^5.2.5",
53
- "@types/node": "^10.12.18",
54
- "agentkeepalive": "^4.0.0",
55
- "autod": "*",
56
- "benchmark": "^2.1.4",
57
- "bluebird": "*",
58
- "busboy": "^0.2.14",
59
- "co": "*",
60
- "coffee": "1",
61
- "egg-ci": "^1.15.0",
62
- "git-contributor": "^1.0.10",
63
- "http-proxy": "^1.16.2",
64
- "intelli-espower-loader": "^1.0.1",
65
- "istanbul": "*",
66
- "jshint": "*",
67
- "mkdirp": "^0.5.1",
68
- "mocha": "3",
69
- "muk": "^0.5.3",
70
- "pedding": "^1.1.0",
71
- "power-assert": "^1.4.2",
72
- "semver": "5",
73
- "spy": "^1.0.0",
74
- "tar": "^4.4.8",
75
- "through2": "^2.0.3",
76
- "typescript": "^3.2.2"
66
+ "@types/busboy": "^1.5.0",
67
+ "@types/default-user-agent": "^1.0.0",
68
+ "@types/jest": "28",
69
+ "@types/mime-types": "^2.1.1",
70
+ "busboy": "^1.6.0",
71
+ "coffee": "5",
72
+ "egg-ci": "2",
73
+ "eslint": "^8.17.0",
74
+ "eslint-config-egg": "^12.0.0",
75
+ "git-contributor": "1",
76
+ "jest": "28",
77
+ "jest-summary-reporter": "^0.0.2",
78
+ "ts-jest": "28",
79
+ "typescript": "4"
77
80
  },
78
81
  "engines": {
79
- "node": ">= 0.10.0"
82
+ "node": ">= 16.0.0"
80
83
  },
81
84
  "ci": {
82
- "type": "github",
83
- "os": {
84
- "github": "linux, windows, macos"
85
- },
86
- "version": "8, 10, 12, 14"
85
+ "version": "16, 18"
86
+ },
87
+ "publishConfig": {
88
+ "tag": "next"
87
89
  },
88
90
  "license": "MIT"
89
91
  }
@@ -0,0 +1,274 @@
1
+ import { EventEmitter } from 'events';
2
+ import { debuglog } from 'util';
3
+ import { Readable, isReadable } from 'stream';
4
+ import { Blob } from 'buffer';
5
+ import { createReadStream } from 'fs';
6
+ import { basename } from 'path';
7
+ import {
8
+ fetch, RequestInit, Headers, FormData,
9
+ } from 'undici';
10
+ import createUserAgent from 'default-user-agent';
11
+ import mime from 'mime-types';
12
+ import { RequestURL, RequestOptions } from './Request';
13
+
14
+ const debug = debuglog('urllib');
15
+
16
+ export type ClientOptions = {
17
+ defaultArgs?: RequestOptions;
18
+ };
19
+
20
+ // https://github.com/octet-stream/form-data
21
+ class BlobFromStream {
22
+ #stream;
23
+ #type;
24
+ constructor(stream: Readable, type: string) {
25
+ this.#stream = stream;
26
+ this.#type = type;
27
+ }
28
+
29
+ stream() {
30
+ return this.#stream;
31
+ }
32
+
33
+ get type(): string {
34
+ return this.#type;
35
+ }
36
+
37
+ get [Symbol.toStringTag]() {
38
+ return 'Blob';
39
+ }
40
+ }
41
+
42
+ class HttpClientRequestTimeoutError extends Error {
43
+ constructor(timeout: number, options: ErrorOptions) {
44
+ const message = `Request timeout for ${timeout} ms`;
45
+ super(message, options);
46
+ this.name = this.constructor.name;
47
+ Error.captureStackTrace(this, this.constructor);
48
+ }
49
+ }
50
+
51
+ const HEADER_USER_AGENT = createUserAgent('node-urllib', '3.0.0');
52
+
53
+ function getFileName(stream: Readable) {
54
+ const filePath: string = (stream as any).path;
55
+ if (filePath) {
56
+ return basename(filePath);
57
+ }
58
+ return '';
59
+ }
60
+
61
+ export class HttpClient extends EventEmitter {
62
+ defaultArgs?: RequestOptions;
63
+
64
+ constructor(clientOptions?: ClientOptions) {
65
+ super();
66
+ this.defaultArgs = clientOptions?.defaultArgs;
67
+ }
68
+
69
+ async request(url: RequestURL, options?: RequestOptions) {
70
+ const requestUrl = typeof url === 'string' ? new URL(url) : url;
71
+ const args = {
72
+ ...this.defaultArgs,
73
+ ...options,
74
+ emitter: this,
75
+ };
76
+ const requestStartTime = Date.now();
77
+ // keep urllib createCallbackResponse style
78
+ const resHeaders: Record<string, string> = {};
79
+ const res = {
80
+ status: -1,
81
+ statusCode: -1,
82
+ statusMessage: '',
83
+ headers: resHeaders,
84
+ size: 0,
85
+ aborted: false,
86
+ rt: 0,
87
+ keepAliveSocket: true,
88
+ requestUrls: [ url.toString() ],
89
+ timing: {
90
+ contentDownload: 0,
91
+ },
92
+ // remoteAddress: remoteAddress,
93
+ // remotePort: remotePort,
94
+ // socketHandledRequests: socketHandledRequests,
95
+ // socketHandledResponses: socketHandledResponses,
96
+ };
97
+
98
+ let requestTimeout = 5000;
99
+ if (args.timeout) {
100
+ if (Array.isArray(args.timeout)) {
101
+ requestTimeout = args.timeout[args.timeout.length - 1] ?? requestTimeout;
102
+ } else {
103
+ requestTimeout = args.timeout;
104
+ }
105
+ }
106
+
107
+ const requestTimeoutController = new AbortController();
108
+ const requestTimerId = setTimeout(() => requestTimeoutController.abort(), requestTimeout);
109
+ const method = (args.method ?? 'GET').toUpperCase();
110
+
111
+ try {
112
+ const headers = new Headers(args.headers ?? {});
113
+ // don't set user-agent
114
+ const disableUserAgent = args.headers &&
115
+ (args.headers['User-Agent'] === null || args.headers['user-agent'] === null);
116
+ if (!disableUserAgent && !headers.has('user-agent')) {
117
+ // need to set user-agent
118
+ headers.set('user-agent', HEADER_USER_AGENT);
119
+ }
120
+ if (args.dataType === 'json' && !headers.has('accept')) {
121
+ headers.set('accept', 'application/json');
122
+ }
123
+
124
+ const requestOptions: RequestInit = {
125
+ method,
126
+ keepalive: true,
127
+ signal: requestTimeoutController.signal,
128
+ };
129
+ if (args.followRedirect === false) {
130
+ requestOptions.redirect = 'manual';
131
+ }
132
+
133
+ const isGETOrHEAD = requestOptions.method === 'GET' || requestOptions.method === 'HEAD';
134
+
135
+ if (args.files) {
136
+ if (isGETOrHEAD) {
137
+ requestOptions.method = 'POST';
138
+ }
139
+ const formData = new FormData();
140
+ const uploadFiles: [string, string | Readable | Buffer][] = [];
141
+ if (Array.isArray(args.files)) {
142
+ for (const [ index, file ] of args.files.entries()) {
143
+ const field = index === 0 ? 'file' : `file${index}`;
144
+ uploadFiles.push([ field, file ]);
145
+ }
146
+ } else if (args.files instanceof Readable || isReadable(args.files as any)) {
147
+ uploadFiles.push([ 'file', args.files as Readable ]);
148
+ } else if (typeof args.files === 'string' || Buffer.isBuffer(args.files)) {
149
+ uploadFiles.push([ 'file', args.files ]);
150
+ } else if (typeof args.files === 'object') {
151
+ for (const field in args.files) {
152
+ uploadFiles.push([ field, args.files[field] ]);
153
+ }
154
+ }
155
+ // set normal fields first
156
+ if (args.data) {
157
+ for (const field in args.data) {
158
+ formData.append(field, args.data[field]);
159
+ }
160
+ }
161
+ for (const [ index, [ field, file ]] of uploadFiles.entries()) {
162
+ if (typeof file === 'string') {
163
+ // FIXME: support non-ascii filename
164
+ // const fileName = encodeURIComponent(basename(file));
165
+ // formData.append(field, await fileFromPath(file, `utf-8''${fileName}`, { type: mime.lookup(fileName) || '' }));
166
+ const fileName = basename(file);
167
+ const fileReader = createReadStream(file);
168
+ formData.append(field, new BlobFromStream(fileReader, mime.lookup(fileName) || ''), fileName);
169
+ } else if (Buffer.isBuffer(file)) {
170
+ formData.append(field, new Blob([ file ]), `bufferfile${index}`);
171
+ } else if (file instanceof Readable || isReadable(file as any)) {
172
+ const fileName = getFileName(file) || `streamfile${index}`;
173
+ formData.append(field, new BlobFromStream(file, mime.lookup(fileName) || ''), fileName);
174
+ }
175
+ }
176
+ requestOptions.body = formData;
177
+ } else if (args.content) {
178
+ if (!isGETOrHEAD) {
179
+ if (isReadable(args.content as Readable)) {
180
+ // disable keepalive
181
+ requestOptions.keepalive = false;
182
+ }
183
+ // handle content
184
+ requestOptions.body = args.content;
185
+ if (args.contentType) {
186
+ headers.set('content-type', args.contentType);
187
+ }
188
+ }
189
+ } else if (args.data) {
190
+ const isStringOrBufferOrReadable = typeof args.data === 'string'
191
+ || Buffer.isBuffer(args.data)
192
+ || isReadable(args.data);
193
+ if (isGETOrHEAD) {
194
+ if (!isStringOrBufferOrReadable) {
195
+ for (const field in args.data) {
196
+ requestUrl.searchParams.append(field, args.data[field]);
197
+ }
198
+ }
199
+ } else {
200
+ if (isStringOrBufferOrReadable) {
201
+ if (isReadable(args.data as Readable)) {
202
+ // disable keepalive
203
+ requestOptions.keepalive = false;
204
+ }
205
+ requestOptions.body = args.data;
206
+ } else {
207
+ if (args.contentType === 'json'
208
+ || args.contentType === 'application/json'
209
+ || headers.get('content-type')?.startsWith('application/json')) {
210
+ requestOptions.body = JSON.stringify(args.data);
211
+ if (!headers.has('content-type')) {
212
+ headers.set('content-type', 'application/json');
213
+ }
214
+ } else {
215
+ requestOptions.body = new URLSearchParams(args.data);
216
+ }
217
+ }
218
+ }
219
+ }
220
+
221
+ debug('%s %s, headers: %j, timeout: %s', requestOptions.method, url, headers, requestTimeout);
222
+ requestOptions.headers = headers;
223
+
224
+ const response = await fetch(requestUrl, requestOptions);
225
+ for (const [ name, value ] of response.headers) {
226
+ res.headers[name] = value;
227
+ }
228
+ res.status = res.statusCode = response.status;
229
+ res.statusMessage = response.statusText;
230
+ if (response.redirected) {
231
+ res.requestUrls.push(response.url);
232
+ }
233
+ if (res.headers['content-length']) {
234
+ res.size = parseInt(res.headers['content-length']);
235
+ }
236
+
237
+ let data: any;
238
+ if (args.streaming || args.dataType === 'stream') {
239
+ data = response.body;
240
+ } else if (args.dataType === 'text') {
241
+ data = await response.text();
242
+ } else if (args.dataType === 'json') {
243
+ if (requestOptions.method === 'HEAD') {
244
+ data = {};
245
+ } else {
246
+ data = await response.json();
247
+ }
248
+ } else {
249
+ // buffer
250
+ data = Buffer.from(await response.arrayBuffer());
251
+ }
252
+ res.rt = res.timing.contentDownload = Date.now() - requestStartTime;
253
+
254
+ return {
255
+ status: res.status,
256
+ data,
257
+ headers: res.headers,
258
+ url: response.url,
259
+ redirected: response.redirected,
260
+ res,
261
+ };
262
+ } catch (e: any) {
263
+ let err = e;
264
+ if (requestTimeoutController.signal.aborted) {
265
+ err = new HttpClientRequestTimeoutError(requestTimeout, { cause: e });
266
+ }
267
+ err.res = res;
268
+ // console.error(err);
269
+ throw err;
270
+ } finally {
271
+ clearTimeout(requestTimerId);
272
+ }
273
+ }
274
+ }
package/src/Request.ts ADDED
@@ -0,0 +1,116 @@
1
+ import { Readable, Writable } from 'stream';
2
+ import { LookupFunction } from 'net';
3
+
4
+ export type HttpMethod = 'GET' | 'POST' | 'DELETE' | 'PUT' | 'HEAD' | 'OPTIONS' | 'PATCH' | 'TRACE' | 'CONNECT';
5
+
6
+ export type RequestURL = string | URL;
7
+
8
+ export type RequestOptions = {
9
+ /** Request method, defaults to GET. Could be GET, POST, DELETE or PUT. Alias 'type'. */
10
+ method?: HttpMethod | Lowercase<HttpMethod>;
11
+ /** Data to be sent. Will be stringify automatically. */
12
+ data?: any;
13
+ /** Force convert data to query string. */
14
+ dataAsQueryString?: boolean;
15
+ /** Manually set the content of payload. If set, data will be ignored. */
16
+ content?: string | Buffer | Readable;
17
+ /** Stream to be pipe to the remote. If set, data and content will be ignored. */
18
+ stream?: Readable;
19
+ /**
20
+ * A writable stream to be piped by the response stream.
21
+ * Responding data will be write to this stream and callback
22
+ * will be called with data set null after finished writing.
23
+ */
24
+ writeStream?: Writable;
25
+ /** consume the writeStream, invoke the callback after writeStream close. */
26
+ consumeWriteStream?: boolean;
27
+ /**
28
+ * The files will send with multipart/form-data format, base on formstream.
29
+ * If method not set, will use POST method by default.
30
+ */
31
+ files?: Array<Readable | Buffer | string> | Record<string, Readable | Buffer | string> | Readable | Buffer | string;
32
+ /** Type of request data, could be 'json'. If it's 'json', will auto set Content-Type: 'application/json' header. */
33
+ contentType?: string;
34
+ /**
35
+ * Type of response data. Could be text or json.
36
+ * If it's text, the callbacked data would be a String.
37
+ * If it's json, the data of callback would be a parsed JSON Object
38
+ * and will auto set Accept: 'application/json' header.
39
+ * Default is buffer.
40
+ */
41
+ dataType?: 'text' | 'json' | 'buffer' | 'stream';
42
+ /**
43
+ * Let you get the res object when request connected, default false.
44
+ * If set to true, `data` will be response readable stream.
45
+ * Equal to `dataType = 'stream'`
46
+ */
47
+ streaming?: boolean;
48
+ /** Fix the control characters (U+0000 through U+001F) before JSON parse response. Default is false. */
49
+ fixJSONCtlChars?: boolean;
50
+ /** Request headers. */
51
+ headers?: Record<string, string>;
52
+ /**
53
+ * Request timeout in milliseconds for connecting phase and response receiving phase.
54
+ * Defaults to exports.
55
+ * TIMEOUT, both are 5s. You can use timeout: 5000 to tell urllib use same timeout on two phase or set them seperately such as
56
+ * timeout: [3000, 5000], which will set connecting timeout to 3s and response 5s.
57
+ */
58
+ timeout?: number | number[];
59
+ /** username:password used in HTTP Basic Authorization. */
60
+ auth?: string;
61
+ /** username:password used in HTTP Digest Authorization. */
62
+ digestAuth?: string;
63
+ /**
64
+ * An array of strings or Buffers of trusted certificates.
65
+ * If this is omitted several well known "root" CAs will be used, like VeriSign.
66
+ * These are used to authorize connections.
67
+ * Notes: This is necessary only if the server uses the self - signed certificate
68
+ */
69
+ ca?: string | Buffer | string[] | Buffer[];
70
+ /**
71
+ * If true, the server certificate is verified against the list of supplied CAs.
72
+ * An 'error' event is emitted if verification fails.Default: true.
73
+ */
74
+ rejectUnauthorized?: boolean;
75
+ /** A string or Buffer containing the private key, certificate and CA certs of the server in PFX or PKCS12 format. */
76
+ pfx?: string | Buffer;
77
+ /**
78
+ * A string or Buffer containing the private key of the client in PEM format.
79
+ * Notes: This is necessary only if using the client certificate authentication
80
+ */
81
+ key?: string | Buffer;
82
+ /**
83
+ * A string or Buffer containing the certificate key of the client in PEM format.
84
+ * Notes: This is necessary only if using the client certificate authentication
85
+ */
86
+ cert?: string | Buffer;
87
+ /** A string of passphrase for the private key or pfx. */
88
+ passphrase?: string;
89
+ /** A string describing the ciphers to use or exclude. */
90
+ ciphers?: string;
91
+ /** The SSL method to use, e.g.SSLv3_method to force SSL version 3. */
92
+ secureProtocol?: string;
93
+ /** follow HTTP 3xx responses as redirects. defaults to false. */
94
+ followRedirect?: boolean;
95
+ /** The maximum number of redirects to follow, defaults to 10. */
96
+ maxRedirects?: number;
97
+ /** Format the redirect url by your self. Default is url.resolve(from, to). */
98
+ formatRedirectUrl?: (a: any, b: any) => void;
99
+ /** Before request hook, you can change every thing here. */
100
+ beforeRequest?: (...args: any[]) => void;
101
+ /** Accept gzip response content and auto decode it, default is false. */
102
+ gzip?: boolean;
103
+ /** Enable timing or not, default is false. */
104
+ timing?: boolean;
105
+ /**
106
+ * Custom DNS lookup function, default is dns.lookup.
107
+ * Require node >= 4.0.0(for http protocol) and node >=8(for https protocol)
108
+ */
109
+ lookup?: LookupFunction;
110
+ /**
111
+ * check request address to protect from SSRF and similar attacks.
112
+ * It receive two arguments(ip and family) and should return true or false to identified the address is legal or not.
113
+ * It rely on lookup and have the same version requirement.
114
+ */
115
+ checkAddress?: (ip: string, family: number | string) => boolean;
116
+ };
@@ -0,0 +1,20 @@
1
+ import { OutgoingHttpHeaders, IncomingMessage } from 'http';
2
+
3
+ export interface HttpClientResponse<T> {
4
+ data: T;
5
+ status: number;
6
+ headers: OutgoingHttpHeaders;
7
+ res: IncomingMessage & {
8
+ /**
9
+ * https://eggjs.org/en/core/httpclient.html#timing-boolean
10
+ */
11
+ timing?: {
12
+ queuing: number;
13
+ dnslookup: number;
14
+ connected: number;
15
+ requestSent: number;
16
+ waiting: number;
17
+ contentDownload: number;
18
+ }
19
+ };
20
+ }
@@ -0,0 +1,31 @@
1
+ /// <reference types="node" />
2
+ import { EventEmitter } from 'events';
3
+ import { RequestURL, RequestOptions } from './Request';
4
+ export declare type ClientOptions = {
5
+ defaultArgs?: RequestOptions;
6
+ };
7
+ export declare class HttpClient extends EventEmitter {
8
+ defaultArgs?: RequestOptions;
9
+ constructor(clientOptions?: ClientOptions);
10
+ request(url: RequestURL, options?: RequestOptions): Promise<{
11
+ status: number;
12
+ data: any;
13
+ headers: Record<string, string>;
14
+ url: string;
15
+ redirected: boolean;
16
+ res: {
17
+ status: number;
18
+ statusCode: number;
19
+ statusMessage: string;
20
+ headers: Record<string, string>;
21
+ size: number;
22
+ aborted: boolean;
23
+ rt: number;
24
+ keepAliveSocket: boolean;
25
+ requestUrls: string[];
26
+ timing: {
27
+ contentDownload: number;
28
+ };
29
+ };
30
+ }>;
31
+ }