blob: a9c35f88e66f3ece755cd0eec5c9b6b49188f26f [file] [log] [blame]
package main
import (
"encoding/json"
"flag"
"fmt"
"html/template"
"mime"
"net/http"
"path/filepath"
"strings"
"github.com/gorilla/mux"
"github.com/unrolled/secure"
"go.skia.org/infra/go/baseapp"
"go.skia.org/infra/go/httputils"
"go.skia.org/infra/go/skerr"
"go.skia.org/infra/go/sklog"
"go.skia.org/infra/scrap/go/client"
"go.skia.org/infra/scrap/go/fakeclient"
"go.skia.org/infra/scrap/go/scrap"
)
// flags
var (
scrapExchange = flag.String("scrapexchange", "http://scrapexchange:9000", "Scrap exchange service HTTP address.")
fakeScrapExchange = flag.Bool("fake_scrapexchange", false, "If set to true, --scrapexchange will be ignored and a fake, in-memory implementation will be used instead.")
)
// server is the state of the server.
type server struct {
scrapClient scrap.ScrapExchange
templates *template.Template
}
// See baseapp.Constructor.
func new() (baseapp.App, error) {
// Need to set the mime-type for wasm files so streaming compile works.
if err := mime.AddExtensionType(".wasm", "application/wasm"); err != nil {
return nil, skerr.Wrap(err)
}
var scrapClient scrap.ScrapExchange
if *fakeScrapExchange {
sklog.Infof("Using fake (in-memory) scrapexchange client")
scrapClient = fakeclient.New(map[string]scrap.ScrapBody{
"@default": {
Type: "sksl",
Body: blueNeuronShaderBody,
SKSLMetaData: &scrap.SKSLMetaData{
ImageURL: "/img/mandrill.png",
},
},
})
} else {
var err error
scrapClient, err = client.New(*scrapExchange)
if err != nil {
sklog.Fatalf("Failed to create scrap exchange client: %s", err)
}
}
srv := &server{
scrapClient: scrapClient,
}
srv.loadTemplates()
return srv, nil
}
// isResourcePathCorsSafe determines if an image is OK to serve to another
// origin. |p| is the resource path relative to the /img directory.
func isResourcePathCorsSafe(p string) bool {
return strings.HasSuffix(p, ".png")
}
// makeCorsResourceHandler is an HTTP handler function designed for serving files from the
// /img directory allowing cross-origin requests. It will only serve images deemed to be
// OK for other sites to access.
func makeCorsResourceHandler(resourcesDir string) func(http.ResponseWriter, *http.Request) {
fileServer := http.FileServer(http.Dir(resourcesDir))
return func(w http.ResponseWriter, r *http.Request) {
if !isResourcePathCorsSafe(r.URL.Path) {
err := skerr.Fmt("%q is not an image", r.URL.Path)
httputils.ReportError(w, err, "Resource not an image.", http.StatusUnauthorized)
return
}
w.Header().Add("Cache-Control", "max-age=300")
w.Header().Add("Access-Control-Allow-Origin", "*")
fileServer.ServeHTTP(w, r)
}
}
func (srv *server) loadTemplates() {
srv.templates = template.Must(template.New("").Delims("{%", "%}").ParseFiles(
filepath.Join(*baseapp.ResourcesDir, "main.html"),
filepath.Join(*baseapp.ResourcesDir, "debugger.html"),
))
}
func (srv *server) pageHandler(w http.ResponseWriter, r *http.Request, p string) {
w.Header().Set("Content-Type", "text/html")
if *baseapp.Local {
srv.loadTemplates()
}
if err := srv.templates.ExecuteTemplate(w, p, map[string]string{
// Look in //shaders/pages/BUILD.bazel for where the nonce templates are injected.
"Nonce": secure.CSPNonce(r.Context()),
}); err != nil {
sklog.Errorf("Failed to expand template: %s", err)
}
}
func (srv *server) mainHandler(w http.ResponseWriter, r *http.Request) {
srv.pageHandler(w, r, "main.html")
}
func (srv *server) debugHandler(w http.ResponseWriter, r *http.Request) {
srv.pageHandler(w, r, "debugger.html")
}
func (srv *server) loadHandler(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
hashOrName := mux.Vars(r)["hashOrName"]
body, err := srv.scrapClient.LoadScrap(r.Context(), scrap.SKSL, hashOrName)
if err != nil {
httputils.ReportError(w, err, "Failed to read JSON file.", http.StatusInternalServerError)
return
}
if err := json.NewEncoder(w).Encode(body); err != nil {
sklog.Errorf("Failed to write response: %s", err)
}
}
func (srv *server) saveHandler(w http.ResponseWriter, r *http.Request) {
// Decode Request.
var req scrap.ScrapBody
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
httputils.ReportError(w, err, "Error decoding JSON.", http.StatusBadRequest)
return
}
if req.Type != scrap.SKSL {
httputils.ReportError(w, fmt.Errorf("Received invalid scrap type: %q", req.Type), "Invalid Type.", http.StatusBadRequest)
return
}
scrapID, err := srv.scrapClient.CreateScrap(r.Context(), req)
if err != nil {
httputils.ReportError(w, err, "Error creating scrap.", http.StatusInternalServerError)
return
}
w.Header().Set("Content-Type", "application/json")
if err := json.NewEncoder(w).Encode(scrapID); err != nil {
sklog.Errorf("Failed to write response: %s", err)
}
}
// See baseapp.App.
func (srv *server) AddHandlers(r *mux.Router) {
r.HandleFunc("/", srv.mainHandler)
r.HandleFunc("/debug", srv.debugHandler)
r.HandleFunc("/_/load/{hashOrName:[@0-9a-zA-Z-_]+}", srv.loadHandler).Methods("GET")
r.HandleFunc("/_/save/", srv.saveHandler).Methods("POST")
// /img/ is an alias for /dist/ and serves(almost) the same files.
// It differs from the /dist/ resource handler (defined in baseapp) in two ways:
//
// 1. The resource handler allows cross-origin resource fetches.
// 2. Only shader images are allowed - all other requests will fail.
r.PathPrefix("/img/").Handler(http.StripPrefix("/img/", http.HandlerFunc(makeCorsResourceHandler(*baseapp.ResourcesDir))))
}
// See baseapp.App.
func (srv *server) AddMiddleware() []mux.MiddlewareFunc {
return []mux.MiddlewareFunc{}
}
func main() {
baseapp.Serve(new, []string{"shaders.skia.org"}, baseapp.AllowWASM{}, baseapp.AllowAnyImage{})
}
// This is the same shader that is the current default on shaders.skia.org (the
// blue neuron-looking one).
const blueNeuronShaderBody = `
// Source: @notargs https://twitter.com/notargs/status/1250468645030858753
float f(vec3 p) {
p.z -= iTime * 10.;
float a = p.z * .1;
p.xy *= mat2(cos(a), sin(a), -sin(a), cos(a));
return .1 - length(cos(p.xy) + sin(p.yz));
}
half4 main(vec2 fragcoord) {
vec3 d = .5 - fragcoord.xy1 / iResolution.y;
vec3 p=vec3(0);
for (int i = 0; i < 32; i++) {
p += f(p) * d;
}
return ((sin(p) + vec3(2, 5, 9)) / length(p)).xyz1;
}`