package main import ( "bytes" "database/sql" "embed" "encoding/json" "errors" "fmt" "io/fs" "net/http" "os" "path" "strconv" "strings" ) //go:embed web var webFiles embed.FS type application struct { store *playgroundStore packages map[string]memoryAsset monaco http.Handler assets http.Handler pages map[string][]byte } func newApplication(store *playgroundStore, packages map[string]memoryAsset, monacoDirectory string) (*application, error) { if info, err := os.Stat(monacoDirectory); err != nil || !info.IsDir() { return nil, fmt.Errorf("Monaco assets not found at %s; run npm install", monacoDirectory) } assets, err := fs.Sub(webFiles, "web/assets") if err != nil { return nil, err } pages := make(map[string][]byte) for _, name := range []string{"home.html", "docs.html", "playground.html"} { content, err := webFiles.ReadFile("web/" + name) if err != nil { return nil, err } pages[name] = content } return &application{ store: store, packages: packages, monaco: http.StripPrefix("/assets/monaco/", http.FileServer(http.Dir(monacoDirectory))), assets: http.StripPrefix("/assets/", http.FileServer(http.FS(assets))), pages: pages, }, nil } func (app *application) routes() http.Handler { mux := http.NewServeMux() mux.HandleFunc("/pkg/", app.servePackage) mux.HandleFunc("/api/playgrounds", app.savePlayground) mux.HandleFunc("/api/playgrounds/", app.getPlayground) mux.Handle("/assets/monaco/", app.monaco) mux.Handle("/assets/", app.assets) mux.HandleFunc("/docs", app.servePage("docs.html")) mux.HandleFunc("/docs/", app.servePage("docs.html")) mux.HandleFunc("/playground", app.servePlayground) mux.HandleFunc("/playground/", app.servePlayground) mux.HandleFunc("/", app.serveHome) return app.headers(mux) } func (app *application) headers(next http.Handler) http.Handler { return http.HandlerFunc(func(response http.ResponseWriter, request *http.Request) { response.Header().Set("Cross-Origin-Opener-Policy", "same-origin") response.Header().Set("Cross-Origin-Embedder-Policy", "require-corp") response.Header().Set("Cross-Origin-Resource-Policy", "same-origin") response.Header().Set("Referrer-Policy", "strict-origin-when-cross-origin") response.Header().Set("X-Content-Type-Options", "nosniff") response.Header().Set("X-Frame-Options", "SAMEORIGIN") next.ServeHTTP(response, request) }) } func (app *application) serveHome(response http.ResponseWriter, request *http.Request) { if request.URL.Path != "/" { http.NotFound(response, request) return } app.writePage(response, request, "home.html") } func (app *application) servePage(name string) http.HandlerFunc { return func(response http.ResponseWriter, request *http.Request) { app.writePage(response, request, name) } } func (app *application) servePlayground(response http.ResponseWriter, request *http.Request) { if request.URL.Path != "/playground" && request.URL.Path != "/playground/" { parts := strings.Split(strings.Trim(request.URL.Path, "/"), "/") if len(parts) != 3 || parts[0] != "playground" || !playgroundIDPattern.MatchString(parts[1]) { http.NotFound(response, request) return } if revision, err := strconv.Atoi(parts[2]); err != nil || revision < 1 { http.NotFound(response, request) return } } app.writePage(response, request, "playground.html") } func (app *application) writePage(response http.ResponseWriter, request *http.Request, name string) { response.Header().Set("Content-Type", "text/html; charset=utf-8") response.Header().Set("Cache-Control", "no-cache") if request.Method == http.MethodHead { return } if request.Method != http.MethodGet { response.Header().Set("Allow", "GET, HEAD") http.Error(response, "method not allowed", http.StatusMethodNotAllowed) return } _, _ = response.Write(app.pages[name]) } func (app *application) servePackage(response http.ResponseWriter, request *http.Request) { if request.Method != http.MethodGet && request.Method != http.MethodHead { response.Header().Set("Allow", "GET, HEAD") http.Error(response, "method not allowed", http.StatusMethodNotAllowed) return } name := strings.TrimPrefix(path.Clean(request.URL.Path), "/pkg/") asset, exists := app.packages[name] if !exists || name == "." { http.NotFound(response, request) return } response.Header().Set("Access-Control-Allow-Origin", "*") response.Header().Set("Cache-Control", "no-cache") response.Header().Set("Content-Type", asset.contentType) response.Header().Set("Cross-Origin-Resource-Policy", "cross-origin") response.Header().Set("ETag", asset.etag) if request.Header.Get("If-None-Match") == asset.etag { response.WriteHeader(http.StatusNotModified) return } http.ServeContent(response, request, name, asset.modified, bytes.NewReader(asset.content)) } func (app *application) savePlayground(response http.ResponseWriter, request *http.Request) { if request.Method != http.MethodPost { response.Header().Set("Allow", "POST") http.Error(response, "method not allowed", http.StatusMethodNotAllowed) return } request.Body = http.MaxBytesReader(response, request.Body, maximumCodeBytes+4096) var input struct { ID string `json:"id"` Title string `json:"title"` Code string `json:"code"` } decoder := json.NewDecoder(request.Body) decoder.DisallowUnknownFields() if err := decoder.Decode(&input); err != nil { writeJSONError(response, "The save request is not valid.", http.StatusBadRequest) return } result, err := app.store.save(request.Context(), input.ID, strings.TrimSpace(input.Title), input.Code) if err != nil { writeJSONError(response, err.Error(), http.StatusBadRequest) return } writeJSON(response, result, http.StatusCreated) } func (app *application) getPlayground(response http.ResponseWriter, request *http.Request) { if request.Method != http.MethodGet { response.Header().Set("Allow", "GET") http.Error(response, "method not allowed", http.StatusMethodNotAllowed) return } parts := strings.Split(strings.TrimPrefix(request.URL.Path, "/api/playgrounds/"), "/") if len(parts) != 2 { http.NotFound(response, request) return } revision, err := strconv.Atoi(parts[1]) if err != nil { http.NotFound(response, request) return } result, err := app.store.get(request.Context(), parts[0], revision) if errors.Is(err, sql.ErrNoRows) { writeJSONError(response, "Playground not found.", http.StatusNotFound) return } if err != nil { writeJSONError(response, "Could not load the playground.", http.StatusInternalServerError) return } response.Header().Set("Cache-Control", "public, max-age=31536000, immutable") writeJSON(response, result, http.StatusOK) } func writeJSON(response http.ResponseWriter, value any, status int) { response.Header().Set("Content-Type", "application/json; charset=utf-8") response.WriteHeader(status) _ = json.NewEncoder(response).Encode(value) } func writeJSONError(response http.ResponseWriter, message string, status int) { writeJSON(response, map[string]string{"error": message}, status) }