diff --git a/server/admin.go b/server/admin.go new file mode 100644 index 0000000..0a07dba --- /dev/null +++ b/server/admin.go @@ -0,0 +1,130 @@ +package main + +import ( + "io" + "net/http" + "os" + "strconv" + "syscall" + "time" + + "github.com/google/uuid" + "github.com/labstack/echo/v4" + "github.com/labstack/gommon/log" +) + +func getClients(c echo.Context) error { + return c.JSONPretty(http.StatusOK, clients, indent) +} + +func getClient(c echo.Context, id uuid.UUID) error { + for _, client := range clients { + if client.ID == id || client.Name == id.String() { + return c.JSONPretty(http.StatusOK, client, indent) + } + } + + return c.NoContent(http.StatusNotFound) +} + +func getLogs(c echo.Context) error { + files, err := os.ReadDir(dataDir) + if err != nil { + log.Error(err) + return c.NoContent(http.StatusInternalServerError) + } + + filenames := []string{} + for _, file := range files { + if LogRegex.MatchString(file.Name()) { + filenames = append(filenames, file.Name()) + } + } + + return c.JSONPretty(http.StatusOK, filenames, indent) +} + +func getClientLogs(c echo.Context, id uuid.UUID) error { + date := c.QueryParam("date") + if date == "" { + date = time.Now().Format(DateFormat) + } + + skipRaw := c.QueryParam("skip") + skip := 0 + if skipRaw != "" { + skip, err = strconv.Atoi(skipRaw) + if err != nil { + return c.JSON(http.StatusBadRequest, echo.Map{"message": "skip is not a number"}) + } + } + + takeRaw := c.QueryParam("take") + take := 1024 + if takeRaw != "" { + take, err = strconv.Atoi(takeRaw) + if err != nil { + return c.JSON(http.StatusBadRequest, echo.Map{"message": "take is not a number"}) + } + } + + f, err := os.OpenFile(filename(dataDir, id.String(), date), syscall.O_RDONLY, 0644) + if err != nil { + return c.JSON(http.StatusNotFound, echo.Map{"message": "file not found"}) + } + + if skip != 0 { + _, err = f.Seek(int64(skip), io.SeekStart) + if err != nil { + log.Error(err) + return c.NoContent(http.StatusInternalServerError) + } + } + + bytes := make([]byte, take) + n, err := f.Read(bytes) + if err != nil { + log.Error(err) + return c.NoContent(http.StatusInternalServerError) + } + + return c.String(http.StatusOK, string(bytes[:n])) +} + +func changeClientName(c echo.Context, id uuid.UUID) error { + name := c.QueryParam("name") + if name == "" { + return c.JSON(http.StatusBadRequest, "missing field 'name'") + } + + var target *Client = nil + + for _, client := range clients { + if client.ID == id || client.Name == id.String() { + target = client + } + + if client.Name == name { + return c.JSON(http.StatusBadRequest, echo.Map{"message": "name already used"}) + } + } + + if target != nil { + target.Name = name + target.UpdatedAt = time.Now() + return c.JSON(http.StatusOK, echo.Map{"message": "ok"}) + } + + return c.JSON(http.StatusNotFound, echo.Map{"message": "not found"}) +} + +func exitClient(c echo.Context, id uuid.UUID) error { + for _, client := range clients { + if client.ID == id || client.Name == id.String() { + client.ExitWanted = true + return c.JSON(http.StatusOK, echo.Map{"message": "ok"}) + } + } + + return c.JSON(http.StatusNotFound, echo.Map{"message": "not found"}) +} diff --git a/server/keys.go b/server/keys.go new file mode 100644 index 0000000..9b0db09 --- /dev/null +++ b/server/keys.go @@ -0,0 +1,105 @@ +package main + +import ( + "fmt" + "net/http" + "os" + "syscall" + "time" + + "github.com/google/uuid" + "github.com/gorilla/websocket" + "github.com/labstack/echo/v4" + "github.com/labstack/gommon/log" +) + +func keys(c echo.Context) error { + ws, err := upgrader.Upgrade(c.Response(), c.Request(), nil) + if err != nil { + return err + } + defer ws.Close() + + id := uuid.New() + idRaw := c.QueryParam("id") + if idRaw != "" { + updatedId, err := uuid.Parse(idRaw) + if err != nil { + log.Errorf("failed to parse id: %s", id) + } else { + id = updatedId + } + } + + now := time.Now() + client := Client{ + ID: id, + Name: "Dummy", + Connected: true, + ConnectedAt: now, + DisconnectedAt: now, + CreatedAt: now, + UpdatedAt: now, + ExitWanted: false, + conn: ws, + } + + clients = append(clients, &client) + fmt.Printf("%s client %s connected\n", time.Now().Format(TimeFormat), client.ID.String()) + + err = ws.WriteMessage(websocket.TextMessage, []byte("welcome")) + if err != nil { + log.Error(err) + } + + f, err := os.OpenFile(filename(dataDir, client.ID.String(), now.Format(DateFormat)), syscall.O_CREAT|syscall.O_APPEND|syscall.O_WRONLY, 0644) + if err != nil { + log.Error(err) + fmt.Printf("%s client %s crashed (couldn't open file)\n", time.Now().Format(TimeFormat), client.ID) + now = time.Now() + + client.Connected = false + client.DisconnectedAt = now + client.UpdatedAt = now + + return c.NoContent(http.StatusInternalServerError) + } + defer f.Close() + + close := func() { + _ = ws.WriteMessage(websocket.CloseMessage, websocket.FormatCloseMessage(websocket.CloseNormalClosure, "")) + _ = ws.Close() + now = time.Now() + + client.ExitWanted = true + client.Connected = false + client.DisconnectedAt = now + client.UpdatedAt = now + } + + for { + if client.ExitWanted { + close() + fmt.Printf("%s client %s disconnected\n", time.Now().Format(TimeFormat), client.ID.String()) + break + } + + _, msg, err := ws.ReadMessage() + if err != nil { + log.Error(err) + close() + fmt.Printf("%s client %s crashed (websocket error)\n", time.Now().Format(TimeFormat), client.ID.String()) + return c.NoContent(http.StatusInternalServerError) + } + + _, err = f.Write(msg) + if err != nil { + log.Error(err) + close() + fmt.Printf("%s client %s crashed (file write error)\n", time.Now().Format(TimeFormat), client.ID.String()) + return c.NoContent(http.StatusInternalServerError) + } + } + + return c.NoContent(http.StatusOK) +} diff --git a/server/main.go b/server/main.go index 8c47ba9..158beb0 100644 --- a/server/main.go +++ b/server/main.go @@ -3,12 +3,10 @@ package main import ( "errors" "fmt" - "io" "net/http" "os" "path" "regexp" - "strconv" "syscall" "time" @@ -22,6 +20,7 @@ import ( 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$`) @@ -113,6 +112,7 @@ func main() { 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) { @@ -144,8 +144,6 @@ func main() { } } - indent := "\t" - // client connection app.GET("/", func(c echo.Context) error { return c.JSONPretty(http.StatusOK, "Hello World!", indent) @@ -153,201 +151,12 @@ func main() { app.GET("/keys", keys) // administration - app.GET("/admin/clients", authed(func(c echo.Context) error { - return c.JSONPretty(http.StatusOK, clients, indent) - })) - - app.GET("/admin/clients/:id", authed(idparam(func(c echo.Context, id uuid.UUID) error { - for _, client := range clients { - if client.ID == id || client.Name == id.String() { - return c.JSONPretty(http.StatusOK, client, indent) - } - } - - return c.NoContent(http.StatusNotFound) - }))) - - app.GET("/admin/logs", authed(func(c echo.Context) error { - files, err := os.ReadDir(dataDir) - if err != nil { - log.Error(err) - return c.NoContent(http.StatusInternalServerError) - } - - filenames := []string{} - for _, file := range files { - if LogRegex.MatchString(file.Name()) { - filenames = append(filenames, file.Name()) - } - } - - return c.JSONPretty(http.StatusOK, filenames, indent) - })) - - app.GET("/admin/logs/:id", authed(idparam(func(c echo.Context, id uuid.UUID) error { - date := c.QueryParam("date") - if date == "" { - date = time.Now().Format(DateFormat) - } - - skipRaw := c.QueryParam("skip") - skip := 0 - if skipRaw != "" { - skip, err = strconv.Atoi(skipRaw) - if err != nil { - return c.JSON(http.StatusBadRequest, echo.Map{"message": "skip is not a number"}) - } - } - - takeRaw := c.QueryParam("take") - take := 1024 - if takeRaw != "" { - take, err = strconv.Atoi(takeRaw) - if err != nil { - return c.JSON(http.StatusBadRequest, echo.Map{"message": "take is not a number"}) - } - } - - f, err := os.OpenFile(filename(dataDir, id.String(), date), syscall.O_RDONLY, 0644) - if err != nil { - return c.JSON(http.StatusNotFound, echo.Map{"message": "file not found"}) - } - - if skip != 0 { - _, err = f.Seek(int64(skip), io.SeekStart) - if err != nil { - log.Error(err) - return c.NoContent(http.StatusInternalServerError) - } - } - - bytes := make([]byte, take) - n, err := f.Read(bytes) - if err != nil { - log.Error(err) - return c.NoContent(http.StatusInternalServerError) - } - - return c.String(http.StatusOK, string(bytes[:n])) - }))) - - app.POST("/admin/name/:id", authed(idparam(func(c echo.Context, id uuid.UUID) error { - name := c.QueryParam("name") - if name == "" { - return c.JSON(http.StatusBadRequest, "missing field 'name'") - } - - var target *Client = nil - - for _, client := range clients { - if client.ID == id || client.Name == id.String() { - target = client - } - - if client.Name == name { - return c.JSON(http.StatusBadRequest, echo.Map{"message": "name already used"}) - } - } - - if target != nil { - target.Name = name - target.UpdatedAt = time.Now() - return c.JSON(http.StatusOK, echo.Map{"message": "ok"}) - } - - return c.JSON(http.StatusNotFound, echo.Map{"message": "not found"}) - }))) - - app.POST("/admin/exit/:id", authed(idparam(func(c echo.Context, id uuid.UUID) error { - for _, client := range clients { - if client.ID == id || client.Name == id.String() { - client.ExitWanted = true - return c.JSON(http.StatusOK, echo.Map{"message": "ok"}) - } - } - - return c.JSON(http.StatusNotFound, echo.Map{"message": "not found"}) - }))) + 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")) } - -func keys(c echo.Context) error { - ws, err := upgrader.Upgrade(c.Response(), c.Request(), nil) - if err != nil { - return err - } - defer ws.Close() - - now := time.Now() - client := Client{ - ID: uuid.New(), - Name: "Dummy", - Connected: true, - ConnectedAt: now, - DisconnectedAt: now, - CreatedAt: now, - UpdatedAt: now, - ExitWanted: false, - conn: ws, - } - - clients = append(clients, &client) - fmt.Printf("%s client %s connected\n", time.Now().Format(TimeFormat), client.ID.String()) - - err = ws.WriteMessage(websocket.TextMessage, []byte("welcome")) - if err != nil { - log.Error(err) - } - - f, err := os.OpenFile(filename(dataDir, client.ID.String(), now.Format(DateFormat)), syscall.O_CREAT|syscall.O_APPEND|syscall.O_WRONLY, 0644) - if err != nil { - log.Error(err) - fmt.Printf("%s client %s crashed (couldn't open file)\n", time.Now().Format(TimeFormat), client.ID) - now = time.Now() - - client.Connected = false - client.DisconnectedAt = now - client.UpdatedAt = now - - return c.NoContent(http.StatusInternalServerError) - } - defer f.Close() - - close := func() { - _ = ws.WriteMessage(websocket.CloseMessage, websocket.FormatCloseMessage(websocket.CloseNormalClosure, "")) - _ = ws.Close() - now = time.Now() - - client.ExitWanted = true - client.Connected = false - client.DisconnectedAt = now - client.UpdatedAt = now - } - - for { - if client.ExitWanted { - close() - fmt.Printf("%s client %s disconnected\n", time.Now().Format(TimeFormat), client.ID.String()) - break - } - - _, msg, err := ws.ReadMessage() - if err != nil { - log.Error(err) - close() - fmt.Printf("%s client %s crashed (websocket error)\n", time.Now().Format(TimeFormat), client.ID.String()) - return c.NoContent(http.StatusInternalServerError) - } - - _, err = f.Write(msg) - if err != nil { - log.Error(err) - close() - fmt.Printf("%s client %s crashed (file write error)\n", time.Now().Format(TimeFormat), client.ID.String()) - return c.NoContent(http.StatusInternalServerError) - } - } - - return c.NoContent(http.StatusOK) -}