mermaid-server/internal/http.go

176 lines
4.7 KiB
Go
Raw Permalink Normal View History

package internal
import (
"encoding/json"
"fmt"
2021-08-14 17:34:07 +02:00
"github.com/tomwright/grace"
"github.com/tomwright/gracehttpserverrunner"
"io/ioutil"
"log"
"net/http"
2020-04-15 18:44:17 +02:00
"net/url"
"strings"
2021-08-14 17:34:07 +02:00
"time"
)
2021-08-14 17:34:07 +02:00
// NewHTTPRunner returns a grace runner that runs a HTTP server.
2022-09-29 17:29:44 +02:00
func NewHTTPRunner(generator Generator, allowAllOrigins bool) grace.Runner {
2021-08-14 17:34:07 +02:00
httpHandler := generateHTTPHandler(generator)
2022-09-29 17:29:44 +02:00
if allowAllOrigins {
httpHandler = allowAllOriginsMiddleware(httpHandler)
}
2021-08-14 17:34:07 +02:00
r := http.NewServeMux()
2022-09-29 17:29:44 +02:00
r.Handle("/generate", httpHandler)
2021-08-14 17:34:07 +02:00
return &gracehttpserverrunner.HTTPServerRunner{
Server: &http.Server{
Addr: ":80",
Handler: r,
},
ShutdownTimeout: time.Second * 5,
}
}
2022-09-29 17:29:44 +02:00
// allowAllOriginsMiddleware sets appropriate CORS headers to allow requests from any origin.
func allowAllOriginsMiddleware(h http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
origin := r.Header.Get("Origin")
if origin == "" {
origin = "*"
}
w.Header().Set("Access-Control-Allow-Origin", origin)
h.ServeHTTP(w, r)
})
}
func writeJSON(rw http.ResponseWriter, value interface{}, status int) {
bytes, err := json.Marshal(value)
if err != nil {
panic("could not marshal value: " + err.Error())
}
rw.Header().Set("Content-Type", "application/json")
rw.WriteHeader(status)
if _, err := rw.Write(bytes); err != nil {
panic("could not write bytes to response: " + err.Error())
}
}
2020-10-02 15:33:34 +02:00
func writeImage(rw http.ResponseWriter, data []byte, status int, imgType string) error {
switch imgType {
case "png":
rw.Header().Set("Content-Type", "image/png")
2020-10-02 15:33:34 +02:00
case "svg":
rw.Header().Set("Content-Type", "image/svg+xml")
2020-10-02 15:33:34 +02:00
default:
return fmt.Errorf("unhandled image type: %s", imgType)
}
rw.WriteHeader(status)
if _, err := rw.Write(data); err != nil {
2020-10-02 15:33:34 +02:00
return fmt.Errorf("could not write image bytes: %w", err)
}
2020-10-02 15:33:34 +02:00
return nil
}
func writeErr(rw http.ResponseWriter, err error, status int) {
log.Printf("[%d] %s", status, err)
writeJSON(rw, map[string]interface{}{
"error": err,
}, status)
}
2020-04-15 18:44:17 +02:00
// URLParam is the URL parameter getDiagramFromGET uses to look for data.
const URLParam = "data"
2020-10-02 15:33:34 +02:00
func getDiagramFromGET(r *http.Request, imgType string) (*Diagram, error) {
2020-04-15 18:44:17 +02:00
if r.Method != http.MethodGet {
2020-10-02 15:33:34 +02:00
return nil, fmt.Errorf("expected HTTP method GET")
2020-04-15 18:44:17 +02:00
}
queryVal := strings.TrimSpace(r.URL.Query().Get(URLParam))
if queryVal == "" {
2020-10-02 15:33:34 +02:00
return nil, fmt.Errorf("missing data")
2020-04-15 18:44:17 +02:00
}
data, err := url.QueryUnescape(queryVal)
if err != nil {
2020-10-02 15:33:34 +02:00
return nil, fmt.Errorf("could not read query param: %s", err)
2020-04-15 18:44:17 +02:00
}
// Create a diagram from the description
d := NewDiagram([]byte(data), imgType)
2020-10-02 15:33:34 +02:00
return d, nil
2020-04-15 18:44:17 +02:00
}
2020-10-02 15:33:34 +02:00
func getDiagramFromPOST(r *http.Request, imgType string) (*Diagram, error) {
2020-04-15 18:44:17 +02:00
if r.Method != http.MethodPost {
2020-10-02 15:33:34 +02:00
return nil, fmt.Errorf("expected HTTP method POST")
2020-04-15 18:44:17 +02:00
}
// Get description from request body
bytes, err := ioutil.ReadAll(r.Body)
if err != nil {
2020-10-02 15:33:34 +02:00
return nil, fmt.Errorf("could not read body: %s", err)
2020-04-15 18:44:17 +02:00
}
// Create a diagram from the description
d := NewDiagram(bytes, imgType)
2020-10-02 15:33:34 +02:00
return d, nil
2020-04-15 18:44:17 +02:00
}
2020-10-02 15:33:34 +02:00
const URLParamImageType = "type"
// generateHTTPHandler returns a HTTP handler used to generate a diagram.
2022-09-29 17:29:44 +02:00
func generateHTTPHandler(generator Generator) http.Handler {
return http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) {
2020-04-15 18:44:17 +02:00
var diagram *Diagram
2020-10-02 15:33:34 +02:00
imgType := r.URL.Query().Get(URLParamImageType)
2020-10-02 15:33:34 +02:00
switch imgType {
case "png", "svg":
case "":
imgType = "svg"
default:
writeErr(rw, fmt.Errorf("unsupported image type (%s) use svg or png", imgType), http.StatusBadRequest)
return
}
2020-10-02 15:33:34 +02:00
var err error
2020-04-15 18:44:17 +02:00
switch r.Method {
case http.MethodGet:
2020-10-02 15:33:34 +02:00
diagram, err = getDiagramFromGET(r, imgType)
2020-04-15 18:44:17 +02:00
case http.MethodPost:
2020-10-02 15:33:34 +02:00
diagram, err = getDiagramFromPOST(r, imgType)
2020-04-15 18:44:17 +02:00
default:
writeErr(rw, fmt.Errorf("unexpected HTTP method %s", r.Method), http.StatusBadRequest)
return
}
2020-10-02 15:33:34 +02:00
if err != nil {
writeErr(rw, err, http.StatusBadRequest)
return
}
2020-04-15 18:44:17 +02:00
if diagram == nil {
writeErr(rw, fmt.Errorf("could not create diagram"), http.StatusInternalServerError)
return
}
// Generate the diagram
2020-04-15 18:44:17 +02:00
if err := generator.Generate(diagram); err != nil {
writeErr(rw, fmt.Errorf("could not generate diagram: %s", err), http.StatusInternalServerError)
return
}
// Output the diagram as an SVG.
// We assume generate always generates an SVG at this point in time.
2020-04-15 18:44:17 +02:00
diagramBytes, err := ioutil.ReadFile(diagram.Output)
if err != nil {
writeErr(rw, fmt.Errorf("could not read diagram bytes: %s", err), http.StatusInternalServerError)
return
}
2020-10-02 15:33:34 +02:00
if err := writeImage(rw, diagramBytes, http.StatusOK, imgType); err != nil {
writeErr(rw, fmt.Errorf("could not write diagram: %w", err), http.StatusInternalServerError)
}
2022-09-29 17:29:44 +02:00
})
}