diff --git a/go.mod b/go.mod index 2f28418..48b4b6f 100644 --- a/go.mod +++ b/go.mod @@ -1,11 +1,12 @@ module automation -go 1.17 +go 1.19 require ( github.com/eclipse/paho.mqtt.golang v1.3.5 github.com/google/uuid v1.3.0 github.com/gorilla/mux v1.8.0 + github.com/jellydator/ttlcache/v3 v3.0.0 github.com/joho/godotenv v1.4.0 github.com/kelseyhightower/envconfig v1.4.0 github.com/kr/pretty v0.3.1 @@ -27,6 +28,7 @@ require ( go.opencensus.io v0.24.0 // indirect golang.org/x/net v0.0.0-20221014081412-f15817d10f9b // indirect golang.org/x/oauth2 v0.0.0-20221014153046-6fdb5e3db783 // indirect + golang.org/x/sync v0.1.0 // indirect golang.org/x/sys v0.0.0-20220728004956-3c1f35247d10 // indirect golang.org/x/text v0.4.0 // indirect google.golang.org/appengine v1.6.7 // indirect diff --git a/go.sum b/go.sum index bdf13f8..383043a 100644 --- a/go.sum +++ b/go.sum @@ -55,6 +55,8 @@ github.com/gorilla/mux v1.8.0 h1:i40aqfkR1h2SlN9hojwV5ZA91wcXFOvkdNIeFDP5koI= github.com/gorilla/mux v1.8.0/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So= github.com/gorilla/websocket v1.4.2 h1:+/TMaTYc4QFitKJxsQ7Yye35DkWvkdLcvGKqM+x0Ufc= github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= +github.com/jellydator/ttlcache/v3 v3.0.0 h1:zmFhqrB/4sKiEiJHhtseJsNRE32IMVmJSs4++4gaQO4= +github.com/jellydator/ttlcache/v3 v3.0.0/go.mod h1:WwTaEmcXQ3MTjOm4bsZoDFiCu/hMvNWLO1w67RXz6h4= github.com/joho/godotenv v1.4.0 h1:3l4+N6zfMWnkbPEXKng2o2/MR5mSwTrBih4ZEkkz1lg= github.com/joho/godotenv v1.4.0/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4= github.com/kelseyhightower/envconfig v1.4.0 h1:Im6hONhd3pLkfDFsbRgu68RDNkGF1r3dvMUtDTo2cv8= @@ -81,12 +83,14 @@ github.com/stretchr/testify v1.8.1 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKs github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= go.opencensus.io v0.24.0 h1:y73uSU6J157QMP2kn2r30vwW1A2W2WFwSCGnAVxeaD0= go.opencensus.io v0.24.0/go.mod h1:vNK8G9p7aAivkbmorf4v+7Hgx+Zs0yY+0fOtgBfjQKo= +go.uber.org/goleak v1.1.10 h1:z+mqJhf6ss6BSfSM671tgKyZBFPTTJM+HLxnhPC3wu0= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/lint v0.0.0-20201208152925-83fdc39ff7b5 h1:2M3HP5CCK1Si9FQhwnzYhXdG6DXeebvUHFpre8QvbyI= golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= @@ -104,6 +108,8 @@ golang.org/x/oauth2 v0.0.0-20221014153046-6fdb5e3db783/go.mod h1:h4gKUeWbJ4rQPri golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.1.0 h1:wsuoTGHzEhffawBOhz5CYhcrV4IdKZbEyZjBMuTp12o= +golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= @@ -121,6 +127,7 @@ golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGm golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= +golang.org/x/tools v0.1.12 h1:VveCTK38A2rkS8ZqFY25HIDFscX5X9OoEhJd3quQmXU= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20220907171357-04be3eba64a2 h1:H2TDz8ibqkAF6YGhCdN3jS9O0/s90v0rJh3X/OLHEUk= google.golang.org/api v0.103.0 h1:9yuVqlu2JCvcLg9p8S3fcFLZij8EPSyvODIY1rkMizQ= diff --git a/integration/google/handler.go b/integration/google/handler.go index aca763e..4b1d138 100644 --- a/integration/google/handler.go +++ b/integration/google/handler.go @@ -2,8 +2,11 @@ package google import ( "encoding/json" + "io" "log" "net/http" + + "github.com/jellydator/ttlcache/v3" ) type DeviceInterface interface { @@ -50,38 +53,78 @@ type executeResponse struct { } `json:"payload"` } +// @TODO We can also implement this as a cache loader function +// Note sure how to report the correct errors in that case? +func (s *Service) getUser(authorization string) (string, int) { + // @TODO Make oids url configurable + + cached := s.cache.Get(authorization) + if cached != nil { + return cached.Value(), http.StatusOK + } + + req, err := http.NewRequest("GET", "https://login.huizinga.dev/api/oidc/userinfo", nil) + if err != nil { + log.Println("Failed to make request to to login server") + return "", http.StatusInternalServerError + } + + req.Header.Set("Authorization", authorization) + client := &http.Client{} + resp, err := client.Do(req) + + // If we get something other than 200, error out + if resp.StatusCode != http.StatusOK { + log.Println("Not logged in...") + return "", resp.StatusCode + } + + // Get the preferred_username from the userinfo + bodyBytes, err := io.ReadAll(resp.Body) + if err != nil { + log.Println("Failed to read body") + return "", resp.StatusCode + } + + var body struct { + PreferredUsername string `json:"preferred_username"` + } + + err = json.Unmarshal(bodyBytes, &body) + if err != nil { + log.Println("Failed to marshal body") + return "", http.StatusInternalServerError + } + + if len(body.PreferredUsername) == 0 { + log.Println("Received empty username from userinfo endpoint") + return "", http.StatusInternalServerError + } + + s.cache.Set(authorization, body.PreferredUsername, ttlcache.DefaultTTL) + + return body.PreferredUsername, http.StatusOK +} + func (s *Service) FullfillmentHandler(w http.ResponseWriter, r *http.Request) { defer r.Body.Close() // Check if we are logged in - { - req, err := http.NewRequest("GET", "https://login.huizinga.dev/api/oidc/userinfo", nil) - if err != nil { - log.Println("Failed to make request to to login server") - w.WriteHeader(http.StatusInternalServerError) - return - } - - if len(r.Header["Authorization"]) > 0 { - req.Header.Set("Authorization", r.Header["Authorization"][0]) - } - client := &http.Client{} - resp, err := client.Do(req) - - // If we get something other than 200, error out - if resp.StatusCode != http.StatusOK { - log.Println("Not logged in...") - w.WriteHeader(resp.StatusCode) + var userID string + if auth, ok := r.Header["Authorization"]; ok && len(auth) > 0 { + var statusCode int + userID, statusCode = s.getUser(auth[0]) + if statusCode != http.StatusOK { + w.WriteHeader(statusCode) return } + } else { + log.Println("No authorization provided") + w.WriteHeader(http.StatusBadRequest) + return } - // @TODO Make sure we receive content type json - // @TODO Get this from userinfo, currently the scope is not set up properly to actually receive the username - userID := "Dreaded_X" - fullfimentReq := &FullfillmentRequest{} - err := json.NewDecoder(r.Body).Decode(&fullfimentReq) if err != nil { w.WriteHeader(http.StatusBadRequest) diff --git a/integration/google/service.go b/integration/google/service.go index fe6225c..c6785d8 100644 --- a/integration/google/service.go +++ b/integration/google/service.go @@ -6,8 +6,10 @@ import ( "errors" "fmt" "net/http" + "time" "github.com/google/uuid" + "github.com/jellydator/ttlcache/v3" "google.golang.org/api/homegraph/v1" ) @@ -30,13 +32,21 @@ type Provider interface { type Service struct { provider Provider deviceService *homegraph.DevicesService + cache *ttlcache.Cache[string, string] } func NewService(provider Provider, service *homegraph.Service) *Service { - return &Service{ + s := Service{ provider: provider, deviceService: homegraph.NewDevicesService(service), + cache: ttlcache.New( + ttlcache.WithTTL[string, string](30 * time.Minute), + ), } + + go s.cache.Start() + + return &s } func (s *Service) RequestSync(ctx context.Context, userID string) error { @@ -69,7 +79,7 @@ func (s *Service) ReportState(ctx context.Context, userID string, states map[str RequestId: uuid.New().String(), Payload: &homegraph.StateAndNotificationPayload{ Devices: &homegraph.ReportStateAndNotificationDevice{ - States: j, + States: j, }, }, })