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 | |
| 12 | use 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 | |
| 39 | use oxedyne_fe2o3_core::{ |
| 40 | file::OsPath, |
| 41 | log::bot::FileConfig, |
| 42 | path::NormalPath, |
| 43 | prelude::*, |
| 44 | }; |
| 45 | use oxedyne_fe2o3_jdat::version::SemVer; |
| 46 | use oxedyne_fe2o3_net::http::encoding; |
| 47 | |
| 48 | use std::{ |
| 49 | collections::{ |
| 50 | BTreeMap, |
| 51 | HashMap, |
| 52 | }, |
| 53 | path::{ |
| 54 | Path, |
| 55 | PathBuf, |
| 56 | }, |
| 57 | sync::{ |
| 58 | Arc, |
| 59 | RwLock, |
| 60 | }, |
| 61 | }; |
| 62 | |
| 63 | use tokio::{ |
| 64 | io::{ |
| 65 | AsyncReadExt, |
| 66 | AsyncWriteExt, |
| 67 | }, |
| 68 | net::{ |
| 69 | TcpListener, |
| 70 | TcpStream, |
| 71 | }, |
| 72 | }; |
| 73 | use 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 | |
| 89 | const HOST: &str = "tiles.test"; |
| 90 | const SENTINEL: &str = "tile-test-sentinel-7f3a"; |
| 91 | |
| 92 | fn 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. |
| 98 | fn 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] |
| 105 | fn 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 | |
| 133 | async 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 | } |