commit 005169f9730ad6709694b3e714e59882b4e13784 Author: Johannes Theiner Date: Mon Jul 20 18:02:54 2020 +0200 fully working version diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..29b636a --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +.idea +*.iml \ No newline at end of file diff --git a/src/client/CurrentWeatherStruct.go b/src/client/CurrentWeatherStruct.go new file mode 100644 index 0000000..88f2778 --- /dev/null +++ b/src/client/CurrentWeatherStruct.go @@ -0,0 +1,60 @@ +package client + +type WeatherResponse struct { + Coordinates Coordinates `json:"coord,omitempty"` + Base string `json:"base,omitempty"` + Main Main `json:"main,omitempty"` + Visibility int `json:"visibility,omitempty"` + Wind Wind `json:"wind,omitempty"` + Clouds Clouds `json:"clouds,omitempty"` + Rain Rain `json:"rain,omitempty"` + Snow Snow `json:"snow,omitempty"` + Date int `json:"dt,omitempty"` + System System `json:"sys,omitempty"` + Timezone int `json:"timezone,omitempty"` + Id int `json:"id,omitempty"` + Name string `json:"name,omitempty"` + Cod int `json:"cod,omitempty"` +} + +type Coordinates struct { + Longitude float32 `json:"lon,omitempty"` + Latitude float32 `json:"lat,omitempty"` +} + +type Main struct { + Temperature float32 `json:"temp,omitempty"` + FeelsLike float32 `json:"feels_like,omitempty"` + TemperatureMin float32 `json:"temp_min,omitempty"` + TemperatureMax float32 `json:"temp_max,omitempty"` + Pressure int `json:"pressure,omitempty"` + Humidity int `json:"humidity,omitempty"` +} + +type Wind struct { + Speed float32 `json:"speed,omitempty"` + Degree int `json:"deg,omitempty"` +} + +type Clouds struct { + All int `json:"all,omitempty"` +} + +type Rain struct { + OneHour int `json:",omitempty"` + ThreeHours int `json:"3h,omitempty"` +} + +type Snow struct { + OneHour int `json:"1h,omitempty"` + ThreeHours int `json:"3h,omitempty"` +} + +type System struct { + Type int `json:",omitempty"` + Id int `json:"id,omitempty"` + Message float32 `json:"message,omitempty"` + Country string `json:"country,omitempty"` + Sunrise int `json:"sunrise,omitempty"` + Sunset int `json:"sunset,omitempty"` +} diff --git a/src/client/WeatherClient.go b/src/client/WeatherClient.go new file mode 100644 index 0000000..5a21bfb --- /dev/null +++ b/src/client/WeatherClient.go @@ -0,0 +1,44 @@ +package client + +import ( + "../config" + "encoding/json" + "io/ioutil" + "log" + "net/http" + "strconv" +) + +func GetByCityName(city string) WeatherResponse { + return get("https://api.openweathermap.org/data/2.5/weather?q=" + city) +} + +func GetByCoordinates(longitude, latitude float64) WeatherResponse { + return get("https://api.openweathermap.org/data/2.5/weather?lat=" + strconv.FormatFloat(latitude, 'f', 6, 32) + "&lon=" + strconv.FormatFloat(longitude, 'f', 6, 32)) +} + +func GetByZipCode(zip int, country string) WeatherResponse { + return get("https://api.openweathermap.org/data/2.5/weather?zip=" + string(zip) + "," + country) +} + +func get(url string) WeatherResponse { + var configuration = config.LoadConfiguration() + + response, err := http.Get(url + "&appid=" + configuration.ApiKey + "&lang=" + configuration.Language + "&units=" + configuration.Units) + if err != nil { + log.Fatal(err) + } + + responseData, err := ioutil.ReadAll(response.Body) + if err != nil { + log.Fatal(err) + } + + var responseObject WeatherResponse + err = json.Unmarshal(responseData, &responseObject) + if err != nil { + log.Fatal(err) + } + + return responseObject +} diff --git a/src/config/Configuration.go b/src/config/Configuration.go new file mode 100644 index 0000000..5108749 --- /dev/null +++ b/src/config/Configuration.go @@ -0,0 +1,50 @@ +package config + +import ( + "fmt" + "os" + "strconv" +) + +type Configuration struct { + ApiKey string + Language string + Units string + + Port int + JwtSecret string +} + +func (config Configuration) String() string { + return fmt.Sprintf("api_key=%s language=%s, units=%s, port=%d, jwt_secret=%s", config.ApiKey, config.Language, config.Units, config.Port, config.JwtSecret) +} + +//returns a new Config struct +func LoadConfiguration() *Configuration { + return &Configuration{ + ApiKey: getEnv("API_KEY", ""), + Language: getEnv("LANGUAGE", "en"), + Units: getEnv("UNITS", "metric"), + Port: getEnvAsInt("PORT", 8080), + JwtSecret: getEnv("JWT_SECRET", "super secret value"), + } +} + +// Simple helper function to read an environment or return a default value +func getEnv(key string, defaultVal string) string { + if value, exists := os.LookupEnv(key); exists { + return value + } + + return defaultVal +} + +// Simple helper function to read an environment variable into integer or return a default value +func getEnvAsInt(name string, defaultVal int) int { + valueStr := getEnv(name, "") + if value, err := strconv.Atoi(valueStr); err == nil { + return value + } + + return defaultVal +} diff --git a/src/main.go b/src/main.go new file mode 100644 index 0000000..8011998 --- /dev/null +++ b/src/main.go @@ -0,0 +1,9 @@ +package main + +import ( + "./server" +) + +func main() { + server.StartServer() +} diff --git a/src/server/WeatherServer.go b/src/server/WeatherServer.go new file mode 100644 index 0000000..db8c8a2 --- /dev/null +++ b/src/server/WeatherServer.go @@ -0,0 +1,133 @@ +package server + +import ( + "../client" + "../config" + "encoding/json" + "github.com/auth0/go-jwt-middleware" + "github.com/dgrijalva/jwt-go" + "github.com/gorilla/mux" + "github.com/justinas/alice" + "github.com/justinas/nosurf" + "github.com/throttled/throttled" + "github.com/throttled/throttled/store/memstore" + cache "github.com/victorspringer/http-cache" + "github.com/victorspringer/http-cache/adapter/memory" + "log" + "net/http" + "strconv" + "time" +) + +func StartServer() { + conf := config.LoadConfiguration() + port := strconv.Itoa(conf.Port) + log.Println("starting server on port " + port) + + store, err := memstore.New(65536) + if err != nil { + log.Fatal(err) + } + + quota := throttled.RateQuota{ + MaxRate: throttled.PerMin(20), MaxBurst: 30, + } + rateLimiter, err := throttled.NewGCRARateLimiter(store, quota) + if err != nil { + log.Fatal(err) + } + + httpRateLimiter := throttled.HTTPRateLimiter{ + RateLimiter: rateLimiter, + VaryBy: &throttled.VaryBy{Path: true}, + } + + memcached, err := memory.NewAdapter( + memory.AdapterWithAlgorithm(memory.LRU), + memory.AdapterWithCapacity(10000000), + ) + if err != nil { + log.Fatal(err) + } + + cacheClient, err := cache.NewClient( + cache.ClientWithAdapter(memcached), + cache.ClientWithTTL(10*time.Minute), + cache.ClientWithRefreshKey("opn"), + ) + if err != nil { + log.Fatal(err) + } + + jwtAuth := jwtmiddleware.New(jwtmiddleware.Options{ + ValidationKeyGetter: func(token *jwt.Token) (interface{}, error) { + return []byte(conf.JwtSecret), nil + }, + SigningMethod: jwt.SigningMethodHS512, + }) + + chain := alice.New(jwtAuth.Handler, timeoutHandler, httpRateLimiter.RateLimit, nosurf.NewPure, cacheClient.Middleware) + + router := mux.NewRouter() + router.HandleFunc("/city/{city}", chain.ThenFunc(cityHandler).ServeHTTP) + router.HandleFunc("/coordinates/{latitude},{longitude}", chain.ThenFunc(coordinatesHandler).ServeHTTP) + router.HandleFunc("/zip/{zip},{country}", chain.ThenFunc(zipCodeHandler).ServeHTTP) + _ = http.ListenAndServe(":"+port, router) +} + +func cityHandler(writer http.ResponseWriter, req *http.Request) { + writer.Header().Set("Content-Type", "application/json") + vars := mux.Vars(req) + data := client.GetByCityName(vars["city"]) + writer.WriteHeader(http.StatusOK) + _ = json.NewEncoder(writer).Encode(convert(data)) +} + +func coordinatesHandler(writer http.ResponseWriter, req *http.Request) { + writer.Header().Set("Content-Type", "application/json") + vars := mux.Vars(req) + + longitude, err := strconv.ParseFloat(vars["longitude"], 32) + if err != nil { + log.Fatal(err) + } + latitude, err := strconv.ParseFloat(vars["latitude"], 32) + if err != nil { + log.Fatal(err) + } + data := client.GetByCoordinates(longitude, latitude) + writer.WriteHeader(http.StatusOK) + _ = json.NewEncoder(writer).Encode(convert(data)) +} + +func zipCodeHandler(writer http.ResponseWriter, req *http.Request) { + writer.Header().Set("Content-Type", "application/json") + vars := mux.Vars(req) + zip, err := strconv.Atoi(vars["zip"]) + if err != nil { + log.Fatal(err) + } + data := client.GetByZipCode(zip, vars["country"]) + writer.WriteHeader(http.StatusOK) + _ = json.NewEncoder(writer).Encode(convert(data)) +} + +func timeoutHandler(h http.Handler) http.Handler { + return http.TimeoutHandler(h, 1*time.Second, "timed out") +} + +func convert(weatherData client.WeatherResponse) Weather { + return Weather{ + TemperatureCurrent: weatherData.Main.Temperature, + TemperatureMin: weatherData.Main.TemperatureMin, + TemperatureMax: weatherData.Main.TemperatureMax, + FeelsLike: weatherData.Main.FeelsLike, + Pressure: weatherData.Main.Pressure, + Humidity: weatherData.Main.Humidity, + WindSpeeed: weatherData.Wind.Speed, + WindDegree: weatherData.Wind.Degree, + Clouds: weatherData.Clouds.All, + Rain: weatherData.Rain.OneHour, + Snow: weatherData.Snow.OneHour, + } +} diff --git a/src/server/WeatherStruct.go b/src/server/WeatherStruct.go new file mode 100644 index 0000000..9c32df3 --- /dev/null +++ b/src/server/WeatherStruct.go @@ -0,0 +1,18 @@ +package server + +type Weather struct { + TemperatureCurrent float32 `json:"temperature_current"` + FeelsLike float32 `json:"feels_like"` + TemperatureMin float32 `json:"temperature_min"` + TemperatureMax float32 `json:"temperature_max"` + Pressure int `json:"pressure"` + Humidity int `json:"humidity"` + + WindSpeeed float32 `json:"wind_speed"` + WindDegree int `json:"wind_degree"` + + Clouds int `json:"clouds"` + + Rain int `json:"rain"` + Snow int `json:"snow"` +}