manabrew_art_cache/
server.rs1use std::sync::Arc;
10
11use crate::{key_from_request_path, mime_for, ImageCache};
12
13pub struct ArtServer {
14 pub port: u16,
15 server: Arc<tiny_http::Server>,
18}
19
20impl Drop for ArtServer {
21 fn drop(&mut self) {
22 self.server.unblock();
23 }
24}
25
26impl ArtServer {
27 pub fn spawn(bind_ip: std::net::IpAddr, cache: Arc<ImageCache>) -> Option<ArtServer> {
30 Self::spawn_on(bind_ip, 0, cache)
31 }
32
33 pub fn spawn_on(
35 bind_ip: std::net::IpAddr,
36 port: u16,
37 cache: Arc<ImageCache>,
38 ) -> Option<ArtServer> {
39 let server = Arc::new(tiny_http::Server::http((bind_ip, port)).ok()?);
40 let port = server.server_addr().to_ip()?.port();
41 let accept = server.clone();
42
43 std::thread::spawn(move || {
44 for request in accept.incoming_requests() {
46 serve(request, &cache);
47 }
48 });
49
50 Some(ArtServer { port, server })
51 }
52}
53
54fn serve(request: tiny_http::Request, cache: &ImageCache) {
55 let raw = request.url().to_string();
56 let Some(key) = key_from_request_path(&raw) else {
57 let _ = request.respond(tiny_http::Response::empty(404));
58 return;
59 };
60 let Some(bytes) = cache.read(key) else {
62 let _ = request.respond(tiny_http::Response::empty(404));
63 return;
64 };
65 let mut response = tiny_http::Response::from_data(bytes);
66 for (name, value) in [
67 ("Content-Type", mime_for(key)),
68 ("Access-Control-Allow-Origin", "*"),
69 ("Cross-Origin-Resource-Policy", "cross-origin"),
70 ("Cache-Control", "public, max-age=31536000, immutable"),
71 ] {
72 if let Ok(header) = tiny_http::Header::from_bytes(name.as_bytes(), value.as_bytes()) {
73 response.add_header(header);
74 }
75 }
76 let _ = request.respond(response);
77}
78
79#[cfg(test)]
80mod tests {
81 use super::*;
82 use std::net::{Ipv4Addr, TcpStream};
83 use std::time::{Duration, Instant};
84
85 fn answers(port: u16) -> bool {
86 TcpStream::connect_timeout(
87 &(Ipv4Addr::LOCALHOST, port).into(),
88 Duration::from_millis(250),
89 )
90 .is_ok()
91 }
92
93 #[test]
94 fn a_cached_key_is_served_to_the_network() {
95 let dir = tempfile::tempdir().expect("temp dir");
96 let cache = Arc::new(ImageCache::new(dir.path().to_path_buf()));
97 cache
98 .store("front/a/b/card.jpg", b"pixels", true)
99 .expect("store");
100
101 let server = ArtServer::spawn(Ipv4Addr::LOCALHOST.into(), cache).expect("spawn");
102 let body = get(server.port, "/scryfall-img/front/a/b/card.jpg");
103 assert!(body.contains("200 OK"), "{body}");
104 assert!(body.ends_with("pixels"), "{body}");
105
106 assert!(get(server.port, "/scryfall-img/front/missing.jpg").contains("404"));
108 assert!(get(server.port, "/scryfall-img/../../etc/passwd").contains("404"));
110 }
111
112 fn get(port: u16, path: &str) -> String {
113 use std::io::{Read, Write};
114 let mut stream =
115 TcpStream::connect((Ipv4Addr::LOCALHOST, port)).expect("connect to art server");
116 write!(
117 stream,
118 "GET {path} HTTP/1.1\r\nHost: localhost\r\nConnection: close\r\n\r\n"
119 )
120 .expect("request");
121 let mut body = String::new();
122 let _ = stream.read_to_string(&mut body);
123 body
124 }
125
126 #[test]
129 fn dropping_the_server_stops_the_port_answering() {
130 let dir = tempfile::tempdir().expect("temp dir");
131 let cache = Arc::new(ImageCache::new(dir.path().to_path_buf()));
132
133 let server = ArtServer::spawn(Ipv4Addr::LOCALHOST.into(), cache).expect("spawn art server");
134 let port = server.port;
135 assert!(answers(port), "the listener should be up while the room is");
136
137 drop(server);
138
139 let deadline = Instant::now() + Duration::from_secs(5);
142 while Instant::now() < deadline {
143 if !answers(port) {
144 return;
145 }
146 std::thread::sleep(Duration::from_millis(50));
147 }
148 panic!("port {port} still answering after the ArtServer was dropped");
149 }
150}