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 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()
@@ -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