mirror of
https://github.com/octoleo/syncthing.git
synced 2024-12-22 19:08:58 +00: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() {
|
func rebuildAssets() {
|
||||||
os.Setenv("SOURCE_DATE_EPOCH", fmt.Sprint(buildStamp()))
|
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() {
|
func lazyRebuildAssets() {
|
||||||
|
@ -12,7 +12,6 @@ import (
|
|||||||
"io"
|
"io"
|
||||||
"io/ioutil"
|
"io/ioutil"
|
||||||
"log"
|
"log"
|
||||||
"mime"
|
|
||||||
"net"
|
"net"
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/url"
|
"net/url"
|
||||||
@ -27,6 +26,7 @@ import (
|
|||||||
"github.com/prometheus/client_golang/prometheus"
|
"github.com/prometheus/client_golang/prometheus"
|
||||||
"github.com/prometheus/client_golang/prometheus/promhttp"
|
"github.com/prometheus/client_golang/prometheus/promhttp"
|
||||||
"github.com/syncthing/syncthing/cmd/strelaypoolsrv/auto"
|
"github.com/syncthing/syncthing/cmd/strelaypoolsrv/auto"
|
||||||
|
"github.com/syncthing/syncthing/lib/assets"
|
||||||
"github.com/syncthing/syncthing/lib/rand"
|
"github.com/syncthing/syncthing/lib/rand"
|
||||||
"github.com/syncthing/syncthing/lib/relay/client"
|
"github.com/syncthing/syncthing/lib/relay/client"
|
||||||
"github.com/syncthing/syncthing/lib/sync"
|
"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) {
|
func handleAssets(w http.ResponseWriter, r *http.Request) {
|
||||||
w.Header().Set("Cache-Control", "no-cache, must-revalidate")
|
w.Header().Set("Cache-Control", "no-cache, must-revalidate")
|
||||||
|
|
||||||
assets := auto.Assets()
|
|
||||||
path := r.URL.Path[1:]
|
path := r.URL.Path[1:]
|
||||||
if path == "" {
|
if path == "" {
|
||||||
path = "index.html"
|
path = "index.html"
|
||||||
}
|
}
|
||||||
|
|
||||||
bs, ok := assets[path]
|
content, ok := auto.Assets()[path]
|
||||||
if !ok {
|
if !ok {
|
||||||
w.WriteHeader(http.StatusNotFound)
|
w.WriteHeader(http.StatusNotFound)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
etag := fmt.Sprintf("%d", auto.Generated)
|
assets.Serve(w, r, assets.Asset{
|
||||||
modified := time.Unix(auto.Generated, 0).UTC()
|
ContentGz: content,
|
||||||
|
Filename: path,
|
||||||
w.Header().Set("Last-Modified", modified.Format(http.TimeFormat))
|
Modified: time.Unix(auto.Generated, 0).UTC(),
|
||||||
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)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func handleRequest(w http.ResponseWriter, r *http.Request) {
|
func handleRequest(w http.ResponseWriter, r *http.Request) {
|
||||||
|
@ -7,18 +7,15 @@
|
|||||||
package api
|
package api
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"compress/gzip"
|
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
|
||||||
"mime"
|
|
||||||
"net/http"
|
"net/http"
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"strconv"
|
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"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/config"
|
||||||
"github.com/syncthing/syncthing/lib/sync"
|
"github.com/syncthing/syncthing/lib/sync"
|
||||||
)
|
)
|
||||||
@ -111,7 +108,7 @@ func (s *staticsServer) serveAsset(w http.ResponseWriter, r *http.Request) {
|
|||||||
if s.assetDir != "" {
|
if s.assetDir != "" {
|
||||||
p := filepath.Join(s.assetDir, theme, filepath.FromSlash(file))
|
p := filepath.Join(s.assetDir, theme, filepath.FromSlash(file))
|
||||||
if _, err := os.Stat(p); err == nil {
|
if _, err := os.Stat(p); err == nil {
|
||||||
mtype := s.mimeTypeForFile(file)
|
mtype := assets.MimeTypeForFile(file)
|
||||||
if len(mtype) != 0 {
|
if len(mtype) != 0 {
|
||||||
w.Header().Set("Content-Type", mtype)
|
w.Header().Set("Content-Type", mtype)
|
||||||
}
|
}
|
||||||
@ -127,7 +124,7 @@ func (s *staticsServer) serveAsset(w http.ResponseWriter, r *http.Request) {
|
|||||||
if s.assetDir != "" {
|
if s.assetDir != "" {
|
||||||
p := filepath.Join(s.assetDir, config.DefaultTheme, filepath.FromSlash(file))
|
p := filepath.Join(s.assetDir, config.DefaultTheme, filepath.FromSlash(file))
|
||||||
if _, err := os.Stat(p); err == nil {
|
if _, err := os.Stat(p); err == nil {
|
||||||
mtype := s.mimeTypeForFile(file)
|
mtype := assets.MimeTypeForFile(file)
|
||||||
if len(mtype) != 0 {
|
if len(mtype) != 0 {
|
||||||
w.Header().Set("Content-Type", mtype)
|
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())
|
assets.Serve(w, r, assets.Asset{
|
||||||
w.Header().Set("Last-Modified", modificationTime.Format(http.TimeFormat))
|
ContentGz: bs,
|
||||||
w.Header().Set("Etag", etag)
|
Filename: file,
|
||||||
|
Modified: modificationTime,
|
||||||
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()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *staticsServer) serveThemes(w http.ResponseWriter, r *http.Request) {
|
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) {
|
func (s *staticsServer) setTheme(theme string) {
|
||||||
s.mut.Lock()
|
s.mut.Lock()
|
||||||
s.theme = theme
|
s.theme = theme
|
||||||
|
@ -13,7 +13,7 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/syncthing/syncthing/lib/auto"
|
"github.com/syncthing/syncthing/lib/api/auto"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestAssets(t *testing.T) {
|
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,
|
// 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/.
|
// 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 contains auto generated files for web assets.
|
||||||
package auto
|
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