Skip to content

Commit 2c8336f

Browse files
authored
Merge pull request #40 from ThorstenHans/feature/default-favicon
Add default favicon for favicon.png and favicon.ico
2 parents 308d842 + ea4dd0e commit 2c8336f

File tree

4 files changed

+94
-11
lines changed

4 files changed

+94
-11
lines changed

readme.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,12 @@ files = [{ source = "test", destination = "/" }]
7373
environment = { CUSTOM_404_PATH = "404.html" }
7474
```
7575

76+
### Fallback favicon
77+
78+
If you haven't specified a favicon in your HTML document, `spin-fileserver` will serve the [Spin logo](./spin-favicon.png) as the fallback favicon. The `spin-fileserver` also serves the fallback favicon if the file (called `favicon.ico` or `favicon.png`) specified in your `<link rel="shortcut icon" ...>` element does not exist.
79+
80+
Remember that there are situations where `spin-fileserver` cannot serve the fallback favicon if no `<link rel="shortcut icon" ...>` element is specified. Browsers try to find the favicon in the root directory of the origin (`somedomain.com/favicon.ico`). If the application doesn't listen for requests targeting that route, it can't intercept requests to non-existing favicons.
81+
7682
### Building from source and using
7783

7884
Prerequisites:

spin-favicon.ico

3.76 KB
Binary file not shown.

spin-favicon.png

48.8 KB
Loading

src/lib.rs

Lines changed: 88 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,14 @@ const FALLBACK_PATH_ENV: &str = "FALLBACK_PATH";
3232
const CUSTOM_404_PATH_ENV: &str = "CUSTOM_404_PATH";
3333
/// Directory fallback path (trying to map `/about/` -> `/about/index.html`).
3434
const DIRECTORY_FALLBACK_PATH: &str = "index.html";
35-
35+
// FAVICON_ICO_FILENAME
36+
const FAVICON_ICO_FILENAME: &str = "favicon.ico";
37+
// FAVICON_PNG_FILENAME
38+
const FAVICON_PNG_FILENAME: &str = "favicon.png";
39+
// Fallback favicon.png that is used when user does not supply a custom one
40+
const FALLBACK_FAVICON_PNG: &[u8] = include_bytes!("../spin-favicon.png");
41+
// Fallback favicon.ico that is used when user does not supply a custom one
42+
const FALLBACK_FAVICON_ICO: &[u8] = include_bytes!("../spin-favicon.ico");
3643
/// Common Content Encodings
3744
#[derive(Debug, Eq, PartialEq)]
3845
pub enum ContentEncoding {
@@ -82,19 +89,55 @@ fn serve(req: Request) -> Result<Response> {
8289
// resolve the requested path and then try to read the file
8390
// None should indicate that the file does not exist after attempting fallback paths
8491
let body = match FileServer::resolve(path) {
85-
Some(path) => FileServer::read(&path, &enc).ok(),
86-
None => None,
92+
FileServerPath::Physical(path) => FileServer::read(&path, &enc).ok(),
93+
FileServerPath::Embedded(resource) => ResourceServer::read(resource, &enc),
94+
FileServerPath::None => None,
8795
};
88-
8996
let etag = FileServer::get_etag(body.clone());
9097
FileServer::send(body, path, enc, &etag, if_none_match)
9198
}
9299

100+
struct ResourceServer {}
101+
102+
impl ResourceServer {
103+
fn read(resource: &'static [u8], encoding: &ContentEncoding) -> Option<Bytes> {
104+
match encoding {
105+
ContentEncoding::Brotli => {
106+
let mut r = brotli::CompressorReader::new(resource, 4096, BROTLI_LEVEL, 20);
107+
let mut buf = vec![];
108+
r.read_to_end(&mut buf).ok()?;
109+
Some(buf.into())
110+
}
111+
_ => Some(resource.into()),
112+
}
113+
}
114+
}
115+
116+
#[derive(Debug, Eq, PartialEq)]
117+
enum FileServerPath {
118+
Physical(PathBuf),
119+
Embedded(&'static [u8]),
120+
None,
121+
}
122+
123+
trait IsFavicon {
124+
fn is_favicon(&self) -> bool;
125+
}
126+
127+
impl IsFavicon for PathBuf {
128+
fn is_favicon(&self) -> bool {
129+
match self.clone().file_name() {
130+
Some(s) => s == FAVICON_ICO_FILENAME || s == FAVICON_PNG_FILENAME,
131+
None => false,
132+
}
133+
}
134+
}
135+
93136
struct FileServer;
94137
impl FileServer {
95138
/// Resolve the request path to a file path.
96-
/// Returns `None` if the path does not exist.
97-
fn resolve(req_path: &str) -> Option<PathBuf> {
139+
/// Returns a `FileServerPath` variant.
140+
fn resolve(req_path: &str) -> FileServerPath {
98141
// fallback to index.html if the path is empty
99142
let mut path = if req_path.is_empty() {
100143
PathBuf::from(DIRECTORY_FALLBACK_PATH)
@@ -107,6 +150,17 @@ impl FileServer {
107150
path.push(DIRECTORY_FALLBACK_PATH);
108151
}
109152

153+
// if path doesn't exist and a favicon is requested, return with corresponding embedded resource
154+
if !path.exists() && path.is_favicon() {
155+
return match path.extension() {
156+
Some(os_string) => match os_string.to_str() {
157+
Some("ico") => FileServerPath::Embedded(FALLBACK_FAVICON_ICO),
158+
Some("png") => FileServerPath::Embedded(FALLBACK_FAVICON_PNG),
159+
_ => FileServerPath::None,
160+
},
161+
None => FileServerPath::None,
162+
};
163+
}
110164
// if still haven't found a file, override with the user-configured fallback path
111165
if !path.exists() {
112166
if let Ok(fallback_path) = std::env::var(FALLBACK_PATH_ENV) {
@@ -115,7 +169,7 @@ impl FileServer {
115169
}
116170

117171
if path.exists() {
118-
return Some(path);
172+
return FileServerPath::Physical(path);
119173
}
120174

121175
// check if user configured a custom 404 path
@@ -125,9 +179,9 @@ impl FileServer {
125179
}
126180

127181
if path.exists() {
128-
Some(path)
182+
FileServerPath::Physical(path)
129183
} else {
130-
None
184+
FileServerPath::None
131185
}
132186
}
133187

@@ -149,8 +203,13 @@ impl FileServer {
149203

150204
/// Return the media type of the file based on the path.
151205
fn mime(uri: &str) -> Option<String> {
152-
let guess = mime_guess::from_path(uri);
153-
guess.first().map(|m| m.to_string())
206+
match uri {
207+
FAVICON_ICO_FILENAME => mime_guess::from_ext("ico"),
208+
FAVICON_PNG_FILENAME => mime_guess::from_ext("png"),
209+
_ => mime_guess::from_path(uri),
210+
}
211+
.first()
212+
.map(|m| m.to_string())
154213
}
155214

156215
fn append_headers(
@@ -404,4 +463,22 @@ mod tests {
404463
let rsp = <super::SpinHttp as spin_http::SpinHttp>::handle_http_request(req);
405464
assert_eq!(rsp.status, 200);
406465
}
466+
467+
#[test]
468+
fn test_serve_fallback_favicon() {
469+
let req = spin_http::Request {
470+
method: spin_http::Method::Get,
471+
uri: "http://thisistest.com/".to_string(),
472+
headers: vec![(
473+
PATH_INFO_HEADER.to_string(),
474+
FAVICON_PNG_FILENAME.to_string(),
475+
)],
476+
params: vec![],
477+
body: None,
478+
};
479+
let rsp = <super::SpinHttp as spin_http::SpinHttp>::handle_http_request(req);
480+
481+
assert_eq!(rsp.status, StatusCode::OK);
482+
assert_eq!(rsp.body.unwrap(), FALLBACK_FAVICON_PNG);
483+
}
407484
}

0 commit comments

Comments
 (0)