| // 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. | |
| package play | |
| import ( | |
| "bytes" | |
| "context" | |
| "encoding/json" | |
| "fmt" | |
| "io" | |
| "log" | |
| "net/http" | |
| "regexp" | |
| "strings" | |
| "time" | |
| "golang.org/x/website/internal/web" | |
| ) | |
| const playgroundURL = "https://play.golang.org" | |
| type Request struct { | |
| Body string | |
| WithVet bool | |
| } | |
| type Response struct { | |
| Errors string | |
| Events []Event | |
| VetErrors string | |
| } | |
| type Event struct { | |
| Message string | |
| Kind string // "stdout" or "stderr" | |
| Delay time.Duration // time to wait before printing Message | |
| } | |
| const expires = 7 * 24 * time.Hour // 1 week | |
| var cacheControlHeader = fmt.Sprintf("public, max-age=%d", int(expires.Seconds())) | |
| // RegisterHandlers registers handlers for the playground endpoints. | |
| func RegisterHandlers(mux *http.ServeMux, godevSite, chinaSite *web.Site) { | |
| mux.Handle("/play/", playHandler(godevSite)) | |
| mux.Handle("golang.google.cn/play/", playHandler(chinaSite)) | |
| for _, pattern := range []string{"golang.org", "go.dev/_", "golang.google.cn/_"} { | |
| mux.HandleFunc(pattern+"/compile", compile) | |
| if pattern != "golang.google.cn/_" { | |
| mux.HandleFunc(pattern+"/share", share) | |
| } | |
| mux.HandleFunc(pattern+"/fmt", fmtHandler) | |
| } | |
| } | |
| func compile(w http.ResponseWriter, r *http.Request) { | |
| if r.Method != "POST" { | |
| http.Error(w, "I only answer to POST requests.", http.StatusMethodNotAllowed) | |
| return | |
| } | |
| ctx := r.Context() | |
| body := r.FormValue("body") | |
| withVet := r.FormValue("withVet") | |
| res := &Response{} | |
| req := &Request{Body: body, WithVet: withVet == "true"} | |
| if err := makeCompileRequest(ctx, backend(r), req, res); err != nil { | |
| log.Printf("ERROR compile error %s: %v", backend(r), err) | |
| http.Error(w, "Internal Server Error", http.StatusInternalServerError) | |
| return | |
| } | |
| 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)} | |
| } | |
| b, err := json.Marshal(out) | |
| if err != nil { | |
| log.Printf("ERROR encoding response: %v", err) | |
| http.Error(w, "Internal Server Error", http.StatusInternalServerError) | |
| return | |
| } | |
| expiresTime := time.Now().Add(expires).UTC() | |
| w.Header().Set("Expires", expiresTime.Format(time.RFC1123)) | |
| w.Header().Set("Cache-Control", cacheControlHeader) | |
| w.Write(b) | |
| } | |
| // makeCompileRequest sends the given Request to the playground compile | |
| // endpoint and stores the response in the given Response. | |
| func makeCompileRequest(ctx context.Context, backend string, req *Request, res *Response) error { | |
| reqJ, err := json.Marshal(req) | |
| if err != nil { | |
| return fmt.Errorf("marshaling request: %v", err) | |
| } | |
| hReq, _ := http.NewRequest("POST", "https://"+backend+"/compile", bytes.NewReader(reqJ)) | |
| hReq.Header.Set("Content-Type", "application/json") | |
| hReq = hReq.WithContext(ctx) | |
| client := &http.Client{ | |
| Timeout: 20 * time.Second, | |
| } | |
| r, err := client.Do(hReq) | |
| if err != nil { | |
| return fmt.Errorf("making request: %v", err) | |
| } | |
| defer r.Body.Close() | |
| if r.StatusCode != http.StatusOK { | |
| b, _ := io.ReadAll(r.Body) | |
| return fmt.Errorf("bad status: %v body:\n%s", r.Status, b) | |
| } | |
| if err := json.NewDecoder(r.Body).Decode(res); err != nil { | |
| return fmt.Errorf("unmarshaling 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() | |
| } | |
| var validID = regexp.MustCompile(`^[A-Za-z0-9_\-]+$`) | |
| func share(w http.ResponseWriter, r *http.Request) { | |
| if id := r.FormValue("id"); r.Method == "GET" && validID.MatchString(id) { | |
| simpleProxy(w, r, playgroundURL+"/p/"+id+".go") | |
| return | |
| } | |
| simpleProxy(w, r, playgroundURL+"/share") | |
| } | |
| func fmtHandler(w http.ResponseWriter, r *http.Request) { | |
| simpleProxy(w, r, "https://"+backend(r)+"/fmt") | |
| } | |
| func simpleProxy(w http.ResponseWriter, r *http.Request, url string) { | |
| if r.Method == "GET" { | |
| r.Body = nil | |
| } else if len(r.Form) > 0 { | |
| r.Body = io.NopCloser(strings.NewReader(r.Form.Encode())) | |
| } | |
| req, _ := http.NewRequest(r.Method, url, r.Body) | |
| req.Header.Set("Content-Type", r.Header.Get("Content-Type")) | |
| req = req.WithContext(r.Context()) | |
| resp, err := http.DefaultClient.Do(req) | |
| if err != nil { | |
| log.Printf("ERROR share error: %v", err) | |
| http.Error(w, "Internal Server Error", http.StatusInternalServerError) | |
| return | |
| } | |
| copyHeader := func(k string) { | |
| if v := resp.Header.Get(k); v != "" { | |
| w.Header().Set(k, v) | |
| } | |
| } | |
| copyHeader("Content-Type") | |
| copyHeader("Content-Length") | |
| defer resp.Body.Close() | |
| w.WriteHeader(resp.StatusCode) | |
| io.Copy(w, resp.Body) | |
| } | |
| func backend(r *http.Request) string { | |
| b := r.URL.Query().Get("backend") | |
| if !isDomainElem(b) { | |
| return "play.golang.org" | |
| } | |
| return b + "play.golang.org" | |
| } | |
| func isDomainElem(s string) bool { | |
| for i := 0; i < len(s); i++ { | |
| if !('a' <= s[i] && s[i] <= 'z' || '0' <= s[i] && s[i] <= '9') { | |
| return false | |
| } | |
| } | |
| return s != "" | |
| } |