1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169
|
// 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"
"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 !allowShare(r) {
http.Error(w, "Forbidden", http.StatusForbidden)
return
}
target, _ := url.Parse(playgroundURL)
p := httputil.NewSingleHostReverseProxy(target)
p.Transport = &urlfetch.Transport{Context: appengine.NewContext(r)}
p.ServeHTTP(w, r)
}
func allowShare(r *http.Request) bool {
if appengine.IsDevAppServer() {
return true
}
switch r.Header.Get("X-AppEngine-Country") {
case "", "ZZ", "CN":
return false
}
return true
}
|