Compare commits
9 commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
eb7059a302 | ||
|
|
8dd8ebacc1 | ||
|
|
7d96b5bdcb | ||
|
|
6c70ddce17 | ||
|
|
357f702925 | ||
|
|
b32ba090a2 | ||
|
|
4ed5335a07 | ||
|
|
96ea58d32b | ||
|
|
01e98aed67 |
12 changed files with 582 additions and 1228 deletions
|
|
@ -1,15 +1,15 @@
|
||||||
module src.lwithers.me.uk/go/htpack/cmd/packserver
|
module src.lwithers.me.uk/go/htpack/cmd/packserver
|
||||||
|
|
||||||
go 1.22
|
go 1.26.0
|
||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/spf13/cobra v1.8.1
|
github.com/spf13/cobra v1.10.2
|
||||||
src.lwithers.me.uk/go/htpack v1.3.3
|
src.lwithers.me.uk/go/htpack v1.5.0
|
||||||
)
|
)
|
||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/gogo/protobuf v1.3.2 // indirect
|
|
||||||
github.com/inconshreveable/mousetrap v1.1.0 // indirect
|
github.com/inconshreveable/mousetrap v1.1.0 // indirect
|
||||||
github.com/spf13/pflag v1.0.5 // indirect
|
github.com/spf13/pflag v1.0.10 // indirect
|
||||||
golang.org/x/sys v0.22.0 // indirect
|
golang.org/x/sys v0.47.0 // indirect
|
||||||
|
google.golang.org/protobuf v1.36.12 // indirect
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -1,45 +1,19 @@
|
||||||
github.com/cpuguy83/go-md2man/v2 v2.0.4/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
|
github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g=
|
||||||
github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q=
|
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
|
||||||
github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q=
|
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
|
||||||
github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
|
github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
|
||||||
github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
|
github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
|
||||||
github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8=
|
|
||||||
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
|
|
||||||
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
|
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
|
||||||
github.com/spf13/cobra v1.8.1 h1:e5/vxKd/rZsfSJMUX1agtjeTDf+qv1/JdBF8gg5k9ZM=
|
github.com/spf13/cobra v1.10.2 h1:DMTTonx5m65Ic0GOoRY2c16WCbHxOOw6xxezuLaBpcU=
|
||||||
github.com/spf13/cobra v1.8.1/go.mod h1:wHxEcudfqmLYa8iTfL+OuZPbBZkmvliBWKIezN3kD9Y=
|
github.com/spf13/cobra v1.10.2/go.mod h1:7C1pvHqHw5A4vrJfjNwvOdzYu0Gml16OCs2GRiTUUS4=
|
||||||
github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=
|
github.com/spf13/pflag v1.0.9/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
|
||||||
github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
|
github.com/spf13/pflag v1.0.10 h1:4EBh2KAYBwaONj6b2Ye1GiHfwjqyROoF4RwYO+vPwFk=
|
||||||
github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
github.com/spf13/pflag v1.0.10/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
|
||||||
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg=
|
||||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
golang.org/x/sys v0.47.0 h1:o7XGOvZQCADBQQ4Y7VNq2dRWQR7JmOUW8Kxx4ZsNgWs=
|
||||||
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
golang.org/x/sys v0.47.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
|
||||||
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
google.golang.org/protobuf v1.36.12 h1:pJOKDDOyeXErUroCihFAd5LQuwXBSpVnKGrj5o/fwxc=
|
||||||
golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
google.golang.org/protobuf v1.36.12/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=
|
||||||
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
|
||||||
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
|
||||||
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
|
||||||
golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
|
||||||
golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
|
|
||||||
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
|
||||||
golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
|
||||||
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
|
||||||
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
|
||||||
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
|
||||||
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
|
||||||
golang.org/x/sys v0.22.0 h1:RI27ohtqKCnwULzJLqkv897zojh5/DwS/ENaMzUOaWI=
|
|
||||||
golang.org/x/sys v0.22.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
|
||||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
|
||||||
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
|
||||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
|
||||||
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
|
||||||
golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
|
|
||||||
golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
|
|
||||||
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
|
||||||
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
|
||||||
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
|
||||||
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
|
||||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
src.lwithers.me.uk/go/htpack v1.5.0 h1:2UYM4AGIPc/XgktmO6AxUyqFxzmYkqg/jINcDCsZsCk=
|
||||||
src.lwithers.me.uk/go/htpack v1.3.3 h1:Xvl6qR9HfSblmCgPyu+ACQ9o3aLQSIy3l8CrMbzj/jc=
|
src.lwithers.me.uk/go/htpack v1.5.0/go.mod h1:5dQlPdwV+ZeBAaFBdwiRI6/w5dpSgPsiotO4ZTYau88=
|
||||||
src.lwithers.me.uk/go/htpack v1.3.3/go.mod h1:qKgCBgZ6iiiuYOxZkYOPVpXLBzp6gXEd4A0ksxgR6Nk=
|
|
||||||
|
|
|
||||||
|
|
@ -55,6 +55,10 @@ func main() {
|
||||||
"Name of index file (index.html or similar)")
|
"Name of index file (index.html or similar)")
|
||||||
rootCmd.Flags().Duration("expiry", 0,
|
rootCmd.Flags().Duration("expiry", 0,
|
||||||
"Tell client how long it can cache data for; 0 means no caching")
|
"Tell client how long it can cache data for; 0 means no caching")
|
||||||
|
rootCmd.Flags().Duration("expiry404", 0,
|
||||||
|
"Tell client how long it can cache 404 responses for; 0 means default (inherit from --expiry, max. 60s); -1 means no caching")
|
||||||
|
rootCmd.Flags().Bool("immutable", false,
|
||||||
|
"Tell client that cached assets are immutable (will not be revalidated on page reload)")
|
||||||
rootCmd.Flags().String("fallback-404", "",
|
rootCmd.Flags().String("fallback-404", "",
|
||||||
"Name of file to return if response would be 404 (spa.html or similar)")
|
"Name of file to return if response would be 404 (spa.html or similar)")
|
||||||
rootCmd.Flags().String("frames", "sameorigin",
|
rootCmd.Flags().String("frames", "sameorigin",
|
||||||
|
|
@ -135,15 +139,35 @@ func run(c *cobra.Command, args []string) error {
|
||||||
|
|
||||||
// parse expiry time
|
// parse expiry time
|
||||||
// NB: https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Cache-Control
|
// NB: https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Cache-Control
|
||||||
|
cacheControl200, cacheControl404 := "no-cache", "no-cache"
|
||||||
|
immutable, err := c.Flags().GetBool("immutable")
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
var immutableDirective string
|
||||||
|
if immutable {
|
||||||
|
immutableDirective = ", immutable"
|
||||||
|
}
|
||||||
|
|
||||||
expiry, err := c.Flags().GetDuration("expiry")
|
expiry, err := c.Flags().GetDuration("expiry")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
if expiry <= 0 {
|
if expiry > 0 {
|
||||||
extraHeaders.Set("Cache-Control", "no-cache")
|
cacheControl200 = fmt.Sprintf("public, max-age=%d%s", expiry/1e9, immutableDirective)
|
||||||
} else {
|
}
|
||||||
extraHeaders.Set("Cache-Control",
|
|
||||||
fmt.Sprintf("public, max-age=%d", expiry/1e9))
|
expiry404, err := c.Flags().GetDuration("expiry404")
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
switch {
|
||||||
|
case expiry404 == 0 && expiry == 0:
|
||||||
|
case expiry404 == 0 && expiry > 0:
|
||||||
|
expiry404 = min(60*time.Second, expiry)
|
||||||
|
fallthrough
|
||||||
|
case expiry404 > 0:
|
||||||
|
cacheControl404 = fmt.Sprintf("public, max-age=%d", expiry404/1e9)
|
||||||
}
|
}
|
||||||
|
|
||||||
// optional index file
|
// optional index file
|
||||||
|
|
@ -199,6 +223,7 @@ func run(c *cobra.Command, args []string) error {
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
packHandler.SetCacheControl(cacheControl200, cacheControl404)
|
||||||
if indexFile != "" {
|
if indexFile != "" {
|
||||||
packHandler.SetIndex(indexFile)
|
packHandler.SetIndex(indexFile)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
10
go.mod
10
go.mod
|
|
@ -1,8 +1,8 @@
|
||||||
module src.lwithers.me.uk/go/htpack
|
module src.lwithers.me.uk/go/htpack
|
||||||
|
|
||||||
require (
|
go 1.26.0
|
||||||
github.com/gogo/protobuf v1.3.2
|
|
||||||
golang.org/x/sys v0.22.0
|
|
||||||
)
|
|
||||||
|
|
||||||
go 1.22
|
require (
|
||||||
|
golang.org/x/sys v0.47.0
|
||||||
|
google.golang.org/protobuf v1.36.12
|
||||||
|
)
|
||||||
|
|
|
||||||
39
go.sum
39
go.sum
|
|
@ -1,33 +1,6 @@
|
||||||
github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q=
|
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
|
||||||
github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q=
|
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
|
||||||
github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8=
|
golang.org/x/sys v0.47.0 h1:o7XGOvZQCADBQQ4Y7VNq2dRWQR7JmOUW8Kxx4ZsNgWs=
|
||||||
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
|
golang.org/x/sys v0.47.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
|
||||||
github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
google.golang.org/protobuf v1.36.12 h1:pJOKDDOyeXErUroCihFAd5LQuwXBSpVnKGrj5o/fwxc=
|
||||||
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
google.golang.org/protobuf v1.36.12/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=
|
||||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
|
||||||
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
|
||||||
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
|
||||||
golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
|
||||||
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
|
||||||
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
|
||||||
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
|
||||||
golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
|
||||||
golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
|
|
||||||
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
|
||||||
golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
|
||||||
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
|
||||||
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
|
||||||
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
|
||||||
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
|
||||||
golang.org/x/sys v0.22.0 h1:RI27ohtqKCnwULzJLqkv897zojh5/DwS/ENaMzUOaWI=
|
|
||||||
golang.org/x/sys v0.22.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
|
||||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
|
||||||
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
|
||||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
|
||||||
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
|
||||||
golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
|
|
||||||
golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
|
|
||||||
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
|
||||||
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
|
||||||
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
|
||||||
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
|
||||||
|
|
|
||||||
44
handler.go
44
handler.go
|
|
@ -3,14 +3,12 @@ package htpack
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"net/http"
|
"net/http"
|
||||||
"os"
|
|
||||||
"path"
|
"path"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"golang.org/x/sys/unix"
|
|
||||||
"src.lwithers.me.uk/go/htpack/packed"
|
"src.lwithers.me.uk/go/htpack/packed"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
@ -21,32 +19,17 @@ const (
|
||||||
|
|
||||||
// New returns a new handler. Standard security headers are set.
|
// New returns a new handler. Standard security headers are set.
|
||||||
func New(packfile string) (*Handler, error) {
|
func New(packfile string) (*Handler, error) {
|
||||||
f, err := os.Open(packfile)
|
mapped, err := packed.MapAndLoad(packfile)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
defer f.Close()
|
|
||||||
|
|
||||||
fi, err := f.Stat()
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
mapped, err := unix.Mmap(int(f.Fd()), 0, int(fi.Size()),
|
|
||||||
unix.PROT_READ, unix.MAP_SHARED)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
_, dir, err := packed.Load(f)
|
|
||||||
if err != nil {
|
|
||||||
unix.Munmap(mapped)
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
h := &Handler{
|
h := &Handler{
|
||||||
mapped: mapped,
|
mapped: mapped.Data,
|
||||||
dir: dir.Files,
|
dir: mapped.Directory.Files,
|
||||||
headers: make(map[string]string),
|
headers: make(map[string]string),
|
||||||
|
cacheControl200: "no-cache",
|
||||||
|
cacheControl404: "no-cache",
|
||||||
startTime: time.Now(),
|
startTime: time.Now(),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -64,6 +47,8 @@ type Handler struct {
|
||||||
mapped []byte
|
mapped []byte
|
||||||
dir map[string]*packed.File
|
dir map[string]*packed.File
|
||||||
headers map[string]string
|
headers map[string]string
|
||||||
|
cacheControl200 string
|
||||||
|
cacheControl404 string
|
||||||
startTime time.Time
|
startTime time.Time
|
||||||
notFound *packed.File
|
notFound *packed.File
|
||||||
}
|
}
|
||||||
|
|
@ -126,6 +111,19 @@ func (h *Handler) SetNotFound(notFound string) error {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// SetCacheControl allows control over the cache-control header. If not called,
|
||||||
|
// "Cache-Control: no-cache" is the default.
|
||||||
|
//
|
||||||
|
// The first and second arguments are required, and specify the header to use
|
||||||
|
// for 200 responses and 404 responses respectively.
|
||||||
|
//
|
||||||
|
// TODO: could be expanded to take extra varargs for more specific control over
|
||||||
|
// particular assets, like different expiry per content-type.
|
||||||
|
func (h *Handler) SetCacheControl(hdr200, hdr404 string) {
|
||||||
|
h.cacheControl200 = hdr200
|
||||||
|
h.cacheControl404 = hdr404
|
||||||
|
}
|
||||||
|
|
||||||
// ServeHTTP handles requests for files. It supports GET and HEAD methods, with
|
// ServeHTTP handles requests for files. It supports GET and HEAD methods, with
|
||||||
// anything else returning a 405. Exact path matches are required, else a 404 is
|
// anything else returning a 405. Exact path matches are required, else a 404 is
|
||||||
// returned.
|
// returned.
|
||||||
|
|
@ -146,6 +144,7 @@ func (h *Handler) ServeHTTP(w http.ResponseWriter, req *http.Request) {
|
||||||
|
|
||||||
info := h.dir[path.Clean(req.URL.Path)]
|
info := h.dir[path.Clean(req.URL.Path)]
|
||||||
if info == nil {
|
if info == nil {
|
||||||
|
w.Header().Set("Cache-Control", h.cacheControl404)
|
||||||
if h.notFound == nil {
|
if h.notFound == nil {
|
||||||
http.NotFound(w, req)
|
http.NotFound(w, req)
|
||||||
return
|
return
|
||||||
|
|
@ -154,6 +153,7 @@ func (h *Handler) ServeHTTP(w http.ResponseWriter, req *http.Request) {
|
||||||
}
|
}
|
||||||
|
|
||||||
// set standard headers
|
// set standard headers
|
||||||
|
w.Header().Set("Cache-Control", h.cacheControl200)
|
||||||
w.Header().Set("Vary", "Accept-Encoding")
|
w.Header().Set("Vary", "Accept-Encoding")
|
||||||
w.Header().Set("Etag", info.Etag)
|
w.Header().Set("Etag", info.Etag)
|
||||||
w.Header().Set("Content-Type", info.ContentType)
|
w.Header().Set("Content-Type", info.ContentType)
|
||||||
|
|
|
||||||
140
packed/load.go
140
packed/load.go
|
|
@ -2,9 +2,13 @@ package packed
|
||||||
|
|
||||||
import (
|
import (
|
||||||
fmt "fmt"
|
fmt "fmt"
|
||||||
|
"io/fs"
|
||||||
"os"
|
"os"
|
||||||
"path"
|
"path"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
|
"golang.org/x/sys/unix"
|
||||||
|
"google.golang.org/protobuf/proto"
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
|
|
@ -17,14 +21,14 @@ const (
|
||||||
VersionInitial = 1
|
VersionInitial = 1
|
||||||
)
|
)
|
||||||
|
|
||||||
// Load a ready-packed file.
|
// Read a ready-packed file.
|
||||||
func Load(f *os.File) (*Header, *Directory, error) {
|
func Read(f []byte) (*Header, *Directory, error) {
|
||||||
hdr, err := loadHeader(f)
|
hdr, err := readHeader(f)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, nil, err
|
return nil, nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
dir, err := loadDirectory(f, hdr)
|
dir, err := readDirectory(f, hdr)
|
||||||
if le, ok := err.(*LoadError); ok {
|
if le, ok := err.(*LoadError); ok {
|
||||||
// augment error
|
// augment error
|
||||||
le.Magic = hdr.Magic
|
le.Magic = hdr.Magic
|
||||||
|
|
@ -33,20 +37,101 @@ func Load(f *os.File) (*Header, *Directory, error) {
|
||||||
return hdr, dir, err // we may have a partial dir
|
return hdr, dir, err // we may have a partial dir
|
||||||
}
|
}
|
||||||
|
|
||||||
// loadHeader retrieves and decodes the header from the start of the file. It
|
// Mapped holds an mmap()ed packfile.
|
||||||
// ensures the magic number and the version number match. Errors are returned
|
type Mapped struct {
|
||||||
// as type LoadError.
|
Data []byte
|
||||||
func loadHeader(f *os.File) (*Header, error) {
|
Header *Header
|
||||||
raw := make([]byte, 36)
|
Directory *Directory
|
||||||
if _, err := f.ReadAt(raw, 0); err != nil {
|
}
|
||||||
return nil, &LoadError{
|
|
||||||
Cause: IOError,
|
func (m *Mapped) Close() error {
|
||||||
Underlying: err,
|
return unix.Munmap(m.Data)
|
||||||
|
}
|
||||||
|
|
||||||
|
// MapAndLoad mmaps the named file and reads the pack data.
|
||||||
|
func MapAndLoad(fname string) (*Mapped, error) {
|
||||||
|
f, err := os.Open(fname)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
defer f.Close()
|
||||||
|
|
||||||
|
st, err := f.Stat()
|
||||||
|
if err != nil {
|
||||||
|
return nil, &fs.PathError{
|
||||||
|
Path: fname,
|
||||||
|
Op: "stat",
|
||||||
|
Err: err,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
data, err := unix.Mmap(int(f.Fd()), 0, int(st.Size()), unix.PROT_READ, unix.MAP_SHARED)
|
||||||
|
if err != nil {
|
||||||
|
return nil, &fs.PathError{
|
||||||
|
Path: fname,
|
||||||
|
Op: "mmap",
|
||||||
|
Err: err,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
hdr, dir, err := Read(data)
|
||||||
|
if err != nil {
|
||||||
|
unix.Munmap(data)
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return &Mapped{
|
||||||
|
Data: data,
|
||||||
|
Header: hdr,
|
||||||
|
Directory: dir,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Load a ready-packed file.
|
||||||
|
//
|
||||||
|
// Deprecated: use [Read] or [MapAndLoad].
|
||||||
|
func Load(f *os.File) (*Header, *Directory, error) {
|
||||||
|
// we need to know the size of the file in order to mmap it
|
||||||
|
st, err := f.Stat()
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil, &fs.PathError{
|
||||||
|
Path: f.Name(),
|
||||||
|
Op: "stat",
|
||||||
|
Err: err,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// compatibility: old code may have worked with non-zero offset
|
||||||
|
offset, err := f.Seek(0, os.SEEK_CUR)
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil, &fs.PathError{
|
||||||
|
Path: f.Name(),
|
||||||
|
Op: "seek",
|
||||||
|
Err: err,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// mmap the file
|
||||||
|
data, err := unix.Mmap(int(f.Fd()), 0, int(st.Size()), unix.PROT_READ, unix.MAP_SHARED)
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil, &fs.PathError{
|
||||||
|
Path: f.Name(),
|
||||||
|
Op: "mmap",
|
||||||
|
Err: err,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
defer unix.Munmap(data)
|
||||||
|
|
||||||
|
// read as though it's from memory
|
||||||
|
return Read(data[offset:])
|
||||||
|
}
|
||||||
|
|
||||||
|
// readHeader retrieves and decodes the header from the start of the file. It
|
||||||
|
// ensures the magic number and the version number match. Errors are returned
|
||||||
|
// as type LoadError.
|
||||||
|
func readHeader(f []byte) (*Header, error) {
|
||||||
hdr := new(Header)
|
hdr := new(Header)
|
||||||
if err := hdr.Unmarshal(raw); err != nil {
|
if err := proto.Unmarshal(f[:36], hdr); err != nil {
|
||||||
return nil, &LoadError{
|
return nil, &LoadError{
|
||||||
Cause: HeaderUnmarshalError,
|
Cause: HeaderUnmarshalError,
|
||||||
Underlying: err,
|
Underlying: err,
|
||||||
|
|
@ -77,35 +162,22 @@ func loadHeader(f *os.File) (*Header, error) {
|
||||||
return hdr, nil
|
return hdr, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// loadDirectory reads the directory from a file. The directory is checked
|
// readDirectory reads the directory from a file. The directory is checked
|
||||||
// for consistency (offsets, filenames) but not integrity (file data is not
|
// for consistency (offsets, filenames) but not integrity (file data is not
|
||||||
// read/checksummed).
|
// read/checksummed).
|
||||||
func loadDirectory(f *os.File, hdr *Header) (*Directory, error) {
|
func readDirectory(f []byte, hdr *Header) (*Directory, error) {
|
||||||
fi, err := f.Stat()
|
fileSize := uint64(len(f))
|
||||||
if err != nil {
|
start := hdr.DirectoryOffset
|
||||||
return nil, &LoadError{
|
end := start + hdr.DirectoryLength
|
||||||
Cause: IOError,
|
|
||||||
Underlying: err,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
fileSize := uint64(fi.Size())
|
|
||||||
|
|
||||||
if hdr.DirectoryOffset+hdr.DirectoryLength > fileSize {
|
if end > fileSize {
|
||||||
return nil, &LoadError{
|
return nil, &LoadError{
|
||||||
Cause: BadOffsetError,
|
Cause: BadOffsetError,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
raw := make([]byte, hdr.DirectoryLength)
|
|
||||||
if _, err := f.ReadAt(raw, int64(hdr.DirectoryOffset)); err != nil {
|
|
||||||
return nil, &LoadError{
|
|
||||||
Cause: IOError,
|
|
||||||
Underlying: err,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
dir := new(Directory)
|
dir := new(Directory)
|
||||||
if err := dir.Unmarshal(raw); err != nil {
|
if err := proto.Unmarshal(f[start:end], dir); err != nil {
|
||||||
return nil, &LoadError{
|
return nil, &LoadError{
|
||||||
Cause: DirectoryUnmarshalError,
|
Cause: DirectoryUnmarshalError,
|
||||||
Underlying: err,
|
Underlying: err,
|
||||||
|
|
|
||||||
39
packed/load_test.go
Normal file
39
packed/load_test.go
Normal file
|
|
@ -0,0 +1,39 @@
|
||||||
|
package packed
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"os"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestMapAndLoad(t *testing.T) {
|
||||||
|
rawPack, err := os.ReadFile("testdata/helloworld.pack")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
rawText, err := os.ReadFile("testdata/helloworld.txt")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
mapped, err := MapAndLoad("testdata/helloworld.pack")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
defer mapped.Close()
|
||||||
|
|
||||||
|
if !bytes.Equal(mapped.Data, rawPack) {
|
||||||
|
t.Error("mapped data does not match raw file")
|
||||||
|
}
|
||||||
|
|
||||||
|
file := mapped.Directory.Files["/helloworld.txt"]
|
||||||
|
if file == nil {
|
||||||
|
t.Fatal("directory does not contain /helloworld.txt")
|
||||||
|
}
|
||||||
|
|
||||||
|
start := file.Uncompressed.Offset
|
||||||
|
end := start + file.Uncompressed.Length
|
||||||
|
if !bytes.Equal(rawText, mapped.Data[start:end]) {
|
||||||
|
t.Error("uncompressed file does not match testdata/helloworld.txt")
|
||||||
|
}
|
||||||
|
}
|
||||||
1429
packed/packed.pb.go
1429
packed/packed.pb.go
File diff suppressed because it is too large
Load diff
|
|
@ -1,6 +1,7 @@
|
||||||
syntax = "proto3";
|
syntax = "proto3";
|
||||||
|
|
||||||
package packed;
|
package lwithers.htpack.packed;
|
||||||
|
option go_package = "src.lwithers.me.uk/htpack/packed";
|
||||||
|
|
||||||
// Header at start of file. This must be a fixed, known size. Fields cannot
|
// Header at start of file. This must be a fixed, known size. Fields cannot
|
||||||
// be zero.
|
// be zero.
|
||||||
|
|
@ -54,6 +55,6 @@ message FileData {
|
||||||
// the pack.
|
// the pack.
|
||||||
fixed64 offset = 1;
|
fixed64 offset = 1;
|
||||||
|
|
||||||
// Length is the
|
// Length of the (possibly compressed) file data, in bytes.
|
||||||
fixed64 length = 2;
|
fixed64 length = 2;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
BIN
packed/testdata/helloworld.pack
vendored
Normal file
BIN
packed/testdata/helloworld.pack
vendored
Normal file
Binary file not shown.
13
packed/testdata/helloworld.txt
vendored
Normal file
13
packed/testdata/helloworld.txt
vendored
Normal file
|
|
@ -0,0 +1,13 @@
|
||||||
|
Hello, world
|
||||||
|
|
||||||
|
This file contains a few lines of plain text, really just enough
|
||||||
|
to trigger the "compression worthwhile" heuristic.
|
||||||
|
|
||||||
|
This file contains a few lines of plain text, really just enough
|
||||||
|
to trigger the "compression worthwhile" heuristic.
|
||||||
|
|
||||||
|
This file contains a few lines of plain text, really just enough
|
||||||
|
to trigger the "compression worthwhile" heuristic.
|
||||||
|
|
||||||
|
This file contains a few lines of plain text, really just enough
|
||||||
|
to trigger the "compression worthwhile" heuristic.
|
||||||
Loading…
Add table
Add a link
Reference in a new issue