From 75a7311a733df8f121db3be5d8c27b0af4c479e3 Mon Sep 17 00:00:00 2001 From: Andrea Giacobino Date: Mon, 1 Jul 2024 16:53:04 +0200 Subject: [PATCH 1/5] feat: add endpoint to get the time for a tzID --- db/db.go | 6 +++++- db/rtree.go | 15 +++++++++++++++ web/server.go | 26 ++++++++++++++++++-------- 3 files changed, 38 insertions(+), 9 deletions(-) diff --git a/db/db.go b/db/db.go index e015c5b..baee135 100644 --- a/db/db.go +++ b/db/db.go @@ -1,9 +1,13 @@ package db -import "errors" +import ( + "errors" + "time" +) type TzDBIndex interface { Lookup(lat, lon float64) (string, error) + LookupTime(tzID string) (local, utc time.Time, isDST bool, zone string, offset int, err error) } var ( diff --git a/db/rtree.go b/db/rtree.go index ff4174c..2476e45 100644 --- a/db/rtree.go +++ b/db/rtree.go @@ -6,6 +6,7 @@ import ( "fmt" "io" "strings" + "time" "archive/zip" @@ -115,6 +116,20 @@ func (g *Geo2TzRTreeIndex) Lookup(lat, lng float64) (tzID string, err error) { return } +func (*Geo2TzRTreeIndex) LookupTime(tzID string) (local, utc time.Time, isDST bool, zone string, offset int, err error) { + tz, err := time.LoadLocation(tzID) + if err != nil { + err = errors.Join(ErrNotFound, err) + return + } + local = time.Now().In(tz) + isDST = local.IsDST() + utc = local.UTC() + zone, offset = local.Zone() + offset /= 3600 // from seconds to hours + return +} + // isPointInPolygonPIP checks if a point is inside a polygon using the Point in Polygon algorithm func isPointInPolygonPIP(point vertex, polygon polygon) bool { oddNodes := false diff --git a/web/server.go b/web/server.go index ff2ad3a..c7c6bd7 100644 --- a/web/server.go +++ b/web/server.go @@ -94,19 +94,29 @@ func NewServer(config ConfigSchema) (*Server, error) { // register routes server.echo.GET("/tz/:lat/:lon", server.handleTzRequest) server.echo.GET("/tz/version", server.handleTzVersion) + server.echo.GET("/time/:tzID", server.handleTimeRequest) return &server, nil } -func (server *Server) handleTzRequest(c echo.Context) error { - // token verification - if server.authEnabled { - requestToken := c.QueryParam(server.config.Web.AuthTokenParamName) - if !isEq(server.authHashedToken, requestToken) { - server.echo.Logger.Errorf("request unauthorized, invalid token: %v", requestToken) - return c.JSON(http.StatusUnauthorized, map[string]interface{}{"message": "unauthorized"}) - } +func (server *Server) handleTimeRequest(c echo.Context) error { + tzID := c.Param("tzID") + local, utc, isDST, zone, offset, err := server.tzDB.LookupTime(tzID) + if err != nil { + server.echo.Logger.Errorf("error loading timezone %s: %v", tzID, err) + return c.JSON(http.StatusNotFound, newErrResponse(err)) } + return c.JSON(http.StatusOK, map[string]interface{}{ + "local": local.Format(time.RFC3339), + "utc": utc.Format(time.RFC3339), + "is_dst": isDST, + "offset": offset, + "zone": zone, + "tz": tzID, + }) +} + +func (server *Server) handleTzRequest(c echo.Context) error { // parse latitude lat, err := parseCoordinate(c.Param(Latitude), Latitude) if err != nil { From efa79d7bd7d3a7c0950b0b1c55c6168e799c0c35 Mon Sep 17 00:00:00 2001 From: Andrea Giacobino Date: Mon, 1 Jul 2024 21:57:35 +0200 Subject: [PATCH 2/5] refactor(web): move authorization to middleware --- web/server.go | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/web/server.go b/web/server.go index c7c6bd7..30c6510 100644 --- a/web/server.go +++ b/web/server.go @@ -72,10 +72,20 @@ func NewServer(config ConfigSchema) (*Server, error) { server.tzDB = tzDB // check token authorization - server.authHashedToken = hash(config.Web.AuthTokenValue) if len(config.Web.AuthTokenValue) > 0 { server.echo.Logger.Info("Authorization enabled") - server.authEnabled = true + server.authHashedToken = hash(config.Web.AuthTokenValue) + authMiddleware := middleware.KeyAuthWithConfig(middleware.KeyAuthConfig{ + KeyLookup: fmt.Sprintf("query:%s,header:%s", config.Web.AuthTokenParamName, config.Web.AuthTokenParamName), + Validator: func(key string, c echo.Context) (bool, error) { + return isEq(server.authHashedToken, key), nil + }, + ErrorHandler: func(err error, c echo.Context) error { + server.echo.Logger.Errorf("request unauthorized, invalid token", err) + return c.JSON(http.StatusUnauthorized, map[string]interface{}{"message": "unauthorized"}) + }, + }) + server.echo.Use(authMiddleware) } else { server.echo.Logger.Info("Authorization disabled") } From b5e1b3d757d9975998693e2d3004e6fe76c48419 Mon Sep 17 00:00:00 2001 From: Andrea Giacobino Date: Mon, 1 Jul 2024 21:58:44 +0200 Subject: [PATCH 3/5] refactor(web): rename version request handler --- web/server.go | 5 ++--- web/server_test.go | 2 +- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/web/server.go b/web/server.go index 30c6510..3dab907 100644 --- a/web/server.go +++ b/web/server.go @@ -103,7 +103,7 @@ func NewServer(config ConfigSchema) (*Server, error) { // register routes server.echo.GET("/tz/:lat/:lon", server.handleTzRequest) - server.echo.GET("/tz/version", server.handleTzVersion) + server.echo.GET("/tz/version", server.handleTzVersionRequest) server.echo.GET("/time/:tzID", server.handleTimeRequest) return &server, nil @@ -139,7 +139,6 @@ func (server *Server) handleTzRequest(c echo.Context) error { server.echo.Logger.Errorf("error parsing longitude: %v", err) return c.JSON(http.StatusBadRequest, newErrResponse(err)) } - // query the coordinates res, err := server.tzDB.Lookup(lat, lon) switch err { @@ -164,7 +163,7 @@ func newErrResponse(err error) map[string]any { return map[string]any{"message": err.Error()} } -func (server *Server) handleTzVersion(c echo.Context) error { +func (server *Server) handleTzVersionRequest(c echo.Context) error { return c.JSON(http.StatusOK, server.tzRelease) } diff --git a/web/server_test.go b/web/server_test.go index 2108a29..29d221e 100644 --- a/web/server_test.go +++ b/web/server_test.go @@ -154,7 +154,7 @@ func Test_TzVersion(t *testing.T) { c.SetPath("/tz/version") // Assertions - if assert.NoError(t, server.handleTzVersion(c)) { + if assert.NoError(t, server.handleTzVersionRequest(c)) { assert.Equal(t, http.StatusOK, rec.Code) var version TzRelease reply := rec.Body.String() From 84cec992142500e299d86a71cf02e00a8c3e8fcc Mon Sep 17 00:00:00 2001 From: Andrea Giacobino Date: Mon, 1 Jul 2024 21:59:48 +0200 Subject: [PATCH 4/5] feat: print examples on startup --- cmd/start.go | 3 +++ 1 file changed, 3 insertions(+) diff --git a/cmd/start.go b/cmd/start.go index 39eefd7..4b0271f 100644 --- a/cmd/start.go +++ b/cmd/start.go @@ -32,6 +32,9 @@ func start(*cobra.Command, []string) { | |__| | __/ (_) / /_| |_ / / \_____|\___|\___/____|\__/___| version %s `, rootCmd.Version) + // print example request + log.Printf("example tz request: curl -s 'http://localhost:2004/tz/45.4642/9.1900'\n") + log.Printf("example time request: curl -s 'http://localhost:2004/time/Europe/Rome'\n") // Start server server, err := web.NewServer(settings) if err != nil { From cc25f3158fd15b936cc5e49b02d881403787b34e Mon Sep 17 00:00:00 2001 From: Andrea Giacobino Date: Tue, 10 Sep 2024 08:23:30 +0200 Subject: [PATCH 5/5] working on time api --- db/db.go | 22 +++++++++++++++++-- db/rtree.go | 34 +++++++++++++++++++++++------ web/server.go | 11 ++-------- web/server_test.go | 54 ++++++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 104 insertions(+), 17 deletions(-) diff --git a/db/db.go b/db/db.go index baee135..7b38e90 100644 --- a/db/db.go +++ b/db/db.go @@ -5,9 +5,27 @@ import ( "time" ) +type TzReply struct { + TZ string `json:"tz,omitempty"` + Coords struct { + Lat float64 `json:"lat"` + Lon float64 `json:"lon"` + } `json:"coords,omitempty"` +} + +type ZoneReply struct { + TzReply + Local time.Time `json:"local"` + UTC time.Time `json:"utc"` + IsDST bool `json:"is_dst"` + Offset int `json:"offset"` + Zone string `json:"zone"` +} + type TzDBIndex interface { - Lookup(lat, lon float64) (string, error) - LookupTime(tzID string) (local, utc time.Time, isDST bool, zone string, offset int, err error) + Lookup(lat, lon float64) (TzReply, error) + LookupZone(lat, lon float64) (ZoneReply, error) + LookupTime(tzID string) (ZoneReply, error) } var ( diff --git a/db/rtree.go b/db/rtree.go index 2476e45..1dcfd0b 100644 --- a/db/rtree.go +++ b/db/rtree.go @@ -116,17 +116,39 @@ func (g *Geo2TzRTreeIndex) Lookup(lat, lng float64) (tzID string, err error) { return } -func (*Geo2TzRTreeIndex) LookupTime(tzID string) (local, utc time.Time, isDST bool, zone string, offset int, err error) { +func (*Geo2TzRTreeIndex) LookupTime(tzID string) (zr ZoneReply, err error) { tz, err := time.LoadLocation(tzID) if err != nil { err = errors.Join(ErrNotFound, err) return } - local = time.Now().In(tz) - isDST = local.IsDST() - utc = local.UTC() - zone, offset = local.Zone() - offset /= 3600 // from seconds to hours + + local := time.Now().In(tz) + zone, offset := local.Zone() + + zr = ZoneReply{ + Local: local, + UTC: local.UTC(), + IsDST: local.IsDST(), + Offset: offset / 3600, // from seconds to hours + Zone: zone, + } + + return +} + +func (g *Geo2TzRTreeIndex) LookupZone(lat, lng float64) (zr ZoneReply, err error) { + tzID, err := g.Lookup(lat, lng) + if err != nil { + return + } + zr, err = g.LookupTime(tzID) + if err != nil { + return + } + zr.TZ = tzID + zr.Coords.Lat = lat + zr.Coords.Lon = lng return } diff --git a/web/server.go b/web/server.go index 3dab907..7bb6f39 100644 --- a/web/server.go +++ b/web/server.go @@ -111,19 +111,12 @@ func NewServer(config ConfigSchema) (*Server, error) { func (server *Server) handleTimeRequest(c echo.Context) error { tzID := c.Param("tzID") - local, utc, isDST, zone, offset, err := server.tzDB.LookupTime(tzID) + zr, err := server.tzDB.LookupTime(tzID) if err != nil { server.echo.Logger.Errorf("error loading timezone %s: %v", tzID, err) return c.JSON(http.StatusNotFound, newErrResponse(err)) } - return c.JSON(http.StatusOK, map[string]interface{}{ - "local": local.Format(time.RFC3339), - "utc": utc.Format(time.RFC3339), - "is_dst": isDST, - "offset": offset, - "zone": zone, - "tz": tzID, - }) + return c.JSON(http.StatusOK, zr) } func (server *Server) handleTzRequest(c echo.Context) error { diff --git a/web/server_test.go b/web/server_test.go index 29d221e..826b1ea 100644 --- a/web/server_test.go +++ b/web/server_test.go @@ -237,3 +237,57 @@ func Test_TzRequest(t *testing.T) { }) } } + +func Test_Auth(t *testing.T) { + settings := ConfigSchema{ + Tz: TzSchema{ + VersionFile: "../tzdata/version.json", + DatabaseName: "../tzdata/timezones.zip", + }, + Web: WebSchema{ + AuthTokenValue: "test", + AuthTokenParamName: "t", + }, + } + server, err := NewServer(settings) + assert.NoError(t, err) + + tests := []struct { + name string + reqPath string + wantCode int + }{ + { + "OK: version endpoint valid token", + "tz/version?t=test", + http.StatusOK, + }, + { + "OK: version endpoint valid token", + "tz/version?t=invalid", + http.StatusUnauthorized, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + req := httptest.NewRequest(http.MethodGet, "/", nil) + rec := httptest.NewRecorder() + c := server.echo.NewContext(req, rec) + c.SetPath(tt.reqPath) + + fn := server.handleTzRequest(c) + if strings.HasPrefix(tt.reqPath, "tz/version") { + fn = server.handleTzVersionRequest(c) + } + if strings.HasPrefix(tt.reqPath, "time/") { + fn = server.handleTimeRequest(c) + } + + // Assertions + if assert.NoError(t, fn) { + assert.Equal(t, tt.wantCode, rec.Code) + } + }) + } +}