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