1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
//! The plain-HTTP view: one file, one link, no client.
//!
//! Every other view in gfeh speaks to software. This one speaks to whoever was sent
//! the URL -- a browser, `curl`, a `<video>` element, something embedded in a mail
//! client -- so its correctness is measured entirely in headers.
//!
//! # What it serves
//!
//! A published file at `/f/{token}`, and nothing else. There is no directory listing,
//! no upload, no path traversal surface, and no way to name an object except by a token
//! somebody deliberately minted for it. That is the whole point of the design: an
//! opaque token is not a path, so a link cannot be edited into a link to something
//! else, and a rename does not break a URL already in circulation.
//!
//! # The headers that matter
//!
//! - **`Last-Modified` is an HTTP-date**, never ISO 8601. Sending the wrong one made an
//! S3 object visible in a listing and unreadable by the AWS SDK for Go. Both formats
//! live in [`gfeh_core`] now so there is one implementation to get right.
//! - **`Range` in all three forms**, including `bytes=-100` meaning the *last* hundred
//! bytes. Reading that as "from byte 100" hands a video player the opening credits
//! when it asked for the index at the end of the file.
//! - **`416` carries `Content-Range: bytes */len`**, which is how a client that asked
//! for too much learns how much there is.
//! - **`ETag` and `If-None-Match`**, answered before the object is opened -- the point
//! of a conditional request is not to move the bytes.
//! - **`If-Modified-Since`**, for the clients that revalidate on a date because that is
//! all they were given. The entity tag wins when both arrive, as RFC 9110 requires: an
//! HTTP-date has one-second resolution, so two writes inside one second share a
//! `Last-Modified` and a client holding the first would be told it is current.
//! - **`Content-Disposition` with a sanitized filename**, because a name is
//! user-controlled and a header injection here would be one in every response.
//!
//! # A link is not a login
//!
//! Requests run as the `public` principal with [`Perm::READ`](gfeh_core::Perm::READ)
//! and nothing else, so the enforcement layer below still has the final say. A revoked
//! link answers `404` rather than `403`: telling the holder that the token is real but
//! switched off is information they should not have.
//!
//! # Example
//!
//! ```
//! use gfeh_core::{Disposition, NodeKind, NodeRef, ObjectStore, OpCtx};
//! use gfeh_http::{Exposed, HttpView, StaticExposures};
//! use gfeh_store::MemStore;
//! use std::sync::Arc;
//!
//! # tokio::runtime::Runtime::new().unwrap().block_on(async {
//! let store = Arc::new(MemStore::new());
//! let cx = OpCtx::system("example");
//!
//! let mut handle = store
//! .create(&cx, &store.root(), "report.pdf", NodeKind::File, Disposition::CreateNew)
//! .await?;
//! handle.write_at(0, bytes::Bytes::from_static(b"%PDF-1.7")).await?;
//! let meta = handle.close().await?;
//!
//! let exposures = StaticExposures::new().with(
//! "s3cr3t-token",
//! Exposed {
//! node: NodeRef::Id(store.partition(), meta.id),
//! filename: None,
//! enabled: true,
//! },
//! );
//!
//! let view = HttpView::builder(store, Arc::new(exposures)).start().await?;
//! let body = reqwest::get(view.url_for("s3cr3t-token")).await?.text().await?;
//! assert_eq!(body, "%PDF-1.7");
//! # Ok::<(), Box<dyn std::error::Error>>(())
//! # }).unwrap();
//! ```
pub use ;
pub use ;