Commit Graph
11 Commits
Author SHA1 Message Date
Roni BalthazarandBrandon Thomas 3a870ca3ce fix(ipp): preserve printer group metadata (#8828)
Co-authored-by: Brandon Thomas <echelon@gmail.com>
2026-10-23 17:55:05 -04:00
Dylan M. Taylor 93e605a2e9 security(ipp): total reply deadline; resumable chunk walk 2026-10-23 14:41:49 -04:00
aiongg 934eb71d84 Integration: merge main
crates/engine/src/file_cmds.rs: batch keeps the save-point closure (takes the output path) and also copies the session's authorize hook into each scratch session, as main now does.
crates/raw/src/sensor.rs: both sides cap the memory of retained tiles; kept the existing retained_segment_bytes check (checked multiplication, same limit) and dropped main's duplicate inline loop.
2026-10-22 15:39:51 +00:00
Dylan M. Taylor c3c6c63074 security: encode printer names in IPP request paths 2026-10-22 07:12:25 -04:00
aiongg 11b1594050 Merge PR #7835: Print: the no-listener test takes a port the OS says is free, not port 1 2026-10-22 02:23:02 +00:00
Codex 82a1f54cc8 Print: the no-listener case refuses through a seam instead of a port it hopes is free
The released-port version only proved the port was unused a moment earlier. Between
drop(free) and the connect, another process can be handed that exact ephemeral port,
and the case then reports a protocol error or blocks instead of Unreachable - the
same flake this branch was meant to remove, just quieter.

connect() now goes through open_tcp(host, port, addr, open), a private helper that
takes the connect call as a parameter. The refusal test decides what is closed, so it
owns no port: it asserts Unreachable, that the message names the endpoint, and that
the reason the connect failed survives. The end-to-end "nobody reachable" case now
names a host that can never resolve (RFC 2606) rather than a port nobody owns.

Fake-server protocol tests are unchanged, and no public injection point or dependency
was added.
2026-10-19 18:28:44 -07:00
Codex c06a7384a5 Print: a scoped IPv6 endpoint escapes its zone id in the request authority
`CUPS_SERVER=fe80::1%3:631` parses as `Tcp("fe80::1%3", 631)`, and host() wrote `[fe80::1%3]:631` into the HTTP `Host:` header and into the `ipp://` printer-uri. A bare "%" is not valid inside an authority - RFC 3986 section 3.2.2 reserves "%" for escaping - so a strict HTTP/1.1 server can answer 400 and the printer-uri is not a readable URI.

The zone is now written "%25"-escaped for the wire and kept raw for the socket, which is the only place a scope means anything (measured on Windows: `("fe80::1%3", 631).to_socket_addrs()` gives `[fe80::1%3]:631`).

The two reviews disagreed about the form. Codex-cloud (fork #68) cited RFC 6874; CodeRabbit here pointed out that RFC 9844 (Aug 2025) obsoletes RFC 6874 and suggested dropping the zone from the authority. Dropping it was considered and not taken: it would send `Host: [fe80::1]:631` and `ipp://[fe80::1]:631/...` for a server reached at `fe80::1%3`, naming a different and ambiguous link-local host from the one the socket opened to, and it would rewrite the printer-uri a queued printer is registered under. The escaped form is what CUPS, curl and the ipp:// URIs CUPS writes use, and current URL libraries (yarl) still read "%25" as the URI form of a zone while rejecting a bare "%".

Regressions: numeric zone ids in both authority forms, an interface-name zone, the raw host after parse, and the unchanged IPv6/IPv4/hostname authorities. The printer-uri assertion no longer echoes host() back, so it pins the authority the fake server receives. Reverting the escape fails the new assertion (checked).

Platform limit, unchanged by this commit: an interface-name zone (`fe80::1%eth0`) does not resolve on Windows (getaddrinfo 11001), so only numeric scope ids are usable there, and the client reports that as `Error::Unreachable`.

cargo test on a standalone copy of the crate: 11 passed / 0 failed; cargo clippy --all-targets -- -D warnings and cargo fmt --check clean.
2026-10-19 18:18:55 -07:00
Codex 909e1cdd84 Print: an IPv6 endpoint writes its authority in brackets, and a mistyped one stays unreachable
Codex-cloud's review of this fix found two more places where an IPv6 print server did not work.

host() feeds the Host: header and the ipp:// printer-uri, and it wrote the endpoint the way it is stored: "::1:631". The last colon reads as the port separator, so CUPS got a malformed authority and a malformed printer-uri even though detect() could now connect. An IPv6 host is bracketed there, and in Display, which is what the user is shown; to_socket_addrs still gets the bare address, which is what it needs.

A bracketed endpoint with anything else after the bracket - "[::1]:abc", "[::1]:70000", "[::1]junk" - fell back to the default port, so a mistyped CUPS_SERVER was accepted whenever something answered on 631 and jobs went to an endpoint nobody configured. The whole string now stays the host: it cannot resolve, so detect() reports it unreachable.

Tests: an_ipv6_endpoint_writes_its_authority_in_brackets, and three mistyped endpoints in the parse test. Both fail before this commit and pass after. photocraft-ipp 11 passed / 0 failed; clippy --all-targets -- -D warnings and fmt --check clean.
2026-10-19 12:13:08 -07:00
Codex 094bb727d3 Print: the no-listener test takes a port the OS says is free, not port 1
The last case of error_statuses_and_broken_replies_are_errors called printers(&Endpoint::Tcp("127.0.0.1", 1)) and expected Error::Unreachable. No port is guaranteed closed: on a FreeBSD CI VM something accepted on port 1 and reset the connection, so the client got as far as sending and the test failed with "print server reply: sending: Broken pipe (os error 32)" instead.

Bind 127.0.0.1:0, read its port and drop the listener, so the case uses a port the OS has just released, and keep the assertion. The fake() helper in the same file already takes its port that way.

A held port really does read differently, which is why the old case asserted a property of the machine rather than of the client: against a listener that accepts and closes immediately, printers() returns "print server reply: reading: An existing connection was forcibly closed by the remote host. (os error 10054)" on Windows, not Error::Unreachable.

cargo test -p photocraft-ipp: 9 passed / 0 failed, the changed test five runs in a row; clippy --all-targets -- -D warnings and cargo fmt --check clean.
2026-10-19 11:38:08 -07:00
Codex ababe94f23 Print: an IPv6 print server is parsed as a host, not split on its last colon
Endpoint::parse split the server string on its last ':', which is wrong for an IPv6 literal: "::1" became host ":" on port 1, and "[::1]:631" kept its brackets inside the host, which to_socket_addrs cannot resolve on Linux or macOS. An IPv6 CUPS endpoint was therefore always refused by detect(), even when CUPS was listening on it.

A bracketed server now takes its host from inside the brackets and its port from after them. A bare address with more than one ':' is the host with the default port, because its last ':' cannot be told apart from a port separator; a zone id (fe80::1%eth0) carries no ':', so a port can still follow it. host:port, a bare host and a socket path are unchanged.

Test: "::1", "[::1]", "[::1]:631", "[fe80::1]:6310", "fe80::1%eth0:631", and the cases that already worked. Fail before, pass after.
2026-10-19 11:22:33 -07:00
Diego Chagas 235e7578f3 Print through CUPS over IPP and the print portal, so File › Print works in the Flatpak (#2728) (#3017)
Main prints through craft-print, which runs `lp`; a Flatpak has no `lp`,
so File › Print fails there. This keeps craft-print as the route wherever
`lp` is on the path (and on Windows) and adds two more behind it:

- `photocraft-ipp`, a pure-Rust IPP client (CUPS-Get-Printers/Classes/
  Default, Print-Job over HTTP/1.1) that talks to CUPS over its socket or
  TCP; the Flatpak manifests grant --socket=cups for it. A `printServer`
  named in the request always takes this route. `file.print.printers`
  lists the queues.
- The XDG Print portal as the app's spooler (`Session::print_spooler`),
  for a sandbox without the CUPS socket or a system without a printer:
  the system print dialog, seeded with copies, paper and orientation.
  PHOTOCRAFT_PRINT_PORTAL=1 prefers it, =0 removes it.

A route that exists but fails (unknown printer, refused job) is an error;
only a missing route moves on. Results gain `backend` (lp, windows, ipp,
portal). docs/printing.md describes the routes.
2026-10-17 15:55:54 -06:00