Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feat/time #44

Draft
wants to merge 5 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions cmd/start.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
26 changes: 24 additions & 2 deletions db/db.go
Original file line number Diff line number Diff line change
@@ -1,9 +1,31 @@
package db

import "errors"
import (
"errors"
"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)
Lookup(lat, lon float64) (TzReply, error)
LookupZone(lat, lon float64) (ZoneReply, error)
LookupTime(tzID string) (ZoneReply, error)
}

var (
Expand Down
37 changes: 37 additions & 0 deletions db/rtree.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import (
"fmt"
"io"
"strings"
"time"

"archive/zip"

Expand Down Expand Up @@ -115,6 +116,42 @@ func (g *Geo2TzRTreeIndex) Lookup(lat, lng float64) (tzID string, err error) {
return
}

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)
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
}

// isPointInPolygonPIP checks if a point is inside a polygon using the Point in Polygon algorithm
func isPointInPolygonPIP(point vertex, polygon polygon) bool {
oddNodes := false
Expand Down
38 changes: 25 additions & 13 deletions web/server.go
Original file line number Diff line number Diff line change
Expand Up @@ -69,13 +69,23 @@
if err != nil {
return nil, errors.Join(ErrorDatabaseFileNotFound, err)
}
server.tzDB = tzDB

Check failure on line 72 in web/server.go

View workflow job for this annotation

GitHub Actions / tests

cannot use tzDB (variable of type *db.Geo2TzRTreeIndex) as db.TzDBIndex value in assignment: *db.Geo2TzRTreeIndex does not implement db.TzDBIndex (wrong type for method Lookup)

Check failure on line 72 in web/server.go

View workflow job for this annotation

GitHub Actions / lint

cannot use tzDB (variable of type *db.Geo2TzRTreeIndex) as db.TzDBIndex value in assignment: *db.Geo2TzRTreeIndex does not implement db.TzDBIndex (wrong type for method Lookup)

Check failure on line 72 in web/server.go

View workflow job for this annotation

GitHub Actions / lint

cannot use tzDB (variable of type *db.Geo2TzRTreeIndex) as db.TzDBIndex value in assignment: *db.Geo2TzRTreeIndex does not implement db.TzDBIndex (wrong type for method Lookup)

// 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")
}
Expand All @@ -93,20 +103,23 @@

// 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
}

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")
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, zr)
}

func (server *Server) handleTzRequest(c echo.Context) error {
// parse latitude
lat, err := parseCoordinate(c.Param(Latitude), Latitude)
if err != nil {
Expand All @@ -119,12 +132,11 @@
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 {
case nil:
tzr := newTzResponse(res, lat, lon)

Check failure on line 139 in web/server.go

View workflow job for this annotation

GitHub Actions / tests

cannot use res (variable of type db.TzReply) as string value in argument to newTzResponse

Check failure on line 139 in web/server.go

View workflow job for this annotation

GitHub Actions / lint

cannot use res (variable of type db.TzReply) as string value in argument to newTzResponse) (typecheck)

Check failure on line 139 in web/server.go

View workflow job for this annotation

GitHub Actions / lint

cannot use res (variable of type db.TzReply) as string value in argument to newTzResponse (typecheck)
return c.JSON(http.StatusOK, tzr)
case db.ErrNotFound:
notFoundErr := fmt.Errorf("timezone not found for coordinates %f,%f", lat, lon)
Expand All @@ -144,7 +156,7 @@
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)
}

Expand Down
56 changes: 55 additions & 1 deletion web/server_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down Expand Up @@ -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)
}
})
}
}
Loading