package spa import ( "fmt" "io/ioutil" "net/http" "time" "github.com/gobuffalo/packr/v2" "github.com/iotaledger/goshimmer/plugins/config" "github.com/labstack/echo" "github.com/pkg/errors" ) var ErrInvalidParameter = errors.New("invalid parameter") var ErrInternalError = errors.New("internal error") var ErrNotFound = errors.New("not found") var ErrForbidden = errors.New("forbidden") // holds SPA assets var appBox = packr.New("SPA_App", "./frontend/build") var assetsBox = packr.New("SPA_Assets", "./frontend/src/assets") func indexRoute(e echo.Context) error { if config.Node.GetBool(CFG_DEV) { res, err := http.Get("http://127.0.0.1:9090/") if err != nil { return err } devIndexHTML, err := ioutil.ReadAll(res.Body) if err != nil { return err } return e.HTMLBlob(http.StatusOK, devIndexHTML) } indexHTML, err := appBox.Find("index.html") if err != nil { return err } return e.HTMLBlob(http.StatusOK, indexHTML) } func setupRoutes(e *echo.Echo) { if config.Node.GetBool("dashboard.dev") { e.Static("/assets", "./plugins/spa/frontend/src/assets") } else { // load assets from packr: either from within the binary or actual disk for _, res := range appBox.List() { e.GET("/app/"+res, echo.WrapHandler(http.StripPrefix("/app", http.FileServer(appBox)))) } for _, res := range assetsBox.List() { e.GET("/assets/"+res, echo.WrapHandler(http.StripPrefix("/assets", http.FileServer(assetsBox)))) } } e.GET("/ws", websocketRoute) e.GET("/", indexRoute) // used to route into the SPA index e.GET("*", indexRoute) apiRoutes := e.Group("/api") setupExplorerRoutes(apiRoutes) e.HTTPErrorHandler = func(err error, c echo.Context) { c.Logger().Error(err) var statusCode int var message string switch errors.Cause(err) { case echo.ErrNotFound: c.Redirect(http.StatusSeeOther, "/") return case echo.ErrUnauthorized: statusCode = http.StatusUnauthorized message = "unauthorized" case ErrForbidden: statusCode = http.StatusForbidden message = "access forbidden" case ErrInternalError: statusCode = http.StatusInternalServerError message = "internal server error" case ErrNotFound: statusCode = http.StatusNotFound message = "not found" case ErrInvalidParameter: statusCode = http.StatusBadRequest message = "bad request" default: statusCode = http.StatusInternalServerError message = "internal server error" } message = fmt.Sprintf("%s, error: %+v", message, err) c.String(statusCode, message) } } func registerWSClient() (uint64, chan interface{}) { // allocate new client id clientsMu.Lock() defer clientsMu.Unlock() clientID := nextClientID channel := make(chan interface{}, 100) clients[clientID] = channel nextClientID++ return clientID, channel } func websocketRoute(c echo.Context) error { defer func() { if r := recover(); r != nil { log.Errorf("recovered from panic within WS handle func: %s", r) } }() ws, err := upgrader.Upgrade(c.Response(), c.Request(), nil) if err != nil { return err } defer ws.Close() ws.EnableWriteCompression(true) // cleanup client websocket clientID, channel := registerWSClient() defer func() { clientsMu.Lock() delete(clients, clientID) close(channel) clientsMu.Unlock() }() msgRateLimiter := time.NewTicker(time.Second / 20) defer msgRateLimiter.Stop() for { <-msgRateLimiter.C msg := <-channel if err := ws.WriteJSON(msg); err != nil { log.Warnf("error while writing to web socket client %s: %s", c.RealIP(), err.Error()) break } if err := ws.SetWriteDeadline(time.Now().Add(webSocketWriteTimeout)); err != nil { log.Warnf("error while setting write deadline on web socket client %s: %s", c.RealIP(), err.Error()) break } } return nil }