blob: ad1c432137f2afbac629bf3522840d7ea748c715 [file] [log] [blame]
// HTTP server for saving, loading, and executing playground examples.
package main
import (
"bytes"
crand "crypto/rand"
"crypto/sha256"
"encoding/binary"
"encoding/hex"
"flag"
"fmt"
"log"
"math/rand"
"net/http"
"os"
"os/exec"
"os/signal"
"syscall"
"time"
)
var (
// This channel is closed when the server begins shutting down.
// No values are ever sent to it.
lameduck chan bool = make(chan bool)
address = flag.String("address", ":8181", "Address to listen on.")
// Note, shutdown triggers on SIGTERM or when the time limit is hit.
enableShutdown = flag.Bool("shutdown", true, "Whether to ever shutdown the machine.")
// Maximum request and output size. Same limit as imposed by Go tour.
// Note: The response includes error and status messages as well as output,
// so it can be larger (usually by a small constant, hard limited to
// 2*maxSize).
// maxSize should be large enough to fit all error and status messages
// written by compilerd to prevent reaching the hard limit.
maxSize = 1 << 16
)
// Seeds the non-secure random number generator.
func seedRNG() error {
var seed int64
err := binary.Read(crand.Reader, binary.LittleEndian, &seed)
if err != nil {
return fmt.Errorf("reseed failed: %v", err)
}
rand.Seed(seed)
return nil
}
//////////////////////////////////////////
// HTTP server
func healthz(w http.ResponseWriter, r *http.Request) {
select {
case <-lameduck:
w.WriteHeader(http.StatusInternalServerError)
default:
w.Write([]byte("ok"))
}
}
func main() {
flag.Parse()
if err := seedRNG(); err != nil {
panic(err)
}
if *enableShutdown {
limit_min := 60
delay_min := limit_min/2 + rand.Intn(limit_min/2)
// VMs will be periodically killed to prevent any owned VMs from causing
// damage. We want to shutdown cleanly before then so we don't cause
// requests to fail.
go waitForShutdown(time.Minute * time.Duration(delay_min))
}
if err := initDBHandles(); err != nil {
log.Fatal(err)
}
http.HandleFunc("/healthz", healthz)
http.HandleFunc("/compile", handlerCompile)
http.HandleFunc("/load", handlerLoad)
http.HandleFunc("/save", handlerSave)
log.Printf("Serving %s\n", *address)
http.ListenAndServe(*address, nil)
}
func waitForShutdown(limit time.Duration) {
var beforeExit func() error
// Shutdown if we get a SIGTERM.
term := make(chan os.Signal, 1)
signal.Notify(term, syscall.SIGTERM)
// Or if the time limit expires.
deadline := time.After(limit)
log.Println("Shutting down at", time.Now().Add(limit))
Loop:
for {
select {
case <-deadline:
// Shutdown the VM.
log.Println("Deadline expired, shutting down.")
beforeExit = exec.Command("sudo", "halt").Run
break Loop
case <-term:
log.Println("Got SIGTERM, shutting down.")
// VM is probably already shutting down, so just exit.
break Loop
}
}
// Fail health checks so we stop getting requests.
close(lameduck)
// Give running requests time to finish.
time.Sleep(30 * time.Second)
// Go ahead and shutdown.
if beforeExit != nil {
err := beforeExit()
if err != nil {
panic(err)
}
}
os.Exit(0)
}
//////////////////////////////////////////
// HTTP request helpers
// Handles CORS options and pre-flight requests.
// Returns false iff response processing should not continue.
func handleCORS(w http.ResponseWriter, r *http.Request) bool {
// CORS headers.
// TODO(nlacasse): Fill the origin header in with actual playground origin
// before going to production.
w.Header().Set("Access-Control-Allow-Origin", "*")
w.Header().Set("Access-Control-Allow-Methods", "POST, OPTIONS")
w.Header().Set("Access-Control-Allow-Headers", "Content-Type, Content-Length, Accept-Encoding")
// CORS sends an OPTIONS pre-flight request to make sure the request will be
// allowed.
if r.Method == "OPTIONS" {
w.WriteHeader(http.StatusOK)
return false
}
return true
}
// Checks if the GET method was used.
// Returns false iff response processing should not continue.
func checkGetMethod(w http.ResponseWriter, r *http.Request) bool {
if r.Method != "GET" {
w.WriteHeader(http.StatusBadRequest)
return false
}
return true
}
// Checks if the POST method was used and returns the request body.
// Returns nil iff response processing should not continue.
func getPostBody(w http.ResponseWriter, r *http.Request) []byte {
if r.Body == nil || r.Method != "POST" {
w.WriteHeader(http.StatusBadRequest)
return nil
}
buf := new(bytes.Buffer)
buf.ReadFrom(r.Body)
return buf.Bytes()
}
//////////////////////////////////////////
// Shared helper functions
func stringHash(data []byte) string {
hv := rawHash(data)
return hex.EncodeToString(hv[:])
}
func rawHash(data []byte) [32]byte {
return sha256.Sum256(data)
}