curlnapi-node 0.1.8
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/index.js +56 -0
- package/index.wrapper.js +79 -0
- package/package.json +14 -0
- package/request.js +72 -0
package/index.js
ADDED
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
const os = require('os')
|
|
2
|
+
const path = require('path')
|
|
3
|
+
const fs = require('fs')
|
|
4
|
+
|
|
5
|
+
function tryRequire(name) {
|
|
6
|
+
try { return require(name) } catch { return null }
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
function isMuslFromFilesystem() {
|
|
10
|
+
try { return fs.readFileSync('/usr/bin/ldd', 'utf-8').includes('musl') } catch { return null }
|
|
11
|
+
}
|
|
12
|
+
function isMuslFromReport() {
|
|
13
|
+
let report = null
|
|
14
|
+
if (typeof process.report?.getReport === 'function') {
|
|
15
|
+
process.report.excludeNetwork = true
|
|
16
|
+
report = process.report.getReport()
|
|
17
|
+
}
|
|
18
|
+
if (!report) return null
|
|
19
|
+
if (report.header && report.header.glibcVersionRuntime) return false
|
|
20
|
+
if (Array.isArray(report.sharedObjects)) {
|
|
21
|
+
if (report.sharedObjects.some(f => f.includes('libc.musl-') || f.includes('ld-musl-'))) return true
|
|
22
|
+
}
|
|
23
|
+
return false
|
|
24
|
+
}
|
|
25
|
+
function isMusl() {
|
|
26
|
+
if (os.platform() !== 'linux') return false
|
|
27
|
+
const a = isMuslFromFilesystem()
|
|
28
|
+
if (a !== null) return a
|
|
29
|
+
const b = isMuslFromReport()
|
|
30
|
+
if (b !== null) return b
|
|
31
|
+
try { return require('child_process').execSync('ldd --version', { encoding: 'utf8' }).includes('musl') } catch { return false }
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function resolveNative() {
|
|
35
|
+
const platform = os.platform()
|
|
36
|
+
const arch = os.arch()
|
|
37
|
+
const candidates = []
|
|
38
|
+
if (platform === 'win32' && arch === 'x64') candidates.push('@wengo/curlnapi-win32-x64-msvc')
|
|
39
|
+
if (platform === 'linux' && arch === 'x64') candidates.push(isMusl() ? '@wengo/curlnapi-linux-x64-musl' : '@wengo/curlnapi-linux-x64-gnu')
|
|
40
|
+
if (platform === 'darwin' && arch === 'x64') candidates.push('curlnapi-darwin-x64')
|
|
41
|
+
if (platform === 'darwin' && arch === 'arm64') candidates.push('curlnapi-darwin-arm64')
|
|
42
|
+
for (const pkg of candidates) {
|
|
43
|
+
const mod = tryRequire(pkg)
|
|
44
|
+
if (mod) return mod
|
|
45
|
+
}
|
|
46
|
+
const root = path.resolve(__dirname, '..')
|
|
47
|
+
const lib64 = path.join(root, 'lib64')
|
|
48
|
+
const bin = path.join(lib64, 'bin')
|
|
49
|
+
if (platform === 'win32') {
|
|
50
|
+
const paths = [lib64, bin, process.env.PATH || ''].filter(Boolean)
|
|
51
|
+
process.env.PATH = paths.join(';')
|
|
52
|
+
}
|
|
53
|
+
return require(path.join(root, 'build', 'Release', 'curlnapi.node'))
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
module.exports = resolveNative()
|
package/index.wrapper.js
ADDED
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
const { castToTypedArray } = require('./request.js')
|
|
2
|
+
let native = null
|
|
3
|
+
try {
|
|
4
|
+
native = require('./index.js')
|
|
5
|
+
} catch (e) {
|
|
6
|
+
throw new Error(`curl_cffi couldn't load native bindings. Set VERBOSE=1 for details.`, process.env['VERBOSE'] === '1' ? { cause: e } : undefined)
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
function canonicalizeHeaders(headers) {
|
|
10
|
+
if (typeof Headers !== 'undefined' && headers instanceof Headers) return [...headers.entries()]
|
|
11
|
+
if (Array.isArray(headers)) return headers
|
|
12
|
+
if (headers && typeof headers === 'object') return Object.entries(headers)
|
|
13
|
+
return []
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function headersToObject(headers) {
|
|
17
|
+
if (!headers) return undefined
|
|
18
|
+
const entries = canonicalizeHeaders(headers)
|
|
19
|
+
return Object.fromEntries(entries)
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
async function parseFetchOptions(resource, init) {
|
|
23
|
+
let url
|
|
24
|
+
let options = { ...init }
|
|
25
|
+
if (typeof Request !== 'undefined' && resource instanceof Request) {
|
|
26
|
+
url = resource.url
|
|
27
|
+
options = { method: resource.method, headers: resource.headers, body: resource.body, ...init }
|
|
28
|
+
} else if (resource && resource.toString) {
|
|
29
|
+
url = resource.toString()
|
|
30
|
+
} else {
|
|
31
|
+
url = resource
|
|
32
|
+
}
|
|
33
|
+
options.headers = canonicalizeHeaders(options?.headers)
|
|
34
|
+
if (options?.body) {
|
|
35
|
+
const { body, type } = await castToTypedArray(options.body)
|
|
36
|
+
options.body = body
|
|
37
|
+
if (type && !options.headers.some(([k]) => String(k).toLowerCase() === 'content-type')) {
|
|
38
|
+
options.headers.push(['Content-Type', type])
|
|
39
|
+
}
|
|
40
|
+
} else {
|
|
41
|
+
delete options.body
|
|
42
|
+
}
|
|
43
|
+
const out = { url, method: options.method, headers: options.headers, body: options.body, signal: options.signal }
|
|
44
|
+
if (typeof options.timeout === 'number') out.timeout = options.timeout
|
|
45
|
+
return out
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
class Impit extends native.Impit {
|
|
49
|
+
constructor(options) {
|
|
50
|
+
const jsCookieJar = options?.cookieJar
|
|
51
|
+
super({
|
|
52
|
+
...options,
|
|
53
|
+
cookieJar: jsCookieJar ? {
|
|
54
|
+
setCookie: async (args) => jsCookieJar.setCookie?.bind?.(jsCookieJar)(...args),
|
|
55
|
+
getCookieString: async (args) => jsCookieJar.getCookieString?.bind?.(jsCookieJar)(args),
|
|
56
|
+
} : undefined,
|
|
57
|
+
headers: headersToObject(options?.headers),
|
|
58
|
+
})
|
|
59
|
+
}
|
|
60
|
+
async fetch(resource, init) {
|
|
61
|
+
const { url, signal, ...options } = await parseFetchOptions(resource, init)
|
|
62
|
+
const waitForAbort = new Promise((_, reject) => {
|
|
63
|
+
signal?.throwIfAborted?.()
|
|
64
|
+
signal?.addEventListener?.('abort', () => reject(signal.reason), { once: true })
|
|
65
|
+
})
|
|
66
|
+
const response = super.fetch(url, options)
|
|
67
|
+
const originalResponse = await Promise.race([response, waitForAbort])
|
|
68
|
+
signal?.throwIfAborted?.()
|
|
69
|
+
signal?.addEventListener?.('abort', () => { originalResponse.abort() })
|
|
70
|
+
if (typeof Headers !== 'undefined') {
|
|
71
|
+
Object.defineProperty(originalResponse, 'headers', { value: new Headers(originalResponse.headers) })
|
|
72
|
+
}
|
|
73
|
+
return originalResponse
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
module.exports.Impit = Impit
|
|
78
|
+
module.exports.ImpitWrapper = native.ImpitWrapper
|
|
79
|
+
module.exports.ImpitResponse = native.ImpitResponse
|
package/package.json
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "curlnapi-node",
|
|
3
|
+
"version": "0.1.8",
|
|
4
|
+
"private": false,
|
|
5
|
+
"description": "Node binding for curl-impersonate with a requests-like API",
|
|
6
|
+
"main": "index.wrapper.js",
|
|
7
|
+
"scripts": {
|
|
8
|
+
"test": "node examples/test.js"
|
|
9
|
+
},
|
|
10
|
+
"optionalDependencies": {
|
|
11
|
+
"@wengo/curlnapi-win32-x64-msvc": "^0.1.0",
|
|
12
|
+
"@wengo/curlnapi-linux-x64-gnu": "^0.1.0"
|
|
13
|
+
}
|
|
14
|
+
}
|
package/request.js
ADDED
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
async function generateMultipartFormData(formData) {
|
|
2
|
+
const boundary = `----formdata-curlcffi-${`${Math.random().toString().slice(0, 5)}`.padStart(11, '0')}`
|
|
3
|
+
const prefix = `--${boundary}\r\nContent-Disposition: form-data`
|
|
4
|
+
const escape = (str) => str.replace(/\n/g, '%0A').replace(/\r/g, '%0D').replace(/"/g, '%22')
|
|
5
|
+
const normalizeLinefeeds = (value) => value.replace(/\r?\n|\r/g, '\r\n')
|
|
6
|
+
const blobParts = []
|
|
7
|
+
const rn = new Uint8Array([13, 10])
|
|
8
|
+
const textEncoder = new TextEncoder()
|
|
9
|
+
for (const [name, value] of formData) {
|
|
10
|
+
if (typeof value === 'string') {
|
|
11
|
+
const chunk = textEncoder.encode(prefix + `; name="${escape(normalizeLinefeeds(name))}"` + `\r\n\r\n${normalizeLinefeeds(value)}\r\n`)
|
|
12
|
+
blobParts.push(chunk)
|
|
13
|
+
} else {
|
|
14
|
+
const chunk = textEncoder.encode(`${prefix}; name="${escape(normalizeLinefeeds(name))}"` + (value.name ? `; filename="${escape(value.name)}"` : '') + '\r\n' + `Content-Type: ${value.type || 'application/octet-stream'}\r\n\r\n`)
|
|
15
|
+
blobParts.push(chunk, value, rn)
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
const chunk = textEncoder.encode(`--${boundary}--\r\n`)
|
|
19
|
+
blobParts.push(chunk)
|
|
20
|
+
const action = async function* () { for (const part of blobParts) { if (part.stream) { yield* part.stream() } else { yield part } } }
|
|
21
|
+
const parts = []
|
|
22
|
+
for await (const part of action()) {
|
|
23
|
+
if (part instanceof Uint8Array) parts.push(part)
|
|
24
|
+
else if (typeof Blob !== 'undefined' && part instanceof Blob) {
|
|
25
|
+
const arrayBuffer = await part.arrayBuffer()
|
|
26
|
+
parts.push(new Uint8Array(arrayBuffer))
|
|
27
|
+
} else {
|
|
28
|
+
throw new TypeError('Unsupported part type')
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
const body = new Uint8Array(parts.reduce((acc, p) => acc + p.length, 0))
|
|
32
|
+
let offset = 0
|
|
33
|
+
for (const p of parts) { body.set(p, offset); offset += p.length }
|
|
34
|
+
return { body, type: `multipart/form-data; boundary=${boundary}` }
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
async function castToTypedArray(body) {
|
|
38
|
+
let typedArray = body
|
|
39
|
+
let type = ''
|
|
40
|
+
if (typeof body === 'string') {
|
|
41
|
+
typedArray = new TextEncoder().encode(body)
|
|
42
|
+
type = 'text/plain;charset=UTF-8'
|
|
43
|
+
} else if (typeof URLSearchParams !== 'undefined' && typedArray instanceof URLSearchParams) {
|
|
44
|
+
typedArray = new TextEncoder().encode(body.toString())
|
|
45
|
+
type = 'application/x-www-form-urlencoded;charset=UTF-8'
|
|
46
|
+
} else if (typeof ArrayBuffer !== 'undefined' && body instanceof ArrayBuffer) {
|
|
47
|
+
typedArray = new Uint8Array(body.slice())
|
|
48
|
+
} else if (ArrayBuffer.isView(body)) {
|
|
49
|
+
typedArray = new Uint8Array(body.buffer.slice(body.byteOffset, body.byteOffset + body.byteLength))
|
|
50
|
+
} else if (typeof Blob !== 'undefined' && body instanceof Blob) {
|
|
51
|
+
typedArray = new Uint8Array(await body.arrayBuffer())
|
|
52
|
+
type = body.type
|
|
53
|
+
} else if (typeof FormData !== 'undefined' && body instanceof FormData) {
|
|
54
|
+
return await generateMultipartFormData(body)
|
|
55
|
+
} else if (typeof ReadableStream !== 'undefined' && body instanceof ReadableStream) {
|
|
56
|
+
const reader = body.getReader()
|
|
57
|
+
const chunks = []
|
|
58
|
+
let done = false
|
|
59
|
+
while (!done) {
|
|
60
|
+
const { done: streamDone, value } = await reader.read()
|
|
61
|
+
done = streamDone
|
|
62
|
+
if (value) chunks.push(value)
|
|
63
|
+
}
|
|
64
|
+
const total = chunks.reduce((acc, c) => acc + c.length, 0)
|
|
65
|
+
typedArray = new Uint8Array(total)
|
|
66
|
+
let offset = 0
|
|
67
|
+
for (const c of chunks) { typedArray.set(c, offset); offset += c.length }
|
|
68
|
+
}
|
|
69
|
+
return { body: typedArray, type }
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
exports.castToTypedArray = castToTypedArray
|