From 1a871c98a1e6b5ecceb3e56d58fa4c6b227856c9 Mon Sep 17 00:00:00 2001 From: Mark McGranaghan Date: Sat, 2 Jan 2016 16:10:00 -0800 Subject: [PATCH 1/2] Suppose a static system --- Procfile | 1 - README.md | 67 +++--------------- server.go | 208 ------------------------------------------------------ 3 files changed, 10 insertions(+), 266 deletions(-) delete mode 100644 Procfile delete mode 100644 server.go diff --git a/Procfile b/Procfile deleted file mode 100644 index 4d0c0cf..0000000 --- a/Procfile +++ /dev/null @@ -1 +0,0 @@ -web: gobyexample diff --git a/README.md b/README.md index b45f827..e8e5480 100644 --- a/README.md +++ b/README.md @@ -1,18 +1,21 @@ ## Go by Example -Content, toolchain, and web server for [Go by Example](https://gobyexample.com). +Content and build toolchain for [Go by Example](https://gobyexample.com), +site that teaches Go via annotated example programs. ### Overview -The Go by Example site is built by extracting code & +The Go by Example site is built by extracting code and comments from source files in `examples` and rendering -that data via the site `templates`. The programs -implementing this build process are in `tools`. +them via the `templates` into a static `public` +directory. The programs implementing this build process +are in `tools`, along with some vendor'd dependencies +in `vendor`. -The build process produces a directory of static files - -`public` - suitable for serving by any modern HTTP server. -We include a lightweight Go server in `server.go`. +The built `public` directory can be served by any +static content system. The production site uses S3 and +CloudFront, for example. ### Building @@ -32,56 +35,6 @@ $ tools/build-loop ``` -### Local Deploy - -To run and view the site locally: - -```bash -$ mkdir -p $GOPATH/src/github.com/mmcgrana -$ cd $GOPATH/src/github.com/mmcgrana -$ git clone git@github.com:mmcgrana/gobyexample.git -$ cd gobyexample -$ go get -$ PORT=5000 CANONICAL_HOST=127.0.0.1 FORCE_HTTPS=0 gobyexample -$ open http://127.0.0.1:5000/ -``` - - -### Heroku Deploy - -To setup the site on Heroku: - -```bash -$ export DEPLOY=$USER -$ export APP=gobyexample-$USER -$ heroku create $APP -r $DEPLOY -$ heroku config:add -a $APP - BUILDPACK_URL=https://github.com/mmcgrana/buildpack-go.git - CANONICAL_HOST=$APP.herokuapp.com \ - FORCE_HTTPS=1 \ - AUTH=go:byexample -$ heroku labs:enable dot-profile-d -a $APP -$ git push $DEPLOY master -$ heroku open -a $APP -``` - -Add a domain + SSL: - -```bash -$ heroku domains:add $DOMAIN -$ heroku addons:add ssl -r $DEPLOY -# order ssl cert for domain -$ cat > /tmp/server.key -$ cat > /tmp/server.crt.orig -$ curl https://knowledge.rapidssl.com/library/VERISIGN/ALL_OTHER/RapidSSL%20Intermediate/RapidSSL_CA_bundle.pem > /tmp/rapidssl_bundle.pem -$ cat /tmp/server.crt.orig /tmp/rapidssl_bundle.pem > /tmp/server.crt -$ heroku certs:add /tmp/server.crt /tmp/server.key -r $DEPLOY -# add ALIAS record from domain to ssl endpoint dns -$ heroku config:add CANONICAL_HOST=$DOMAIN -r $DEPLOY -$ heroku open -r $DEPLOY -``` - - ### License This work is copyright Mark McGranaghan and licensed under a diff --git a/server.go b/server.go deleted file mode 100644 index fc420c8..0000000 --- a/server.go +++ /dev/null @@ -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" -} From 7ce10fd6f1c2caaf1bf2c4db6ea9f629a6e7f410 Mon Sep 17 00:00:00 2001 From: Mark McGranaghan Date: Sat, 2 Jan 2016 17:33:10 -0800 Subject: [PATCH 2/2] Add in our Ruby script --- tools/upload-site | 42 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 42 insertions(+) create mode 100755 tools/upload-site diff --git a/tools/upload-site b/tools/upload-site new file mode 100755 index 0000000..b1eda68 --- /dev/null +++ b/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