blob: 81ff51372e960000bafc891ac06081fa3da4c509 [file] [log] [blame]
// fiddle is the web server for fiddle.
package main
import (
"context"
"encoding/base64"
"encoding/json"
"flag"
"fmt"
"html/template"
ttemplate "html/template"
"net/http"
"path/filepath"
"regexp"
"strconv"
"strings"
"sync"
"github.com/gorilla/mux"
"go.skia.org/infra/fiddlek/go/named"
"go.skia.org/infra/fiddlek/go/runner"
"go.skia.org/infra/fiddlek/go/source"
"go.skia.org/infra/fiddlek/go/store"
"go.skia.org/infra/fiddlek/go/types"
"go.skia.org/infra/go/common"
"go.skia.org/infra/go/httputils"
"go.skia.org/infra/go/metrics2"
"go.skia.org/infra/go/sklog"
"go.skia.org/infra/go/util"
)
// flags
var (
fiddleRoot = flag.String("fiddle_root", "", "Directory location where all the work is done.")
local = flag.Bool("local", false, "Running locally if true. As opposed to in production.")
promPort = flag.String("prom_port", ":20000", "Metrics service address (e.g., ':10110')")
port = flag.String("port", ":8000", "HTTP service address (e.g., ':8000')")
resourcesDir = flag.String("resources_dir", "", "The directory to find templates, JS, and CSS files. If blank the current directory will be used.")
sourceImageDir = flag.String("source_image_dir", "./source", "The directory to load the source images from.")
)
var (
templates *template.Template
funcMap = ttemplate.FuncMap{
"chop": func(s string) string {
if len(s) > 6 {
return s[:6]
}
return s
},
}
defaultFiddle *types.FiddleContext = &types.FiddleContext{
Code: `void draw(SkCanvas* canvas) {
SkPaint p;
p.setColor(SK_ColorRED);
p.setAntiAlias(true);
p.setStyle(SkPaint::kStroke_Style);
p.setStrokeWidth(10);
canvas->drawLine(20, 20, 100, 100, p);
}`,
Options: types.Options{
Width: 256,
Height: 256,
Source: 0,
},
}
// trailingToMedia maps the end of each image URL to the store.Media type
// that it corresponds to.
trailingToMedia = map[string]store.Media{
"_raster.png": store.CPU,
"_gpu.png": store.GPU,
".pdf": store.PDF,
".skp": store.SKP,
}
// parseCompilerOutput parses the compiler output to look for lines
// that begin with "draw.cpp:<N>:<M>:" where N and M are the line and column
// number where the error occurred. It also strips off the full path name
// of draw.cpp.
//
// For example if we had the following input line:
//
// "/usr/local../src/draw.cpp:8:5: error: expected ‘)’ before ‘canvas’\n void draw(SkCanvas* canvas) {\n ^\n"
//
// Then the re.FindAllStringSubmatch(s, -1) will return a match of the form:
//
// [][]string{
// []string{
// "/usr/local.../src/draw.cpp:8:5: error: expected ‘)’ before ‘canvas’",
// "/usr/local.../src/",
// "draw.cpp:8:5: error: expected ‘)’ before ‘canvas’",
// "8",
// "5",
// },
// }
//
// Note that slice items 2, 3, and 4 are the ones we are really interested in.
parseCompilerOutput = regexp.MustCompile("^(.*/)(draw.cpp:([0-9]+):([-0-9]+):.*)")
namedFailures = metrics2.GetCounter("named_failures", nil)
maybeSecViolations = metrics2.GetCounter("maybe_sec_container_violation", nil)
runs = metrics2.GetCounter("runs", nil)
tryNamedLiveness = metrics2.NewLiveness("try_named")
fiddleStore *store.Store
src *source.Source
names *named.Named
failingNamed = []store.Named{}
failingMutex = sync.Mutex{}
run *runner.Runner
)
func loadTemplates() {
templates = template.Must(template.New("").Delims("{%", "%}").Funcs(funcMap).ParseFiles(
filepath.Join(*resourcesDir, "templates/index.html"),
filepath.Join(*resourcesDir, "templates/iframe.html"),
filepath.Join(*resourcesDir, "templates/failing.html"),
filepath.Join(*resourcesDir, "templates/named.html"),
// Sub templates used by other templates.
filepath.Join(*resourcesDir, "templates/header.html"),
filepath.Join(*resourcesDir, "templates/menu.html"),
))
}
func healthzHandler(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusOK)
}
func mainHandler(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "text/html")
if *local {
loadTemplates()
}
cp := *defaultFiddle
cp.Sources = src.ListAsJSON()
cp.Version = run.Version()
if err := templates.ExecuteTemplate(w, "index.html", cp); err != nil {
sklog.Errorf("Failed to expand template: %s", err)
}
}
func failedHandler(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "text/html")
if *local {
loadTemplates()
}
failingMutex.Lock()
defer failingMutex.Unlock()
if err := templates.ExecuteTemplate(w, "failing.html", failingNamed); err != nil {
sklog.Errorf("Failed to expand template: %s", err)
}
}
func namedHandler(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "text/html")
if *local {
loadTemplates()
}
named, err := fiddleStore.ListAllNames()
if err != nil {
httputils.ReportError(w, r, err, "Failed to retrieve list of named fiddles.")
}
if err := templates.ExecuteTemplate(w, "named.html", named); err != nil {
sklog.Errorf("Failed to expand template: %s", err)
}
}
// iframeHandle handles permalinks to individual fiddles.
func iframeHandle(w http.ResponseWriter, r *http.Request) {
id := mux.Vars(r)["id"]
fiddleHash, err := names.DereferenceID(id)
if err != nil {
http.NotFound(w, r)
sklog.Errorf("Invalid id: %s", err)
return
}
if *local {
loadTemplates()
}
code, options, err := fiddleStore.GetCode(fiddleHash)
if err != nil {
http.NotFound(w, r)
return
}
context := &types.FiddleContext{
Hash: id,
Code: code,
Options: *options,
}
w.Header().Set("Content-Type", "text/html")
if err := templates.ExecuteTemplate(w, "iframe.html", context); err != nil {
sklog.Errorf("Failed to expand template: %s", err)
}
}
func loadContext(w http.ResponseWriter, r *http.Request) (*types.FiddleContext, error) {
id := mux.Vars(r)["id"]
fiddleHash, err := names.DereferenceID(id)
if err != nil {
return nil, fmt.Errorf("Invalid id: %s", err)
}
if *local {
loadTemplates()
}
code, options, err := fiddleStore.GetCode(fiddleHash)
if err != nil {
return nil, fmt.Errorf("Fiddle not found.")
}
return &types.FiddleContext{
Version: run.Version(),
Sources: src.ListAsJSON(),
Hash: id,
Code: code,
Options: *options,
}, nil
}
// embedHandle returns a JSON description of a fiddle.
func embedHandle(w http.ResponseWriter, r *http.Request) {
w.Header().Add("Access-Control-Allow-Origin", "*")
context, err := loadContext(w, r)
if err != nil {
http.NotFound(w, r)
sklog.Errorf("Failed to load context: %s", err)
return
}
w.Header().Set("Content-Type", "application/json")
enc := json.NewEncoder(w)
if err := enc.Encode(context); err != nil {
httputils.ReportError(w, r, err, "Failed to JSON Encode response.")
}
}
// individualHandle handles permalinks to individual fiddles.
func individualHandle(w http.ResponseWriter, r *http.Request) {
context, err := loadContext(w, r)
if err != nil {
http.NotFound(w, r)
sklog.Errorf("Failed to load context: %s", err)
return
}
w.Header().Set("Content-Type", "text/html")
if err := templates.ExecuteTemplate(w, "index.html", context); err != nil {
sklog.Errorf("Failed to expand template: %s", err)
}
}
// imageHandler serves up images from the fiddle store.
//
// The URLs look like:
//
// /i/cbb8dee39e9f1576cd97c2d504db8eee_raster.png
// /i/cbb8dee39e9f1576cd97c2d504db8eee_gpu.png
// /i/cbb8dee39e9f1576cd97c2d504db8eee.pdf
// /i/cbb8dee39e9f1576cd97c2d504db8eee.skp
//
// or
//
// /i/@some_name.png
// /i/@some_name_gpu.png
// /i/@some_name.pdf
// /i/@some_name.skp
func imageHandler(w http.ResponseWriter, r *http.Request) {
w.Header().Add("Access-Control-Allow-Origin", "*")
id := mux.Vars(r)["id"]
fiddleHash, media, err := names.DereferenceImageID(id)
if fiddleHash == id {
w.Header().Add("Cache-Control", "max-age=36000")
}
if err != nil {
http.NotFound(w, r)
sklog.Errorf("Invalid id: %s", err)
return
}
body, contentType, _, err := fiddleStore.GetMedia(fiddleHash, media)
if err != nil {
http.NotFound(w, r)
sklog.Errorf("Failed to retrieve media: %s", err)
return
}
w.Header().Set("Content-Type", contentType)
if _, err := w.Write(body); err != nil {
sklog.Errorf("Failed to write image: %s", err)
}
}
// sourceHandler serves up source image thumbnails.
//
// The URLs look like:
//
// /s/NNN
//
// Where NNN is the id of the source image.
func sourceHandler(w http.ResponseWriter, r *http.Request) {
w.Header().Add("Access-Control-Allow-Origin", "*")
id := mux.Vars(r)["id"]
i, err := strconv.Atoi(id)
if err != nil {
http.NotFound(w, r)
sklog.Errorf("Invalid source id: %s", err)
return
}
b, ok := src.Thumbnail(i)
if !ok {
http.NotFound(w, r)
sklog.Errorf("Unknown source id %s", id)
return
}
w.Header().Add("Cache-Control", "max-age=360000")
w.Header().Set("Content-Type", "image/png")
if _, err := w.Write(b); err != nil {
sklog.Errorf("Failed to write image: %s", err)
return
}
}
func runHandler(w http.ResponseWriter, r *http.Request) {
w.Header().Add("Access-Control-Allow-Origin", "*")
w.Header().Add("Access-Control-Allow-Headers", "Content-Type")
w.Header().Add("Access-Control-Allow-Methods", "POST, GET")
if r.Method == "OPTIONS" {
return
}
sklog.Infof("RemoteAddr: %s %s", r.RemoteAddr, r.Header.Get("X-Forwarded-For"))
req := &types.FiddleContext{}
dec := json.NewDecoder(r.Body)
defer util.Close(r.Body)
if err := dec.Decode(req); err != nil {
httputils.ReportError(w, r, err, "Failed to decode request.")
return
}
resp, err, msg := runImpl(context.Background(), req)
if err != nil {
httputils.ReportError(w, r, err, msg)
return
}
w.Header().Set("Content-Type", "application/json")
enc := json.NewEncoder(w)
enc.SetIndent("", " ")
if err := enc.Encode(resp); err != nil {
httputils.ReportError(w, r, err, "Failed to JSON Encode response.")
}
}
func runImpl(ctx context.Context, req *types.FiddleContext) (*types.RunResults, error, string) {
resp := &types.RunResults{
CompileErrors: []types.CompileError{},
FiddleHash: "",
}
if err := run.ValidateOptions(&req.Options); err != nil {
return resp, fmt.Errorf("Invalid Options: %s", err), "Invalid options."
}
sklog.Infof("Request: %#v", *req)
// The fast path returns quickly if the fiddle already exists.
if req.Fast {
sklog.Infof("Trying the fast path.")
if fiddleHash, err := req.Options.ComputeHash(req.Code); err == nil {
if _, _, err := fiddleStore.GetCode(fiddleHash); err == nil {
resp.FiddleHash = fiddleHash
if req.Options.TextOnly {
if b, _, _, err := fiddleStore.GetMedia(fiddleHash, store.TXT); err != nil {
sklog.Infof("Failed to get text: %s", err)
} else {
resp.Text = string(b)
return resp, nil, ""
}
} else {
return resp, nil, ""
}
} else {
sklog.Infof("Failed to match hash: %s", err)
}
} else {
sklog.Infof("Failed to compute hash: %s", err)
}
}
res, err := run.Run(*local, req)
if err != nil {
return resp, fmt.Errorf("Failed to run the fiddle: %s", err), "Failed to run the fiddle."
}
maybeSecViolation := false
if res.Execute.Errors != "" {
sklog.Infof("Execution errors: %q", res.Execute.Errors)
maybeSecViolation = true
resp.RunTimeError = fmt.Sprintf("Failed to run, possibly violated security container: %q", res.Execute.Errors)
}
// Take the compiler output and strip off all the implementation dependant information
// and format it to be retured in types.RunResults.
if res.Compile.Output != "" {
lines := strings.Split(res.Compile.Output, "\n")
for _, line := range lines {
if strings.TrimSpace(line) == "" {
continue
}
match := parseCompilerOutput.FindAllStringSubmatch(line, -1)
if match == nil || len(match[0]) < 5 {
// Skip the ninja generated lines.
if strings.HasPrefix(line, "ninja:") || strings.HasPrefix(line, "[") {
continue
}
resp.CompileErrors = append(resp.CompileErrors, types.CompileError{
Text: line,
Line: 0,
Col: 0,
})
continue
}
line_num, err := strconv.Atoi(match[0][3])
if err != nil {
sklog.Errorf("Failed to parse compiler output line number: %#v: %s", match, err)
continue
}
col_num, err := strconv.Atoi(match[0][4])
if err != nil {
sklog.Errorf("Failed to parse compiler output column number: %#v: %s", match, err)
continue
}
resp.CompileErrors = append(resp.CompileErrors, types.CompileError{
Text: match[0][2],
Line: line_num,
Col: col_num,
})
}
}
// Since the compile failed we will only store the code, not the media.
if res.Compile.Errors != "" || res.Execute.Errors != "" {
res = nil
}
fiddleHash := ""
// Store the fiddle, but only if we are not in Fast mode and errors occurred.
if !(res == nil && req.Fast) {
fiddleHash, err = fiddleStore.Put(req.Code, req.Options, res)
if err != nil {
return resp, fmt.Errorf("Failed to store the fiddle: %s", err), "Failed to store the fiddle."
}
}
if maybeSecViolation {
maybeSecViolations.Inc(1)
sklog.Warningf("Attempted Security Container Violation for https://fiddle.skia.org/c/%s", fiddleHash)
}
runs.Inc(1)
resp.FiddleHash = fiddleHash
if req.Options.TextOnly && res != nil {
// decode
decodedText, err := base64.StdEncoding.DecodeString(res.Execute.Output.Text)
if err != nil {
return resp, fmt.Errorf("Text wasn't properly encoded base64: %s", err), "Failed to base64 decode text result."
}
resp.Text = string(decodedText)
}
return resp, nil, ""
}
func templateHandler(name string) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "text/html")
if *local {
loadTemplates()
}
if err := templates.ExecuteTemplate(w, name, struct{}{}); err != nil {
sklog.Errorf("Failed to expand template: %s", err)
}
}
}
func makeResourceHandler() func(http.ResponseWriter, *http.Request) {
fileServer := http.FileServer(http.Dir(*resourcesDir))
return func(w http.ResponseWriter, r *http.Request) {
w.Header().Add("Cache-Control", "max-age=300")
w.Header().Add("Access-Control-Allow-Origin", "*")
fileServer.ServeHTTP(w, r)
}
}
func main() {
common.InitWithMust(
"fiddle",
common.PrometheusOpt(promPort),
common.MetricsLoggingOpt(),
)
loadTemplates()
var err error
fiddleStore, err = store.New(*local)
if err != nil {
sklog.Fatalf("Failed to connect to store: %s", err)
}
run, err = runner.New(*local, *sourceImageDir)
if err != nil {
sklog.Fatalf("Failed to initialize runner: %s", err)
}
go run.Metrics()
src, err = source.New(*sourceImageDir)
if err != nil {
sklog.Fatalf("Failed to initialize source images: %s", err)
}
names = named.New(fiddleStore)
r := mux.NewRouter()
r.PathPrefix("/res/").HandlerFunc(makeResourceHandler())
r.HandleFunc("/i/{id:[@0-9a-zA-Z._]+}", imageHandler)
r.HandleFunc("/c/{id:[@0-9a-zA-Z_]+}", individualHandle)
r.HandleFunc("/e/{id:[@0-9a-zA-Z_]+}", embedHandle)
r.HandleFunc("/iframe/{id:[@0-9a-zA-Z_]+}", iframeHandle)
r.HandleFunc("/s/{id:[0-9]+}", sourceHandler)
r.HandleFunc("/f/", failedHandler)
r.HandleFunc("/named/", namedHandler)
r.HandleFunc("/", mainHandler)
r.HandleFunc("/_/run", runHandler)
r.HandleFunc("/healthz", healthzHandler)
h := httputils.LoggingGzipRequestResponse(r)
h = httputils.HealthzAndHTTPS(h)
http.Handle("/", h)
sklog.Infoln("Ready to serve.")
sklog.Fatal(http.ListenAndServe(*port, nil))
}