1// Copyright (C) 2014-2020 The Syncthing Authors. 2// 3// This Source Code Form is subject to the terms of the Mozilla Public 4// License, v. 2.0. If a copy of the MPL was not distributed with this file, 5// You can obtain one at https://mozilla.org/MPL/2.0/. 6 7// Package assets hold utilities for serving static assets. 8// 9// The actual assets live in auto subpackages instead of here, 10// because the set of assets varies per program. 11package assets 12 13import ( 14 "compress/gzip" 15 "fmt" 16 "io" 17 "mime" 18 "net/http" 19 "path/filepath" 20 "strconv" 21 "strings" 22 "time" 23) 24 25// An Asset is an embedded file to be served over HTTP. 26type Asset struct { 27 Content string // Contents of asset, possibly gzipped. 28 Gzipped bool 29 Length int // Length of (decompressed) Content. 30 Filename string // Original filename, determines Content-Type. 31 Modified time.Time // Determines ETag and Last-Modified. 32} 33 34// Serve writes a gzipped asset to w. 35func Serve(w http.ResponseWriter, r *http.Request, asset Asset) { 36 header := w.Header() 37 38 mtype := MimeTypeForFile(asset.Filename) 39 if mtype != "" { 40 header.Set("Content-Type", mtype) 41 } 42 43 etag := fmt.Sprintf(`"%x"`, asset.Modified.Unix()) 44 header.Set("ETag", etag) 45 header.Set("Last-Modified", asset.Modified.Format(http.TimeFormat)) 46 47 t, err := time.Parse(http.TimeFormat, r.Header.Get("If-Modified-Since")) 48 if err == nil && !asset.Modified.After(t) { 49 w.WriteHeader(http.StatusNotModified) 50 return 51 } 52 53 if r.Header.Get("If-None-Match") == etag { 54 w.WriteHeader(http.StatusNotModified) 55 return 56 } 57 58 switch { 59 case !asset.Gzipped: 60 header.Set("Content-Length", strconv.Itoa(len(asset.Content))) 61 io.WriteString(w, asset.Content) 62 case strings.Contains(r.Header.Get("Accept-Encoding"), "gzip"): 63 header.Set("Content-Encoding", "gzip") 64 header.Set("Content-Length", strconv.Itoa(len(asset.Content))) 65 io.WriteString(w, asset.Content) 66 default: 67 header.Set("Content-Length", strconv.Itoa(asset.Length)) 68 // gunzip for browsers that don't want gzip. 69 var gr *gzip.Reader 70 gr, _ = gzip.NewReader(strings.NewReader(asset.Content)) 71 io.Copy(w, gr) 72 gr.Close() 73 } 74} 75 76// MimeTypeForFile returns the appropriate MIME type for an asset, 77// based on the filename. 78// 79// We use a built in table of the common types since the system 80// TypeByExtension might be unreliable. But if we don't know, we delegate 81// to the system. All our text files are in UTF-8. 82func MimeTypeForFile(file string) string { 83 ext := filepath.Ext(file) 84 switch ext { 85 case ".htm", ".html": 86 return "text/html; charset=utf-8" 87 case ".css": 88 return "text/css; charset=utf-8" 89 case ".js": 90 return "application/javascript; charset=utf-8" 91 case ".json": 92 return "application/json; charset=utf-8" 93 case ".png": 94 return "image/png" 95 case ".ttf": 96 return "application/x-font-ttf" 97 case ".woff": 98 return "application/x-font-woff" 99 case ".svg": 100 return "image/svg+xml; charset=utf-8" 101 default: 102 return mime.TypeByExtension(ext) 103 } 104} 105