Skip to content

Commit 762645b

Browse files
committed
Merge branch 'go1.8'
2 parents 5fe27a1 + 0a3bf90 commit 762645b

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

57 files changed

+537
-17343
lines changed

Dockerfile

+2-2
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
1-
FROM golang:1.6-onbuild
1+
FROM golang:1.8-onbuild
22

33
RUN mkdir /data
44
VOLUME ["/data"]
55
WORKDIR /data
66
EXPOSE 5000
7-
ENTRYPOINT ["app"]
7+
ENTRYPOINT ["app"]

README.md

+69-39
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
1-
`simplehttp2server` serves the current directory on an HTTP/2.0 capable server.
2-
This server is for development purposes only.
1+
`simplehttp2server` serves the current directory on an HTTP/2.0 capable server. This server is for development purposes only. `simplehttp2server` takes a JSON config that allows you to configure headers, redirects and URL rewrites in a lightweight JSON fromat.
32

4-
# Download
3+
The format is partially compatible with [Firebase’s JSON config]. Please see [disclaimer](#firebase-disclaimer) below.
54

6-
## Binaries
5+
# Installation
6+
## Binaries
77
`simplehttp2server` is `go get`-able:
88

99
```
@@ -24,57 +24,87 @@ $ brew install simplehttp2server
2424
If you have Docker set up, you can serve the current directory via `simplehttp2server` using the following command:
2525

2626
```
27-
$ docker run -p 5000:5000 -v $PWD:/data surma/simplehttp2server
27+
$ docker run -p 5000:5000 -v $PWD:/data surma/simplehttp2server [-config firebase.json]
2828
```
2929

30-
# Push Manifest
30+
# Config
3131

32-
`simplehttp2server` supports the [push manifest](https://www.npmjs.com/package/http2-push-manifest).
33-
All requests will be looked up in a file named `push.json`. If there is a key
34-
for the request path, all resources under that key will be pushed.
32+
`simplehttp2server` can be configured with the `-config` flag and a JSON config file. This way you can add custom headers, rewrite rules and redirects. It is partially compatible with [Firebase’s JSON config].
3533

36-
Example `push.json`:
34+
All `source` fields take the [Extglob] syntax.
3735

38-
```JS
36+
## Redirects
37+
38+
```js
3939
{
40-
"/": {
41-
"/css/app.css": {
42-
"type": "style",
43-
"weight": 1
44-
},
45-
// ...
46-
},
47-
"/page.html": {
48-
"/css/page.css": {
49-
"type": "style",
50-
"weight": 1
51-
},
52-
// ...
53-
}
54-
}
40+
"redirects": [
41+
{
42+
"source": "/**/.*",
43+
"destination": "https://google.com",
44+
"type": 301
45+
}
46+
]
5547
```
5648
57-
Support for `weight` and `type` is not implemented yet. Pushes cannot trigger additional pushes.
49+
## Rewrites
50+
51+
Rewrites are useful for SPAs, where all paths return `index.html` and the routing is taking care of in the app itself. Rewrites are only applied when the original target file does not exist.
52+
53+
```js
54+
{
55+
"rewrites": [
56+
{
57+
"source": "/app/**",
58+
"destination": "/index.html"
59+
}
60+
]
61+
}
62+
```
5863
59-
# TLS Certificate
64+
## Headers
6065
61-
Since HTTP/2 requires TLS, `simplehttp2server` checks if `cert.pem` and
62-
`key.pem` are present. If not, a self-signed certificate will be generated.
66+
```js
67+
{
68+
"headers": [
69+
{
70+
"source": "/**.html",
71+
"headers": [
72+
{
73+
"key": "Cache-Control",
74+
"value": "max-age=3600"
75+
}
76+
]
77+
},
78+
{
79+
"source": "/index.html",
80+
"headers": [
81+
{
82+
"key": "Cache-Control",
83+
"value": "no-cache"
84+
},
85+
{
86+
"key": "Link",
87+
"value": "</header.jpg>; rel=preload; as=image, </app.js>; rel=preload; as=script"
88+
}
89+
]
90+
}
91+
]
92+
}
93+
```
6394
64-
# Delays
95+
For details see the [Firebase’s documentation][Firebase’s JSON config].
6596
66-
`simplehttp2server` can add artificial delays to responses to emulate processing
67-
time. The command line flags `-mindelay` and `-maxdelay` allow you to delay
68-
responses with a random delay form the interval `[minDelay, maxDelay]` in milliseconds.
97+
## Firebase Disclaimer
6998
70-
If a request has a `delay` query parameter (like `GET /index.html?delay=4000`),
71-
that delay will take precedence.
99+
I haven’t tested if the behavior of `simplehttp2server` _always_ matches the live server of Firebase, and some options (like `trailingSlash` and `cleanUrls`) are completely missing. Please open an issue if you find a discrepancy! The support is not offically endorsed by Firebase (yet 😜), so don’t rely on it!
72100
73-
# Other features
101+
## HTTP/2 PUSH
74102
75-
* Support for serving Single Page Applications (SPAs) using the `-spa` flag
76-
* Support for throttling network throughput *per request* using the `-throttle` flag
103+
Any `Link` headers with `rel=preload` will be translated to a HTTP/2 PUSH, [as is common practice on static hosting platforms and CDNs](https://w3c.github.io/preload/#server-push-http-2). See the [example](#headers) above.
77104
78105
# License
79106
80107
Apache 2.
108+
109+
[Extglob]: https://www.npmjs.com/package/extglob
110+
[Firebase’s JSON config]: https://firebase.google.com/docs/hosting/full-config

extglob.go

+25
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
package main
2+
3+
import (
4+
"fmt"
5+
"os"
6+
"regexp"
7+
"strings"
8+
)
9+
10+
var (
11+
parensExpr = regexp.MustCompile("([^\\\\])\\(([^)]+)\\)")
12+
questionExpr = regexp.MustCompile("\\?([^(])")
13+
)
14+
15+
func CompileExtGlob(extglob string) (*regexp.Regexp, error) {
16+
tmp := extglob
17+
tmp = strings.Replace(tmp, ".", "\\.", -1)
18+
tmp = strings.Replace(tmp, "**", ".🐷", -1)
19+
tmp = strings.Replace(tmp, "*", fmt.Sprintf("[^%c]*", os.PathSeparator), -1)
20+
tmp = questionExpr.ReplaceAllString(tmp, fmt.Sprintf("[^%c]$1", os.PathSeparator))
21+
tmp = parensExpr.ReplaceAllString(tmp, "($2)$1")
22+
tmp = strings.Replace(tmp, ")@", ")", -1)
23+
tmp = strings.Replace(tmp, ".🐷", ".*", -1)
24+
return regexp.Compile("^" + tmp + "$")
25+
}

extglob_test.go

+55
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
package main
2+
3+
import (
4+
"testing"
5+
)
6+
7+
type TableEntry struct {
8+
Glob string
9+
Matches []string
10+
NonMatches []string
11+
}
12+
13+
var (
14+
table = []TableEntry{
15+
{
16+
Glob: "asdf/*.jpg",
17+
Matches: []string{"asdf/asdf.jpg", "asdf/asdf_asdf.jpg", "asdf/.jpg"},
18+
NonMatches: []string{"asdf/asdf/asdf.jpg", "xxxasdf/asdf.jpgxxx"},
19+
},
20+
{
21+
Glob: "asdf/**.jpg",
22+
Matches: []string{"asdf/asdf.jpg", "asdf/asdf_asdf.jpg", "asdf/asdf/asdf.jpg", "asdf/asdf/asdf/asdf/asdf.jpg"},
23+
NonMatches: []string{"/asdf/asdf.jpg", "asdff/asdf.jpg", "xxxasdf/asdf.jpgxxx"},
24+
},
25+
{
26+
Glob: "asdf/*.@(jpg|jpeg)",
27+
Matches: []string{"asdf/asdf.jpg", "asdf/asdf_asdf.jpeg"},
28+
NonMatches: []string{"/asdf/asdf.jpg", "asdff/asdf.jpg"},
29+
},
30+
{
31+
Glob: "**/*.js",
32+
Matches: []string{"asdf/asdf.js", "asdf/asdf/asdfasdf_asdf.js", "/asdf/asdf.js", "/asdf/aasdf-asdf.2.1.4.js"},
33+
NonMatches: []string{"/asdf/asdf.jpg", "asdf.js"},
34+
},
35+
}
36+
)
37+
38+
func Test_CompileExtGlob(t *testing.T) {
39+
for _, entry := range table {
40+
r, err := CompileExtGlob(entry.Glob)
41+
if err != nil {
42+
t.Fatalf("Couldn’t compile glob %s: %s", entry.Glob, err)
43+
}
44+
for _, match := range entry.Matches {
45+
if !r.MatchString(match) {
46+
t.Fatalf("%s didn’t match %s", entry.Glob, match)
47+
}
48+
}
49+
for _, nonmatch := range entry.NonMatches {
50+
if r.MatchString(nonmatch) {
51+
t.Fatalf("%s matched %s", entry.Glob, nonmatch)
52+
}
53+
}
54+
}
55+
}

firebase.go

+134
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,134 @@
1+
package main
2+
3+
import (
4+
"encoding/json"
5+
"fmt"
6+
"log"
7+
"net/http"
8+
"os"
9+
"path/filepath"
10+
"strings"
11+
)
12+
13+
type FirebaseManifest struct {
14+
Public string `json:"public"`
15+
Redirects []struct {
16+
Source string `json:"source"`
17+
Destination string `json:"destination"`
18+
Type int `json:"type,omitempty"`
19+
} `json:"redirects"`
20+
Rewrites []struct {
21+
Source string `json:"source"`
22+
Destination string `json:"destination"`
23+
} `json:"rewrites"`
24+
Headers []struct {
25+
Source string `json:"source"`
26+
Headers []struct {
27+
Key string `json:"key"`
28+
Value string `json:"value"`
29+
} `json:"headers"`
30+
} `json:"headers"`
31+
Hosting *FirebaseManifest `json:"Hosting"`
32+
}
33+
34+
func (mf FirebaseManifest) processRedirects(w http.ResponseWriter, r *http.Request) (bool, error) {
35+
for _, redirect := range mf.Redirects {
36+
pattern, err := CompileExtGlob(redirect.Source)
37+
if err != nil {
38+
return false, fmt.Errorf("Invalid redirect extglob %s: %s", redirect.Source, err)
39+
}
40+
if pattern.MatchString(r.URL.Path) {
41+
http.Redirect(w, r, redirect.Destination, redirect.Type)
42+
return true, nil
43+
}
44+
}
45+
if mf.Hosting != nil {
46+
return mf.Hosting.processRedirects(w, r)
47+
}
48+
return false, nil
49+
}
50+
51+
func (mf FirebaseManifest) processRewrites(r *http.Request) error {
52+
for _, rewrite := range mf.Rewrites {
53+
pattern, err := CompileExtGlob(rewrite.Source)
54+
if err != nil {
55+
return fmt.Errorf("Invalid rewrite extglob %s: %s", rewrite.Source, err)
56+
}
57+
if pattern.MatchString(r.URL.Path) {
58+
r.URL.Path = strings.TrimSuffix(rewrite.Destination, "index.html")
59+
return nil
60+
}
61+
}
62+
if mf.Hosting != nil {
63+
return mf.Hosting.processRewrites(r)
64+
}
65+
return nil
66+
}
67+
68+
func (mf FirebaseManifest) processHosting(w http.ResponseWriter, r *http.Request) error {
69+
for _, headerSet := range mf.Headers {
70+
pattern, err := CompileExtGlob(headerSet.Source)
71+
if err != nil {
72+
return fmt.Errorf("Invalid hosting.header extglob %s: %s", headerSet.Source, err)
73+
}
74+
if pattern.MatchString(r.URL.Path) {
75+
for _, header := range headerSet.Headers {
76+
w.Header().Set(header.Key, header.Value)
77+
}
78+
}
79+
}
80+
if mf.Hosting != nil {
81+
return mf.Hosting.processHosting(w, r)
82+
}
83+
return nil
84+
}
85+
86+
func processWithConfig(w http.ResponseWriter, r *http.Request, config string) string {
87+
dir := "."
88+
mf, err := readManifest(config)
89+
if err != nil {
90+
log.Printf("Could read Firebase file %s: %s", config, err)
91+
return dir
92+
}
93+
if mf.Public != "" {
94+
dir = mf.Public
95+
}
96+
97+
done, err := mf.processRedirects(w, r)
98+
if err != nil {
99+
log.Printf("Processing redirects failed: %s", err)
100+
return dir
101+
}
102+
if done {
103+
return dir
104+
}
105+
106+
// Rewrites only happen if the target file does not exist
107+
if _, err = os.Stat(filepath.Join(dir, r.URL.Path)); err != nil {
108+
err = mf.processRewrites(r)
109+
if err != nil {
110+
log.Printf("Processing rewrites failed: %s", err)
111+
return dir
112+
}
113+
}
114+
115+
err = mf.processHosting(w, r)
116+
if err != nil {
117+
log.Printf("Processing rewrites failed: %s", err)
118+
return dir
119+
}
120+
121+
return dir
122+
}
123+
124+
func readManifest(path string) (FirebaseManifest, error) {
125+
fmf := FirebaseManifest{}
126+
f, err := os.Open(path)
127+
if err != nil {
128+
return fmf, err
129+
}
130+
defer f.Close()
131+
dec := json.NewDecoder(f)
132+
err = dec.Decode(&fmf)
133+
return fmf, err
134+
}

http2/.gitignore

-1
This file was deleted.

http2/AUTHORS

-19
This file was deleted.

0 commit comments

Comments
 (0)