package main import ( "errors" "fmt" "net/http" "os" "path" "regexp" "syscall" "time" "github.com/google/uuid" "github.com/gorilla/websocket" "github.com/joho/godotenv" "github.com/labstack/echo/v4" "github.com/labstack/echo/v4/middleware" "github.com/labstack/gommon/log" ) const TimeFormat = "2006-01-02 15:04:05" const DateFormat = "2006-01-02" const indent = "\t" var LogRegex = regexp.MustCompile(`(?m)^\d{4}-\d{2}-\d{2}_.*\.txt$`) type Client struct { ID uuid.UUID `json:"id"` Name string `json:"name"` Connected bool `json:"connected"` ConnectedAt time.Time `json:"connectedAt"` DisconnectedAt time.Time `json:"disconnectedAt"` CreatedAt time.Time `json:"createdAt"` UpdatedAt time.Time `json:"updatedAt"` ExitWanted bool `json:"exitWanted"` conn *websocket.Conn `json:"-"` } var clients = []*Client{} var dataDir = "." var token = "" var upgrader = websocket.Upgrader{} type IDParam struct { ID string `param:"id"` } func authed(next echo.HandlerFunc) echo.HandlerFunc { return func(c echo.Context) error { if c.Request().Header.Get("Authorization") != token { return c.NoContent(http.StatusUnauthorized) } return next(c) } } func idparam(next func(echo.Context, uuid.UUID) error) echo.HandlerFunc { return func(c echo.Context) error { var data IDParam err := c.Bind(&data) if err != nil || data.ID == "" { return c.JSON(http.StatusBadRequest, echo.Map{"message": "missing field 'id'"}) } id, err := uuid.Parse(data.ID) if err != nil { return c.JSON(http.StatusBadRequest, echo.Map{"message": "field 'id' is not a uuid"}) } return next(c, id) } } func filename(dataDir string, id string, date string) string { return fmt.Sprintf("%s/%s_%s.txt", dataDir, date, id) } func main() { _ = godotenv.Load() dataDir = os.Getenv("DATA_DIR") if dataDir == "" { log.Fatal("DATA_DIR is not defined") } token = os.Getenv("TOKEN") if token == "" { log.Fatal("TOKEN is not defined") } f, err := os.OpenFile(path.Join(dataDir, "startup"), syscall.O_CREAT|syscall.O_APPEND|syscall.O_WRONLY, 0644) if err != nil { log.Fatalf("DATA_DIR %s is not writable (%v)\n", dataDir, err) } _, err = f.Write([]byte(fmt.Sprintf("%s\n", time.Now().Format(TimeFormat)))) if err != nil { log.Fatalf("DATA_DIR test file couldn't be written to (%v)\n", err) } err = f.Close() if err != nil { log.Fatalf("DATA_DIR test file couldn't be closed (%v)\n", err) } app := echo.New() app.Logger.SetLevel(log.INFO) app.Use(middleware.RecoverWithConfig(middleware.RecoverConfig{ StackSize: 1 << 10, LogLevel: log.ERROR, })) app.Use(middleware.Secure()) app.Use(middleware.LoggerWithConfig(middleware.LoggerConfig{ Format: "${time_custom} ${method} ${uri} ---> ${status} in ${latency_human} (${bytes_out} bytes)\n", CustomTimeFormat: TimeFormat, })) log.SetHeader("${time_rfc3339} ${level}") app.Use(middleware.RemoveTrailingSlash()) app.OnAddRouteHandler = func(host string, route echo.Route, handler echo.HandlerFunc, middleware []echo.MiddlewareFunc) { now := time.Now() fmt.Printf("%s registered %-6s %s\n", now.Format(TimeFormat), route.Method, route.Path) } app.HTTPErrorHandler = func(err error, c echo.Context) { if c.Response().Committed { log.Error(err) return } code := http.StatusInternalServerError var response any = err.Error() var httpErr *echo.HTTPError if errors.As(err, &httpErr) { code = httpErr.Code response = httpErr } if code >= 500 { log.Error(err) } err2 := c.JSON(code, response) if err2 != nil { log.Error(err2) } } // client connection app.GET("/", func(c echo.Context) error { return c.JSONPretty(http.StatusOK, "Hello World!", indent) }) app.GET("/keys", keys) // administration app.GET("/admin/clients", authed(getClients)) app.GET("/admin/clients/:id", authed(idparam(getClient))) app.GET("/admin/logs", authed(getLogs)) app.GET("/admin/logs/:id", authed(idparam(getClientLogs))) app.POST("/admin/name/:id", authed(idparam(changeClientName))) app.POST("/admin/exit/:id", authed(idparam(exitClient))) log.Fatal(app.Start(":8080")) }