blob: 0ddaa008cd2d4eae2709e59d88c46636be780e41 [file] [log] [blame]
// Copyright 2015 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
// +build appengine
// Package proxy proxies requests to the sandbox compiler service and the
// playground share handler.
// It is designed to run only on the instance of godoc that serves golang.org.
package proxy
import (
"bytes"
"crypto/sha1"
"encoding/json"
"fmt"
"io"
"io/ioutil"
"net/http"
"net/http/httputil"
"net/url"
"strings"
"time"
"golang.org/x/net/context"
"google.golang.org/appengine"
"google.golang.org/appengine/log"
"google.golang.org/appengine/memcache"
"google.golang.org/appengine/urlfetch"
)
type Request struct {
Body string
}
type Response struct {
Errors string
Events []Event
}
type Event struct {
Message string
Kind string // "stdout" or "stderr"
Delay time.Duration // time to wait before printing Message
}
const (
// We need to use HTTP here for "reasons", but the traffic isn't
// sensitive and it only travels across Google's internal network
// so we should be OK.
sandboxURL = "http://sandbox.golang.org/compile"
playgroundURL = "https://play.golang.org"
)
const expires = 7 * 24 * time.Hour // 1 week
var cacheControlHeader = fmt.Sprintf("public, max-age=%d", int(expires.Seconds()))
func RegisterHandlers(mux *http.ServeMux) {
mux.HandleFunc("/compile", compile)
mux.HandleFunc("/share", share)
}
func compile(w http.ResponseWriter, r *http.Request) {
if r.Method != "POST" {
http.Error(w, "I only answer to POST requests.", http.StatusMethodNotAllowed)
return
}
c := appengine.NewContext(r)
body := r.FormValue("body")
res := &Response{}
key := cacheKey(body)
if _, err := memcache.Gob.Get(c, key, res); err != nil {
if err != memcache.ErrCacheMiss {
log.Errorf(c, "getting response cache: %v", err)
}
req := &Request{Body: body}
if err := makeSandboxRequest(c, req, res); err != nil {
log.Errorf(c, "compile error: %v", err)
http.Error(w, "Internal Server Error", http.StatusInternalServerError)
return
}
item := &memcache.Item{Key: key, Object: res}
if err := memcache.Gob.Set(c, item); err != nil {
log.Errorf(c, "setting response cache: %v", err)
}
}
expiresTime := time.Now().Add(expires).UTC()
w.Header().Set("Expires", expiresTime.Format(time.RFC1123))
w.Header().Set("Cache-Control", cacheControlHeader)
var out interface{}
switch r.FormValue("version") {
case "2":
out = res
default: // "1"
out = struct {
CompileErrors string `json:"compile_errors"`
Output string `json:"output"`
}{res.Errors, flatten(res.Events)}
}
if err := json.NewEncoder(w).Encode(out); err != nil {
log.Errorf(c, "encoding response: %v", err)
}
}
// makeSandboxRequest sends the given Request to the sandbox
// and stores the response in the given Response.
func makeSandboxRequest(c context.Context, req *Request, res *Response) error {
reqJ, err := json.Marshal(req)
if err != nil {
return fmt.Errorf("marshalling request: %v", err)
}
r, err := urlfetch.Client(c).Post(sandboxURL, "application/json", bytes.NewReader(reqJ))
if err != nil {
return fmt.Errorf("making request: %v", err)
}
defer r.Body.Close()
if r.StatusCode != http.StatusOK {
b, _ := ioutil.ReadAll(r.Body)
return fmt.Errorf("bad status: %v body:\n%s", r.Status, b)
}
err = json.NewDecoder(r.Body).Decode(res)
if err != nil {
return fmt.Errorf("unmarshalling response: %v", err)
}
return nil
}
// flatten takes a sequence of Events and returns their contents, concatenated.
func flatten(seq []Event) string {
var buf bytes.Buffer
for _, e := range seq {
buf.WriteString(e.Message)
}
return buf.String()
}
func cacheKey(body string) string {
h := sha1.New()
io.WriteString(h, body)
return fmt.Sprintf("prog-%x", h.Sum(nil))
}
func share(w http.ResponseWriter, r *http.Request) {
if googleCN(r) {
http.Error(w, http.StatusText(http.StatusForbidden), http.StatusForbidden)
return
}
target, _ := url.Parse(playgroundURL)
p := httputil.NewSingleHostReverseProxy(target)
p.Transport = &urlfetch.Transport{Context: appengine.NewContext(r)}
p.ServeHTTP(w, r)
}
func googleCN(r *http.Request) bool {
if r.FormValue("googlecn") != "" {
return true
}
if appengine.IsDevAppServer() {
return false
}
if strings.HasSuffix(r.Host, ".cn") {
return true
}
switch r.Header.Get("X-AppEngine-Country") {
case "", "ZZ", "CN":
return true
}
return false
}