mirror of
https://github.com/mountain-loop/yaak.git
synced 2026-08-27 05:44:02 +02:00
Apply send cookies as a delta; judge NAT64/6to4 addresses by the IPv4 they carry
This commit is contained in:
@@ -39,6 +39,7 @@ arguments; `--help` lists them all.
|
|||||||
| `--allow-private-networks` | off | Let sends reach private, loopback and link-local addresses. **Off by default; see below.** |
|
| `--allow-private-networks` | off | Let sends reach private, loopback and link-local addresses. **Off by default; see below.** |
|
||||||
| `--allow-hosts` | empty | Only these hosts (`api.example.com`, `*.example.com`). Empty means any host not denied. |
|
| `--allow-hosts` | empty | Only these hosts (`api.example.com`, `*.example.com`). Empty means any host not denied. |
|
||||||
| `--deny-hosts` | empty | Never these hosts. Checked before the allow list. |
|
| `--deny-hosts` | empty | Never these hosts. Checked before the allow list. |
|
||||||
|
| `--nat64-prefixes` | empty | Network-specific NAT64 prefixes (`/96`) whose embedded IPv4 should be judged; the well-known and local-use prefixes always are. |
|
||||||
| `--max-request-bytes` | 16 MiB | Largest rendered request accepted from the tab. |
|
| `--max-request-bytes` | 16 MiB | Largest rendered request accepted from the tab. |
|
||||||
| `--max-response-bytes` | 64 MiB | Largest upstream body relayed before the send is cut off. |
|
| `--max-response-bytes` | 64 MiB | Largest upstream body relayed before the send is cut off. |
|
||||||
| `--max-timeout-secs` | 60 | Ceiling on a send's timeout; a request asking for more (or none) gets this. |
|
| `--max-timeout-secs` | 60 | Ceiling on a send's timeout; a request asking for more (or none) gets this. |
|
||||||
@@ -55,7 +56,9 @@ sits on. So by default it refuses to connect to:
|
|||||||
- loopback (`127/8`, `::1`), private (`10/8`, `172.16/12`, `192.168/16`,
|
- loopback (`127/8`, `::1`), private (`10/8`, `172.16/12`, `192.168/16`,
|
||||||
`fc00::/7`), link-local (`169.254/16` — where cloud metadata lives — and
|
`fc00::/7`), link-local (`169.254/16` — where cloud metadata lives — and
|
||||||
`fe80::/10`), carrier-grade NAT, multicast, reserved and unspecified ranges,
|
`fe80::/10`), carrier-grade NAT, multicast, reserved and unspecified ranges,
|
||||||
and IPv4 addresses tunnelled inside IPv6 forms (`::ffff:a.b.c.d`, NAT64);
|
and IPv4 addresses carried inside IPv6 forms (`::ffff:a.b.c.d`, the
|
||||||
|
well-known and local-use NAT64 prefixes, 6to4 — plus any NAT64 prefix you
|
||||||
|
name with `--nat64-prefixes`);
|
||||||
- anything not `http://` or `https://`.
|
- anything not `http://` or `https://`.
|
||||||
|
|
||||||
The check runs **on the resolved addresses, after DNS**, for every hop of a
|
The check runs **on the resolved addresses, after DNS**, for every hop of a
|
||||||
|
|||||||
@@ -49,6 +49,13 @@ pub struct Config {
|
|||||||
#[arg(long, env = "YAAK_PROXY_DENY_HOSTS", value_delimiter = ',')]
|
#[arg(long, env = "YAAK_PROXY_DENY_HOSTS", value_delimiter = ',')]
|
||||||
pub deny_hosts: Vec<String>,
|
pub deny_hosts: Vec<String>,
|
||||||
|
|
||||||
|
/// NAT64 prefixes (/96) in use on this network, comma-separated, e.g. `2001:db8:64::`. The
|
||||||
|
/// well-known (64:ff9b::/96) and local-use (64:ff9b:1::/48) prefixes are always recognised;
|
||||||
|
/// a network-specific prefix has to be named here or an IPv6 address under it could reach
|
||||||
|
/// an internal IPv4 host through the translator.
|
||||||
|
#[arg(long, env = "YAAK_PROXY_NAT64_PREFIXES", value_delimiter = ',')]
|
||||||
|
pub nat64_prefixes: Vec<std::net::Ipv6Addr>,
|
||||||
|
|
||||||
/// Largest request the proxy accepts from the tab (the rendered request JSON, body included).
|
/// Largest request the proxy accepts from the tab (the rendered request JSON, body included).
|
||||||
#[arg(long, env = "YAAK_PROXY_MAX_REQUEST_BYTES", default_value_t = 16 * 1024 * 1024)]
|
#[arg(long, env = "YAAK_PROXY_MAX_REQUEST_BYTES", default_value_t = 16 * 1024 * 1024)]
|
||||||
pub max_request_bytes: usize,
|
pub max_request_bytes: usize,
|
||||||
|
|||||||
@@ -26,6 +26,9 @@ pub struct DestinationPolicy {
|
|||||||
allow_private_networks: bool,
|
allow_private_networks: bool,
|
||||||
allow_hosts: Vec<HostPattern>,
|
allow_hosts: Vec<HostPattern>,
|
||||||
deny_hosts: Vec<HostPattern>,
|
deny_hosts: Vec<HostPattern>,
|
||||||
|
/// NAT64 prefixes (/96) in use on this network beyond the well-known ones. An IPv6
|
||||||
|
/// address under one of these is really an IPv4 destination, and is judged as that.
|
||||||
|
nat64_prefixes: Vec<Ipv6Addr>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, Debug)]
|
#[derive(Clone, Debug)]
|
||||||
@@ -60,11 +63,13 @@ impl DestinationPolicy {
|
|||||||
allow_private_networks: bool,
|
allow_private_networks: bool,
|
||||||
allow_hosts: &[String],
|
allow_hosts: &[String],
|
||||||
deny_hosts: &[String],
|
deny_hosts: &[String],
|
||||||
|
nat64_prefixes: &[Ipv6Addr],
|
||||||
) -> Self {
|
) -> Self {
|
||||||
Self {
|
Self {
|
||||||
allow_private_networks,
|
allow_private_networks,
|
||||||
allow_hosts: allow_hosts.iter().filter_map(|h| HostPattern::parse(h)).collect(),
|
allow_hosts: allow_hosts.iter().filter_map(|h| HostPattern::parse(h)).collect(),
|
||||||
deny_hosts: deny_hosts.iter().filter_map(|h| HostPattern::parse(h)).collect(),
|
deny_hosts: deny_hosts.iter().filter_map(|h| HostPattern::parse(h)).collect(),
|
||||||
|
nat64_prefixes: nat64_prefixes.to_vec(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -105,6 +110,17 @@ impl DestinationPolicy {
|
|||||||
if self.allow_private_networks {
|
if self.allow_private_networks {
|
||||||
return Ok(());
|
return Ok(());
|
||||||
}
|
}
|
||||||
|
// An address under a configured NAT64 prefix reaches an IPv4 host; judge that host.
|
||||||
|
if let IpAddr::V6(v6) = ip
|
||||||
|
&& self.nat64_prefixes.iter().any(|p| p.segments()[..6] == v6.segments()[..6])
|
||||||
|
{
|
||||||
|
let v4 = trailing_v4(&v6);
|
||||||
|
if let Some(reason) = non_public_v4(v4) {
|
||||||
|
return Err(format!(
|
||||||
|
"Refusing to connect to {ip} (NAT64 for {v4}): {reason}. This proxy only sends to public addresses"
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
match non_public_reason(ip) {
|
match non_public_reason(ip) {
|
||||||
Some(reason) => Err(format!(
|
Some(reason) => Err(format!(
|
||||||
"Refusing to connect to {ip}: {reason}. This proxy only sends to public addresses"
|
"Refusing to connect to {ip}: {reason}. This proxy only sends to public addresses"
|
||||||
@@ -118,8 +134,10 @@ impl DestinationPolicy {
|
|||||||
///
|
///
|
||||||
/// Every range here is one a hosted relay must never be talked into reaching: the machine
|
/// Every range here is one a hosted relay must never be talked into reaching: the machine
|
||||||
/// itself, the network it sits on, and the link-local range where cloud metadata services
|
/// itself, the network it sits on, and the link-local range where cloud metadata services
|
||||||
/// (169.254.169.254) live. IPv4 addresses tunnelled inside IPv6 forms are unwrapped and judged
|
/// (169.254.169.254) live. IPv4 addresses carried inside IPv6 forms — IPv4-mapped, the
|
||||||
/// as IPv4, since that is what the socket would connect to.
|
/// well-known and local-use NAT64 prefixes, 6to4 — are unwrapped and judged as IPv4, since
|
||||||
|
/// that is where the packets end up. A network-specific NAT64 prefix is not knowable here;
|
||||||
|
/// see `--nat64-prefix`.
|
||||||
pub fn non_public_reason(ip: IpAddr) -> Option<&'static str> {
|
pub fn non_public_reason(ip: IpAddr) -> Option<&'static str> {
|
||||||
match ip {
|
match ip {
|
||||||
IpAddr::V4(v4) => non_public_v4(v4),
|
IpAddr::V4(v4) => non_public_v4(v4),
|
||||||
@@ -127,7 +145,7 @@ pub fn non_public_reason(ip: IpAddr) -> Option<&'static str> {
|
|||||||
if let Some(v4) = v6.to_ipv4_mapped() {
|
if let Some(v4) = v6.to_ipv4_mapped() {
|
||||||
return non_public_v4(v4);
|
return non_public_v4(v4);
|
||||||
}
|
}
|
||||||
if let Some(v4) = nat64_embedded_v4(&v6) {
|
if let Some(v4) = embedded_v4(&v6) {
|
||||||
return non_public_v4(v4);
|
return non_public_v4(v4);
|
||||||
}
|
}
|
||||||
if v6.is_loopback() {
|
if v6.is_loopback() {
|
||||||
@@ -180,15 +198,33 @@ fn non_public_v4(v4: Ipv4Addr) -> Option<&'static str> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// The IPv4 address inside a NAT64 (64:ff9b::/96) address, if this is one.
|
/// The IPv4 address an IPv6 address stands for, when it is one of the standard translation
|
||||||
fn nat64_embedded_v4(v6: &Ipv6Addr) -> Option<Ipv4Addr> {
|
/// forms: NAT64 well-known prefix (64:ff9b::/96), NAT64 local-use prefix (64:ff9b:1::/48,
|
||||||
|
/// RFC 8215), or 6to4 (2002::/16, where the IPv4 sits in the next 32 bits).
|
||||||
|
fn embedded_v4(v6: &Ipv6Addr) -> Option<Ipv4Addr> {
|
||||||
let s = v6.segments();
|
let s = v6.segments();
|
||||||
if s[0] == 0x64 && s[1] == 0xff9b && s[2..6].iter().all(|x| *x == 0) {
|
if s[0] == 0x64 && s[1] == 0xff9b && s[2..6].iter().all(|x| *x == 0) {
|
||||||
let o = v6.octets();
|
return Some(trailing_v4(v6));
|
||||||
Some(Ipv4Addr::new(o[12], o[13], o[14], o[15]))
|
|
||||||
} else {
|
|
||||||
None
|
|
||||||
}
|
}
|
||||||
|
if s[0] == 0x64 && s[1] == 0xff9b && s[2] == 1 {
|
||||||
|
return Some(trailing_v4(v6));
|
||||||
|
}
|
||||||
|
if s[0] == 0x2002 {
|
||||||
|
return Some(Ipv4Addr::new(
|
||||||
|
(s[1] >> 8) as u8,
|
||||||
|
(s[1] & 0xff) as u8,
|
||||||
|
(s[2] >> 8) as u8,
|
||||||
|
(s[2] & 0xff) as u8,
|
||||||
|
));
|
||||||
|
}
|
||||||
|
None
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The last 32 bits of an IPv6 address as an IPv4 address (where every /96 NAT64 prefix
|
||||||
|
/// keeps it).
|
||||||
|
fn trailing_v4(v6: &Ipv6Addr) -> Ipv4Addr {
|
||||||
|
let o = v6.octets();
|
||||||
|
Ipv4Addr::new(o[12], o[13], o[14], o[15])
|
||||||
}
|
}
|
||||||
|
|
||||||
/// An [`HttpSender`] that checks each hop's URL against the policy before delegating.
|
/// An [`HttpSender`] that checks each hop's URL against the policy before delegating.
|
||||||
@@ -273,12 +309,26 @@ mod tests {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn a_configured_nat64_prefix_is_judged_by_the_address_it_carries() {
|
||||||
|
// A made-up global prefix, standing in for whatever the network's translator uses
|
||||||
|
let prefix: Ipv6Addr = "2a02:1234:64::".parse().unwrap();
|
||||||
|
let policy = DestinationPolicy::new(false, &[], &[], &[prefix]);
|
||||||
|
assert!(policy.check_ip(ip("2a02:1234:64::a9fe:a9fe")).is_err(), "metadata behind NAT64");
|
||||||
|
assert!(policy.check_ip(ip("2a02:1234:64::0a00:1")).is_err(), "10.0.0.1 behind NAT64");
|
||||||
|
assert!(policy.check_ip(ip("2a02:1234:64::0101:0101")).is_ok(), "1.1.1.1 behind NAT64");
|
||||||
|
// Without the prefix configured the same address is an ordinary global v6, and passes:
|
||||||
|
// that is exactly the gap the option exists to close.
|
||||||
|
let policy = DestinationPolicy::new(false, &[], &[], &[]);
|
||||||
|
assert!(policy.check_ip(ip("2a02:1234:64::a9fe:a9fe")).is_ok());
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn private_networks_can_be_opted_in() {
|
fn private_networks_can_be_opted_in() {
|
||||||
let policy = DestinationPolicy::new(true, &[], &[]);
|
let policy = DestinationPolicy::new(true, &[], &[], &[]);
|
||||||
assert!(policy.check_url("http://127.0.0.1/").is_ok());
|
assert!(policy.check_url("http://127.0.0.1/").is_ok());
|
||||||
assert!(policy.check_url("http://169.254.169.254/").is_ok());
|
assert!(policy.check_url("http://169.254.169.254/").is_ok());
|
||||||
let policy = DestinationPolicy::new(false, &[], &[]);
|
let policy = DestinationPolicy::new(false, &[], &[], &[]);
|
||||||
assert!(policy.check_url("http://127.0.0.1/").is_err());
|
assert!(policy.check_url("http://127.0.0.1/").is_err());
|
||||||
assert!(policy.check_url("http://[::1]/").is_err());
|
assert!(policy.check_url("http://[::1]/").is_err());
|
||||||
assert!(policy.check_url("http://169.254.169.254/latest/meta-data").is_err());
|
assert!(policy.check_url("http://169.254.169.254/latest/meta-data").is_err());
|
||||||
@@ -290,6 +340,7 @@ mod tests {
|
|||||||
false,
|
false,
|
||||||
&["*.example.com".into(), "api.test".into()],
|
&["*.example.com".into(), "api.test".into()],
|
||||||
&["bad.example.com".into()],
|
&["bad.example.com".into()],
|
||||||
|
&[],
|
||||||
);
|
);
|
||||||
assert!(policy.check_url("https://example.com/").is_ok());
|
assert!(policy.check_url("https://example.com/").is_ok());
|
||||||
assert!(policy.check_url("https://a.b.example.com/").is_ok());
|
assert!(policy.check_url("https://a.b.example.com/").is_ok());
|
||||||
@@ -302,7 +353,7 @@ mod tests {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn only_http_schemes() {
|
fn only_http_schemes() {
|
||||||
let policy = DestinationPolicy::new(true, &[], &[]);
|
let policy = DestinationPolicy::new(true, &[], &[], &[]);
|
||||||
assert!(policy.check_url("ftp://example.com/").is_err());
|
assert!(policy.check_url("ftp://example.com/").is_err());
|
||||||
assert!(policy.check_url("file:///etc/passwd").is_err());
|
assert!(policy.check_url("file:///etc/passwd").is_err());
|
||||||
assert!(policy.check_url("https://example.com/").is_ok());
|
assert!(policy.check_url("https://example.com/").is_ok());
|
||||||
|
|||||||
@@ -53,6 +53,7 @@ async fn main() {
|
|||||||
config.allow_private_networks,
|
config.allow_private_networks,
|
||||||
&config.allow_hosts,
|
&config.allow_hosts,
|
||||||
&config.deny_hosts,
|
&config.deny_hosts,
|
||||||
|
&config.nat64_prefixes,
|
||||||
);
|
);
|
||||||
let state = AppState {
|
let state = AppState {
|
||||||
limits: Arc::new(SendLimits {
|
limits: Arc::new(SendLimits {
|
||||||
|
|||||||
@@ -0,0 +1,107 @@
|
|||||||
|
//! Carrying a send's cookie changes back into a jar.
|
||||||
|
//!
|
||||||
|
//! A send starts from a snapshot of the jar and hands back the jar as the
|
||||||
|
//! transaction left it. Writing that whole result over the jar would also
|
||||||
|
//! write over anything the user changed *while* the send was in flight — a
|
||||||
|
//! cookie edited or deleted in the jar view, or set by another send. So the
|
||||||
|
//! send's contribution is taken as a difference (what it added, changed, or
|
||||||
|
//! removed relative to its snapshot) and applied to whatever the jar holds now.
|
||||||
|
|
||||||
|
use crate::models::{Cookie, CookieDomain};
|
||||||
|
|
||||||
|
/// The identity of a cookie in a jar: two cookies with the same name, domain
|
||||||
|
/// and path are the same cookie, whatever their value or attributes.
|
||||||
|
type CookieKey = (String, CookieDomain, String);
|
||||||
|
|
||||||
|
fn key(c: &Cookie) -> CookieKey {
|
||||||
|
(c.name.clone(), c.domain.clone(), c.path.clone())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Apply the changes between `before` (the snapshot a send started from) and
|
||||||
|
/// `after` (the jar as the send left it) to `current` (the jar as it is now).
|
||||||
|
///
|
||||||
|
/// Cookies the send removed are removed; cookies it added or changed replace
|
||||||
|
/// their counterpart in `current`, or are appended. Cookies the send did not
|
||||||
|
/// touch are left exactly as `current` has them.
|
||||||
|
pub fn apply_cookie_changes(
|
||||||
|
current: Vec<Cookie>,
|
||||||
|
before: &[Cookie],
|
||||||
|
after: &[Cookie],
|
||||||
|
) -> Vec<Cookie> {
|
||||||
|
let removed: Vec<CookieKey> =
|
||||||
|
before.iter().filter(|b| !after.iter().any(|a| key(a) == key(b))).map(key).collect();
|
||||||
|
let changed: Vec<&Cookie> = after.iter().filter(|a| !before.iter().any(|b| b == *a)).collect();
|
||||||
|
|
||||||
|
let mut result: Vec<Cookie> =
|
||||||
|
current.into_iter().filter(|c| !removed.contains(&key(c))).collect();
|
||||||
|
for cookie in changed {
|
||||||
|
match result.iter_mut().find(|c| key(c) == key(cookie)) {
|
||||||
|
Some(existing) => *existing = cookie.clone(),
|
||||||
|
None => result.push(cookie.clone()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
result
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
use crate::models::CookieExpires;
|
||||||
|
|
||||||
|
fn cookie(name: &str, value: &str) -> Cookie {
|
||||||
|
Cookie {
|
||||||
|
name: name.to_string(),
|
||||||
|
value: value.to_string(),
|
||||||
|
domain: CookieDomain::HostOnly("example.com".to_string()),
|
||||||
|
expires: CookieExpires::SessionEnd,
|
||||||
|
path: "/".to_string(),
|
||||||
|
secure: false,
|
||||||
|
http_only: false,
|
||||||
|
same_site: None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn a_send_that_changed_nothing_leaves_the_jar_alone() {
|
||||||
|
let before = vec![cookie("a", "1")];
|
||||||
|
let current = vec![cookie("a", "edited"), cookie("b", "2")];
|
||||||
|
assert_eq!(apply_cookie_changes(current.clone(), &before, &before), current);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn additions_and_changes_land_without_touching_concurrent_edits() {
|
||||||
|
let before = vec![cookie("a", "1"), cookie("b", "2")];
|
||||||
|
let after = vec![cookie("a", "1"), cookie("b", "3"), cookie("c", "4")];
|
||||||
|
// Meanwhile the user edited `a` and added `d`.
|
||||||
|
let current = vec![cookie("a", "edited"), cookie("b", "2"), cookie("d", "5")];
|
||||||
|
assert_eq!(
|
||||||
|
apply_cookie_changes(current, &before, &after),
|
||||||
|
vec![
|
||||||
|
cookie("a", "edited"),
|
||||||
|
cookie("b", "3"),
|
||||||
|
cookie("d", "5"),
|
||||||
|
cookie("c", "4")
|
||||||
|
]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn a_cookie_the_send_removed_is_removed() {
|
||||||
|
let before = vec![cookie("a", "1"), cookie("b", "2")];
|
||||||
|
let after = vec![cookie("b", "2")];
|
||||||
|
let current = vec![cookie("a", "1"), cookie("b", "2"), cookie("c", "3")];
|
||||||
|
assert_eq!(
|
||||||
|
apply_cookie_changes(current, &before, &after),
|
||||||
|
vec![cookie("b", "2"), cookie("c", "3")]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn a_cookie_the_user_deleted_mid_send_stays_deleted_unless_the_send_set_it() {
|
||||||
|
let before = vec![cookie("a", "1")];
|
||||||
|
let after = vec![cookie("a", "1")]; // untouched by the send
|
||||||
|
assert_eq!(apply_cookie_changes(vec![], &before, &after), vec![]);
|
||||||
|
let after = vec![cookie("a", "fresh")]; // the send set it again
|
||||||
|
assert_eq!(apply_cookie_changes(vec![], &before, &after), vec![cookie("a", "fresh")]);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -10,6 +10,7 @@ use yaak_database::SqlitePool;
|
|||||||
|
|
||||||
pub mod blob_manager;
|
pub mod blob_manager;
|
||||||
pub mod client_db;
|
pub mod client_db;
|
||||||
|
pub mod cookies;
|
||||||
mod connection_or_tx;
|
mod connection_or_tx;
|
||||||
pub mod error;
|
pub mod error;
|
||||||
pub mod migrate;
|
pub mod migrate;
|
||||||
|
|||||||
@@ -694,22 +694,22 @@ export function __wbg_versions_215a3ab1c9d5745a(arg0) {
|
|||||||
return ret;
|
return ret;
|
||||||
}
|
}
|
||||||
export function __wbindgen_cast_0000000000000001(arg0, arg1) {
|
export function __wbindgen_cast_0000000000000001(arg0, arg1) {
|
||||||
// Cast intrinsic for `Closure(Closure { owned: true, function: Function { arguments: [Externref], shim_idx: 1116, ret: Result(Unit), inner_ret: Some(Result(Unit)) }, mutable: true }) -> Externref`.
|
// Cast intrinsic for `Closure(Closure { owned: true, function: Function { arguments: [Externref], shim_idx: 1117, ret: Result(Unit), inner_ret: Some(Result(Unit)) }, mutable: true }) -> Externref`.
|
||||||
const ret = makeMutClosure(arg0, arg1, wasm_bindgen__convert__closures_____invoke__ha1c2fa93df0107f3);
|
const ret = makeMutClosure(arg0, arg1, wasm_bindgen__convert__closures_____invoke__ha1c2fa93df0107f3);
|
||||||
return ret;
|
return ret;
|
||||||
}
|
}
|
||||||
export function __wbindgen_cast_0000000000000002(arg0, arg1) {
|
export function __wbindgen_cast_0000000000000002(arg0, arg1) {
|
||||||
// Cast intrinsic for `Closure(Closure { owned: true, function: Function { arguments: [NamedExternref("Event")], shim_idx: 211, ret: Unit, inner_ret: Some(Unit) }, mutable: true }) -> Externref`.
|
// Cast intrinsic for `Closure(Closure { owned: true, function: Function { arguments: [NamedExternref("Event")], shim_idx: 212, ret: Unit, inner_ret: Some(Unit) }, mutable: true }) -> Externref`.
|
||||||
const ret = makeMutClosure(arg0, arg1, wasm_bindgen__convert__closures_____invoke__ha7903b6e296dd8f4);
|
const ret = makeMutClosure(arg0, arg1, wasm_bindgen__convert__closures_____invoke__ha7903b6e296dd8f4);
|
||||||
return ret;
|
return ret;
|
||||||
}
|
}
|
||||||
export function __wbindgen_cast_0000000000000003(arg0, arg1) {
|
export function __wbindgen_cast_0000000000000003(arg0, arg1) {
|
||||||
// Cast intrinsic for `Closure(Closure { owned: true, function: Function { arguments: [NamedExternref("IDBVersionChangeEvent")], shim_idx: 164, ret: Result(Unit), inner_ret: Some(Result(Unit)) }, mutable: true }) -> Externref`.
|
// Cast intrinsic for `Closure(Closure { owned: true, function: Function { arguments: [NamedExternref("IDBVersionChangeEvent")], shim_idx: 83, ret: Result(Unit), inner_ret: Some(Result(Unit)) }, mutable: true }) -> Externref`.
|
||||||
const ret = makeMutClosure(arg0, arg1, wasm_bindgen__convert__closures_____invoke__hdf19cb46f9aecb24);
|
const ret = makeMutClosure(arg0, arg1, wasm_bindgen__convert__closures_____invoke__hdf19cb46f9aecb24);
|
||||||
return ret;
|
return ret;
|
||||||
}
|
}
|
||||||
export function __wbindgen_cast_0000000000000004(arg0, arg1) {
|
export function __wbindgen_cast_0000000000000004(arg0, arg1) {
|
||||||
// Cast intrinsic for `Closure(Closure { owned: true, function: Function { arguments: [], shim_idx: 209, ret: Unit, inner_ret: Some(Unit) }, mutable: true }) -> Externref`.
|
// Cast intrinsic for `Closure(Closure { owned: true, function: Function { arguments: [], shim_idx: 210, ret: Unit, inner_ret: Some(Unit) }, mutable: true }) -> Externref`.
|
||||||
const ret = makeMutClosure(arg0, arg1, wasm_bindgen__convert__closures_____invoke__ha1b480b83daa641f);
|
const ret = makeMutClosure(arg0, arg1, wasm_bindgen__convert__closures_____invoke__ha1b480b83daa641f);
|
||||||
return ret;
|
return ret;
|
||||||
}
|
}
|
||||||
|
|||||||
Binary file not shown.
@@ -29,8 +29,10 @@ use serde::{Deserialize, Serialize};
|
|||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
use wasm_bindgen::prelude::*;
|
use wasm_bindgen::prelude::*;
|
||||||
use yaak_models::blob_manager::{BlobManager, BodyChunk};
|
use yaak_models::blob_manager::{BlobManager, BodyChunk};
|
||||||
|
use yaak_models::cookies::apply_cookie_changes;
|
||||||
use yaak_models::models::{
|
use yaak_models::models::{
|
||||||
AnyModel, CookieJar, HttpRequest, HttpResponseEvent, HttpResponseEventData, HttpSendSettings,
|
AnyModel, Cookie, CookieJar, HttpRequest, HttpResponseEvent, HttpResponseEventData,
|
||||||
|
HttpSendSettings,
|
||||||
};
|
};
|
||||||
use yaak_models::models_ops;
|
use yaak_models::models_ops;
|
||||||
use yaak_models::query_manager::QueryManager;
|
use yaak_models::query_manager::QueryManager;
|
||||||
@@ -214,6 +216,14 @@ struct ResponseIdReq {
|
|||||||
response_id: String,
|
response_id: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Deserialize)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
struct PersistSendCookiesReq {
|
||||||
|
cookie_jar_id: String,
|
||||||
|
before: Vec<Cookie>,
|
||||||
|
after: Vec<Cookie>,
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Deserialize)]
|
#[derive(Deserialize)]
|
||||||
#[serde(rename_all = "camelCase")]
|
#[serde(rename_all = "camelCase")]
|
||||||
struct InsertResponseEventsReq {
|
struct InsertResponseEventsReq {
|
||||||
@@ -341,6 +351,20 @@ fn dispatch(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// The cookies a send set or cleared, applied to the jar as it is *now* rather than
|
||||||
|
// written over it, so an edit made while the send was in flight survives.
|
||||||
|
"web_persist_send_cookies" => {
|
||||||
|
let req: PersistSendCookiesReq = from_js(payload)?;
|
||||||
|
if req.before == req.after {
|
||||||
|
return to_json(());
|
||||||
|
}
|
||||||
|
let db = host.queries.connect();
|
||||||
|
let jar = db.get_cookie_jar(&req.cookie_jar_id).map_err(js_error)?;
|
||||||
|
let cookies = apply_cookie_changes(jar.cookies.clone(), &req.before, &req.after);
|
||||||
|
db.upsert_cookie_jar(&CookieJar { cookies, ..jar }, source).map_err(js_error)?;
|
||||||
|
to_json(())
|
||||||
|
}
|
||||||
|
|
||||||
// The tab's half of the send timeline: the events the proxy streamed back, recorded
|
// The tab's half of the send timeline: the events the proxy streamed back, recorded
|
||||||
// under the response they belong to. Same rows the desktop's send task writes, and the
|
// under the response they belong to. Same rows the desktop's send task writes, and the
|
||||||
// writes fan out to every tab as `model_writes` like any other.
|
// writes fan out to every tab as `model_writes` like any other.
|
||||||
|
|||||||
@@ -305,12 +305,14 @@ class TimelineWriter {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Carry the send's cookie changes into the jar. The worker applies them as a
|
||||||
|
* difference against the jar as it is now (see `apply_cookie_changes` in
|
||||||
|
* yaak-models), so an edit made while the send was in flight survives rather
|
||||||
|
* than being written over by the send's stale snapshot.
|
||||||
|
*/
|
||||||
async function persistCookies(db: WorkerConnection, jar: CookieJar, cookies: Cookie[]): Promise<void> {
|
async function persistCookies(db: WorkerConnection, jar: CookieJar, cookies: Cookie[]): Promise<void> {
|
||||||
// The desktop compares before writing so a jar edited mid-send isn't clobbered
|
await db.rpc("web_persist_send_cookies", { cookieJarId: jar.id, before: jar.cookies, after: cookies });
|
||||||
// by an unchanged copy. Structural equality is enough here: cookies are plain
|
|
||||||
// data and the proxy hands back the whole jar.
|
|
||||||
if (JSON.stringify(cookies) === JSON.stringify(jar.cookies)) return;
|
|
||||||
await db.rpc("models_upsert", { model: { ...jar, cookies } });
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
Reference in New Issue
Block a user