diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 00000000..11b226d8 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,6 @@ +* +!package.json +!package-lock.json +!tsconfig.json +!bin +!src diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 00000000..62ff3372 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,22 @@ +FROM node:14.16.0-buster + +ARG UID=1000 +ARG GID=1000 +ARG WASI_SDK_VERSION_MAJOR=12 +ARG WASI_SDK_VERSION_MINOR=0 + +ENV WASI_ROOT=/home/node/wasi-sdk-12.0 + +RUN wget https://github.com/WebAssembly/wasi-sdk/releases/download/wasi-sdk-${WASI_SDK_VERSION_MAJOR}/wasi-sdk-${WASI_SDK_VERSION_MAJOR}.${WASI_SDK_VERSION_MINOR}-linux.tar.gz -P /tmp + +RUN tar xvf /tmp/wasi-sdk-${WASI_SDK_VERSION_MAJOR}.${WASI_SDK_VERSION_MINOR}-linux.tar.gz --directory /home/node + +RUN mkdir /home/node/llhttp + +WORKDIR /home/node/llhttp + +COPY . . + +RUN npm ci + +USER node diff --git a/bin/build_wasm.ts b/bin/build_wasm.ts new file mode 100644 index 00000000..f80fef77 --- /dev/null +++ b/bin/build_wasm.ts @@ -0,0 +1,80 @@ +import { execSync } from 'child_process'; +import { copyFileSync, mkdirSync, writeFileSync } from 'fs'; +import { stringify } from 'javascript-stringify'; +import { join, resolve } from 'path'; +import { constants } from '..'; + +const { WASI_ROOT } = process.env; +const WASM_OUT = resolve(__dirname, '../build/wasm'); +const WASM_SRC = resolve(__dirname, '../'); + +if (process.argv[2] === '--setup') { + try { + mkdirSync(join(WASM_SRC, 'build')); + process.exit(0); + } catch (error) { + if (error.code !== 'EEXIST') { + throw error; + } + process.exit(0); + } +} + +if (process.argv[2] === '--docker') { + let cmd = 'docker run --rm -it'; + // Try to avoid root permission problems on compiled assets + // when running on linux. + // It will work flawessly if uid === gid === 1000 + // there will be some warnings otherwise. + if (process.platform === 'linux') { + cmd += ` --user ${process.getuid()}:${process.getegid()}`; + } + cmd += ` --mount type=bind,source=${WASM_SRC}/build,target=/home/node/llhttp/build llhttp_wasm_builder npm run wasm`; + execSync(cmd, { cwd: WASM_SRC, stdio: 'inherit' }); + process.exit(0); +} + +if (!WASI_ROOT) { + throw new Error('Please setup the WASI_ROOT env variable.'); +} + +try { + mkdirSync(WASM_OUT); +} catch (error) { + if (error.code !== 'EEXIST') { + throw error; + } +} + +// Build ts +execSync('npm run build', { cwd: WASM_SRC, stdio: 'inherit' }); + +// Build wasm binary +execSync(`${WASI_ROOT}/bin/clang \ + --sysroot=${WASI_ROOT}/share/wasi-sysroot \ + -target wasm32-unknown-wasi \ + -Ofast \ + -fno-exceptions \ + -fvisibility=hidden \ + -mexec-model=reactor \ + -Wl,-error-limit=0 \ + -Wl,-O3 \ + -Wl,--lto-O3 \ + -Wl,--strip-all \ + -Wl,--allow-undefined \ + -Wl,--export-dynamic \ + -Wl,--export-table \ + -Wl,--export=malloc \ + -Wl,--export=free \ + ${join(WASM_SRC, 'build', 'c')}/*.c \ + ${join(WASM_SRC, 'src', 'native')}/*.c \ + -I${join(WASM_SRC, 'build')} \ + -o ${join(WASM_OUT, 'llhttp.wasm')}`, { stdio: 'inherit' }); + +// Copy constants for `.js` and `.ts` users. +copyFileSync(join(WASM_SRC, 'lib', 'llhttp', 'constants.js'), join(WASM_OUT, 'constants.js')); +copyFileSync(join(WASM_SRC, 'lib', 'llhttp', 'constants.js.map'), join(WASM_OUT, 'constants.js.map')); +copyFileSync(join(WASM_SRC, 'lib', 'llhttp', 'constants.d.ts'), join(WASM_OUT, 'constants.d.ts')); +copyFileSync(join(WASM_SRC, 'lib', 'llhttp', 'utils.js'), join(WASM_OUT, 'utils.js')); +copyFileSync(join(WASM_SRC, 'lib', 'llhttp', 'utils.js.map'), join(WASM_OUT, 'utils.js.map')); +copyFileSync(join(WASM_SRC, 'lib', 'llhttp', 'utils.d.ts'), join(WASM_OUT, 'utils.d.ts')); diff --git a/examples/wasm.ts b/examples/wasm.ts new file mode 100644 index 00000000..995fed83 --- /dev/null +++ b/examples/wasm.ts @@ -0,0 +1,248 @@ +/** + * A minimal Parser that mimicks a small fraction of the Node.js parser + * API. + * To run: + * - `npm run build-wasm` + * - `npx ts-node examples/wasm.ts` + */ +import { readFileSync } from 'fs'; +import { resolve } from 'path'; +import * as constants from '../build/wasm/constants'; + +const bin = readFileSync(resolve(__dirname, '../build/wasm/llhttp.wasm')); +const mod = new WebAssembly.Module(bin); + +const REQUEST = constants.TYPE.REQUEST; +const RESPONSE = constants.TYPE.RESPONSE; +const kOnMessageBegin = 0; +const kOnHeaders = 1; +const kOnHeadersComplete = 2; +const kOnBody = 3; +const kOnMessageComplete = 4; +const kOnExecute = 5; + +const kPtr = Symbol('kPtr'); +const kUrl = Symbol('kUrl'); +const kStatusMessage = Symbol('kStatusMessage'); +const kHeadersFields = Symbol('kHeadersFields'); +const kHeadersValues = Symbol('kHeadersValues'); +const kBody = Symbol('kBody'); +const kReset = Symbol('kReset'); +const kCheckErr = Symbol('kCheckErr'); + +const cstr = (ptr: number, len: number): string => + Buffer.from(memory.buffer, ptr, len).toString(); + +const wasm_on_message_begin = (p: number) => { + const i = instMap.get(p); + i[kReset](); + return i[kOnMessageBegin](); +}; + +const wasm_on_url = (p: number, at: number, length: number) => { + instMap.get(p)[kUrl] = cstr(at, length); + return 0; +}; + +const wasm_on_status = (p: number, at: number, length: number) => { + instMap.get(p)[kStatusMessage] = cstr(at, length); + return 0; +}; + +const wasm_on_header_field = (p: number, at: number, length: number) => { + const i= instMap.get(p) + i[kHeadersFields].push(cstr(at, length)); + return 0; +}; + +const wasm_on_header_value = (p: number, at: number, length: number) => { + const i = instMap.get(p); + i[kHeadersValues].push(cstr(at, length)); + return 0; +}; + +const wasm_on_headers_complete = (p: number) => { + const i = instMap.get(p); + const type = get_type(p); + const versionMajor = get_version_major(p); + const versionMinor = get_version_minor(p); + const rawHeaders = []; + let method; + let url; + let statusCode; + let statusMessage; + const upgrade = get_upgrade(p); + const shouldKeepAlive = should_keep_alive(p); + + for (let c = 0; c < i[kHeadersFields].length; c++) { + rawHeaders.push(i[kHeadersFields][c], i[kHeadersValues][c]) + } + + if (type === HTTPParser.REQUEST) { + method = constants.METHODS[get_method(p)]; + url = i[kUrl]; + } else if (type === HTTPParser.RESPONSE) { + statusCode = get_status_code(p); + statusMessage = i[kStatusMessage]; + } + return i[kOnHeadersComplete](versionMajor, versionMinor, rawHeaders, method, +url, statusCode, statusMessage, upgrade, shouldKeepAlive); +}; + +const wasm_on_body = (p: number, at: number, length: number) => { + const i = instMap.get(p); + const body = Buffer.from(memory.buffer, at, length); + return i[kOnBody](body); +}; + +const wasm_on_message_complete = (p: number) => { + return instMap.get(p)[kOnMessageComplete](); +}; + +const instMap = new Map(); + +const inst = new WebAssembly.Instance(mod, { + env: { + wasm_on_message_begin, + wasm_on_url, + wasm_on_status, + wasm_on_header_field, + wasm_on_header_value, + wasm_on_headers_complete, + wasm_on_body, + wasm_on_message_complete, + }, +}); + +const memory = inst.exports.memory as any; +const alloc = inst.exports.llhttp_alloc as CallableFunction; +const malloc = inst.exports.malloc as CallableFunction; +const execute = inst.exports.llhttp_execute as CallableFunction; +const get_type = inst.exports.llhttp_get_type as CallableFunction; +const get_upgrade = inst.exports.llhttp_get_upgrade as CallableFunction; +const should_keep_alive = inst.exports.llhttp_should_keep_alive as CallableFunction; +const get_method = inst.exports.llhttp_get_method as CallableFunction; +const get_status_code = inst.exports.llhttp_get_status_code as CallableFunction; +const get_version_minor = inst.exports.llhttp_get_http_minor as CallableFunction; +const get_version_major = inst.exports.llhttp_get_http_major as CallableFunction; +const get_error_reason = inst.exports.llhttp_get_error_reason as CallableFunction; +const free = inst.exports.free as CallableFunction; +const initialize = inst.exports._initialize as CallableFunction; + +initialize(); // wasi reactor + +class HTTPParser { + static REQUEST = REQUEST; + static RESPONSE = RESPONSE; + static kOnMessageBegin = kOnMessageBegin; + static kOnHeaders = kOnHeaders; + static kOnHeadersComplete = kOnHeadersComplete; + static kOnBody = kOnBody; + static kOnMessageComplete = kOnMessageComplete; + static kOnExecute = kOnExecute; + + [kPtr]: number; + [kUrl]: string; + [kStatusMessage]: null|string; + [kHeadersFields]: []|[string]; + [kHeadersValues]: []|[string]; + [kBody]: null|Buffer; + + constructor(type: constants.TYPE) { + this[kPtr] = alloc(constants.TYPE[type]); + instMap.set(this[kPtr], this); + + this[kUrl] = ''; + this[kStatusMessage] = null; + this[kHeadersFields] = []; + this[kHeadersValues] = []; + this[kBody] = null; + } + + [kReset]() { + this[kUrl] = ''; + this[kStatusMessage] = null; + this[kHeadersFields] = []; + this[kHeadersValues] = []; + this[kBody] = null; + } + + [kOnMessageBegin]() { + return 0; + } + + [kOnHeaders](rawHeaders: [string]) {} + + [kOnHeadersComplete](versionMajor: number, versionMinor: number, rawHeaders: [string], method: string, + url: string, statusCode: number, statusMessage: string, upgrade: boolean, shouldKeepAlive: boolean) { + return 0; + } + + [kOnBody](body: Buffer) { + this[kBody] = body; + return 0; + } + + [kOnMessageComplete]() { + return 0; + } + + destroy() { + instMap.delete(this[kPtr]); + free(this[kPtr]); + } + + execute(data: Buffer) { + const ptr = malloc(data.byteLength); + const u8 = new Uint8Array(memory.buffer); + u8.set(data, ptr); + const ret = execute(this[kPtr], ptr, data.length); + free(ptr); + this[kCheckErr](ret); + return ret; + } + + [kCheckErr](n: number) { + if (n === constants.ERROR.OK) { + return; + } + const ptr = get_error_reason(this[kPtr]); + const u8 = new Uint8Array(memory.buffer); + const len = u8.indexOf(0, ptr) - ptr; + throw new Error(cstr(ptr, len)); + } +} + + +{ + const p = new HTTPParser(HTTPParser.REQUEST); + + p.execute(Buffer.from([ + 'POST /owo HTTP/1.1', + 'X: Y', + 'Content-Length: 9', + '', + 'uh, meow?', + '', + ].join('\r\n'))); + + console.log(p); + + p.destroy(); +} + +{ + const p = new HTTPParser(HTTPParser.RESPONSE); + + p.execute(Buffer.from([ + 'HTTP/1.1 200 OK', + 'X: Y', + 'Content-Length: 9', + '', + 'uh, meow?' + ].join('\r\n'))); + + console.log(p); + + p.destroy(); +} diff --git a/package-lock.json b/package-lock.json index 312f761f..6a48a868 100644 --- a/package-lock.json +++ b/package-lock.json @@ -610,6 +610,12 @@ "integrity": "sha1-6PvzdNxVb/iUehDcsFctYz8s+hA=", "dev": true }, + "javascript-stringify": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/javascript-stringify/-/javascript-stringify-2.0.1.tgz", + "integrity": "sha512-yV+gqbd5vaOYjqlbk16EG89xB5udgjqQF3C5FAORDg4f/IS1Yc5ERCv5e/57yBcfJYw05V5JyIXabhwb75Xxow==", + "dev": true + }, "js-tokens": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", diff --git a/package.json b/package.json index bde42077..c897e732 100644 --- a/package.json +++ b/package.json @@ -12,6 +12,9 @@ "bench": "ts-node bench/", "build": "ts-node bin/generate.ts", "build-ts": "tsc", + "prebuild-wasm": "docker build -t llhttp_wasm_builder . && npm run wasm -- --setup", + "build-wasm": "npm run wasm -- --docker", + "wasm": "ts-node bin/build_wasm.ts", "clean": "rm -rf lib && rm -rf test/tmp", "prepare": "npm run clean && npm run build-ts", "lint": "tslint -c tslint.json bin/*.ts src/*.ts src/**/*.ts test/*.ts test/**/*.ts", @@ -38,6 +41,7 @@ "devDependencies": { "@types/mocha": "^5.2.7", "@types/node": "^10.17.52", + "javascript-stringify": "^2.0.1", "llparse-dot": "^1.0.1", "llparse-test-fixture": "^5.0.1", "mdgator": "^1.1.2", diff --git a/src/native/api.c b/src/native/api.c index 1d68639f..8a4bdd26 100644 --- a/src/native/api.c +++ b/src/native/api.c @@ -24,6 +24,70 @@ void llhttp_init(llhttp_t* parser, llhttp_type_t type, } +#if defined(__wasm__) + +extern int wasm_on_message_begin(llhttp_t * p); +extern int wasm_on_url(llhttp_t* p, const char* at, size_t length); +extern int wasm_on_status(llhttp_t* p, const char* at, size_t length); +extern int wasm_on_header_field(llhttp_t* p, const char* at, size_t length); +extern int wasm_on_header_value(llhttp_t* p, const char* at, size_t length); +extern int wasm_on_headers_complete(llhttp_t * p); +extern int wasm_on_body(llhttp_t* p, const char* at, size_t length); +extern int wasm_on_message_complete(llhttp_t * p); + +const llhttp_settings_t wasm_settings = { + wasm_on_message_begin, + wasm_on_url, + wasm_on_status, + wasm_on_header_field, + wasm_on_header_value, + wasm_on_headers_complete, + wasm_on_body, + wasm_on_message_complete, + NULL, + NULL, +}; + + +llhttp_t* llhttp_alloc(llhttp_type_t type) { + llhttp_t* parser = malloc(sizeof(llhttp_t)); + llhttp_init(parser, type, &wasm_settings); + return parser; +} + +void llhttp_free(llhttp_t* parser) { + free(parser); +} + +/* Some getters required to get stuff from the parser */ + +uint8_t llhttp_get_type(llhttp_t* parser) { + return parser->type; +} + +uint8_t llhttp_get_http_major(llhttp_t* parser) { + return parser->http_major; +} + +uint8_t llhttp_get_http_minor(llhttp_t* parser) { + return parser->http_minor; +} + +uint8_t llhttp_get_method(llhttp_t* parser) { + return parser->method; +} + +int llhttp_get_status_code(llhttp_t* parser) { + return parser->status_code; +} + +uint8_t llhttp_get_upgrade(llhttp_t* parser) { + return parser->upgrade; +} + +#endif // defined(__wasm__) + + void llhttp_reset(llhttp_t* parser) { llhttp_type_t type = parser->type; const llhttp_settings_t* settings = parser->settings; diff --git a/src/native/api.h b/src/native/api.h index fb19618d..7fdbfc10 100644 --- a/src/native/api.h +++ b/src/native/api.h @@ -5,6 +5,12 @@ extern "C" { #endif #include +#if defined(__wasm__) +#define LLHTTP_EXPORT __attribute__((visibility("default"))) +#else +#define LLHTTP_EXPORT +#endif + typedef llhttp__internal_t llhttp_t; typedef struct llhttp_settings_s llhttp_settings_t; @@ -55,15 +61,46 @@ struct llhttp_settings_s { * the `parser` here. In practice, `settings` has to be either a static * variable or be allocated with `malloc`, `new`, etc. */ +LLHTTP_EXPORT void llhttp_init(llhttp_t* parser, llhttp_type_t type, const llhttp_settings_t* settings); +#if defined(__wasm__) + +LLHTTP_EXPORT +llhttp_t* llhttp_alloc(llhttp_type_t type); + +LLHTTP_EXPORT +void llhttp_free(llhttp_t* parser); + +LLHTTP_EXPORT +uint8_t llhttp_get_type(llhttp_t* parser); + +LLHTTP_EXPORT +uint8_t llhttp_get_http_major(llhttp_t* parser); + +LLHTTP_EXPORT +uint8_t llhttp_get_http_minor(llhttp_t* parser); + +LLHTTP_EXPORT +uint8_t llhttp_get_method(llhttp_t* parser); + +LLHTTP_EXPORT +int llhttp_get_status_code(llhttp_t* parser); + +LLHTTP_EXPORT +uint8_t llhttp_get_upgrade(llhttp_t* parser); + +#endif // defined(__wasm__) + /* Reset an already initialized parser back to the start state, preserving the * existing parser type, callback settings, user data, and lenient flags. */ +LLHTTP_EXPORT void llhttp_reset(llhttp_t* parser); /* Initialize the settings object */ +LLHTTP_EXPORT void llhttp_settings_init(llhttp_settings_t* settings); /* Parse full or partial request/response, invoking user callbacks along the @@ -82,6 +119,7 @@ void llhttp_settings_init(llhttp_settings_t* settings); * to return the same error upon each successive call up until `llhttp_init()` * is called. */ +LLHTTP_EXPORT llhttp_errno_t llhttp_execute(llhttp_t* parser, const char* data, size_t len); /* This method should be called when the other side has no further bytes to @@ -92,16 +130,19 @@ llhttp_errno_t llhttp_execute(llhttp_t* parser, const char* data, size_t len); * connection. This method will invoke `on_message_complete()` callback if the * request was terminated safely. Otherwise a error code would be returned. */ +LLHTTP_EXPORT llhttp_errno_t llhttp_finish(llhttp_t* parser); /* Returns `1` if the incoming message is parsed until the last byte, and has * to be completed by calling `llhttp_finish()` on EOF */ +LLHTTP_EXPORT int llhttp_message_needs_eof(const llhttp_t* parser); /* Returns `1` if there might be any other messages following the last that was * successfully parsed. */ +LLHTTP_EXPORT int llhttp_should_keep_alive(const llhttp_t* parser); /* Make further calls of `llhttp_execute()` return `HPE_PAUSED` and set @@ -110,6 +151,7 @@ int llhttp_should_keep_alive(const llhttp_t* parser); * Important: do not call this from user callbacks! User callbacks must return * `HPE_PAUSED` if pausing is required. */ +LLHTTP_EXPORT void llhttp_pause(llhttp_t* parser); /* Might be called to resume the execution after the pause in user's callback. @@ -117,6 +159,7 @@ void llhttp_pause(llhttp_t* parser); * * Call this only if `llhttp_execute()` returns `HPE_PAUSED`. */ +LLHTTP_EXPORT void llhttp_resume(llhttp_t* parser); /* Might be called to resume the execution after the pause in user's callback. @@ -124,9 +167,11 @@ void llhttp_resume(llhttp_t* parser); * * Call this only if `llhttp_execute()` returns `HPE_PAUSED_UPGRADE` */ +LLHTTP_EXPORT void llhttp_resume_after_upgrade(llhttp_t* parser); /* Returns the latest return error */ +LLHTTP_EXPORT llhttp_errno_t llhttp_get_errno(const llhttp_t* parser); /* Returns the verbal explanation of the latest returned error. @@ -134,6 +179,7 @@ llhttp_errno_t llhttp_get_errno(const llhttp_t* parser); * Note: User callback should set error reason when returning the error. See * `llhttp_set_error_reason()` for details. */ +LLHTTP_EXPORT const char* llhttp_get_error_reason(const llhttp_t* parser); /* Assign verbal description to the returned error. Must be called in user @@ -141,6 +187,7 @@ const char* llhttp_get_error_reason(const llhttp_t* parser); * * Note: `HPE_USER` error code might be useful in user callbacks. */ +LLHTTP_EXPORT void llhttp_set_error_reason(llhttp_t* parser, const char* reason); /* Returns the pointer to the last parsed byte before the returned error. The @@ -148,12 +195,15 @@ void llhttp_set_error_reason(llhttp_t* parser, const char* reason); * * Note: this method might be useful for counting the number of parsed bytes. */ +LLHTTP_EXPORT const char* llhttp_get_error_pos(const llhttp_t* parser); /* Returns textual name of error code */ +LLHTTP_EXPORT const char* llhttp_errno_name(llhttp_errno_t err); /* Returns textual name of HTTP method */ +LLHTTP_EXPORT const char* llhttp_method_name(llhttp_method_t method); @@ -166,6 +216,7 @@ const char* llhttp_method_name(llhttp_method_t method); * * **(USE AT YOUR OWN RISK)** */ +LLHTTP_EXPORT void llhttp_set_lenient_headers(llhttp_t* parser, int enabled); @@ -179,6 +230,7 @@ void llhttp_set_lenient_headers(llhttp_t* parser, int enabled); * * **(USE AT YOUR OWN RISK)** */ +LLHTTP_EXPORT void llhttp_set_lenient_chunked_length(llhttp_t* parser, int enabled);