@fireemu/darwin-arm64 0.43.0 → 0.44.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.
@@ -1,4 +1,4 @@
1
- """The HTTP server for `onRequest` and `onCall` functions.
1
+ """The HTTP server for `onRequest`, `onCall`, task queue and Auth blocking functions.
2
2
 
3
3
  The daemon proxies `/<project>/<region>/<name>/...` here with the `x-fireemu-runner-secret` header
4
4
  (`crates/fireemu-adapter-functions/src/http.rs`, `tools/runner-node/http-admission.mjs`). The
@@ -6,16 +6,22 @@ function sees the path below its mount point, as it does in production, and answ
6
6
  `flask.Request` handlers the SDK wraps. An unhandled error is functions-framework's crash response.
7
7
  """
8
8
 
9
+ import contextlib
9
10
  import hmac
10
11
  import os
12
+ import sys
11
13
 
12
14
  import flask
13
15
  from functions_framework import crash_handler, read_request
14
16
  from werkzeug.serving import WSGIRequestHandler, make_server
15
17
 
18
+ from .diagnostics import ASYNC_HANDLER, describe, is_async_handler
19
+ from .environment import EnvironmentGate
16
20
  from .manifest import DEFAULT_REGION
17
21
 
18
22
  SECRET_ENVIRON_KEY = "HTTP_X_FIREEMU_RUNNER_SECRET"
23
+ # Task queue and Auth blocking functions enter through the HTTP wrapper as well (the Node runner's `signatureType`).
24
+ HTTP_TRIGGERS = frozenset(("http", "tasks", "blockingAuth"))
19
25
  _METHODS = ["GET", "HEAD", "POST", "PUT", "PATCH", "DELETE", "OPTIONS"]
20
26
 
21
27
 
@@ -33,20 +39,68 @@ def _flask_app() -> flask.Flask:
33
39
  return app
34
40
 
35
41
 
42
+ class ResponseBodyFailed(Exception):
43
+ """A response body failed while it was being sent. The details were logged inside the function's environment."""
44
+
45
+
46
+ def _log_failure(name: str, error: BaseException) -> None:
47
+ diagnostic, _ = describe(error)
48
+ sys.stderr.write(f"[functions] {name}: {diagnostic[:8192]}\n")
49
+
50
+
51
+ class _HeldBody:
52
+ """A response body that keeps its function's environment until the body is finished, whatever happens to it.
53
+
54
+ Closing the body, and running the callbacks `Response.call_on_close` registered, is user code: if it raises the
55
+ gate must still be released, or the process is locked with a secret in its environment. Errors are described
56
+ here, while the environment is still held, and the server sees only a plain exception.
57
+ """
58
+
59
+ def __init__(self, body, held: contextlib.ExitStack, name: str):
60
+ self._body = body
61
+ self._iterator = iter(body)
62
+ self._held = held
63
+ self._name = name
64
+
65
+ def __iter__(self):
66
+ return self
67
+
68
+ def __next__(self):
69
+ try:
70
+ return next(self._iterator)
71
+ except StopIteration:
72
+ raise
73
+ except BaseException as error:
74
+ _log_failure(self._name, error)
75
+ raise ResponseBodyFailed(type(error).__name__) from None
76
+
77
+ def close(self):
78
+ try:
79
+ close = getattr(self._body, "close", None)
80
+ if close is not None:
81
+ close()
82
+ except BaseException as error: # the response has been sent; there is nobody to tell but the log
83
+ _log_failure(self._name, error)
84
+ finally:
85
+ self._held.close()
86
+
87
+
36
88
  class RunnerRouter:
37
89
  """Admission and routing in front of the Flask app."""
38
90
 
39
- def __init__(self, functions: dict, manifest: dict, project: str, secret: str):
91
+ def __init__(self, functions: dict, manifest: dict, project: str, secret: str, gate: EnvironmentGate | None = None):
40
92
  self._app = _flask_app()
93
+ self._gate = gate if gate is not None else EnvironmentGate({})
41
94
  self._project = project
42
95
  self._secret = secret.encode()
43
96
  self._routes = {}
44
97
  for spec in manifest["functions"]:
45
- if spec["trigger"]["type"] == "http":
98
+ if spec["trigger"]["type"] in HTTP_TRIGGERS:
46
99
  self._routes[spec["name"]] = (
47
100
  spec.get("region", DEFAULT_REGION),
48
101
  functions[spec["entryPoint"]],
49
102
  spec["entryPoint"],
103
+ spec.get("platformOptions", {}).get("secrets"),
50
104
  )
51
105
 
52
106
  @staticmethod
@@ -57,6 +111,10 @@ class RunnerRouter:
57
111
 
58
112
  def __call__(self, environ, start_response):
59
113
  presented = environ.pop(SECRET_ENVIRON_KEY, "").encode()
114
+ if not self._secret:
115
+ # A runner with no secret would accept any request without the header (an empty value equals an empty
116
+ # secret). The Node runner answers every request this way (`http-admission.mjs`).
117
+ return self._plain(start_response, "500 Internal Server Error", "FIREEMU_RUNNER_SECRET is required")
60
118
  if not hmac.compare_digest(presented, self._secret):
61
119
  return self._plain(start_response, "403 Forbidden", "forbidden")
62
120
  parts = environ.get("PATH_INFO", "").split("/")
@@ -70,12 +128,26 @@ class RunnerRouter:
70
128
  environ["SCRIPT_NAME"] = environ.get("SCRIPT_NAME", "") + "/".join(parts[:4])
71
129
  environ["PATH_INFO"] = "/" + "/".join(parts[4:])
72
130
  environ["fireemu.function"] = route[1]
73
- # The variables a production instance has from its start name the function it serves. One process
74
- # serves the whole codebase here, so they name the call that started most recently, as for events.
75
- os.environ["FUNCTION_TARGET"] = route[2]
76
- os.environ["FUNCTION_SIGNATURE_TYPE"] = "http"
77
- os.environ["K_SERVICE"] = name
78
- return self._app(environ, start_response)
131
+ if is_async_handler(route[1]):
132
+ # Production answers 500 as well (the coroutine is not a response); this says why.
133
+ sys.stderr.write(f"[functions] {name}: {ASYNC_HANDLER}\n")
134
+ return self._plain(start_response, "500 Internal Server Error", ASYNC_HANDLER)
135
+ # The environment this function is entitled to is held until its response is finished, a streamed body
136
+ # included: the secrets it declares are in the process environment for exactly that long.
137
+ held = contextlib.ExitStack()
138
+ held.enter_context(self._gate.hold(route[3]))
139
+ try:
140
+ # The variables a production instance has from its start name the function it serves. One process
141
+ # serves the whole codebase here, so they name the call that started most recently, as for events.
142
+ os.environ["FUNCTION_TARGET"] = route[2]
143
+ os.environ["FUNCTION_SIGNATURE_TYPE"] = "http"
144
+ os.environ["K_SERVICE"] = name
145
+ body = self._app(environ, start_response)
146
+ except BaseException as error:
147
+ _log_failure(name, error)
148
+ held.close()
149
+ raise ResponseBodyFailed(type(error).__name__) from None
150
+ return _HeldBody(body, held, name)
79
151
 
80
152
 
81
153
  class _QuietHandler(WSGIRequestHandler):
@@ -83,9 +155,9 @@ class _QuietHandler(WSGIRequestHandler):
83
155
  pass
84
156
 
85
157
 
86
- def start_server(functions: dict, manifest: dict, secret: str):
158
+ def start_server(functions: dict, manifest: dict, secret: str, gate: EnvironmentGate | None = None):
87
159
  """Starts the server on a free loopback port; returns `(server, port)`."""
88
- router = RunnerRouter(functions, manifest, os.environ.get("GCLOUD_PROJECT", ""), secret)
160
+ router = RunnerRouter(functions, manifest, os.environ.get("GCLOUD_PROJECT", ""), secret, gate)
89
161
  server = make_server("127.0.0.1", 0, router, threaded=True, request_handler=_QuietHandler)
90
162
  import threading
91
163
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@fireemu/darwin-arm64",
3
- "version": "0.43.0",
3
+ "version": "0.44.0",
4
4
  "description": "The fireemu daemon for darwin-arm64",
5
5
  "homepage": "https://t-k.github.io/fireemu/",
6
6
  "repository": {