Permalink
Please sign in to comment.
Showing
with
52 additions
and 266 deletions.
- +0 −1 Procfile
- +10 −57 README.md
- +0 −208 server.go
- +42 −0 tools/upload-site
1
Procfile
| @@ -1 +0,0 @@ | ||
| -web: gobyexample |
67
README.md
208
server.go
| @@ -1,208 +0,0 @@ | ||
| -package main | ||
| - | ||
| -import ( | ||
| - "encoding/base64" | ||
| - "fmt" | ||
| - "github.com/gorilla/mux" | ||
| - "io/ioutil" | ||
| - "net" | ||
| - "net/http" | ||
| - "os" | ||
| - "os/signal" | ||
| - "strings" | ||
| - "sync/atomic" | ||
| - "syscall" | ||
| - "time" | ||
| -) | ||
| - | ||
| -func check(err error) { | ||
| - if err != nil { | ||
| - panic(err) | ||
| - } | ||
| -} | ||
| - | ||
| -func config(k string) string { | ||
| - v := os.Getenv(k) | ||
| - if v == "" { | ||
| - panic("missing " + k) | ||
| - } | ||
| - return v | ||
| -} | ||
| - | ||
| -func runLogging(logs chan string) { | ||
| - for log := range logs { | ||
| - fmt.Println(log) | ||
| - } | ||
| -} | ||
| - | ||
| -func wrapLogging(f http.HandlerFunc, logs chan string) http.HandlerFunc { | ||
| - return func(res http.ResponseWriter, req *http.Request) { | ||
| - start := time.Now() | ||
| - f(res, req) | ||
| - method := req.Method | ||
| - path := req.URL.Path | ||
| - elapsed := float64(time.Since(start)) / 1000000.0 | ||
| - logs <- fmt.Sprintf("request at=finish method=%s path=%s elapsed=%f", method, path, elapsed) | ||
| - } | ||
| -} | ||
| - | ||
| -func wrapCanonicalHost(f http.HandlerFunc, canonicalHost string, forceHttps bool) http.HandlerFunc { | ||
| - return func(res http.ResponseWriter, req *http.Request) { | ||
| - scheme := "http" | ||
| - if h, ok := req.Header["X-Forwarded-Proto"]; ok { | ||
| - if h[0] == "https" { | ||
| - scheme = "https" | ||
| - } | ||
| - } | ||
| - | ||
| - hostPort := strings.Split(req.Host, ":") | ||
| - host := hostPort[0] | ||
| - | ||
| - if (forceHttps && (scheme != "https")) || host != canonicalHost { | ||
| - if forceHttps { | ||
| - scheme = "https" | ||
| - } | ||
| - hostPort[0] = canonicalHost | ||
| - url := scheme + "://" + strings.Join(hostPort, ":") + req.URL.String() | ||
| - http.Redirect(res, req, url, 301) | ||
| - return | ||
| - } | ||
| - | ||
| - f(res, req) | ||
| - } | ||
| -} | ||
| - | ||
| -type Authenticator func(string, string) bool | ||
| - | ||
| -func testAuth(r *http.Request, auth Authenticator) bool { | ||
| - s := strings.SplitN(r.Header.Get("Authorization"), " ", 2) | ||
| - if len(s) != 2 || s[0] != "Basic" { | ||
| - return false | ||
| - } | ||
| - b, err := base64.StdEncoding.DecodeString(s[1]) | ||
| - if err != nil { | ||
| - return false | ||
| - } | ||
| - pair := strings.SplitN(string(b), ":", 2) | ||
| - if len(pair) != 2 { | ||
| - return false | ||
| - } | ||
| - return auth(pair[0], pair[1]) | ||
| -} | ||
| - | ||
| -func requireAuth(w http.ResponseWriter, r *http.Request) { | ||
| - w.Header().Set("WWW-Authenticate", `Basic realm="private"`) | ||
| - w.WriteHeader(401) | ||
| - w.Write([]byte("401 Unauthorized\n")) | ||
| -} | ||
| - | ||
| -func wrapAuth(h http.HandlerFunc, a Authenticator) http.HandlerFunc { | ||
| - return func(w http.ResponseWriter, r *http.Request) { | ||
| - if testAuth(r, a) { | ||
| - h(w, r) | ||
| - } else { | ||
| - requireAuth(w, r) | ||
| - } | ||
| - } | ||
| -} | ||
| - | ||
| -var reqCount int64 = 0 | ||
| - | ||
| -func wrapReqCount(h http.HandlerFunc, reqCountPtr *int64) http.HandlerFunc { | ||
| - return func(w http.ResponseWriter, r *http.Request) { | ||
| - atomic.AddInt64(reqCountPtr, 1) | ||
| - h(w, r) | ||
| - atomic.AddInt64(reqCountPtr, -1) | ||
| - } | ||
| -} | ||
| - | ||
| -func static(res http.ResponseWriter, req *http.Request) { | ||
| - http.ServeFile(res, req, "public"+req.URL.Path) | ||
| -} | ||
| - | ||
| -func notFound(res http.ResponseWriter, req *http.Request) { | ||
| - http.ServeFile(res, req, "public/404.html") | ||
| -} | ||
| - | ||
| -func checkAuth(user, pass string) bool { | ||
| - auth := os.Getenv("AUTH") | ||
| - if auth == "" { | ||
| - return true | ||
| - } | ||
| - return auth == strings.Join([]string{user, pass}, ":") | ||
| -} | ||
| - | ||
| -func routerHandlerFunc(router *mux.Router) http.HandlerFunc { | ||
| - return func(res http.ResponseWriter, req *http.Request) { | ||
| - router.ServeHTTP(res, req) | ||
| - } | ||
| -} | ||
| - | ||
| -func router() *mux.Router { | ||
| - router := mux.NewRouter() | ||
| - router.HandleFunc("/", static).Methods("GET") | ||
| - router.HandleFunc("/favicon.ico", static).Methods("GET") | ||
| - router.HandleFunc("/play.png", static).Methods("GET") | ||
| - router.HandleFunc("/site.css", static).Methods("GET") | ||
| - entries, err := ioutil.ReadDir("public") | ||
| - check(err) | ||
| - for _, f := range entries { | ||
| - if !strings.Contains(f.Name(), ".") { | ||
| - router.HandleFunc("/" + f.Name(), static).Methods("GET") | ||
| - } | ||
| - } | ||
| - router.NotFoundHandler = http.HandlerFunc(notFound) | ||
| - return router | ||
| -} | ||
| - | ||
| -func main() { | ||
| - logs := make(chan string, 10000) | ||
| - go runLogging(logs) | ||
| - | ||
| - handler := routerHandlerFunc(router()) | ||
| - if os.Getenv("AUTH") != "" { | ||
| - handler = wrapAuth(handler, checkAuth) | ||
| - } | ||
| - handler = wrapCanonicalHost(handler, config("CANONICAL_HOST"), config("FORCE_HTTPS") == "1") | ||
| - handler = wrapLogging(handler, logs) | ||
| - handler = wrapReqCount(handler, &reqCount) | ||
| - | ||
| - server := &http.Server{Handler: handler} | ||
| - listener, listenErr := net.Listen("tcp", ":"+config("PORT")) | ||
| - if listenErr != nil { | ||
| - panic(listenErr) | ||
| - } | ||
| - | ||
| - stop := make(chan bool, 1) | ||
| - sig := make(chan os.Signal, 1) | ||
| - go func() { | ||
| - signal.Notify(sig, syscall.SIGINT, syscall.SIGTERM) | ||
| - logs <- "trap at=start" | ||
| - <-sig | ||
| - for { | ||
| - reqCountCurrent := atomic.LoadInt64(&reqCount) | ||
| - if reqCountCurrent > 0 { | ||
| - logs <- fmt.Sprintf("trap at=draining remaining=%d", reqCountCurrent) | ||
| - time.Sleep(time.Second) | ||
| - } else { | ||
| - logs <- fmt.Sprintf("trap at=finish") | ||
| - stop <- true | ||
| - return | ||
| - } | ||
| - } | ||
| - }() | ||
| - | ||
| - go func() { | ||
| - logs <- "serve at=start" | ||
| - server.Serve(listener) | ||
| - logs <- "serve at=finish" | ||
| - }() | ||
| - | ||
| - <-stop | ||
| - logs <- "close at=start" | ||
| - closeErr := listener.Close() | ||
| - if closeErr != nil { | ||
| - panic(closeErr) | ||
| - } | ||
| - logs <- "close at=finish" | ||
| -} |
42
tools/upload-site
| @@ -0,0 +1,42 @@ | ||
| +#!/usr/bin/env ruby | ||
| + | ||
| +# Upload the contents in public/ to the S3 bucket from which we serve | ||
| +# gobyexample.com. We use this instead of `aws iam sync` because that command | ||
| +# doesn't correctly guess the text/html mime time of the extension-less files. | ||
| +# We didn't write this in Go because we had already written it in Ruby for | ||
| +# another website and didn't want to re-write it. | ||
| + | ||
| +require "aws-sdk" | ||
| +require "set" | ||
| + | ||
| +s3 = Aws::S3::Client.new( | ||
| + region: "us-east-1", | ||
| + credentials: Aws::Credentials.new(ENV["AWS_ACCESS_KEY_ID"], ENV["AWS_SECRET_ACCESS_KEY"]) | ||
| +) | ||
| + | ||
| +# (Re-)upload each file to S3. We're not worried about what's currently there. | ||
| +Dir.glob("./public/**/**").each do |local_path| | ||
| + next if File.directory?(local_path) | ||
| + | ||
| + # Derive final path. | ||
| + s3_path = local_path.sub("./public/", "") | ||
| + | ||
| + # Infer content type, including for HTML files that need pretty URLs. | ||
| + content_type = | ||
| + case s3_path | ||
| + when /\.ico$/ then "image/x-icon" | ||
| + when /\.png$/ then "image/png" | ||
| + when /\.css$/ then "text/css" | ||
| + else "text/html" | ||
| + end | ||
| + | ||
| + puts("Uploading #{s3_path} (#{content_type})") | ||
| + | ||
| + File.open(local_path, "rb") do |local_file| | ||
| + s3.put_object( | ||
| + bucket: "gobyexample.com", | ||
| + key: s3_path, | ||
| + content_type: content_type, | ||
| + body: local_file) | ||
| + end | ||
| +end |
0 comments on commit
26fad2a