@sproutboat/toolchain 0.4.18 → 0.4.19

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/CHANGELOG.md CHANGED
@@ -1,5 +1,11 @@
1
1
  # @sproutboat/toolchain
2
2
 
3
+ ## 0.4.19
4
+
5
+ ### Patch Changes
6
+
7
+ - 872198d: Standalone binaries accept HTTP/1.0 requests instead of answering 505 (baronunread/sproutboat#236), so they work behind nginx's default `proxy_http_version 1.0`. uWebSockets only parsed `HTTP/1.1` request lines. A helper now edits its `HttpParser.h` before each native-fetch compile to accept `HTTP/1.0` and mark the request as ancient, which makes uWebSockets close the connection after the response, as HTTP/1.0 expects.
8
+
3
9
  ## 0.4.18
4
10
 
5
11
  ### Patch Changes
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sproutboat/toolchain",
3
- "version": "0.4.18",
3
+ "version": "0.4.19",
4
4
  "license": "MIT",
5
5
  "type": "module",
6
6
  "repository": {
@@ -30,6 +30,8 @@ async function fixture(): Promise<{ archive: string; sha256: string }> {
30
30
  "}\n" +
31
31
  "#define PORF_CORO_STACK_SIZE (256u * 1024u)\n",
32
32
  "compiler/index.js":
33
+ "import { hashId } from './modules.js';\n" +
34
+ " const uwsDir = uwebsockets.ensureUWebSockets();\n" +
33
35
  " '-fno-ident', '-ffunction-sections', '-fdata-sections',\n ...darwinReleaseCompileArgs,\n uSocketsArchive,\n '-lm'\n",
34
36
  "compiler/precompile.js":
35
37
  "const fs = require('node:fs');\n" +
@@ -47,6 +49,7 @@ async function fixture(): Promise<{ archive: string; sha256: string }> {
47
49
  " len = i;\n" +
48
50
  " }\n\n arr.length = len;\n\n return new ${name}(arr);\n};\n",
49
51
  "compiler/uwebsockets.js":
52
+ 'export const makeUWebSocketsShimSource = () => `\n#include "App.h"\n' +
50
53
  "static const size_t REQUEST_BODY_MAX_BYTES = 1024u * 1024u;\n" +
51
54
  "int porf_native_fetch_read_value(struct jsval value, const char** out_buf, size_t* out_len, char** out_owned);\n" +
52
55
  "static std::string_view lookup_status_line(i32 status) {\n" +
@@ -199,6 +202,11 @@ test("the default call (no url/expectedSha256 override) never touches the networ
199
202
  },
200
203
  });
201
204
  expect(await readFile(join(dir, "runtime/index.js"), "utf8")).not.toBe("");
205
+ // #236: the native-fetch build edits uWebSockets to accept HTTP/1.0.
206
+ const index = await readFile(join(dir, "compiler/index.js"), "utf8");
207
+ expect(index).toContain("import { sbPatchHttp10 } from './sb-http10.js';");
208
+ expect(index).toContain("const uwsDir = uwebsockets.ensureUWebSockets();\n sbPatchHttp10(uwsDir);");
209
+ expect(await readFile(join(dir, "compiler/sb-http10.js"), "utf8")).toContain("export const sbPatchHttp10");
202
210
  // #235: every compiled unit keeps JavaScript's separate float roundings.
203
211
  expect(await readFile(join(dir, "compiler/index.js"), "utf8")).toContain("'-ffp-contract=off',");
204
212
  // #168: the real vendored source takes the null guard on the `then` probe.
package/src/patch.test.ts CHANGED
@@ -44,7 +44,9 @@ int porf_native_fetch_read_value(jsval value, const char** out_buf, size_t* out_
44
44
  // body limit (#56), the #156 status-line switch, #163's collect_headers, and
45
45
  // #176's read_value forward decl / is_forbidden_response_header /
46
46
  // write_response_value.
47
- const SHIM = `static const size_t REQUEST_BODY_MAX_BYTES = 1024u * 1024u;
47
+ const SHIM = `export const makeUWebSocketsShimSource = () => \`
48
+ #include "App.h"
49
+ static const size_t REQUEST_BODY_MAX_BYTES = 1024u * 1024u;
48
50
 
49
51
  int porf_native_fetch_read_value(struct jsval value, const char** out_buf, size_t* out_len, char** out_owned);
50
52
 
package/src/patch.ts CHANGED
@@ -200,6 +200,61 @@ const FP_CONTRACT_ANCHOR = " '-fno-ident', '-ffunction-sections', '-fdata-s
200
200
  const FP_CONTRACT_INJECT = " '-ffp-contract=off', // sproutboat #235: JS rounds a * b + c twice\n";
201
201
  const FP_CONTRACT_MARKER = "sproutboat #235";
202
202
 
203
+ // baronunread/sproutboat#236: uWebSockets answers any request line that isn't
204
+ // HTTP/1.1 with 505, so a standalone sprout behind nginx's default
205
+ // `proxy_http_version 1.0` failed every request. Its HttpRequest already has
206
+ // isAncient(), which closes the connection after the response; the parser just
207
+ // never sets it. uWebSockets is header-only and lives in Porffor's deps cache,
208
+ // not in the Porffor checkout, so a helper written next to compiler/index.js
209
+ // edits HttpParser.h right before each native-fetch compile (marker-guarded).
210
+ const HTTP10_HELPER = String.raw`// sproutboat #236: accept HTTP/1.0 request lines in uWebSockets.
211
+ import fs from 'node:fs';
212
+
213
+ const MARK = 'sb_http10_request';
214
+ const edits = [
215
+ [
216
+ 'struct HttpParser {',
217
+ '/* sproutboat #236: set by consumeRequestLine for an HTTP/1.0 request line. */\ninline thread_local bool sb_http10_request = false;\n\nstruct HttpParser {'
218
+ ],
219
+ [
220
+ '/* Scan until single SP, assume next is / (origin request) */\n char *start = data;',
221
+ '/* Scan until single SP, assume next is / (origin request) */\n sb_http10_request = false;\n char *start = data;'
222
+ ],
223
+ [
224
+ 'if (memcmp(" HTTP/1.1\\r\\n", data, std::min<unsigned int>(11, (unsigned int) (end - data))) == 0) {\n return nullptr;\n }',
225
+ 'if (memcmp(" HTTP/1.1\\r\\n", data, std::min<unsigned int>(11, (unsigned int) (end - data))) == 0) {\n return nullptr;\n }\n if (memcmp(" HTTP/1.0\\r\\n", data, std::min<unsigned int>(11, (unsigned int) (end - data))) == 0) {\n return nullptr;\n }'
226
+ ],
227
+ [
228
+ 'if (memcmp(" HTTP/1.1\\r\\n", data, 11) == 0) {\n return data + 11;\n }',
229
+ 'if (memcmp(" HTTP/1.1\\r\\n", data, 11) == 0) {\n return data + 11;\n }\n if (memcmp(" HTTP/1.0\\r\\n", data, 11) == 0) {\n sb_http10_request = true;\n return data + 11;\n }'
230
+ ],
231
+ [ 'req->ancientHttp = false;', 'req->ancientHttp = sb_http10_request;' ]
232
+ ];
233
+
234
+ export const sbPatchHttp10 = uwsDir => {
235
+ const file = uwsDir + '/src/HttpParser.h';
236
+ let src = fs.readFileSync(file, 'utf8');
237
+ if (src.includes(MARK)) return;
238
+ for (const [ from, to ] of edits) {
239
+ if (!src.includes(from)) throw new Error('sproutboat #236: uWebSockets HttpParser.h changed, anchor not found: ' + from.slice(0, 60));
240
+ src = src.replace(from, to);
241
+ }
242
+ fs.writeFileSync(file, src);
243
+ };
244
+ `;
245
+ const HTTP10_IMPORT_ANCHOR = "import { hashId } from './modules.js';\n";
246
+ const HTTP10_IMPORT_INJECT = "import { sbPatchHttp10 } from './sb-http10.js'; // sproutboat #236\n";
247
+ const HTTP10_IMPORT_MARKER = "./sb-http10.js";
248
+ const HTTP10_CALL_ANCHOR = " const uwsDir = uwebsockets.ensureUWebSockets();\n";
249
+ const HTTP10_CALL_INJECT = " sbPatchHttp10(uwsDir);\n";
250
+ const HTTP10_CALL_MARKER = "sbPatchHttp10(uwsDir)";
251
+ // The shim is content-cached, so a header edit alone would never recompile it.
252
+ // This line changes the shim once, which rebuilds it against the edited header.
253
+ const HTTP10_SHIM_ANCHOR = 'export const makeUWebSocketsShimSource = () => `\n#include "App.h"\n';
254
+ const HTTP10_SHIM_INJECT =
255
+ 'export const makeUWebSocketsShimSource = () => `\n#include "App.h"\n// sproutboat #236: built against an HttpParser.h that accepts HTTP/1.0\n';
256
+ const HTTP10_SHIM_MARKER = "sproutboat #236: built against";
257
+
203
258
  // baronunread/sproutboat#168: Porffor represents null as an object-typed value
204
259
  // with pointer 0, so __Porffor_promise_resolve's cheap `then` probe treated a
205
260
  // promise resolving to null as an object and looked `then` up at memory offset
@@ -775,6 +830,7 @@ const done = new Set<string>();
775
830
 
776
831
  /** Edits to Porffor's uWebSockets shim: `[marker, anchor, inject, what]`. */
777
832
  const UWS_EDITS = [
833
+ [HTTP10_SHIM_MARKER, HTTP10_SHIM_ANCHOR, HTTP10_SHIM_INJECT, "shim rebuild for HTTP/1.0 (#236)"],
778
834
  [BODY_MARKER, BODY_ANCHOR, BODY_INJECT, "request body limit"],
779
835
  [STATUS_SIG_MARKER, STATUS_SIG_ANCHOR, STATUS_SIG_INJECT, "status-line return type (#156)"],
780
836
  [STATUS_303_MARKER, STATUS_303_ANCHOR, STATUS_303_INJECT, "status-line 303 case (#156)"],
@@ -961,6 +1017,7 @@ export async function patchUwebsockets(root: string): Promise<void> {
961
1017
  }
962
1018
 
963
1019
  async function patchCompilerArgs(root: string): Promise<void> {
1020
+ await writeFile(resolve(root, "compiler/sb-http10.js"), HTTP10_HELPER);
964
1021
  const file = resolve(root, "compiler/index.js");
965
1022
  let src = await readFile(file, "utf8");
966
1023
  let changed = false;
@@ -968,6 +1025,8 @@ async function patchCompilerArgs(root: string): Promise<void> {
968
1025
  [LINK_MARKER, LINK_ANCHOR, LINK_INJECT, "extra link args"],
969
1026
  [CFLAGS_MARKER, CFLAGS_ANCHOR, CFLAGS_INJECT, "extra compiler flags"],
970
1027
  [FP_CONTRACT_MARKER, FP_CONTRACT_ANCHOR, FP_CONTRACT_INJECT, "floating-point contraction"],
1028
+ [HTTP10_IMPORT_MARKER, HTTP10_IMPORT_ANCHOR, HTTP10_IMPORT_INJECT, "HTTP/1.0 helper import (#236)"],
1029
+ [HTTP10_CALL_MARKER, HTTP10_CALL_ANCHOR, HTTP10_CALL_INJECT, "HTTP/1.0 parser edit (#236)"],
971
1030
  ] as const) {
972
1031
  if (src.includes(marker)) continue;
973
1032
  const at = src.indexOf(anchor);