Oregami
Repositories/oxedyne/fe2o3

oxedyne/fe2o3/fe2o3_steel/tests/tiles_https.rs

9.4 KiB, 1 run

created by r1870400018:60846, which is this file's identity for as long as the history lasts, whatever it is later renamed to

download · who wrote it · its history

1//! A tile served end to end: a TLS connection into `handle_https`, a real PMTiles archive
2//! behind the route, and the log file read afterwards for any trace of where the viewer looked.
3//!
4//! The archive is `fe2o3_geom/tests/data/protomaps/sample.pmtiles`, written by the reference
5//! Python writer from Protomaps tiles. The log runs at trace level into a file of its own, so
6//! anything the server would say about the request is there to be found; a sentinel line
7//! logged beside the request proves the file is the one being written.
8//!
9//! [Written with AI entirely](https://need2know.ai/entirely-ai/code)\
10//! Anthropic Claude
11
12use oxedyne_fe2o3_steel::{
13 app::https::AppWebHandler,
14 srv::{
15 api::ApiHandlerRegistry,
16 cfg::{
17 ServerConfig,
18 TileConfig,
19 },
20 context::{
21 self,
22 Protocol,
23 ServerContext,
24 VhostRuntime,
25 },
26 id,
27 tiles::{
28 TileService,
29 TileSource,
30 },
31 webhook::WebhookRegistry,
32 ws::{
33 handler::AppWebSocketHandler,
34 syntax::WebSocketSyntax,
35 },
36 },
37};
38
39use oxedyne_fe2o3_core::{
40 file::OsPath,
41 log::bot::FileConfig,
42 path::NormalPath,
43 prelude::*,
44};
45use oxedyne_fe2o3_jdat::version::SemVer;
46use oxedyne_fe2o3_net::http::encoding;
47
48use std::{
49 collections::{
50 BTreeMap,
51 HashMap,
52 },
53 path::{
54 Path,
55 PathBuf,
56 },
57 sync::{
58 Arc,
59 RwLock,
60 },
61};
62
63use tokio::{
64 io::{
65 AsyncReadExt,
66 AsyncWriteExt,
67 },
68 net::{
69 TcpListener,
70 TcpStream,
71 },
72};
73use tokio_rustls::{
74 rustls::{
75 pki_types::{
76 CertificateDer,
77 PrivateKeyDer,
78 PrivatePkcs8KeyDer,
79 ServerName,
80 },
81 ClientConfig,
82 RootCertStore,
83 ServerConfig as TlsServerConfig,
84 },
85 TlsAcceptor,
86 TlsConnector,
87};
88
89const HOST: &str = "tiles.test";
90const SENTINEL: &str = "tile-test-sentinel-7f3a";
91
92fn data(rel: &str) -> PathBuf {
93 PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("../fe2o3_geom/tests/data/protomaps").join(rel)
94}
95
96/// An empty database map of the type the server holds, taken from the constructor's own
97/// signature, since the tile route never reaches a database.
98fn no_dbs<T>(_: fn(&Path, &[u8]) -> Outcome<T>)
99 -> Arc<RwLock<HashMap<String, (Arc<RwLock<T>>, id::Uid)>>>
100{
101 Arc::new(RwLock::new(HashMap::new()))
102}
103
104#[test]
105fn a_tile_is_served_through_https_and_no_log_line_names_it() -> Outcome<()> {
106 // The log, at trace level, into a file of its own.
107 let dir = std::env::temp_dir().join(fmt!("steel_tiles_https_{}", std::process::id()));
108 res!(std::fs::create_dir_all(&dir), File, Write);
109 let mut log_cfg = log_get_config!();
110 log_cfg.level = res!(LogLevel::from_str("trace"));
111 let file_cfg = FileConfig::new(dir.clone(), "tiles".to_string(), "log".to_string(), 0, None);
112 let log_path = file_cfg.path();
113 log_cfg.file = Some(file_cfg);
114 log_set_config!(log_cfg);
115
116 let runtime = res!(tokio::runtime::Runtime::new(), Init);
117 let outcome = runtime.block_on(async { serve_and_ask().await });
118 log_finish_wait!();
119 let body = res!(outcome);
120
121 let log = res!(std::fs::read_to_string(&log_path), File, Read);
122 let _ = std::fs::remove_dir_all(&dir);
123 assert!(log.contains(SENTINEL), "The log file was not the one written: {:?}", log_path);
124 for needle in ["13/6729/4865", "/t/sample", "6729", "12/2957/2545"] {
125 assert!(!log.contains(needle), "The log names a tile ({}):\n{}", needle, log);
126 }
127 // And the tile itself was the archive's.
128 let want = res!(std::fs::read(data("13_6729_4865.mvt")), File, Read);
129 assert_eq!(body, want);
130 Ok(())
131}
132
133async fn serve_and_ask() -> Outcome<Vec<u8>> {
134 oxedyne_fe2o3_net::tls::ensure_crypto_provider();
135 // A self-signed certificate for the vhost, trusted by the client alone.
136 let cert = res!(rcgen::generate_simple_self_signed(vec![HOST.to_string()]), Init);
137 let der = res!(cert.serialize_der(), Init);
138 let key = PrivateKeyDer::Pkcs8(PrivatePkcs8KeyDer::from(cert.serialize_private_key_der()));
139 let server_tls = res!(TlsServerConfig::builder()
140 .with_no_client_auth()
141 .with_single_cert(vec![CertificateDer::from(der.clone())], key), Init);
142 let mut roots = RootCertStore::empty();
143 res!(roots.add(CertificateDer::from(der)), Init);
144 let client_tls = ClientConfig::builder().with_root_certificates(roots).with_no_client_auth();
145 let tls = (Arc::new(server_tls), Arc::new(client_tls));
146
147 // A vhost carrying only the tile route, over the real sample archive, access log off.
148 let mut builds = BTreeMap::new();
149 builds.insert("sample".to_string(), data("sample.pmtiles"));
150 let tile_cfg = TileConfig {
151 prefix: "/t".to_string(),
152 current: "sample".to_string(),
153 builds,
154 allow_origins: vec!["https://oxegen.io".to_string()],
155 attribution: "© OpenStreetMap".to_string(),
156 };
157 let tiles = res!(TileService::<TileSource>::new(&tile_cfg, HOST, 0, TileSource::open));
158 let cfg = ServerConfig::default();
159 let web_handler: AppWebHandler<HashMap<String, OsPath>> = AppWebHandler::new(
160 cfg.clone(),
161 PathBuf::new(),
162 HashMap::new(),
163 vec![fmt!("index.html")],
164 true,
165 Vec::new(),
166 Vec::new(),
167 Arc::new(WebhookRegistry::new()),
168 Arc::new(ApiHandlerRegistry::new()),
169 None,
170 None,
171 None,
172 None,
173 None,
174 Arc::new(Vec::new()),
175 );
176 let runtime = Arc::new(VhostRuntime {
177 hostnames: vec![HOST.to_string()],
178 web_handler,
179 ws_handler: AppWebSocketHandler::new(None),
180 ws_syntax: res!(WebSocketSyntax::new("steel_ws", &SemVer::new(0, 1, 0), "Tiles test")),
181 redirects: Vec::new(),
182 proxy_routes: Vec::new(),
183 ws_routes: Vec::new(),
184 term_manager: None,
185 uses_sessions: false,
186 permissions_policy: None,
187 tiles: Some(Arc::new(tiles)),
188 access_log: false,
189 });
190 let mut vhosts = HashMap::new();
191 vhosts.insert(HOST.to_string(), runtime);
192 let protocol = Protocol::Web {
193 vhosts: Arc::new(vhosts),
194 default_vhost: HOST.to_string(),
195 dev_mode: true,
196 };
197 let root = std::env::temp_dir().normalise().absolute();
198 let context = ServerContext::new(cfg, root, no_dbs(context::new_db), Vec::new(), protocol,
199 None, None);
200
201 // One request over a fresh TLS connection, answered by `handle_https`: the head, lower
202 // cased, and the body.
203 let fetch = async |path: &str| -> Outcome<(String, Vec<u8>)> {
204 let listener = res!(TcpListener::bind("127.0.0.1:0").await, Network, Init);
205 let addr = res!(listener.local_addr(), Network, Init);
206 let ctx = context.clone();
207 let acceptor = TlsAcceptor::from(tls.0.clone());
208 let server = tokio::spawn(async move {
209 let (tcp, peer) = match listener.accept().await {
210 Ok(c) => c,
211 Err(e) => return Err(err!(e, "Accept failed."; Network)),
212 };
213 let stream = match acceptor.accept(tcp).await {
214 Ok(s) => s,
215 Err(e) => return Err(err!(e, "TLS accept failed."; Network)),
216 };
217 ctx.handle_https(stream, Some(HOST.to_string()), peer).await
218 });
219 let tcp = res!(TcpStream::connect(addr).await, Network, Init);
220 let name = res!(ServerName::try_from(HOST.to_string()), Invalid);
221 let mut stream = res!(TlsConnector::from(tls.1.clone()).connect(name, tcp).await, Network);
222 let request = fmt!("GET {} HTTP/1.1\r\nHost: {}\r\nAccept-Encoding: gzip\r\n\
223 Cookie: session=abc123\r\nConnection: close\r\n\r\n", path, HOST);
224 res!(stream.write_all(request.as_bytes()).await, Network, Write);
225 res!(stream.flush().await, Network, Write);
226 let mut reply = Vec::new();
227 // The server closes after a `Connection: close` request; a close without
228 // close_notify still leaves the bytes read.
229 let _ = stream.read_to_end(&mut reply).await;
230 let _ = server.await;
231 let split = match reply.windows(4).position(|w| w == b"\r\n\r\n") {
232 Some(i) => i,
233 None => return Err(err!("No complete response: {:?}.",
234 String::from_utf8_lossy(&reply); Test)),
235 };
236 Ok((String::from_utf8_lossy(&reply[..split]).to_lowercase(), reply[split + 4..].to_vec()))
237 };
238
239 info!("{}: asking for a tile", SENTINEL);
240 let (head, body) = res!(fetch("/t/sample/13/6729/4865.mvt").await);
241 assert!(head.starts_with("http/1.1 200"), "{}", head);
242 assert!(head.contains("content-encoding: gzip"), "{}", head);
243 assert!(!head.contains("set-cookie"), "{}", head);
244 let plain = res!(encoding::gunzip(&body));
245 // A tile in the run of three the writer made from one stored content: the open ocean the
246 // reference reader decompressed to 75 bytes.
247 let (head2, body2) = res!(fetch("/t/sample/12/2957/2545.mvt").await);
248 assert!(head2.starts_with("http/1.1 200"), "{}", head2);
249 assert_eq!(res!(encoding::gunzip(&body2)).len(), 75);
250 // A tile the archive does not hold is an empty answer, not an error.
251 let (head3, _) = res!(fetch("/t/sample/13/0/0.mvt").await);
252 assert!(head3.starts_with("http/1.1 204"), "{}", head3);
253 info!("{}: done", SENTINEL);
254 Ok(plain)
255}