diff --git a/api/endpoints.go b/api/endpoints.go index fd063e0..106f6ae 100644 --- a/api/endpoints.go +++ b/api/endpoints.go @@ -9,7 +9,7 @@ import ( "github.com/gin-gonic/gin" ) -func Listen(address string) { +func Listen(address string, staticPath string) { router := gin.Default() router.GET("/info", read.Info) @@ -17,10 +17,16 @@ func Listen(address string) { router.POST("/set", write.Pixel) router.OPTIONS("/set", func(c *gin.Context) { c.Header("Access-Control-Allow-Origin", "*") - c.Header("Access-Control-Allow-Headers", "*") + c.Header("Vary", "Origin") + c.Header("Access-Control-Allow-Headers", "content-type") c.Header("Access-Control-Allow-Methods", "*") + c.Header("Access-Control-Allow-Credentials", "true") }) + if staticPath != "" { + router.Static("/static", staticPath) + } + server := &http.Server{ Addr: address, Handler: router, diff --git a/api/read/read.go b/api/read/read.go index 8370afe..fd55bb1 100644 --- a/api/read/read.go +++ b/api/read/read.go @@ -1,6 +1,8 @@ package read import ( + "errors" + "git.tek.govt.hu/dowerx/place/api/structs" "git.tek.govt.hu/dowerx/place/config" "git.tek.govt.hu/dowerx/place/storage" @@ -35,5 +37,5 @@ func Tile(c *gin.Context) { } func Continuous(c *gin.Context) { - + panic(errors.New("unimplomented")) } diff --git a/api/write/write.go b/api/write/write.go index 4b16433..46ea9ba 100644 --- a/api/write/write.go +++ b/api/write/write.go @@ -4,13 +4,15 @@ import ( "errors" "git.tek.govt.hu/dowerx/place/api/structs" + "git.tek.govt.hu/dowerx/place/auth" + "git.tek.govt.hu/dowerx/place/config" "git.tek.govt.hu/dowerx/place/storage" "github.com/gin-gonic/gin" ) -func Pixel(c *gin.Context) { - // TODO: apply timeout using cookies +var timeout int = config.GetConfig().Timeout +func Pixel(c *gin.Context) { var info struct { structs.Color structs.Coordinates @@ -20,6 +22,17 @@ func Pixel(c *gin.Context) { return } + cookie, err := c.Cookie("place") + if err != nil { + cookie = auth.SetCookie(timeout) + c.SetCookie("place", cookie, timeout, "/", "", false, false) + } else { + if auth.GetCookie(cookie) { + c.Status(403) + return + } + } + if err := storage.SetPixel(info.X, info.Y, &info); err != nil { c.AbortWithError(500, err) return diff --git a/auth/auth.go b/auth/auth.go new file mode 100644 index 0000000..0695528 --- /dev/null +++ b/auth/auth.go @@ -0,0 +1,51 @@ +package auth + +import ( + "context" + "math/rand" + "time" + + "github.com/redis/go-redis/v9" +) + +var ctx context.Context +var rdb *redis.Client + +func generateToken(length int) string { + validRunes := []rune("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ123456789") + token := make([]rune, length) + for i := range token { + token[i] = validRunes[rand.Intn(len(validRunes))] + } + return string(token) +} + +func Connect(options *redis.Options) { + ctx = context.Background() + rdb = redis.NewClient(options) + + if err := rdb.Ping(ctx).Err(); err != nil { + panic(err) + } +} + +func Close() { + rdb.Close() +} + +func SetCookie(timeout int) string { + cookie := generateToken(32) + err := rdb.Set(ctx, cookie, cookie, time.Duration(timeout)*time.Second).Err() + if err != nil { + panic(err) + } + return cookie +} + +func GetCookie(cookie string) bool { + resp := rdb.Exists(ctx, cookie) + if resp.Err() != nil { + panic(resp.Err()) + } + return resp.Val() == 1 +} diff --git a/config/config.go b/config/config.go index 272e863..8837ad1 100644 --- a/config/config.go +++ b/config/config.go @@ -9,6 +9,7 @@ import ( type Config struct { Address string StoragePath string + StaticPath string TileSize int CanvasSize int SaveFrequency int @@ -23,6 +24,7 @@ func GetConfig() Config { conf = &Config{} flag.StringVar(&conf.Address, "address", ":8080", "API base") flag.StringVar(&conf.StoragePath, "storage", "/data", "image storage path") + flag.StringVar(&conf.StaticPath, "static", "/static", "web static path") flag.IntVar(&conf.TileSize, "tile_size", 128, "width of a tile") flag.IntVar(&conf.CanvasSize, "canvas_size", 4, "width of the canvas (in tiles)") flag.IntVar(&conf.SaveFrequency, "save", 60, "seconds between saves") diff --git a/docker/docker-compose.yml b/docker/docker-compose.yml new file mode 100644 index 0000000..4929197 --- /dev/null +++ b/docker/docker-compose.yml @@ -0,0 +1,6 @@ +services: + redis: + image: redis:alpine + network_mode: host + environment: + - REDIS_ARGS=--requirepass redis \ No newline at end of file diff --git a/main.go b/main.go index b1562b7..92b3d8a 100644 --- a/main.go +++ b/main.go @@ -2,13 +2,21 @@ package main import ( "git.tek.govt.hu/dowerx/place/api" + "git.tek.govt.hu/dowerx/place/auth" "git.tek.govt.hu/dowerx/place/config" "git.tek.govt.hu/dowerx/place/storage" ) func main() { conf := config.GetConfig() + storage.Load(conf.StoragePath, conf.CanvasSize, conf.TileSize) + defer storage.Save() + storage.StartSaves(conf.SaveFrequency) - api.Listen(conf.Address) + + auth.Connect(&conf.Redis) + defer auth.Close() + + api.Listen(conf.Address, conf.StaticPath) } diff --git a/storage/storage.go b/storage/storage.go index 08ae021..1323721 100644 --- a/storage/storage.go +++ b/storage/storage.go @@ -6,9 +6,8 @@ import ( "fmt" "image" "image/color" - "image/draw" "log" - "os" + "path/filepath" "time" ) @@ -26,10 +25,6 @@ func (err *WrongSizeError) Error() string { } func Load(path string, canvasSize int, tileSize int) { - if err := os.Chdir(path); err != nil { - panic(err) - } - tiles = make([][]tile, canvasSize) for i := range tiles { tiles[i] = make([]tile, canvasSize) @@ -37,31 +32,16 @@ func Load(path string, canvasSize int, tileSize int) { for y := range tiles { for x := range tiles[y] { - tiles[y][x].image = image.NewRGBA(image.Rect(0, 0, tileSize, tileSize)) - filename := fmt.Sprintf("%d-%d.png", x, y) - file, err := os.Open(filename) + err := tiles[y][x].load(filepath.Join(path, fmt.Sprintf("%d-%d.png", x, y)), tileSize) if err != nil { tiles[y][x].fill() - if err = tiles[y][x].save(filename); err != nil { + if err = tiles[y][x].save(); err != nil { panic(err) } - log.Printf("Created tile (%d-%d)\n", x, y) - } else { - img, _, err := image.Decode(file) - if err != nil { - panic(err) - } - draw.Draw(tiles[y][x].image, img.Bounds(), img, img.Bounds().Min, draw.Src) + log.Printf("Loaded tile (%d-%d)\n", x, y) } - defer file.Close() - - if tiles[y][x].image.Bounds().Size().X != tileSize && tiles[y][x].image.Bounds().Size().Y != tileSize { - panic(WrongSizeError{ExpectedSize: tileSize, ActualSize: tiles[y][x].image.Bounds().Size(), X: x, Y: y}) - } - - tiles[y][x].dirty = false } } } @@ -70,9 +50,7 @@ func Save() { for y := range tiles { for x := range tiles[y] { if tiles[y][x].dirty { - filename := fmt.Sprintf("%d-%d.png", x, y) - - if err := tiles[y][x].save(filename); err != nil { + if err := tiles[y][x].save(); err != nil { panic(err) } } diff --git a/storage/tile.go b/storage/tile.go index 57dbb08..17eb3fc 100644 --- a/storage/tile.go +++ b/storage/tile.go @@ -10,11 +10,32 @@ import ( type tile struct { dirty bool + path string image *image.RGBA } -func (t *tile) save(path string) error { - file, err := os.OpenFile(path, os.O_WRONLY, 0644) +func (t *tile) load(path string, tileSize int) error { + t.image = image.NewRGBA(image.Rect(0, 0, tileSize, tileSize)) + t.path = path + file, err := os.Open(path) + if err != nil { + return err + } + defer file.Close() + + img, _, err := image.Decode(file) + if err != nil { + return err + } + + draw.Draw(t.image, img.Bounds(), img, img.Bounds().Min, draw.Src) + t.dirty = false + + return nil +} + +func (t *tile) save() error { + file, err := os.OpenFile(t.path, os.O_WRONLY|os.O_CREATE, 0644) if err != nil { return err } diff --git a/web/place.js b/web/place.js index 196dbd6..d9cf314 100644 --- a/web/place.js +++ b/web/place.js @@ -17,25 +17,37 @@ async function setPixel(canvas, ctx, x, y, c) { x = x - bounds.left; y = y - bounds.top; - try { - await fetch(`${apiBase}/set`, { - method: "POST", - headers: { - 'Content-Type': 'application/json' - }, - body: JSON.stringify({ - x: x, - y: y, - r: c.r, - g: c.g, - b: c.b - }) - }); + let response = await fetch(`${apiBase}/set`, { + method: "POST", + credentials: 'include', + headers: { + 'Content-Type': 'application/json' + }, + body: JSON.stringify({ + x: x, + y: y, + r: c.r, + g: c.g, + b: c.b + }) + }); - ctx.fillStyle = `rgb(${c.r}, ${c.g}, ${c.b})`; - ctx.fillRect(x, y, 1, 1); - } catch { - alert(`failed to set pixel at ${x}-${y} to ${c}`); + console.log(response); + + switch (response.status) { + case 200: + ctx.fillStyle = `rgb(${c.r}, ${c.g}, ${c.b})`; + ctx.fillRect(x, y, 1, 1); + break; + case 400: + alert("missing parameters in request"); + break; + case 403: + alert("not allowed to set another pixel yet"); + break; + case 500: + alert(`failed to set pixel at ${x}-${y} to ${c}`); + break; } }