mirror of
https://github.com/syncthing/syncthing.git
synced 2024-11-15 09:58:57 -07:00
cmd/strelaypoolsrv, lib/api: Factor out static asset serving (#6624)
This commit is contained in:
parent
da99203dcd
commit
06365e5635
2
build.go
2
build.go
@ -687,7 +687,7 @@ func listFiles(dir string) []string {
|
||||
|
||||
func rebuildAssets() {
|
||||
os.Setenv("SOURCE_DATE_EPOCH", fmt.Sprint(buildStamp()))
|
||||
runPrint(goCmd, "generate", "github.com/syncthing/syncthing/lib/auto", "github.com/syncthing/syncthing/cmd/strelaypoolsrv/auto")
|
||||
runPrint(goCmd, "generate", "github.com/syncthing/syncthing/lib/api/auto", "github.com/syncthing/syncthing/cmd/strelaypoolsrv/auto")
|
||||
}
|
||||
|
||||
func lazyRebuildAssets() {
|
||||
|
@ -12,7 +12,6 @@ import (
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"log"
|
||||
"mime"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/url"
|
||||
@ -27,6 +26,7 @@ import (
|
||||
"github.com/prometheus/client_golang/prometheus"
|
||||
"github.com/prometheus/client_golang/prometheus/promhttp"
|
||||
"github.com/syncthing/syncthing/cmd/strelaypoolsrv/auto"
|
||||
"github.com/syncthing/syncthing/lib/assets"
|
||||
"github.com/syncthing/syncthing/lib/rand"
|
||||
"github.com/syncthing/syncthing/lib/relay/client"
|
||||
"github.com/syncthing/syncthing/lib/sync"
|
||||
@ -263,78 +263,22 @@ func handleMetrics(w http.ResponseWriter, r *http.Request) {
|
||||
func handleAssets(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Set("Cache-Control", "no-cache, must-revalidate")
|
||||
|
||||
assets := auto.Assets()
|
||||
path := r.URL.Path[1:]
|
||||
if path == "" {
|
||||
path = "index.html"
|
||||
}
|
||||
|
||||
bs, ok := assets[path]
|
||||
content, ok := auto.Assets()[path]
|
||||
if !ok {
|
||||
w.WriteHeader(http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
|
||||
etag := fmt.Sprintf("%d", auto.Generated)
|
||||
modified := time.Unix(auto.Generated, 0).UTC()
|
||||
|
||||
w.Header().Set("Last-Modified", modified.Format(http.TimeFormat))
|
||||
w.Header().Set("Etag", etag)
|
||||
|
||||
mtype := mimeTypeForFile(path)
|
||||
if len(mtype) != 0 {
|
||||
w.Header().Set("Content-Type", mtype)
|
||||
}
|
||||
|
||||
if t, err := time.Parse(http.TimeFormat, r.Header.Get("If-Modified-Since")); err == nil && modified.Add(time.Second).After(t) {
|
||||
w.WriteHeader(http.StatusNotModified)
|
||||
return
|
||||
}
|
||||
|
||||
if match := r.Header.Get("If-None-Match"); match != "" {
|
||||
if strings.Contains(match, etag) {
|
||||
w.WriteHeader(http.StatusNotModified)
|
||||
return
|
||||
}
|
||||
}
|
||||
if strings.Contains(r.Header.Get("Accept-Encoding"), "gzip") {
|
||||
w.Header().Set("Content-Encoding", "gzip")
|
||||
w.Header().Set("Content-Length", strconv.Itoa(len(bs)))
|
||||
io.WriteString(w, bs)
|
||||
} else {
|
||||
// ungzip if browser not send gzip accepted header
|
||||
var gr *gzip.Reader
|
||||
gr, _ = gzip.NewReader(strings.NewReader(bs))
|
||||
io.Copy(w, gr)
|
||||
gr.Close()
|
||||
}
|
||||
}
|
||||
|
||||
func mimeTypeForFile(file string) string {
|
||||
// We use a built in table of the common types since the system
|
||||
// TypeByExtension might be unreliable. But if we don't know, we delegate
|
||||
// to the system.
|
||||
ext := filepath.Ext(file)
|
||||
switch ext {
|
||||
case ".htm", ".html":
|
||||
return "text/html"
|
||||
case ".css":
|
||||
return "text/css"
|
||||
case ".js":
|
||||
return "application/javascript"
|
||||
case ".json":
|
||||
return "application/json"
|
||||
case ".png":
|
||||
return "image/png"
|
||||
case ".ttf":
|
||||
return "application/x-font-ttf"
|
||||
case ".woff":
|
||||
return "application/x-font-woff"
|
||||
case ".svg":
|
||||
return "image/svg+xml"
|
||||
default:
|
||||
return mime.TypeByExtension(ext)
|
||||
}
|
||||
assets.Serve(w, r, assets.Asset{
|
||||
ContentGz: content,
|
||||
Filename: path,
|
||||
Modified: time.Unix(auto.Generated, 0).UTC(),
|
||||
})
|
||||
}
|
||||
|
||||
func handleRequest(w http.ResponseWriter, r *http.Request) {
|
||||
|
@ -7,18 +7,15 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"compress/gzip"
|
||||
"fmt"
|
||||
"io"
|
||||
"mime"
|
||||
"net/http"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/syncthing/syncthing/lib/auto"
|
||||
"github.com/syncthing/syncthing/lib/api/auto"
|
||||
"github.com/syncthing/syncthing/lib/assets"
|
||||
"github.com/syncthing/syncthing/lib/config"
|
||||
"github.com/syncthing/syncthing/lib/sync"
|
||||
)
|
||||
@ -111,7 +108,7 @@ func (s *staticsServer) serveAsset(w http.ResponseWriter, r *http.Request) {
|
||||
if s.assetDir != "" {
|
||||
p := filepath.Join(s.assetDir, theme, filepath.FromSlash(file))
|
||||
if _, err := os.Stat(p); err == nil {
|
||||
mtype := s.mimeTypeForFile(file)
|
||||
mtype := assets.MimeTypeForFile(file)
|
||||
if len(mtype) != 0 {
|
||||
w.Header().Set("Content-Type", mtype)
|
||||
}
|
||||
@ -127,7 +124,7 @@ func (s *staticsServer) serveAsset(w http.ResponseWriter, r *http.Request) {
|
||||
if s.assetDir != "" {
|
||||
p := filepath.Join(s.assetDir, config.DefaultTheme, filepath.FromSlash(file))
|
||||
if _, err := os.Stat(p); err == nil {
|
||||
mtype := s.mimeTypeForFile(file)
|
||||
mtype := assets.MimeTypeForFile(file)
|
||||
if len(mtype) != 0 {
|
||||
w.Header().Set("Content-Type", mtype)
|
||||
}
|
||||
@ -144,39 +141,11 @@ func (s *staticsServer) serveAsset(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
}
|
||||
|
||||
etag := fmt.Sprintf("%d", modificationTime.Unix())
|
||||
w.Header().Set("Last-Modified", modificationTime.Format(http.TimeFormat))
|
||||
w.Header().Set("Etag", etag)
|
||||
|
||||
if t, err := time.Parse(http.TimeFormat, r.Header.Get("If-Modified-Since")); err == nil {
|
||||
if modificationTime.Equal(t) || modificationTime.Before(t) {
|
||||
w.WriteHeader(http.StatusNotModified)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
if match := r.Header.Get("If-None-Match"); match != "" {
|
||||
if strings.Contains(match, etag) {
|
||||
w.WriteHeader(http.StatusNotModified)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
mtype := s.mimeTypeForFile(file)
|
||||
if len(mtype) != 0 {
|
||||
w.Header().Set("Content-Type", mtype)
|
||||
}
|
||||
if strings.Contains(r.Header.Get("Accept-Encoding"), "gzip") {
|
||||
w.Header().Set("Content-Encoding", "gzip")
|
||||
w.Header().Set("Content-Length", strconv.Itoa(len(bs)))
|
||||
io.WriteString(w, bs)
|
||||
} else {
|
||||
// ungzip if browser not send gzip accepted header
|
||||
var gr *gzip.Reader
|
||||
gr, _ = gzip.NewReader(strings.NewReader(bs))
|
||||
io.Copy(w, gr)
|
||||
gr.Close()
|
||||
}
|
||||
assets.Serve(w, r, assets.Asset{
|
||||
ContentGz: bs,
|
||||
Filename: file,
|
||||
Modified: modificationTime,
|
||||
})
|
||||
}
|
||||
|
||||
func (s *staticsServer) serveThemes(w http.ResponseWriter, r *http.Request) {
|
||||
@ -185,33 +154,6 @@ func (s *staticsServer) serveThemes(w http.ResponseWriter, r *http.Request) {
|
||||
})
|
||||
}
|
||||
|
||||
func (s *staticsServer) mimeTypeForFile(file string) string {
|
||||
// We use a built in table of the common types since the system
|
||||
// TypeByExtension might be unreliable. But if we don't know, we delegate
|
||||
// to the system. All our files are UTF-8.
|
||||
ext := filepath.Ext(file)
|
||||
switch ext {
|
||||
case ".htm", ".html":
|
||||
return "text/html; charset=utf-8"
|
||||
case ".css":
|
||||
return "text/css; charset=utf-8"
|
||||
case ".js":
|
||||
return "application/javascript; charset=utf-8"
|
||||
case ".json":
|
||||
return "application/json; charset=utf-8"
|
||||
case ".png":
|
||||
return "image/png"
|
||||
case ".ttf":
|
||||
return "application/x-font-ttf"
|
||||
case ".woff":
|
||||
return "application/x-font-woff"
|
||||
case ".svg":
|
||||
return "image/svg+xml; charset=utf-8"
|
||||
default:
|
||||
return mime.TypeByExtension(ext)
|
||||
}
|
||||
}
|
||||
|
||||
func (s *staticsServer) setTheme(theme string) {
|
||||
s.mut.Lock()
|
||||
s.theme = theme
|
||||
|
@ -13,7 +13,7 @@ import (
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/syncthing/syncthing/lib/auto"
|
||||
"github.com/syncthing/syncthing/lib/api/auto"
|
||||
)
|
||||
|
||||
func TestAssets(t *testing.T) {
|
@ -4,7 +4,7 @@
|
||||
// License, v. 2.0. If a copy of the MPL was not distributed with this file,
|
||||
// You can obtain one at https://mozilla.org/MPL/2.0/.
|
||||
|
||||
//go:generate go run ../../script/genassets.go -o gui.files.go ../../gui
|
||||
//go:generate go run ../../../script/genassets.go -o gui.files.go ../../../gui
|
||||
|
||||
// Package auto contains auto generated files for web assets.
|
||||
package auto
|
97
lib/assets/assets.go
Normal file
97
lib/assets/assets.go
Normal file
@ -0,0 +1,97 @@
|
||||
// Copyright (C) 2014-2020 The Syncthing Authors.
|
||||
//
|
||||
// This Source Code Form is subject to the terms of the Mozilla Public
|
||||
// License, v. 2.0. If a copy of the MPL was not distributed with this file,
|
||||
// You can obtain one at https://mozilla.org/MPL/2.0/.
|
||||
|
||||
// Package assets hold utilities for serving static assets.
|
||||
//
|
||||
// The actual assets live in auto subpackages instead of here,
|
||||
// because the set of assets varies per program.
|
||||
package assets
|
||||
|
||||
import (
|
||||
"compress/gzip"
|
||||
"fmt"
|
||||
"io"
|
||||
"mime"
|
||||
"net/http"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
// Asset is the type of arguments to Serve.
|
||||
type Asset struct {
|
||||
ContentGz string // gzipped contents of asset.
|
||||
Filename string // Original filename, determines Content-Type.
|
||||
Modified time.Time // Determines ETag and Last-Modified.
|
||||
}
|
||||
|
||||
// Serve writes a gzipped asset to w.
|
||||
func Serve(w http.ResponseWriter, r *http.Request, asset Asset) {
|
||||
header := w.Header()
|
||||
|
||||
mtype := MimeTypeForFile(asset.Filename)
|
||||
if mtype != "" {
|
||||
header.Set("Content-Type", mtype)
|
||||
}
|
||||
|
||||
etag := fmt.Sprintf(`"%x"`, asset.Modified.Unix())
|
||||
header.Set("ETag", etag)
|
||||
header.Set("Last-Modified", asset.Modified.Format(http.TimeFormat))
|
||||
|
||||
t, err := time.Parse(http.TimeFormat, r.Header.Get("If-Modified-Since"))
|
||||
if err == nil && !asset.Modified.After(t) {
|
||||
w.WriteHeader(http.StatusNotModified)
|
||||
return
|
||||
}
|
||||
|
||||
if r.Header.Get("If-None-Match") == etag {
|
||||
w.WriteHeader(http.StatusNotModified)
|
||||
return
|
||||
}
|
||||
|
||||
if strings.Contains(r.Header.Get("Accept-Encoding"), "gzip") {
|
||||
header.Set("Content-Encoding", "gzip")
|
||||
header.Set("Content-Length", strconv.Itoa(len(asset.ContentGz)))
|
||||
io.WriteString(w, asset.ContentGz)
|
||||
} else {
|
||||
// gunzip for browsers that don't want gzip.
|
||||
var gr *gzip.Reader
|
||||
gr, _ = gzip.NewReader(strings.NewReader(asset.ContentGz))
|
||||
io.Copy(w, gr)
|
||||
gr.Close()
|
||||
}
|
||||
}
|
||||
|
||||
// MimeTypeForFile returns the appropriate MIME type for an asset,
|
||||
// based on the filename.
|
||||
//
|
||||
// We use a built in table of the common types since the system
|
||||
// TypeByExtension might be unreliable. But if we don't know, we delegate
|
||||
// to the system. All our text files are in UTF-8.
|
||||
func MimeTypeForFile(file string) string {
|
||||
ext := filepath.Ext(file)
|
||||
switch ext {
|
||||
case ".htm", ".html":
|
||||
return "text/html; charset=utf-8"
|
||||
case ".css":
|
||||
return "text/css; charset=utf-8"
|
||||
case ".js":
|
||||
return "application/javascript; charset=utf-8"
|
||||
case ".json":
|
||||
return "application/json; charset=utf-8"
|
||||
case ".png":
|
||||
return "image/png"
|
||||
case ".ttf":
|
||||
return "application/x-font-ttf"
|
||||
case ".woff":
|
||||
return "application/x-font-woff"
|
||||
case ".svg":
|
||||
return "image/svg+xml; charset=utf-8"
|
||||
default:
|
||||
return mime.TypeByExtension(ext)
|
||||
}
|
||||
}
|
103
lib/assets/assets_test.go
Normal file
103
lib/assets/assets_test.go
Normal file
@ -0,0 +1,103 @@
|
||||
// Copyright (C) 2020 The Syncthing Authors.
|
||||
//
|
||||
// This Source Code Form is subject to the terms of the Mozilla Public
|
||||
// License, v. 2.0. If a copy of the MPL was not distributed with this file,
|
||||
// You can obtain one at https://mozilla.org/MPL/2.0/.
|
||||
|
||||
package assets
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"compress/gzip"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
func compress(s string) string {
|
||||
var sb strings.Builder
|
||||
gz := gzip.NewWriter(&sb)
|
||||
|
||||
io.WriteString(gz, s)
|
||||
gz.Close()
|
||||
return sb.String()
|
||||
}
|
||||
|
||||
func decompress(p []byte) (out []byte) {
|
||||
r, err := gzip.NewReader(bytes.NewBuffer(p))
|
||||
if err == nil {
|
||||
out, err = ioutil.ReadAll(r)
|
||||
}
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func TestServe(t *testing.T) {
|
||||
indexHTML := `<html>Hello, world!</html>`
|
||||
indexGz := compress(indexHTML)
|
||||
|
||||
handler := func(w http.ResponseWriter, r *http.Request) {
|
||||
Serve(w, r, Asset{
|
||||
ContentGz: indexGz,
|
||||
Filename: r.URL.Path[1:],
|
||||
Modified: time.Unix(0, 0),
|
||||
})
|
||||
}
|
||||
|
||||
for _, acceptGzip := range []bool{true, false} {
|
||||
r := httptest.NewRequest("GET", "http://localhost/index.html", nil)
|
||||
if acceptGzip {
|
||||
r.Header.Set("accept-encoding", "gzip, deflate")
|
||||
}
|
||||
|
||||
w := httptest.NewRecorder()
|
||||
handler(w, r)
|
||||
res := w.Result()
|
||||
|
||||
if res.StatusCode != http.StatusOK {
|
||||
t.Fatalf("wanted OK, got status %d", res.StatusCode)
|
||||
}
|
||||
if ctype := res.Header.Get("Content-Type"); ctype != "text/html; charset=utf-8" {
|
||||
t.Errorf("unexpected Content-Type %q", ctype)
|
||||
}
|
||||
// ETags must be quoted ASCII strings:
|
||||
// https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/ETag
|
||||
if etag := res.Header.Get("ETag"); etag != `"0"` {
|
||||
t.Errorf("unexpected ETag %q", etag)
|
||||
}
|
||||
|
||||
body, _ := ioutil.ReadAll(res.Body)
|
||||
if acceptGzip {
|
||||
body = decompress(body)
|
||||
}
|
||||
if string(body) != indexHTML {
|
||||
t.Fatalf("unexpected content %q", body)
|
||||
}
|
||||
}
|
||||
|
||||
r := httptest.NewRequest("GET", "http://localhost/index.html", nil)
|
||||
r.Header.Set("if-none-match", `"0"`)
|
||||
w := httptest.NewRecorder()
|
||||
handler(w, r)
|
||||
res := w.Result()
|
||||
|
||||
if res.StatusCode != http.StatusNotModified {
|
||||
t.Fatalf("wanted NotModified, got status %d", res.StatusCode)
|
||||
}
|
||||
|
||||
r = httptest.NewRequest("GET", "http://localhost/index.html", nil)
|
||||
r.Header.Set("if-modified-since", time.Now().Format(http.TimeFormat))
|
||||
w = httptest.NewRecorder()
|
||||
handler(w, r)
|
||||
res = w.Result()
|
||||
|
||||
if res.StatusCode != http.StatusNotModified {
|
||||
t.Fatalf("wanted NotModified, got status %d", res.StatusCode)
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue
Block a user