Finish up the initial packserver implementation
This commit is contained in:
parent
ea77f42dc3
commit
527b361b51
|
@ -1 +1,2 @@
|
|||
/cmd/htpacker/htpacker
|
||||
/cmd/packserver/packserver
|
||||
|
|
|
@ -0,0 +1,10 @@
|
|||
module github.com/lwithers/htpack/cmd/packserver
|
||||
|
||||
go 1.12
|
||||
|
||||
require (
|
||||
github.com/inconshreveable/mousetrap v1.0.0 // indirect
|
||||
github.com/lwithers/htpack v0.0.0-20190412081623-ea77f42dc393
|
||||
github.com/spf13/cobra v0.0.3
|
||||
github.com/spf13/pflag v1.0.3 // indirect
|
||||
)
|
|
@ -0,0 +1,15 @@
|
|||
github.com/gogo/protobuf v1.2.1 h1:/s5zKNz0uPFCZ5hddgPdo2TK2TVrUNMn0OOX8/aZMTE=
|
||||
github.com/gogo/protobuf v1.2.1/go.mod h1:hp+jE20tsWTFYpLwKvXlhS1hjn+gTNwPg2I6zVXpSg4=
|
||||
github.com/inconshreveable/mousetrap v1.0.0 h1:Z8tu5sraLXCXIcARxBp/8cbvlwVa7Z1NHg9XEKhtSvM=
|
||||
github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8=
|
||||
github.com/kisielk/errcheck v1.1.0/go.mod h1:EZBBE59ingxPouuu3KfxchcWSUPOHkagtvWXihfKN4Q=
|
||||
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
|
||||
github.com/lwithers/htpack v0.0.0-20190412081623-ea77f42dc393 h1:h++VdZ2eeJC9hf+W+LTVsYdYclJZcz6H5DYAMtGfzBA=
|
||||
github.com/lwithers/htpack v0.0.0-20190412081623-ea77f42dc393/go.mod h1:+9noAoJ9IIiHkwn2Z2Po5upZOKItKKFgYr/cMESGYrc=
|
||||
github.com/spf13/cobra v0.0.3 h1:ZlrZ4XsMRm04Fr5pSFxBgfND2EBVa1nLpiy1stUsX/8=
|
||||
github.com/spf13/cobra v0.0.3/go.mod h1:1l0Ry5zgKvJasoi3XT1TypsSe7PqH0Sj9dhYf7v3XqQ=
|
||||
github.com/spf13/pflag v1.0.3 h1:zPAT6CGy6wXeQ7NtTnaTerfKOsV6V6F8agHXFiazDkg=
|
||||
github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4=
|
||||
golang.org/x/sys v0.0.0-20180924175946-90868a75fefd h1:ELJRxcWg6//yYBDjuf/SnMg1+X0jj5+BP5xXF31wl4w=
|
||||
golang.org/x/sys v0.0.0-20180924175946-90868a75fefd/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/tools v0.0.0-20180221164845-07fd8470d635/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
|
@ -0,0 +1,219 @@
|
|||
/*
|
||||
Packserver is a standalone HTTP server that serves up one or more pack files.
|
||||
*/
|
||||
package main
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"github.com/lwithers/htpack"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
var rootCmd = &cobra.Command{
|
||||
Use: "packserver",
|
||||
Short: "packserver is an HTTP server which serves .htpack files",
|
||||
Long: `packserver can efficiently serve a pre-packed file tree over HTTP(S).
|
||||
The files must first have been prepared using the ‘htpacker’ tool.
|
||||
|
||||
In order to use HTTPS, specify the --key (or -k) flag. This should name a
|
||||
PEM-encoded key file. This file may also contain the certificate; if not, then
|
||||
pass the --cert (or -c) flag in addition.
|
||||
|
||||
Pack files may be specified as "/prefix=file", or just as "file" (which implies
|
||||
"/=file"). Any /prefix present in the request URL will be stripped off before
|
||||
searching the .htpack for the named file. Only one .htpack file can be served
|
||||
at a particular prefix, and serving matches the longest (most specific)
|
||||
prefixes first.`,
|
||||
RunE: run,
|
||||
}
|
||||
|
||||
func main() {
|
||||
rootCmd.Flags().StringP("bind", "b", ":8080",
|
||||
"Address to listen on / bind to")
|
||||
rootCmd.Flags().StringP("key", "k", "",
|
||||
"Path to PEM-encoded HTTPS key")
|
||||
rootCmd.Flags().StringP("cert", "c", "",
|
||||
"Path to PEM-encoded HTTPS cert")
|
||||
rootCmd.Flags().StringSliceP("header", "H", nil,
|
||||
"Extra headers; use flag once for each, in form -H header=value")
|
||||
rootCmd.Flags().String("header-file", "",
|
||||
"Path to text file containing one line for each header=value to add")
|
||||
rootCmd.Flags().Duration("expiry", 0,
|
||||
"Tell client how long it can cache data for; 0 means no caching")
|
||||
|
||||
if err := rootCmd.Execute(); err != nil {
|
||||
fmt.Fprintln(os.Stderr, err)
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
||||
|
||||
func run(c *cobra.Command, args []string) error {
|
||||
bindAddr, err := c.Flags().GetString("bind")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// parse TLS arguments
|
||||
keyFile, err := c.Flags().GetString("key")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
certFile, err := c.Flags().GetString("cert")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
switch {
|
||||
case keyFile == "" && certFile == "":
|
||||
// nothing to do
|
||||
case keyFile == "":
|
||||
return errors.New("cannot specify --cert without --key")
|
||||
case certFile == "":
|
||||
certFile = keyFile
|
||||
}
|
||||
|
||||
// parse extra headers
|
||||
extraHeaders := make(http.Header)
|
||||
hdrs, err := c.Flags().GetStringSlice("header")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
for _, hdr := range hdrs {
|
||||
pos := strings.IndexRune(hdr, '=')
|
||||
if pos == -1 {
|
||||
return fmt.Errorf("header %q must be in form "+
|
||||
"name=value", hdr)
|
||||
}
|
||||
extraHeaders.Add(hdr[:pos], hdr[pos+1:])
|
||||
}
|
||||
|
||||
hdrfile, err := c.Flags().GetString("header-file")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if err := loadHeaderFile(hdrfile, extraHeaders); err != nil {
|
||||
fmt.Fprintln(os.Stderr, "--header-file:", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
// parse expiry time
|
||||
// NB: https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Cache-Control
|
||||
expiry, err := c.Flags().GetDuration("expiry")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if expiry <= 0 {
|
||||
extraHeaders.Set("Cache-Control", "no-store")
|
||||
} else {
|
||||
extraHeaders.Set("Cache-Control",
|
||||
fmt.Sprintf("public, max-age=%d", expiry/1e9))
|
||||
}
|
||||
|
||||
// verify .htpack specifications
|
||||
if len(args) == 0 {
|
||||
return errors.New("must specify one or more .htpack files")
|
||||
}
|
||||
|
||||
packPaths := make(map[string]string)
|
||||
for _, arg := range args {
|
||||
prefix, packfile := "/", arg
|
||||
if pos := strings.IndexRune(arg, '='); pos != -1 {
|
||||
prefix, packfile = arg[:pos], arg[pos+1:]
|
||||
}
|
||||
|
||||
prefix = filepath.Clean(prefix)
|
||||
if prefix[0] != '/' {
|
||||
return fmt.Errorf("%s: prefix must start with '/'", arg)
|
||||
}
|
||||
|
||||
if other, used := packPaths[prefix]; used {
|
||||
return fmt.Errorf("%s: prefix %q already used by %s",
|
||||
arg, prefix, other)
|
||||
}
|
||||
packPaths[prefix] = packfile
|
||||
}
|
||||
|
||||
// load packfiles, registering handlers as we go
|
||||
for prefix, packfile := range packPaths {
|
||||
var handler http.Handler
|
||||
handler, err = htpack.New(packfile)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
handler = &addHeaders{
|
||||
extraHeaders: extraHeaders,
|
||||
handler: handler,
|
||||
}
|
||||
|
||||
if prefix != "/" {
|
||||
http.Handle(prefix+"/",
|
||||
http.StripPrefix(prefix, handler))
|
||||
} else {
|
||||
http.Handle("/", handler)
|
||||
}
|
||||
}
|
||||
|
||||
// main server loop
|
||||
if keyFile == "" {
|
||||
err = http.ListenAndServe(bindAddr, nil)
|
||||
} else {
|
||||
err = http.ListenAndServeTLS(bindAddr, certFile, keyFile, nil)
|
||||
}
|
||||
if err != nil {
|
||||
fmt.Fprintln(os.Stderr, err)
|
||||
os.Exit(1)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func loadHeaderFile(hdrfile string, extraHeaders http.Header) error {
|
||||
if hdrfile == "" {
|
||||
return nil
|
||||
}
|
||||
|
||||
f, err := os.Open(hdrfile)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer f.Close()
|
||||
|
||||
scanner := bufio.NewScanner(f)
|
||||
var lineNum int
|
||||
for scanner.Scan() {
|
||||
line := strings.TrimSpace(scanner.Text())
|
||||
lineNum++
|
||||
if line == "" {
|
||||
continue
|
||||
}
|
||||
|
||||
pos := strings.IndexRune(line, '=')
|
||||
if pos == -1 {
|
||||
return fmt.Errorf("%s: line %d: not in form "+
|
||||
"header=value", hdrfile, lineNum)
|
||||
}
|
||||
extraHeaders.Add(line[:pos], line[pos+1:])
|
||||
}
|
||||
if err := scanner.Err(); err != nil {
|
||||
return fmt.Errorf("%s: %v", hdrfile, err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
type addHeaders struct {
|
||||
extraHeaders http.Header
|
||||
handler http.Handler
|
||||
}
|
||||
|
||||
func (ah *addHeaders) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
for name, values := range ah.extraHeaders {
|
||||
w.Header()[name] = append(w.Header()[name], values...)
|
||||
}
|
||||
ah.handler.ServeHTTP(w, r)
|
||||
}
|
Loading…
Reference in New Issue