Skip to content

Commit

Permalink
Merge branch 'go1.8'
Browse files Browse the repository at this point in the history
  • Loading branch information
surma committed Feb 22, 2017
2 parents 5fe27a1 + 0a3bf90 commit 762645b
Show file tree
Hide file tree
Showing 57 changed files with 537 additions and 17,343 deletions.
4 changes: 2 additions & 2 deletions Dockerfile
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
FROM golang:1.6-onbuild
FROM golang:1.8-onbuild

RUN mkdir /data
VOLUME ["/data"]
WORKDIR /data
EXPOSE 5000
ENTRYPOINT ["app"]
ENTRYPOINT ["app"]
108 changes: 69 additions & 39 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
`simplehttp2server` serves the current directory on an HTTP/2.0 capable server.
This server is for development purposes only.
`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.

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

## Binaries
# Installation
## Binaries
`simplehttp2server` is `go get`-able:

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

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

# Push Manifest
# Config

`simplehttp2server` supports the [push manifest](https://www.npmjs.com/package/http2-push-manifest).
All requests will be looked up in a file named `push.json`. If there is a key
for the request path, all resources under that key will be pushed.
`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].

Example `push.json`:
All `source` fields take the [Extglob] syntax.

```JS
## Redirects

```js
{
"/": {
"/css/app.css": {
"type": "style",
"weight": 1
},
// ...
},
"/page.html": {
"/css/page.css": {
"type": "style",
"weight": 1
},
// ...
}
}
"redirects": [
{
"source": "/**/.*",
"destination": "https://google.com",
"type": 301
}
]
```
Support for `weight` and `type` is not implemented yet. Pushes cannot trigger additional pushes.
## Rewrites
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.
```js
{
"rewrites": [
{
"source": "/app/**",
"destination": "/index.html"
}
]
}
```
# TLS Certificate
## Headers
Since HTTP/2 requires TLS, `simplehttp2server` checks if `cert.pem` and
`key.pem` are present. If not, a self-signed certificate will be generated.
```js
{
"headers": [
{
"source": "/**.html",
"headers": [
{
"key": "Cache-Control",
"value": "max-age=3600"
}
]
},
{
"source": "/index.html",
"headers": [
{
"key": "Cache-Control",
"value": "no-cache"
},
{
"key": "Link",
"value": "</header.jpg>; rel=preload; as=image, </app.js>; rel=preload; as=script"
}
]
}
]
}
```
# Delays
For details see the [Firebase’s documentation][Firebase’s JSON config].
`simplehttp2server` can add artificial delays to responses to emulate processing
time. The command line flags `-mindelay` and `-maxdelay` allow you to delay
responses with a random delay form the interval `[minDelay, maxDelay]` in milliseconds.
## Firebase Disclaimer
If a request has a `delay` query parameter (like `GET /index.html?delay=4000`),
that delay will take precedence.
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!
# Other features
## HTTP/2 PUSH
* Support for serving Single Page Applications (SPAs) using the `-spa` flag
* Support for throttling network throughput *per request* using the `-throttle` flag
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.
# License
Apache 2.
[Extglob]: https://www.npmjs.com/package/extglob
[Firebase’s JSON config]: https://firebase.google.com/docs/hosting/full-config
25 changes: 25 additions & 0 deletions extglob.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
package main

import (
"fmt"
"os"
"regexp"
"strings"
)

var (
parensExpr = regexp.MustCompile("([^\\\\])\\(([^)]+)\\)")
questionExpr = regexp.MustCompile("\\?([^(])")
)

func CompileExtGlob(extglob string) (*regexp.Regexp, error) {
tmp := extglob
tmp = strings.Replace(tmp, ".", "\\.", -1)
tmp = strings.Replace(tmp, "**", ".🐷", -1)
tmp = strings.Replace(tmp, "*", fmt.Sprintf("[^%c]*", os.PathSeparator), -1)
tmp = questionExpr.ReplaceAllString(tmp, fmt.Sprintf("[^%c]$1", os.PathSeparator))
tmp = parensExpr.ReplaceAllString(tmp, "($2)$1")
tmp = strings.Replace(tmp, ")@", ")", -1)
tmp = strings.Replace(tmp, ".🐷", ".*", -1)
return regexp.Compile("^" + tmp + "$")
}
55 changes: 55 additions & 0 deletions extglob_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
package main

import (
"testing"
)

type TableEntry struct {
Glob string
Matches []string
NonMatches []string
}

var (
table = []TableEntry{
{
Glob: "asdf/*.jpg",
Matches: []string{"asdf/asdf.jpg", "asdf/asdf_asdf.jpg", "asdf/.jpg"},
NonMatches: []string{"asdf/asdf/asdf.jpg", "xxxasdf/asdf.jpgxxx"},
},
{
Glob: "asdf/**.jpg",
Matches: []string{"asdf/asdf.jpg", "asdf/asdf_asdf.jpg", "asdf/asdf/asdf.jpg", "asdf/asdf/asdf/asdf/asdf.jpg"},
NonMatches: []string{"/asdf/asdf.jpg", "asdff/asdf.jpg", "xxxasdf/asdf.jpgxxx"},
},
{
Glob: "asdf/*.@(jpg|jpeg)",
Matches: []string{"asdf/asdf.jpg", "asdf/asdf_asdf.jpeg"},
NonMatches: []string{"/asdf/asdf.jpg", "asdff/asdf.jpg"},
},
{
Glob: "**/*.js",
Matches: []string{"asdf/asdf.js", "asdf/asdf/asdfasdf_asdf.js", "/asdf/asdf.js", "/asdf/aasdf-asdf.2.1.4.js"},
NonMatches: []string{"/asdf/asdf.jpg", "asdf.js"},
},
}
)

func Test_CompileExtGlob(t *testing.T) {
for _, entry := range table {
r, err := CompileExtGlob(entry.Glob)
if err != nil {
t.Fatalf("Couldn’t compile glob %s: %s", entry.Glob, err)
}
for _, match := range entry.Matches {
if !r.MatchString(match) {
t.Fatalf("%s didn’t match %s", entry.Glob, match)
}
}
for _, nonmatch := range entry.NonMatches {
if r.MatchString(nonmatch) {
t.Fatalf("%s matched %s", entry.Glob, nonmatch)
}
}
}
}
134 changes: 134 additions & 0 deletions firebase.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,134 @@
package main

import (
"encoding/json"
"fmt"
"log"
"net/http"
"os"
"path/filepath"
"strings"
)

type FirebaseManifest struct {
Public string `json:"public"`
Redirects []struct {
Source string `json:"source"`
Destination string `json:"destination"`
Type int `json:"type,omitempty"`
} `json:"redirects"`
Rewrites []struct {
Source string `json:"source"`
Destination string `json:"destination"`
} `json:"rewrites"`
Headers []struct {
Source string `json:"source"`
Headers []struct {
Key string `json:"key"`
Value string `json:"value"`
} `json:"headers"`
} `json:"headers"`
Hosting *FirebaseManifest `json:"Hosting"`
}

func (mf FirebaseManifest) processRedirects(w http.ResponseWriter, r *http.Request) (bool, error) {
for _, redirect := range mf.Redirects {
pattern, err := CompileExtGlob(redirect.Source)
if err != nil {
return false, fmt.Errorf("Invalid redirect extglob %s: %s", redirect.Source, err)
}
if pattern.MatchString(r.URL.Path) {
http.Redirect(w, r, redirect.Destination, redirect.Type)
return true, nil
}
}
if mf.Hosting != nil {
return mf.Hosting.processRedirects(w, r)
}
return false, nil
}

func (mf FirebaseManifest) processRewrites(r *http.Request) error {
for _, rewrite := range mf.Rewrites {
pattern, err := CompileExtGlob(rewrite.Source)
if err != nil {
return fmt.Errorf("Invalid rewrite extglob %s: %s", rewrite.Source, err)
}
if pattern.MatchString(r.URL.Path) {
r.URL.Path = strings.TrimSuffix(rewrite.Destination, "index.html")
return nil
}
}
if mf.Hosting != nil {
return mf.Hosting.processRewrites(r)
}
return nil
}

func (mf FirebaseManifest) processHosting(w http.ResponseWriter, r *http.Request) error {
for _, headerSet := range mf.Headers {
pattern, err := CompileExtGlob(headerSet.Source)
if err != nil {
return fmt.Errorf("Invalid hosting.header extglob %s: %s", headerSet.Source, err)
}
if pattern.MatchString(r.URL.Path) {
for _, header := range headerSet.Headers {
w.Header().Set(header.Key, header.Value)
}
}
}
if mf.Hosting != nil {
return mf.Hosting.processHosting(w, r)
}
return nil
}

func processWithConfig(w http.ResponseWriter, r *http.Request, config string) string {
dir := "."
mf, err := readManifest(config)
if err != nil {
log.Printf("Could read Firebase file %s: %s", config, err)
return dir
}
if mf.Public != "" {
dir = mf.Public
}

done, err := mf.processRedirects(w, r)
if err != nil {
log.Printf("Processing redirects failed: %s", err)
return dir
}
if done {
return dir
}

// Rewrites only happen if the target file does not exist
if _, err = os.Stat(filepath.Join(dir, r.URL.Path)); err != nil {
err = mf.processRewrites(r)
if err != nil {
log.Printf("Processing rewrites failed: %s", err)
return dir
}
}

err = mf.processHosting(w, r)
if err != nil {
log.Printf("Processing rewrites failed: %s", err)
return dir
}

return dir
}

func readManifest(path string) (FirebaseManifest, error) {
fmf := FirebaseManifest{}
f, err := os.Open(path)
if err != nil {
return fmf, err
}
defer f.Close()
dec := json.NewDecoder(f)
err = dec.Decode(&fmf)
return fmf, err
}
1 change: 0 additions & 1 deletion http2/.gitignore

This file was deleted.

19 changes: 0 additions & 19 deletions http2/AUTHORS

This file was deleted.

Loading

0 comments on commit 762645b

Please sign in to comment.