Skip to content

Commit 18bd131

Browse files
committed
feat: expose real Pingora RSS host APIs
Reimplement the usable request and response hosts against Pingora 0.8.1 RequestHeader and ResponseHeader methods. Keep modeled or async network hosts unbound, and prove header and URI mutations through the loopback proxy test.
1 parent eb71170 commit 18bd131

8 files changed

Lines changed: 428 additions & 19 deletions

File tree

Cargo.lock

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
[package]
2-
name = "rustscript-pingora-gateway-policy"
2+
name = "rustscript-pingora-gateway"
33
version = "0.1.0"
44
edition = "2024"
55
license = "Apache-2.0"

README.md

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,32 @@ RustScript receives the `RequestHeader` owned by the accepted Pingora `Session`.
1919

2020
The project depends on the upstream `pingora` crate with its `proxy` feature. It does not fork or patch Pingora.
2121

22+
## Supported RustScript host API
23+
24+
Every bound host below reads or mutates the live Pingora `RequestHeader` or `ResponseHeader`. Names follow the corresponding Pingora header fields and methods:
25+
26+
| RustScript host | Pingora operation |
27+
| --- | --- |
28+
| `pingora::request::method` | `RequestHeader.method` |
29+
| `pingora::request::path` | `RequestHeader.uri.path()` |
30+
| `pingora::request::query` | `RequestHeader.uri.query()` |
31+
| `pingora::request::uri` | `RequestHeader.uri` |
32+
| `pingora::request::version` | `RequestHeader.version` |
33+
| `pingora::request::header` | `RequestHeader.headers.get()` |
34+
| `pingora::request::insert_header` | `RequestHeader::insert_header()` |
35+
| `pingora::request::append_header` | `RequestHeader::append_header()` |
36+
| `pingora::request::remove_header` | `RequestHeader::remove_header()` |
37+
| `pingora::request::set_method` | `RequestHeader::set_method()` |
38+
| `pingora::request::set_uri` | `RequestHeader::set_uri()` |
39+
| `pingora::response::status` | `ResponseHeader.status` |
40+
| `pingora::response::set_status` | `ResponseHeader::set_status()` |
41+
| `pingora::response::header` | `ResponseHeader.headers.get()` |
42+
| `pingora::response::insert_header` | `ResponseHeader::insert_header()` |
43+
| `pingora::response::append_header` | `ResponseHeader::append_header()` |
44+
| `pingora::response::remove_header` | `ResponseHeader::remove_header()` |
45+
46+
The gateway deliberately does not bind the old modeled `request::id`, `request::scheme`, `request::client_ip`, `request::port`, `tcp`, `tls`, `websocket`, `upstream::send`, or `proxy::pipe` APIs. Their implementations returned hard-coded metadata or changed only in-memory fixture state. Request and response body I/O is also omitted because Pingora exposes it asynchronously through the session and `ProxyHttp` lifecycle. Those operations cannot be implemented truthfully inside the synchronous policy VM host-call boundary.
47+
2248
The policy bytecode is compiled once when the gateway starts. Each request runs a fresh VM with JIT disabled and a fixed fuel budget. Script host calls reject framing and hop-by-hop headers such as `Content-Length`, `Transfer-Encoding`, and `Connection`; local empty responses are emitted with `Content-Length: 0` so the downstream connection can be reused.
2349

2450
## Run a live proxy

scripts/gateway_policy.rss

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
11
use pingora;
22
let method: string = pingora::request::method();
33
let path: string = pingora::request::path();
4+
let query: string = pingora::request::query();
45
let tier: string = pingora::request::header("x-user-tier");
6+
let rewrite: string = pingora::request::header("x-rustscript-rewrite");
57

68
let request_marked: bool = pingora::request::insert_header("x-rustscript-checked", "true");
79
let policy_marked: bool = pingora::response::insert_header("x-rustscript-policy", "gateway_policy");
@@ -11,6 +13,19 @@ if path == "/admin" && tier != "pro" {
1113
pingora::response::insert_header("x-rustscript-deny-reason", "upgrade required");
1214
} else if path == "/canary" && method == "GET" {
1315
pingora::response::insert_header("x-rustscript-upstream", "loopback-upstream");
16+
if rewrite == "true" {
17+
pingora::request::insert_header("x-original-query", query);
18+
pingora::request::append_header("x-request-value", "one");
19+
pingora::request::append_header("x-request-value", "two");
20+
pingora::request::remove_header("x-remove-me");
21+
pingora::request::set_method("POST");
22+
pingora::request::set_uri("/rewritten?source=rustscript");
23+
24+
pingora::response::insert_header("x-original-target", path + "?" + query);
25+
pingora::response::append_header("x-rustscript-policy", "rewritten");
26+
pingora::response::insert_header("x-remove-response", "temporary");
27+
pingora::response::remove_header("x-remove-response");
28+
}
1429
}
1530

16-
pingora::response::status();
31+
pingora::response::status();

src/bin/gateway.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
use std::{env, fs, net::SocketAddr, process};
22

33
use pingora::{proxy::http_proxy_service, server::Server};
4-
use rustscript_pingora_gateway_policy::{ScriptedGatewayPolicy, ScriptedProxy};
4+
use rustscript_pingora_gateway::{ScriptedGatewayPolicy, ScriptedProxy};
55

66
fn usage() -> ! {
77
eprintln!("usage: gateway --upstream HOST:PORT [--listen HOST:PORT] [--script PATH]");

src/lib.rs

Lines changed: 187 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
use std::{cell::RefCell, net::SocketAddr};
1+
use std::{cell::RefCell, collections::HashSet, net::SocketAddr};
22

33
use async_trait::async_trait;
44
use pingora::{
@@ -134,16 +134,22 @@ impl ProxyHttp for ScriptedProxy {
134134
upstream_response: &mut ResponseHeader,
135135
ctx: &mut Self::CTX,
136136
) -> PingoraResult<()> {
137+
let mut inserted = HashSet::new();
137138
for (name, value) in &ctx.response_headers {
138-
upstream_response
139-
.insert_header(name.clone(), value.clone())
140-
.map_err(|err| {
141-
Error::because(
142-
ErrorType::InternalError,
143-
"failed to apply RustScript response header",
144-
err,
145-
)
146-
})?;
139+
let result = if inserted.insert(name.to_ascii_lowercase()) {
140+
upstream_response.insert_header(name.clone(), value.clone())
141+
} else {
142+
upstream_response
143+
.append_header(name.clone(), value.clone())
144+
.map(|_| ())
145+
};
146+
result.map_err(|err| {
147+
Error::because(
148+
ErrorType::InternalError,
149+
"failed to apply RustScript response header",
150+
err,
151+
)
152+
})?;
147153
}
148154
Ok(())
149155
}
@@ -248,6 +254,12 @@ fn bind_pingora_hosts(vm: &mut Vm) {
248254
host::pingora::request_method_host,
249255
);
250256
vm.bind_static_args_function("pingora::request::path", host::pingora::request_path_host);
257+
vm.bind_static_args_function("pingora::request::query", host::pingora::request_query_host);
258+
vm.bind_static_args_function("pingora::request::uri", host::pingora::request_uri_host);
259+
vm.bind_static_args_function(
260+
"pingora::request::version",
261+
host::pingora::request_version_host,
262+
);
251263
vm.bind_static_args_function(
252264
"pingora::request::header",
253265
host::pingora::request_header_host,
@@ -256,6 +268,22 @@ fn bind_pingora_hosts(vm: &mut Vm) {
256268
"pingora::request::insert_header",
257269
host::pingora::request_insert_header_host,
258270
);
271+
vm.bind_static_args_function(
272+
"pingora::request::append_header",
273+
host::pingora::request_append_header_host,
274+
);
275+
vm.bind_static_args_function(
276+
"pingora::request::remove_header",
277+
host::pingora::request_remove_header_host,
278+
);
279+
vm.bind_static_args_function(
280+
"pingora::request::set_method",
281+
host::pingora::request_set_method_host,
282+
);
283+
vm.bind_static_args_function(
284+
"pingora::request::set_uri",
285+
host::pingora::request_set_uri_host,
286+
);
259287
vm.bind_static_args_function(
260288
"pingora::response::set_status",
261289
host::pingora::response_set_status_host,
@@ -268,6 +296,18 @@ fn bind_pingora_hosts(vm: &mut Vm) {
268296
"pingora::response::insert_header",
269297
host::pingora::response_insert_header_host,
270298
);
299+
vm.bind_static_args_function(
300+
"pingora::response::append_header",
301+
host::pingora::response_append_header_host,
302+
);
303+
vm.bind_static_args_function(
304+
"pingora::response::remove_header",
305+
host::pingora::response_remove_header_host,
306+
);
307+
vm.bind_static_args_function(
308+
"pingora::response::header",
309+
host::pingora::response_header_host,
310+
);
271311
}
272312

273313
mod host {
@@ -345,16 +385,46 @@ mod host {
345385
return_one(request_method(args))
346386
}
347387

348-
/// Returns the live Pingora request path.
388+
/// Returns the path component of the live Pingora request URI.
349389
#[pd_host_function(name = "pingora::request::path")]
350390
pub(super) fn request_path_impl() -> VmResult<String> {
351-
with_request(|request| Ok(String::from_utf8_lossy(request.raw_path()).into_owned()))
391+
with_request(|request| Ok(request.uri.path().to_string()))
352392
}
353393

354394
pub(crate) fn request_path_host(args: &[Value]) -> VmResult<CallOutcome> {
355395
return_one(request_path(args))
356396
}
357397

398+
/// Returns the query component of the live Pingora request URI.
399+
#[pd_host_function(name = "pingora::request::query")]
400+
pub(super) fn request_query_impl() -> VmResult<String> {
401+
with_request(|request| Ok(request.uri.query().unwrap_or("").to_string()))
402+
}
403+
404+
pub(crate) fn request_query_host(args: &[Value]) -> VmResult<CallOutcome> {
405+
return_one(request_query(args))
406+
}
407+
408+
/// Returns the live Pingora request URI.
409+
#[pd_host_function(name = "pingora::request::uri")]
410+
pub(super) fn request_uri_impl() -> VmResult<String> {
411+
with_request(|request| Ok(request.uri.to_string()))
412+
}
413+
414+
pub(crate) fn request_uri_host(args: &[Value]) -> VmResult<CallOutcome> {
415+
return_one(request_uri(args))
416+
}
417+
418+
/// Returns the HTTP version of the live Pingora request.
419+
#[pd_host_function(name = "pingora::request::version")]
420+
pub(super) fn request_version_impl() -> VmResult<String> {
421+
with_request(|request| Ok(format!("{:?}", request.version)))
422+
}
423+
424+
pub(crate) fn request_version_host(args: &[Value]) -> VmResult<CallOutcome> {
425+
return_one(request_version(args))
426+
}
427+
358428
/// Reads a header from the live Pingora request.
359429
#[pd_host_function(name = "pingora::request::header")]
360430
pub(super) fn request_header_impl(name: &str) -> VmResult<String> {
@@ -390,6 +460,66 @@ mod host {
390460
return_one(request_insert_header(args))
391461
}
392462

463+
/// Calls Pingora RequestHeader::append_header on the live request.
464+
#[pd_host_function(name = "pingora::request::append_header")]
465+
pub(super) fn request_append_header_impl(name: &str, value: &str) -> VmResult<bool> {
466+
ensure_script_header_allowed(name)?;
467+
with_request(|request| {
468+
request
469+
.append_header(name.to_string(), value.to_string())
470+
.map_err(|err| {
471+
VmError::HostError(format!("Pingora request append_header: {err}"))
472+
})
473+
})
474+
}
475+
476+
pub(crate) fn request_append_header_host(args: &[Value]) -> VmResult<CallOutcome> {
477+
return_one(request_append_header(args))
478+
}
479+
480+
/// Calls Pingora RequestHeader::remove_header on the live request.
481+
#[pd_host_function(name = "pingora::request::remove_header")]
482+
pub(super) fn request_remove_header_impl(name: &str) -> VmResult<bool> {
483+
ensure_script_header_allowed(name)?;
484+
with_request(|request| Ok(request.remove_header(name).is_some()))
485+
}
486+
487+
pub(crate) fn request_remove_header_host(args: &[Value]) -> VmResult<CallOutcome> {
488+
return_one(request_remove_header(args))
489+
}
490+
491+
/// Calls Pingora RequestHeader::set_method on the live request.
492+
#[pd_host_function(name = "pingora::request::set_method")]
493+
pub(super) fn request_set_method_impl(method: &str) -> VmResult<bool> {
494+
with_request(|request| {
495+
let method = method.parse().map_err(|err| {
496+
VmError::HostError(format!("Pingora request invalid method: {err}"))
497+
})?;
498+
request.set_method(method);
499+
Ok(true)
500+
})
501+
}
502+
503+
pub(crate) fn request_set_method_host(args: &[Value]) -> VmResult<CallOutcome> {
504+
return_one(request_set_method(args))
505+
}
506+
507+
/// Calls Pingora RequestHeader::set_uri on the live request.
508+
#[pd_host_function(name = "pingora::request::set_uri")]
509+
pub(super) fn request_set_uri_impl(uri: &str) -> VmResult<bool> {
510+
with_request(|request| {
511+
let uri = uri.parse().map_err(|err| {
512+
VmError::HostError(format!("Pingora request invalid URI: {err}"))
513+
})?;
514+
request.set_uri(uri);
515+
Ok(true)
516+
})
517+
}
518+
519+
pub(crate) fn request_set_uri_host(args: &[Value]) -> VmResult<CallOutcome> {
520+
return_one(request_set_uri(args))
521+
}
522+
393523
/// Calls Pingora ResponseHeader::set_status on the live response.
394524
#[pd_host_function(name = "pingora::response::set_status")]
395525
pub(super) fn response_set_status_impl(status: i64) -> VmResult<bool> {
@@ -417,6 +547,23 @@ mod host {
417547
return_one(response_status(args))
418548
}
419549

550+
/// Reads a header from the live Pingora response.
551+
#[pd_host_function(name = "pingora::response::header")]
552+
pub(super) fn response_header_impl(name: &str) -> VmResult<String> {
553+
with_response(|response| {
554+
Ok(response
555+
.headers
556+
.get(name)
557+
.and_then(|value| value.to_str().ok())
558+
.unwrap_or("")
559+
.to_string())
560+
})
561+
}
562+
563+
pub(crate) fn response_header_host(args: &[Value]) -> VmResult<CallOutcome> {
564+
return_one(response_header(args))
565+
}
566+
420567
/// Calls Pingora ResponseHeader::insert_header on the live response.
421568
#[pd_host_function(name = "pingora::response::insert_header")]
422569
pub(super) fn response_insert_header_impl(name: &str, value: &str) -> VmResult<bool> {
@@ -434,5 +581,33 @@ mod host {
434581
pub(crate) fn response_insert_header_host(args: &[Value]) -> VmResult<CallOutcome> {
435582
return_one(response_insert_header(args))
436583
}
584+
585+
/// Calls Pingora ResponseHeader::append_header on the live response.
586+
#[pd_host_function(name = "pingora::response::append_header")]
587+
pub(super) fn response_append_header_impl(name: &str, value: &str) -> VmResult<bool> {
588+
ensure_script_header_allowed(name)?;
589+
with_response(|response| {
590+
response
591+
.append_header(name.to_string(), value.to_string())
592+
.map_err(|err| {
593+
VmError::HostError(format!("Pingora response append_header: {err}"))
594+
})
595+
})
596+
}
597+
598+
pub(crate) fn response_append_header_host(args: &[Value]) -> VmResult<CallOutcome> {
599+
return_one(response_append_header(args))
600+
}
601+
602+
/// Calls Pingora ResponseHeader::remove_header on the live response.
603+
#[pd_host_function(name = "pingora::response::remove_header")]
604+
pub(super) fn response_remove_header_impl(name: &str) -> VmResult<bool> {
605+
ensure_script_header_allowed(name)?;
606+
with_response(|response| Ok(response.remove_header(name).is_some()))
607+
}
608+
609+
pub(crate) fn response_remove_header_host(args: &[Value]) -> VmResult<CallOutcome> {
610+
return_one(response_remove_header(args))
611+
}
437612
}
438613
}

0 commit comments

Comments
 (0)