Skip to content

Commit

Permalink
client/web: add support for zst precomppressed assets
Browse files Browse the repository at this point in the history
This will enable us to reduce the size of these embedded assets.

Updates tailscale/corp#20099

Signed-off-by: James Tucker <james@tailscale.com>
  • Loading branch information
raggi committed May 17, 2024
1 parent adb7a86 commit d1f3ef8
Show file tree
Hide file tree
Showing 10 changed files with 171 additions and 25 deletions.
62 changes: 61 additions & 1 deletion client/web/assets.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
package web

import (
"fmt"
"io"
"io/fs"
"log"
Expand All @@ -16,7 +17,9 @@ import (
"strings"
"time"

"github.com/klauspost/compress/zstd"
prebuilt "github.com/tailscale/web-client-prebuilt"
"tailscale.com/tsweb/tswebutil"
)

var start = time.Now()
Expand Down Expand Up @@ -63,7 +66,64 @@ func assetsHandler(devMode bool) (_ http.Handler, cleanup func()) {
}), nil
}

func openPrecompressedFile(w http.ResponseWriter, r *http.Request, path string, fs fs.FS) (fs.File, error) {
type zstFile struct {
f fs.File
*zstd.Decoder
}

func newZSTFile(f fs.File) (*zstFile, error) {
zr, err := zstd.NewReader(f)
if err != nil {
return nil, err
}
return &zstFile{f: f, Decoder: zr}, nil
}

func (z *zstFile) Seek(offset int64, whence int) (int64, error) {
reset := func() error {
if seeker, ok := z.f.(io.Seeker); ok {
seeker.Seek(0, io.SeekStart)
} else {
return fmt.Errorf("not seekable: %w", os.ErrInvalid)
}
return z.Decoder.Reset(z.f)
}

switch whence {
case io.SeekStart:
if err := reset(); err != nil {
return 0, err
}
return io.CopyN(io.Discard, z, offset)
case io.SeekCurrent:
if offset >= 0 {
io.CopyN(io.Discard, z, offset)
} else {
return 0, fmt.Errorf("unsupported negative seek: %w", os.ErrInvalid)
}
case io.SeekEnd:
if offset != 0 {
return 0, fmt.Errorf("unsupported non-zero offset for SeekEnd: %w", os.ErrInvalid)
}
return io.Copy(io.Discard, z)
}
return 0, os.ErrInvalid
}

func (z *zstFile) Close() error {
z.Decoder.Close()
return z.f.Close()
}

func openPrecompressedFile(w http.ResponseWriter, r *http.Request, path string, fs fs.FS) (io.ReadCloser, error) {
if f, err := fs.Open(path + ".zst"); err == nil {
if tswebutil.AcceptsEncoding(r, "zstd") {
w.Header().Set("Content-Encoding", "zstd")
return f, nil
}
return newZSTFile(f)
}
// TODO(raggi): remove this code path when no longer used
if f, err := fs.Open(path + ".gz"); err == nil {
w.Header().Set("Content-Encoding", "gzip")
return f, nil
Expand Down
6 changes: 5 additions & 1 deletion cmd/build-webclient/build-webclient.go
Original file line number Diff line number Diff line change
Expand Up @@ -73,10 +73,14 @@ func build(toolDir, appDir string) error {
if err := os.Remove(f); err != nil {
log.Printf("Failed to cleanup %q: %v", f, err)
}
// Removing intermediate ".br" version, we use ".gz" asset.
// Removing ".br" version, we use the ".zst" asset.
if err := os.Remove(f + ".br"); err != nil {
log.Printf("Failed to cleanup %q: %v", f+".gz", err)
}
// Removing ".gz" version, we use the ".zst" asset.
if err := os.Remove(f + ".gz"); err != nil {
log.Printf("Failed to cleanup %q: %v", f+".gz", err)
}
}

return nil
Expand Down
1 change: 1 addition & 0 deletions cmd/derper/depaware.txt
Original file line number Diff line number Diff line change
Expand Up @@ -122,6 +122,7 @@ tailscale.com/cmd/derper dependencies: (generated by github.com/tailscale/depawa
tailscale.com/tstime/rate from tailscale.com/derp+
tailscale.com/tsweb from tailscale.com/cmd/derper
tailscale.com/tsweb/promvarz from tailscale.com/tsweb
tailscale.com/tsweb/tswebutil from tailscale.com/tsweb
tailscale.com/tsweb/varz from tailscale.com/tsweb+
tailscale.com/types/dnstype from tailscale.com/tailcfg
tailscale.com/types/empty from tailscale.com/ipn
Expand Down
1 change: 1 addition & 0 deletions cmd/stund/depaware.txt
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@ tailscale.com/cmd/stund dependencies: (generated by github.com/tailscale/depawar
tailscale.com/tailcfg from tailscale.com/version
tailscale.com/tsweb from tailscale.com/cmd/stund
tailscale.com/tsweb/promvarz from tailscale.com/tsweb
tailscale.com/tsweb/tswebutil from tailscale.com/tsweb
tailscale.com/tsweb/varz from tailscale.com/tsweb+
tailscale.com/types/dnstype from tailscale.com/tailcfg
tailscale.com/types/ipproto from tailscale.com/tailcfg
Expand Down
8 changes: 8 additions & 0 deletions cmd/tailscale/depaware.txt
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,13 @@ tailscale.com/cmd/tailscale dependencies: (generated by github.com/tailscale/dep
L 💣 github.com/jsimonetti/rtnetlink from tailscale.com/net/netmon
L github.com/jsimonetti/rtnetlink/internal/unix from github.com/jsimonetti/rtnetlink
github.com/kballard/go-shellquote from tailscale.com/cmd/tailscale/cli
github.com/klauspost/compress from github.com/klauspost/compress/zstd
github.com/klauspost/compress/fse from github.com/klauspost/compress/huff0
github.com/klauspost/compress/huff0 from github.com/klauspost/compress/zstd
github.com/klauspost/compress/internal/cpuinfo from github.com/klauspost/compress/huff0+
github.com/klauspost/compress/internal/snapref from github.com/klauspost/compress/zstd
github.com/klauspost/compress/zstd from tailscale.com/client/web
github.com/klauspost/compress/zstd/internal/xxhash from github.com/klauspost/compress/zstd
💣 github.com/mattn/go-colorable from tailscale.com/cmd/tailscale/cli
💣 github.com/mattn/go-isatty from tailscale.com/cmd/tailscale/cli+
L 💣 github.com/mdlayher/netlink from github.com/google/nftables+
Expand Down Expand Up @@ -124,6 +131,7 @@ tailscale.com/cmd/tailscale dependencies: (generated by github.com/tailscale/dep
tailscale.com/tstime from tailscale.com/control/controlhttp+
tailscale.com/tstime/mono from tailscale.com/tstime/rate
tailscale.com/tstime/rate from tailscale.com/cmd/tailscale/cli+
tailscale.com/tswebutil from tailscale.com/client/web
tailscale.com/types/dnstype from tailscale.com/tailcfg
tailscale.com/types/empty from tailscale.com/ipn
tailscale.com/types/ipproto from tailscale.com/net/flowtrack+
Expand Down
3 changes: 2 additions & 1 deletion cmd/tailscaled/depaware.txt
Original file line number Diff line number Diff line change
Expand Up @@ -125,7 +125,7 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de
github.com/klauspost/compress/huff0 from github.com/klauspost/compress/zstd
github.com/klauspost/compress/internal/cpuinfo from github.com/klauspost/compress/huff0+
github.com/klauspost/compress/internal/snapref from github.com/klauspost/compress/zstd
github.com/klauspost/compress/zstd from tailscale.com/util/zstdframe
github.com/klauspost/compress/zstd from tailscale.com/util/zstdframe+
github.com/klauspost/compress/zstd/internal/xxhash from github.com/klauspost/compress/zstd
github.com/kortschak/wol from tailscale.com/ipn/ipnlocal
LD github.com/kr/fs from github.com/pkg/sftp
Expand Down Expand Up @@ -338,6 +338,7 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de
tailscale.com/tstime/mono from tailscale.com/net/tstun+
tailscale.com/tstime/rate from tailscale.com/derp+
tailscale.com/tsweb/varz from tailscale.com/cmd/tailscaled
tailscale.com/tswebutil from tailscale.com/client/web
tailscale.com/types/appctype from tailscale.com/ipn/ipnlocal
tailscale.com/types/dnstype from tailscale.com/ipn/ipnlocal+
tailscale.com/types/empty from tailscale.com/ipn+
Expand Down
22 changes: 3 additions & 19 deletions tsweb/tsweb.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,10 +25,10 @@ import (
"sync"
"time"

"go4.org/mem"
"tailscale.com/envknob"
"tailscale.com/metrics"
"tailscale.com/net/tsaddr"
"tailscale.com/tsweb/tswebutil"
"tailscale.com/tsweb/varz"
"tailscale.com/types/logger"
"tailscale.com/util/vizerror"
Expand Down Expand Up @@ -92,25 +92,9 @@ func allowDebugAccessWithKey(r *http.Request) bool {

// AcceptsEncoding reports whether r accepts the named encoding
// ("gzip", "br", etc).
// deprecated: use tswebutil.AcceptsEncoding instead.
func AcceptsEncoding(r *http.Request, enc string) bool {
h := r.Header.Get("Accept-Encoding")
if h == "" {
return false
}
if !strings.Contains(h, enc) && !mem.ContainsFold(mem.S(h), mem.S(enc)) {
return false
}
remain := h
for len(remain) > 0 {
var part string
part, remain, _ = strings.Cut(remain, ",")
part = strings.TrimSpace(part)
part, _, _ = strings.Cut(part, ";")
if part == enc {
return true
}
}
return false
return tswebutil.AcceptsEncoding(r, enc)
}

// Protected wraps a provided debug handler, h, returning a Handler
Expand Down
35 changes: 35 additions & 0 deletions tsweb/tswebutil/tswebutil.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
// Copyright (c) Tailscale Inc & AUTHORS
// SPDX-License-Identifier: BSD-3-Clause

// Package tswebutil contains helper code used in various Tailscale webservers, without the tsweb kitchen sink.
package tswebutil

import (
"net/http"
"strings"

"go4.org/mem"
)

// AcceptsEncoding reports whether r accepts the named encoding
// ("gzip", "br", etc).
func AcceptsEncoding(r *http.Request, enc string) bool {
h := r.Header.Get("Accept-Encoding")
if h == "" {
return false
}
if !strings.Contains(h, enc) && !mem.ContainsFold(mem.S(h), mem.S(enc)) {
return false
}
remain := h
for len(remain) > 0 {
var part string
part, remain, _ = strings.Cut(remain, ",")
part = strings.TrimSpace(part)
part, _, _ = strings.Cut(part, ";")
if part == enc {
return true
}
}
return false
}
37 changes: 37 additions & 0 deletions tsweb/tswebutil/tswebutil_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
// Copyright (c) Tailscale Inc & AUTHORS
// SPDX-License-Identifier: BSD-3-Clause

package tswebutil

import (
"net/http"
"testing"
)

func TestAcceptsEncoding(t *testing.T) {
tests := []struct {
in, enc string
want bool
}{
{"", "gzip", false},
{"gzip", "gzip", true},
{"foo,gzip", "gzip", true},
{"foo, gzip", "gzip", true},
{"foo, gzip ", "gzip", true},
{"gzip, foo ", "gzip", true},
{"gzip, foo ", "br", false},
{"gzip, foo ", "fo", false},
{"gzip;q=1.2, foo ", "gzip", true},
{" gzip;q=1.2, foo ", "gzip", true},
}
for i, tt := range tests {
h := make(http.Header)
if tt.in != "" {
h.Set("Accept-Encoding", tt.in)
}
got := AcceptsEncoding(&http.Request{Header: h}, tt.enc)
if got != tt.want {
t.Errorf("%d. got %v; want %v", i, got, tt.want)
}
}
}
21 changes: 18 additions & 3 deletions util/precompress/precompress.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,9 @@ import (
"path/filepath"

"github.com/andybalholm/brotli"
"github.com/klauspost/compress/zstd"
"golang.org/x/sync/errgroup"
"tailscale.com/tsweb"
"tailscale.com/tsweb/tswebutil"
)

// PrecompressDir compresses static assets in dirPath using Gzip and Brotli, so
Expand Down Expand Up @@ -63,13 +64,19 @@ type Options struct {
// OpenPrecompressedFile opens a file from fs, preferring compressed versions
// generated by PrecompressDir if possible.
func OpenPrecompressedFile(w http.ResponseWriter, r *http.Request, path string, fs fs.FS) (fs.File, error) {
if tsweb.AcceptsEncoding(r, "br") {
if tswebutil.AcceptsEncoding(r, "zstd") {
if f, err := fs.Open(path + ".zst"); err == nil {
w.Header().Set("Content-Encoding", "zstd")
return f, nil
}
}
if tswebutil.AcceptsEncoding(r, "br") {
if f, err := fs.Open(path + ".br"); err == nil {
w.Header().Set("Content-Encoding", "br")
return f, nil
}
}
if tsweb.AcceptsEncoding(r, "gzip") {
if tswebutil.AcceptsEncoding(r, "gzip") {
if f, err := fs.Open(path + ".gz"); err == nil {
w.Header().Set("Content-Encoding", "gzip")
return f, nil
Expand Down Expand Up @@ -104,6 +111,14 @@ func Precompress(path string, options Options) error {
if err != nil {
return err
}
zstdLevel := zstd.WithEncoderLevel(zstd.SpeedBestCompression)
if options.FastCompression {
zstdLevel = zstd.WithEncoderLevel(zstd.SpeedFastest)
}
err = writeCompressed(contents, func(w io.Writer) (io.WriteCloser, error) {
// Per RFC 8878, encoders should avoid window sizes larger than 8MB, which is the max that Chrome acccepts.
return zstd.NewWriter(w, zstdLevel, zstd.WithWindowSize(8<<20))
}, path+".zst", fi.Mode())
brotliLevel := brotli.BestCompression
if options.FastCompression {
brotliLevel = brotli.BestSpeed
Expand Down

0 comments on commit d1f3ef8

Please sign in to comment.