From 6755f453a324a5ecaae209abe66d9bc451f086fe Mon Sep 17 00:00:00 2001 From: Johanan Idicula Date: Wed, 26 Oct 2022 22:20:02 -0400 Subject: [PATCH 001/105] docs(README): Clean up code blocks Removes prompt character in code blocks so when it's rendered on GitHub, the copy button is useful for quick pasting in the terminal. Also updates the go installation commands for the wego dependency setup - `go get` is no longer supported outside a module, and the `go install` command needs to reference a specific git reference. --- README.md | 20 +++++++++++--------- 1 file changed, 11 insertions(+), 9 deletions(-) diff --git a/README.md b/README.md index 4669960..e629fe5 100644 --- a/README.md +++ b/README.md @@ -581,9 +581,9 @@ wttr.in has the following external dependencies: * [wego](https://github.com/schachmat/wego), weather client for terminal After you install [golang](https://golang.org/doc/install), install `wego`: - - $ go get -u github.com/schachmat/wego - $ go install github.com/schachmat/wego +```bash +go install github.com/schachmat/wego@latest +``` ### Install Python dependencies @@ -605,13 +605,15 @@ You can install most of them using `pip`. Some python package use LLVM, so install it first: - $ apt-get install llvm-7 llvm-7-dev - +```bash +apt-get install llvm-7 llvm-7-dev +``` If `virtualenv` is used: - - $ virtualenv -p python3 ve - $ ve/bin/pip3 install -r requirements.txt - $ ve/bin/python3 bin/srv.py +```bash +virtualenv -p python3 ve +ve/bin/pip3 install -r requirements.txt +ve/bin/python3 bin/srv.py +``` Also, you need to install the geoip2 database. You can use a free database GeoLite2 that can be downloaded from (http://dev.maxmind.com/geoip/geoip2/geolite2/). From b54ebc341f27276ce61d404e8b1a678e21790ed8 Mon Sep 17 00:00:00 2001 From: Mykhailo <56037377+hidalgo-vntu@users.noreply.github.com> Date: Sun, 30 Oct 2022 21:55:21 +0200 Subject: [PATCH 002/105] Create ukr-help.txt Ukrainian help --- share/translations/ukr-help.txt | 66 +++++++++++++++++++++++++++++++++ 1 file changed, 66 insertions(+) create mode 100644 share/translations/ukr-help.txt diff --git a/share/translations/ukr-help.txt b/share/translations/ukr-help.txt new file mode 100644 index 0000000..a9f03fd --- /dev/null +++ b/share/translations/ukr-help.txt @@ -0,0 +1,66 @@ +Використання: + + $ curl wttr.in # поточне місцеположення + $ curl wttr.in/kbp # погода в аеропорту Бориспіль (код ICAO: KBP) + +Підтримуються наступні типи місцеположень: + + /paris # місто + /~Eiffel+tower # будь-яке місцеположення + /Киів # юнікодне ім'я будь-якого місцеположення будь-якою мовою + /muc # код аеропорту ICAO (3 літери) + /@stackoverflow.com # доменне им'я + /94107 # поштовый індекс (тільки для США) + /-78.46,106.79 # GPS-координати + +Спеціальні умовні місцеположення: + + /moon # Фаза Місяця (додайте ,+US або ,+France для міста Moon у США або Франції) + /moon@2016-10-25 # Фаза Місяця для вказаної дати (@2016-10-25) + +Одиниці вимірювань: + + ?m # метричні (СІ) (використовуються всюди крім США) + ?u # USCS (використовуються у США) + ?M # показувати швидкість вітру в м/с + +Опції відображення: + + ?0 # тільки поточна погода + ?1 # погода сьогодні + 1 день + ?2 # погода сьогодні + 2 дня + ?n # вузька версія (тільки день та ніч) + ?q # тиха версія (без тексту "Прогноз погоди") + ?Q # надтиха версія (без "Прогноз погоди", немає назви міста) + ?T # відключити послідовності терміналу (без кольорів) + +PNG-опції: + + /paris.png # сгенерувати PNG-файл + ?p # добавити рамку навколо + ?t # transparency=150 (прозорість 150) + transparency=... # прозорість від 0 до 255 (255 = не прозорий) + +Опції можна комбінувати: + + /Paris?0pq + /Paris?0pq&lang=fr + /Paris_0pq.png # в PNG-запитах опції вказуються після знаку _ + /Rome_0pq_lang=it.png # довгі опції розділяются знаком підкреслення _ + +Локалізація: + + $ curl fr.wttr.in/Paris + $ curl wttr.in/paris?lang=fr + $ curl -H "Accept-Language: fr" wttr.in/paris + +Мови що підтримуються: + + FULL_TRANSLATION (підтримується) + PARTIAL_TRANSLATION (в процесі) + +Спеціальні строрінки: + + /:help # показати цю сторінку + /:bash.function # показати рекомендовану функцію wttr() + /:translation # показати список перекладачів wttr.in From d70dae679e612ca00c5a549b40c2c87f7279a617 Mon Sep 17 00:00:00 2001 From: rafael <72521368+rafaeeelv@users.noreply.github.com> Date: Sun, 6 Nov 2022 21:18:23 -0300 Subject: [PATCH 003/105] remove extra lines and update translations --- share/translations/pt-br.txt | 52 +++++++----------------------------- 1 file changed, 9 insertions(+), 43 deletions(-) diff --git a/share/translations/pt-br.txt b/share/translations/pt-br.txt index a43ffe1..b15da8e 100644 --- a/share/translations/pt-br.txt +++ b/share/translations/pt-br.txt @@ -17,8 +17,8 @@ 266: Chuvisco : Light drizzle 281: Chuvisco gelado : Freezing drizzle 284: Chuvisco muito gelado : Heavy freezing drizzle -293: Garoa irregular : Patchy light rain -296: Garoa : Light rain +293: Chuvisco irregular : Patchy light rain +296: Chuvisco : Light rain 299: Chuva moderada ocasional : Moderate rain at times 302: Chuva moderada : Moderate rain 305: Chuva forte ocasional : Heavy rain at times @@ -34,48 +34,14 @@ 335: Neve forte irregular : Patchy heavy snow 338: Neve forte : Heavy snow 350: Pelotas de gelo : Ice pellets -353: Chuveiro de garoa : Light rain shower -356: Chuveiro de chuva moderada ou forte : Moderate or heavy rain shower -359: Chuveiro de chuva torrencial : Torrential rain shower -362: Chuveiro de granizo fraco : Light sleet showers -365: Chuveiro de granizo moderada ou forte : Moderate or heavy sleet showers -368: Chuveiro de neve fraca : Light snow showers -371: Chuveiro de neve moderada ou forte : Moderate or heavy snow showers +353: Chuva fraca : Light rain shower +356: Chuva moderada ou forte : Moderate or heavy rain shower +359: Chuva torrencial : Torrential rain shower +362: Chuva de granizo fraco : Light sleet showers +365: Chuva de granizo moderada ou forte : Moderate or heavy sleet showers +368: Chuva com neve fraca : Light snow showers +371: Chuva com neve moderada ou forte : Moderate or heavy snow showers 386: Tempestate com garoa irregular : Patchy light rain with thunder 389: Tempestade com chuva moderada ou forte : Moderate or heavy rain with thunder 392: Tempestade com neve fraca : Patchy light snow with thunder 395: Tempestade com neve moderada ou forte : Moderate or heavy snow with thunder - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - From 0c729ec887080daefbd757ce494a32f26223729d Mon Sep 17 00:00:00 2001 From: rafael <72521368+rafaeeelv@users.noreply.github.com> Date: Sun, 6 Nov 2022 21:28:42 -0300 Subject: [PATCH 004/105] update translations --- share/translations/pt-br-help.txt | 21 ++++++++++----------- 1 file changed, 10 insertions(+), 11 deletions(-) diff --git a/share/translations/pt-br-help.txt b/share/translations/pt-br-help.txt index d096721..822bfeb 100644 --- a/share/translations/pt-br-help.txt +++ b/share/translations/pt-br-help.txt @@ -3,24 +3,24 @@ Instruções: $ curl wttr.in # o tempo na sua localização atual $ curl wttr.in/muc # o tempo no aeroporto de Munique -Tipos de localização suportados: +Tipos de locais suportados: /paris # o nome de uma cidade - /~Eiffel+tower # o nome de qualquer lugar famoso - /Москва # nome Unicode de qualquer lugar em qualquer idioma + /~Eiffel+tower # o nome de um lugar famoso + /Москва # o nome Unicode de qualquer lugar em qualquer idioma /muc # o código de um aeroporto (3 letras) /@stackoverflow.com # o nome de um domínio web /94107 # um código de área - /-78.46,106.79 # coordenadas do GPS + /-78.46,106.79 # as coordenadas do GPS de um lugar Lugares especiais: - /moon # A fase da lua (crescente ,+US ou ,+France para estas cidades) - /moon@2016-10-25 # A fase da lua em uma determinada data (@2016-10-25) + /moon # a fase da ua (crescente ,+US ou ,+France para estas cidades) + /moon@2016-10-25 # a fase da Lua em uma determinada data (@2016-10-25) Unidades: - ?m # métricas (SI) (o padrão em todos os lugares exceto nos EUA) + ?m # Sistema Internacional de Unidades (SI) (o padrão em todos os lugares exceto nos EUA) ?u # Sistema Unificado de Clasificaçāo de Solo ou USCS (o padrão nos EUA) ?M # mostrar a velocidade do vento em m/s @@ -48,13 +48,13 @@ As opções podem ser usadas em conjunto: /Paris_0pq.png # em PNG as opções se especificam depois do caracter _ /Rome_0pq_lang=it.png # uma longa sequência de opções podem ser separadas pelo caracter _ -Localizaçāo: +Tradução: $ curl fr.wttr.in/Paris $ curl wttr.in/paris?lang=fr $ curl -H "Accept-Language: fr" wttr.in/paris -Línguas suportadas: +Idiomas suportadas: FULL_TRANSLATION (suportadas) PARTIAL_TRANSLATION (em andamento) @@ -63,5 +63,4 @@ URLs especiais: /:help # mostra esta página /:bash.function # sugere uma função wttr() em bash - /:translation # mostra informação a respeito dos tradutores - + /:translation # mostra informações a respeito dos tradutores From 66111b1fddf69f4b37f30f4651aaf939232955e7 Mon Sep 17 00:00:00 2001 From: Igor Chubin Date: Wed, 16 Nov 2022 17:21:07 +0100 Subject: [PATCH 005/105] Add Makefile --- Makefile | 2 ++ 1 file changed, 2 insertions(+) create mode 100644 Makefile diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..6f9238c --- /dev/null +++ b/Makefile @@ -0,0 +1,2 @@ +srv: + go build -o srv cmd/*.go From 87c93f1d062009f27ce283fa8ec206b9305d4f10 Mon Sep 17 00:00:00 2001 From: Igor Chubin Date: Wed, 16 Nov 2022 17:21:28 +0100 Subject: [PATCH 006/105] Add go.* --- go.mod | 8 ++++++++ go.sum | 4 ++++ 2 files changed, 12 insertions(+) create mode 100644 go.mod create mode 100644 go.sum diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..b875ace --- /dev/null +++ b/go.mod @@ -0,0 +1,8 @@ +module github.com/chubin/wttr.in/v2 + +go 1.16 + +require ( + github.com/hashicorp/golang-lru v0.6.0 + github.com/robfig/cron v1.2.0 +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..d30f05d --- /dev/null +++ b/go.sum @@ -0,0 +1,4 @@ +github.com/hashicorp/golang-lru v0.6.0 h1:uL2shRDx7RTrOrTCUZEGP/wJUFiUI8QT6E7z5o8jga4= +github.com/hashicorp/golang-lru v0.6.0/go.mod h1:iADmTwqILo4mZ8BN3D2Q6+9jd8WM5uGBxy+E8yxSoD4= +github.com/robfig/cron v1.2.0 h1:ZjScXvvxeQ63Dbyxy76Fj3AT3Ut0aKsyd2/tl3DTMuQ= +github.com/robfig/cron v1.2.0/go.mod h1:JGuDeoQd7Z6yL4zQhZ3OPEVHB7fL6Ka6skscFHfmt2k= From c77c1227d58c8775ef917fbea29d63f5d184a105 Mon Sep 17 00:00:00 2001 From: Igor Chubin Date: Sat, 19 Nov 2022 19:19:25 +0100 Subject: [PATCH 007/105] Add cmd/log.go --- cmd/log.go | 106 +++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 106 insertions(+) create mode 100644 cmd/log.go diff --git a/cmd/log.go b/cmd/log.go new file mode 100644 index 0000000..a832c47 --- /dev/null +++ b/cmd/log.go @@ -0,0 +1,106 @@ +package main + +import ( + "fmt" + "net/http" + "os" + "sync" + "time" +) + +// Logging request. +// + +// RequestLogger logs all incoming HTTP requests. +type RequestLogger struct { + buf map[logEntry]int + filename string + m sync.Mutex + + period time.Duration + lastFlush time.Time +} + +type logEntry struct { + Proto string + IP string + URI string + UserAgent string +} + +// NewRequestLogger returns a new RequestLogger for the specified log file. +// Flush logging entries after period of time. +func NewRequestLogger(filename string, period time.Duration) *RequestLogger { + return &RequestLogger{ + buf: map[logEntry]int{}, + filename: filename, + m: sync.Mutex{}, + period: period, + } +} + +// Log logs information about a HTTP request. +func (rl *RequestLogger) Log(r *http.Request) error { + le := logEntry{ + Proto: "http", + IP: readUserIP(r), + URI: r.RequestURI, + UserAgent: r.Header.Get("User-Agent"), + } + + rl.m.Lock() + rl.buf[le]++ + rl.m.Unlock() + + if time.Since(rl.lastFlush) > rl.period { + return rl.flush() + } + return nil +} + +// flush stores log data to disk, and flushes the buffer. +func (rl *RequestLogger) flush() error { + rl.m.Lock() + defer rl.m.Unlock() + + // It is possible, that while waiting the mutex, + // the buffer was already flushed. + if time.Since(rl.lastFlush) <= rl.period { + return nil + } + + // Generate log output. + output := "" + for k, hitsNumber := range rl.buf { + output += fmt.Sprintf("%s %3d %s\n", time.Now().Format(time.RFC3339), hitsNumber, k.String()) + } + + // Open log file. + f, err := os.OpenFile(rl.filename, os.O_APPEND|os.O_WRONLY|os.O_CREATE, 0600) + if err != nil { + return err + } + defer f.Close() + + // Save output to log file. + _, err = f.Write([]byte(output)) + if err != nil { + return err + } + + // Flush buffer. + rl.buf = map[logEntry]int{} + rl.lastFlush = time.Now() + + return nil +} + +func (e *logEntry) String() string { + return fmt.Sprintf( + "%s %s %s %s", + e.Proto, + e.IP, + e.URI, + e.UserAgent, + ) +} From d9afb06ac5310655e11f96f750ffeb2f4e336ab5 Mon Sep 17 00:00:00 2001 From: Igor Chubin Date: Sat, 19 Nov 2022 19:19:42 +0100 Subject: [PATCH 008/105] Add cmd/config.go --- cmd/config.go | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) create mode 100644 cmd/config.go diff --git a/cmd/config.go b/cmd/config.go new file mode 100644 index 0000000..9aab0d0 --- /dev/null +++ b/cmd/config.go @@ -0,0 +1,24 @@ +package main + +// Config of the program. +type Config struct { + Logging +} + +// Logging configuration. +type Logging struct { + + // AccessLog path. + AccessLog string + + // Interval between access log flushes, in seconds. + Interval int +} + +// Conf contains the current configuration. +var Conf = Config{ + Logging{ + AccessLog: "/wttr.in/log/access.log", + Interval: 300, + }, +} From a8e8aa7a52a42431c6cb4a2bf673b0518204391c Mon Sep 17 00:00:00 2001 From: Igor Chubin Date: Sat, 19 Nov 2022 19:20:35 +0100 Subject: [PATCH 009/105] Activate insternal access logging --- cmd/srv.go | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/cmd/srv.go b/cmd/srv.go index e289d77..94e41b8 100644 --- a/cmd/srv.go +++ b/cmd/srv.go @@ -73,7 +73,14 @@ func copyHeader(dst, src http.Header) { } func main() { + logger := NewRequestLogger( + Conf.Logging.AccessLog, + time.Duration(Conf.Logging.Interval)*time.Second) + http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { + if err := logger.Log(r); err != nil { + log.Println(err) + } // printStat() response := processRequest(r) From 7d801e3b4db057160fc9e09450a9f273b403970b Mon Sep 17 00:00:00 2001 From: Igor Chubin Date: Sat, 19 Nov 2022 19:20:44 +0100 Subject: [PATCH 010/105] Fix Makefile --- Makefile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Makefile b/Makefile index 6f9238c..7790552 100644 --- a/Makefile +++ b/Makefile @@ -1,2 +1,2 @@ -srv: +srv: cmd/*.go go build -o srv cmd/*.go From 5bb0c4f1fe0bc759525f0c777283bdc5ecda2bd6 Mon Sep 17 00:00:00 2001 From: Igor Chubin Date: Sat, 19 Nov 2022 19:34:11 +0100 Subject: [PATCH 011/105] Move server config to Config struct --- cmd/config.go | 17 +++++++++++++++++ cmd/log.go | 1 + cmd/srv.go | 3 +-- 3 files changed, 19 insertions(+), 2 deletions(-) diff --git a/cmd/config.go b/cmd/config.go index 9aab0d0..03ee9a1 100644 --- a/cmd/config.go +++ b/cmd/config.go @@ -3,6 +3,7 @@ package main // Config of the program. type Config struct { Logging + Server } // Logging configuration. @@ -15,10 +16,26 @@ type Logging struct { Interval int } +// Server configuration. +type Server struct { + + // PortHTTP is port where HTTP server must listen. + // If 0, HTTP is disabled. + PortHTTP int + + // PortHTTP is port where the HTTPS server must listen. + // If 0, HTTPS is disabled. + PortHTTPS int +} + // Conf contains the current configuration. var Conf = Config{ Logging{ AccessLog: "/wttr.in/log/access.log", Interval: 300, }, + Server{ + PortHTTP: 8083, + PortHTTPS: 0, + }, } diff --git a/cmd/log.go b/cmd/log.go index a832c47..7385206 100644 --- a/cmd/log.go +++ b/cmd/log.go @@ -95,6 +95,7 @@ func (rl *RequestLogger) flush() error { return nil } +// String returns string representation of logEntry. func (e *logEntry) String() string { return fmt.Sprintf( "%s %s %s %s", diff --git a/cmd/srv.go b/cmd/srv.go index 94e41b8..d0b2eed 100644 --- a/cmd/srv.go +++ b/cmd/srv.go @@ -11,7 +11,6 @@ import ( lru "github.com/hashicorp/golang-lru" ) -const serverPort = 8083 const uplinkSrvAddr = "127.0.0.1:9002" const uplinkTimeout = 30 const prefetchInterval = 300 @@ -90,5 +89,5 @@ func main() { w.Write(response.Body) }) - log.Fatal(http.ListenAndServe(fmt.Sprintf(":%d", serverPort), nil)) + log.Fatal(http.ListenAndServe(fmt.Sprintf(":%d", Conf.Server.PortHTTP), nil)) } From bef93212c3d1256034870d3d788881e4a1ac895d Mon Sep 17 00:00:00 2001 From: Igor Chubin Date: Sun, 20 Nov 2022 10:03:31 +0100 Subject: [PATCH 012/105] Add https support --- cmd/config.go | 12 ++++++++++-- cmd/log.go | 3 +++ cmd/processRequest.go | 13 +++++++++++++ cmd/srv.go | 45 +++++++++++++++++++++++++++++++++++++------ 4 files changed, 65 insertions(+), 8 deletions(-) diff --git a/cmd/config.go b/cmd/config.go index 03ee9a1..84cd6f9 100644 --- a/cmd/config.go +++ b/cmd/config.go @@ -26,6 +26,12 @@ type Server struct { // PortHTTP is port where the HTTPS server must listen. // If 0, HTTPS is disabled. PortHTTPS int + + // TLSCertFile contains path to cert file for TLS Server. + TLSCertFile string + + // TLSCertFile contains path to key file for TLS Server. + TLSKeyFile string } // Conf contains the current configuration. @@ -35,7 +41,9 @@ var Conf = Config{ Interval: 300, }, Server{ - PortHTTP: 8083, - PortHTTPS: 0, + PortHTTP: 8083, + PortHTTPS: 8084, + TLSCertFile: "/wttr.in/etc/fullchain.pem", + TLSKeyFile: "/wttr.in/etc/privkey.pem", }, } diff --git a/cmd/log.go b/cmd/log.go index 7385206..3de1b1c 100644 --- a/cmd/log.go +++ b/cmd/log.go @@ -47,6 +47,9 @@ func (rl *RequestLogger) Log(r *http.Request) error { URI: r.RequestURI, UserAgent: r.Header.Get("User-Agent"), } + if r.TLS != nil { + le.Proto = "https" + } rl.m.Lock() rl.buf[le]++ diff --git a/cmd/processRequest.go b/cmd/processRequest.go index 84011c3..4b9aede 100644 --- a/cmd/processRequest.go +++ b/cmd/processRequest.go @@ -84,6 +84,10 @@ func get(req *http.Request) responseWithHeader { } } + if proxyReq.Header.Get("X-Forwarded-For") == "" { + proxyReq.Header.Set("X-Forwarded-For", ipFromAddr(req.RemoteAddr)) + } + res, err := client.Do(proxyReq) if err != nil { @@ -197,3 +201,12 @@ func readUserIP(r *http.Request) string { func randInt(min int, max int) int { return min + rand.Intn(max-min) } + +// ipFromAddr returns IP address from a ADDR:PORT pair. +func ipFromAddr(s string) string { + pos := strings.LastIndex(s, ":") + if pos == -1 { + return s + } + return s[:pos] +} diff --git a/cmd/srv.go b/cmd/srv.go index d0b2eed..f85d3f2 100644 --- a/cmd/srv.go +++ b/cmd/srv.go @@ -71,12 +71,33 @@ func copyHeader(dst, src http.Header) { } } -func main() { - logger := NewRequestLogger( - Conf.Logging.AccessLog, - time.Duration(Conf.Logging.Interval)*time.Second) +func serveHTTP(mux *http.ServeMux, port int, errs chan<- error) { + errs <- http.ListenAndServe(fmt.Sprintf(":%d", port), mux) +} - http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { +func serveHTTPS(mux *http.ServeMux, port int, errs chan<- error) { + errs <- http.ListenAndServeTLS(fmt.Sprintf(":%d", port), + Conf.Server.TLSCertFile, Conf.Server.TLSKeyFile, mux) +} + +func main() { + var ( + // mux is main HTTP/HTTP requests multiplexer. + mux *http.ServeMux = http.NewServeMux() + + // logger is optimized requests logger. + logger *RequestLogger = NewRequestLogger( + Conf.Logging.AccessLog, + time.Duration(Conf.Logging.Interval)*time.Second) + + // errs is the servers errors channel. + errs chan error = make(chan error, 1) + + // numberOfServers started. If 0, exit. + numberOfServers int + ) + + mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { if err := logger.Log(r); err != nil { log.Println(err) } @@ -89,5 +110,17 @@ func main() { w.Write(response.Body) }) - log.Fatal(http.ListenAndServe(fmt.Sprintf(":%d", Conf.Server.PortHTTP), nil)) + if Conf.Server.PortHTTP != 0 { + go serveHTTP(mux, Conf.Server.PortHTTP, errs) + numberOfServers++ + } + if Conf.Server.PortHTTPS != 0 { + go serveHTTPS(mux, Conf.Server.PortHTTPS, errs) + numberOfServers++ + } + if numberOfServers == 0 { + log.Println("no servers configured; exiting") + return + } + log.Fatal(<-errs) // block until one of the servers writes an error } From b2b918637ebc0e6baed8cffa95b5e1fc90fdc39c Mon Sep 17 00:00:00 2001 From: Igor Chubin Date: Sun, 20 Nov 2022 14:00:37 +0100 Subject: [PATCH 013/105] Use custom servers/timeouts for HTTP/HTTPS --- cmd/srv.go | 31 ++++++++++++++++++++++++++++--- 1 file changed, 28 insertions(+), 3 deletions(-) diff --git a/cmd/srv.go b/cmd/srv.go index f85d3f2..693e1e4 100644 --- a/cmd/srv.go +++ b/cmd/srv.go @@ -2,6 +2,7 @@ package main import ( "context" + "crypto/tls" "fmt" "log" "net" @@ -72,12 +73,36 @@ func copyHeader(dst, src http.Header) { } func serveHTTP(mux *http.ServeMux, port int, errs chan<- error) { - errs <- http.ListenAndServe(fmt.Sprintf(":%d", port), mux) + srv := &http.Server{ + Addr: fmt.Sprintf(":%d", port), + ReadTimeout: 5 * time.Second, + WriteTimeout: 10 * time.Second, + IdleTimeout: 1 * time.Second, + Handler: mux, + } + // srv.SetKeepAlivesEnabled(false) + errs <- srv.ListenAndServe() } func serveHTTPS(mux *http.ServeMux, port int, errs chan<- error) { - errs <- http.ListenAndServeTLS(fmt.Sprintf(":%d", port), - Conf.Server.TLSCertFile, Conf.Server.TLSKeyFile, mux) + tlsConfig := &tls.Config{ + // CipherSuites: []uint16{ + // tls.TLS_CHACHA20_POLY1305_SHA256, + // tls.TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256, + // tls.TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256, + // }, + // MinVersion: tls.VersionTLS13, + } + srv := &http.Server{ + Addr: fmt.Sprintf(":%d", port), + ReadTimeout: 5 * time.Second, + WriteTimeout: 20 * time.Second, + IdleTimeout: 1 * time.Second, + TLSConfig: tlsConfig, + Handler: mux, + } + // srv.SetKeepAlivesEnabled(false) + errs <- srv.ListenAndServeTLS(Conf.Server.TLSCertFile, Conf.Server.TLSKeyFile) } func main() { From ff4f258f2d991a2849e2b5e848518417d929f607 Mon Sep 17 00:00:00 2001 From: Igor Chubin Date: Sun, 20 Nov 2022 14:00:59 +0100 Subject: [PATCH 014/105] Don't redirect if TLS is already in use --- cmd/processRequest.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cmd/processRequest.go b/cmd/processRequest.go index 4b9aede..27e6e19 100644 --- a/cmd/processRequest.go +++ b/cmd/processRequest.go @@ -145,7 +145,7 @@ func redirectInsecure(req *http.Request) (*responseWithHeader, bool) { return nil, false } - if strings.ToLower(req.Header.Get("X-Forwarded-Proto")) == "https" { + if req.TLS != nil || strings.ToLower(req.Header.Get("X-Forwarded-Proto")) == "https" { return nil, false } From d804310586056b410080f3b5244b9fa7991ae55e Mon Sep 17 00:00:00 2001 From: Igor Chubin Date: Sun, 20 Nov 2022 17:45:06 +0100 Subject: [PATCH 015/105] Add cmd/logsuppress.go --- cmd/logsuppress.go | 69 ++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 69 insertions(+) create mode 100644 cmd/logsuppress.go diff --git a/cmd/logsuppress.go b/cmd/logsuppress.go new file mode 100644 index 0000000..d8ca9e6 --- /dev/null +++ b/cmd/logsuppress.go @@ -0,0 +1,69 @@ +package main + +import ( + "os" + "strings" + "sync" +) + +// LogSuppressor provides io.Writer interface for logging +// with lines suppression. For usage with log.Logger. +type LogSuppressor struct { + filename string + suppress []string + linePrefix string + + logFile *os.File + m sync.Mutex +} + +// NewLogSuppressor creates a new LogSuppressor for specified +// filename and lines to be suppressed. +func NewLogSuppressor(filename string, suppress []string, linePrefix string) *LogSuppressor { + return &LogSuppressor{ + filename: filename, + suppress: suppress, + linePrefix: linePrefix, + } +} + +// Open opens log file. +func (ls *LogSuppressor) Open() error { + var err error + ls.logFile, err = os.OpenFile(ls.filename, os.O_APPEND|os.O_WRONLY|os.O_CREATE, 0600) + return err +} + +// Close closes log file. +func (ls *LogSuppressor) Close() error { + return ls.logFile.Close() +} + +// Write writes p to log, and returns number f bytes written. +// Implements io.Writer interface. +func (ls *LogSuppressor) Write(p []byte) (n int, err error) { + var ( + output string + ) + + ls.m.Lock() + defer ls.m.Unlock() + + lines := strings.Split(string(p), ls.linePrefix) + for _, line := range lines { + if (func() bool { + for _, suppress := range ls.suppress { + if strings.Contains(line, suppress) { + return true + } + } + return false + })() { + continue + } + output += line + } + + n, err = ls.logFile.Write([]byte(output)) + return n, err +} From d4e96dbf3aab5c97076ee88c389fccc31b0d3a8b Mon Sep 17 00:00:00 2001 From: Igor Chubin Date: Sun, 20 Nov 2022 17:53:51 +0100 Subject: [PATCH 016/105] Write HTTP errors to log --- cmd/config.go | 4 ++++ cmd/srv.go | 28 ++++++++++++++++++++++------ 2 files changed, 26 insertions(+), 6 deletions(-) diff --git a/cmd/config.go b/cmd/config.go index 84cd6f9..bf6cf90 100644 --- a/cmd/config.go +++ b/cmd/config.go @@ -12,6 +12,9 @@ type Logging struct { // AccessLog path. AccessLog string + // ErrorsLog path. + ErrorsLog string + // Interval between access log flushes, in seconds. Interval int } @@ -38,6 +41,7 @@ type Server struct { var Conf = Config{ Logging{ AccessLog: "/wttr.in/log/access.log", + ErrorsLog: "/wttr.in/log/errors.log", Interval: 300, }, Server{ diff --git a/cmd/srv.go b/cmd/srv.go index 693e1e4..14d1f10 100644 --- a/cmd/srv.go +++ b/cmd/srv.go @@ -4,6 +4,7 @@ import ( "context" "crypto/tls" "fmt" + "io" "log" "net" "net/http" @@ -16,6 +17,7 @@ const uplinkSrvAddr = "127.0.0.1:9002" const uplinkTimeout = 30 const prefetchInterval = 300 const lruCacheSize = 12800 +const logLineStart = "LOG_LINE_START " // plainTextAgents contains signatures of the plain-text agents var plainTextAgents = []string{ @@ -72,19 +74,19 @@ func copyHeader(dst, src http.Header) { } } -func serveHTTP(mux *http.ServeMux, port int, errs chan<- error) { +func serveHTTP(mux *http.ServeMux, port int, logFile io.Writer, errs chan<- error) { srv := &http.Server{ Addr: fmt.Sprintf(":%d", port), + ErrorLog: log.New(logFile, logLineStart, log.LstdFlags), ReadTimeout: 5 * time.Second, WriteTimeout: 10 * time.Second, IdleTimeout: 1 * time.Second, Handler: mux, } - // srv.SetKeepAlivesEnabled(false) errs <- srv.ListenAndServe() } -func serveHTTPS(mux *http.ServeMux, port int, errs chan<- error) { +func serveHTTPS(mux *http.ServeMux, port int, logFile io.Writer, errs chan<- error) { tlsConfig := &tls.Config{ // CipherSuites: []uint16{ // tls.TLS_CHACHA20_POLY1305_SHA256, @@ -95,13 +97,13 @@ func serveHTTPS(mux *http.ServeMux, port int, errs chan<- error) { } srv := &http.Server{ Addr: fmt.Sprintf(":%d", port), + ErrorLog: log.New(logFile, logLineStart, log.LstdFlags), ReadTimeout: 5 * time.Second, WriteTimeout: 20 * time.Second, IdleTimeout: 1 * time.Second, TLSConfig: tlsConfig, Handler: mux, } - // srv.SetKeepAlivesEnabled(false) errs <- srv.ListenAndServeTLS(Conf.Server.TLSCertFile, Conf.Server.TLSKeyFile) } @@ -120,8 +122,22 @@ func main() { // numberOfServers started. If 0, exit. numberOfServers int + + errorsLog *LogSuppressor = NewLogSuppressor( + Conf.Logging.ErrorsLog, + []string{ + "error reading preface from client", + "TLS handshake error from", + }, + logLineStart, + ) ) + err := errorsLog.Open() + if err != nil { + log.Fatalln("errors log:", err) + } + mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { if err := logger.Log(r); err != nil { log.Println(err) @@ -136,11 +152,11 @@ func main() { }) if Conf.Server.PortHTTP != 0 { - go serveHTTP(mux, Conf.Server.PortHTTP, errs) + go serveHTTP(mux, Conf.Server.PortHTTP, errorsLog, errs) numberOfServers++ } if Conf.Server.PortHTTPS != 0 { - go serveHTTPS(mux, Conf.Server.PortHTTPS, errs) + go serveHTTPS(mux, Conf.Server.PortHTTPS, errorsLog, errs) numberOfServers++ } if numberOfServers == 0 { From 5b240c590e9c34a740afd6bbf535dcd5395176be Mon Sep 17 00:00:00 2001 From: Igor Chubin Date: Sun, 20 Nov 2022 17:54:42 +0100 Subject: [PATCH 017/105] Print PREFETCH only when prefetching --- cmd/peakHandling.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cmd/peakHandling.go b/cmd/peakHandling.go index eb98191..d279564 100644 --- a/cmd/peakHandling.go +++ b/cmd/peakHandling.go @@ -55,10 +55,10 @@ func syncMapLen(sm *sync.Map) int { func prefetchPeakRequests(peakRequestMap *sync.Map) { peakRequestLen := syncMapLen(peakRequestMap) - log.Printf("PREFETCH: Prefetching %d requests\n", peakRequestLen) if peakRequestLen == 0 { return } + log.Printf("PREFETCH: Prefetching %d requests\n", peakRequestLen) sleepBetweenRequests := time.Duration(prefetchInterval*1000/peakRequestLen) * time.Millisecond peakRequestMap.Range(func(key interface{}, value interface{}) bool { go func(r http.Request) { From 2c367d015724d15217acdd2fa4297c46b34bb3ae Mon Sep 17 00:00:00 2001 From: Igor Chubin Date: Sun, 20 Nov 2022 17:55:17 +0100 Subject: [PATCH 018/105] Return err from ProcessRequest instead of panic --- cmd/processRequest.go | 39 +++++++++++++++++++++++---------------- cmd/srv.go | 10 +++++++++- 2 files changed, 32 insertions(+), 17 deletions(-) diff --git a/cmd/processRequest.go b/cmd/processRequest.go index 27e6e19..1739fea 100644 --- a/cmd/processRequest.go +++ b/cmd/processRequest.go @@ -11,11 +11,14 @@ import ( "time" ) -func processRequest(r *http.Request) responseWithHeader { - var response responseWithHeader +func processRequest(r *http.Request) (*responseWithHeader, error) { + var ( + response *responseWithHeader + err error + ) - if response, ok := redirectInsecure(r); ok { - return *response + if resp, ok := redirectInsecure(r); ok { + return resp, nil } if dontCache(r) { @@ -40,31 +43,36 @@ func processRequest(r *http.Request) responseWithHeader { } time.Sleep(30 * time.Millisecond) cacheBody, ok = lruCache.Get(cacheDigest) - cacheEntry = cacheBody.(responseWithHeader) + if ok && cacheBody != nil { + cacheEntry = cacheBody.(responseWithHeader) + } } if cacheEntry.InProgress { log.Printf("TIMEOUT: %s\n", cacheDigest) } if ok && !cacheEntry.InProgress && cacheEntry.Expires.After(time.Now()) { - response = cacheEntry + response = &cacheEntry foundInCache = true } } if !foundInCache { lruCache.Add(cacheDigest, responseWithHeader{InProgress: true}) - response = get(r) + response, err = get(r) + if err != nil { + return nil, err + } if response.StatusCode == 200 || response.StatusCode == 304 || response.StatusCode == 404 { - lruCache.Add(cacheDigest, response) + lruCache.Add(cacheDigest, *response) } else { log.Printf("REMOVE: %d response for %s from cache\n", response.StatusCode, cacheDigest) lruCache.Remove(cacheDigest) } } - return response + return response, nil } -func get(req *http.Request) responseWithHeader { +func get(req *http.Request) (*responseWithHeader, error) { client := &http.Client{} @@ -72,7 +80,7 @@ func get(req *http.Request) responseWithHeader { proxyReq, err := http.NewRequest(req.Method, queryURL, req.Body) if err != nil { - log.Printf("Request: %s\n", err) + return nil, err } // proxyReq.Header.Set("Host", req.Host) @@ -89,23 +97,22 @@ func get(req *http.Request) responseWithHeader { } res, err := client.Do(proxyReq) - if err != nil { - panic(err) + return nil, err } body, err := ioutil.ReadAll(res.Body) if err != nil { - log.Println(err) + return nil, err } - return responseWithHeader{ + return &responseWithHeader{ InProgress: false, Expires: time.Now().Add(time.Duration(randInt(1000, 1500)) * time.Second), Body: body, Header: res.Header, StatusCode: res.StatusCode, - } + }, nil } // implementation of the cache.get_signature of original wttr.in diff --git a/cmd/srv.go b/cmd/srv.go index 14d1f10..0b10e2d 100644 --- a/cmd/srv.go +++ b/cmd/srv.go @@ -143,7 +143,15 @@ func main() { log.Println(err) } // printStat() - response := processRequest(r) + response, err := processRequest(r) + if err != nil { + log.Println(err) + return + } + if response.StatusCode == 0 { + log.Println("status code 0", response) + return + } copyHeader(w.Header(), response.Header) w.Header().Set("Access-Control-Allow-Origin", "*") From b6687ee037ed3e9f78a4d98ac4a31711193178ea Mon Sep 17 00:00:00 2001 From: Igor Chubin Date: Sat, 26 Nov 2022 15:40:04 +0100 Subject: [PATCH 019/105] Add gopkg.in/yaml.v3 to go.mod --- go.mod | 1 + go.sum | 3 +++ 2 files changed, 4 insertions(+) diff --git a/go.mod b/go.mod index b875ace..7cf799a 100644 --- a/go.mod +++ b/go.mod @@ -5,4 +5,5 @@ go 1.16 require ( github.com/hashicorp/golang-lru v0.6.0 github.com/robfig/cron v1.2.0 + gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/go.sum b/go.sum index d30f05d..fbffd34 100644 --- a/go.sum +++ b/go.sum @@ -2,3 +2,6 @@ github.com/hashicorp/golang-lru v0.6.0 h1:uL2shRDx7RTrOrTCUZEGP/wJUFiUI8QT6E7z5o github.com/hashicorp/golang-lru v0.6.0/go.mod h1:iADmTwqILo4mZ8BN3D2Q6+9jd8WM5uGBxy+E8yxSoD4= github.com/robfig/cron v1.2.0 h1:ZjScXvvxeQ63Dbyxy76Fj3AT3Ut0aKsyd2/tl3DTMuQ= github.com/robfig/cron v1.2.0/go.mod h1:JGuDeoQd7Z6yL4zQhZ3OPEVHB7fL6Ka6skscFHfmt2k= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= From 762e0fe8f0c7f0d02341d963ce206f986d66c8d4 Mon Sep 17 00:00:00 2001 From: Igor Chubin Date: Sun, 27 Nov 2022 15:51:41 +0100 Subject: [PATCH 020/105] Move RequestProcessor to a struct --- cmd/peakHandling.go | 37 +++++++++++-------------- cmd/processRequest.go | 64 +++++++++++++++++++++++++++++++++---------- cmd/srv.go | 36 +++++++++--------------- 3 files changed, 79 insertions(+), 58 deletions(-) diff --git a/cmd/peakHandling.go b/cmd/peakHandling.go index d279564..461f7ff 100644 --- a/cmd/peakHandling.go +++ b/cmd/peakHandling.go @@ -9,28 +9,31 @@ import ( "github.com/robfig/cron" ) -var peakRequest30 sync.Map -var peakRequest60 sync.Map - -func initPeakHandling() { +func (rp *RequestProcessor) startPeakHandling() { c := cron.New() // cronTime := fmt.Sprintf("%d,%d * * * *", 30-prefetchInterval/60, 60-prefetchInterval/60) - c.AddFunc("24 * * * *", prefetchPeakRequests30) - c.AddFunc("54 * * * *", prefetchPeakRequests60) + c.AddFunc( + "24 * * * *", + func() { rp.prefetchPeakRequests(&rp.peakRequest30) }, + ) + c.AddFunc( + "54 * * * *", + func() { rp.prefetchPeakRequests(&rp.peakRequest60) }, + ) c.Start() } -func savePeakRequest(cacheDigest string, r *http.Request) { +func (rp *RequestProcessor) savePeakRequest(cacheDigest string, r *http.Request) { _, min, _ := time.Now().Clock() if min == 30 { - peakRequest30.Store(cacheDigest, *r) + rp.peakRequest30.Store(cacheDigest, *r) } else if min == 0 { - peakRequest60.Store(cacheDigest, *r) + rp.peakRequest60.Store(cacheDigest, *r) } } -func prefetchRequest(r *http.Request) { - processRequest(r) +func (rp *RequestProcessor) prefetchRequest(r *http.Request) { + rp.ProcessRequest(r) } func syncMapLen(sm *sync.Map) int { @@ -53,7 +56,7 @@ func syncMapLen(sm *sync.Map) int { return count } -func prefetchPeakRequests(peakRequestMap *sync.Map) { +func (rp *RequestProcessor) prefetchPeakRequests(peakRequestMap *sync.Map) { peakRequestLen := syncMapLen(peakRequestMap) if peakRequestLen == 0 { return @@ -62,18 +65,10 @@ func prefetchPeakRequests(peakRequestMap *sync.Map) { sleepBetweenRequests := time.Duration(prefetchInterval*1000/peakRequestLen) * time.Millisecond peakRequestMap.Range(func(key interface{}, value interface{}) bool { go func(r http.Request) { - prefetchRequest(&r) + rp.prefetchRequest(&r) }(value.(http.Request)) peakRequestMap.Delete(key) time.Sleep(sleepBetweenRequests) return true }) } - -func prefetchPeakRequests30() { - prefetchPeakRequests(&peakRequest30) -} - -func prefetchPeakRequests60() { - prefetchPeakRequests(&peakRequest60) -} diff --git a/cmd/processRequest.go b/cmd/processRequest.go index 1739fea..da03f34 100644 --- a/cmd/processRequest.go +++ b/cmd/processRequest.go @@ -8,12 +8,48 @@ import ( "net" "net/http" "strings" + "sync" "time" + + lru "github.com/hashicorp/golang-lru" ) -func processRequest(r *http.Request) (*responseWithHeader, error) { +type ResponseWithHeader struct { + InProgress bool // true if the request is being processed + Expires time.Time // expiration time of the cache entry + + Body []byte + Header http.Header + StatusCode int // e.g. 200 +} + +// RequestProcessor handles incoming requests. +type RequestProcessor struct { + peakRequest30 sync.Map + peakRequest60 sync.Map + lruCache *lru.Cache +} + +// NewRequestProcessor returns new RequestProcessor. +func NewRequestProcessor() (*RequestProcessor, error) { + lruCache, err := lru.New(lruCacheSize) + if err != nil { + return nil, err + } + + return &RequestProcessor{ + lruCache: lruCache, + }, nil +} + +// Start starts async request processor jobs, such as peak handling. +func (rp *RequestProcessor) Start() { + rp.startPeakHandling() +} + +func (rp *RequestProcessor) ProcessRequest(r *http.Request) (*ResponseWithHeader, error) { var ( - response *responseWithHeader + response *ResponseWithHeader err error ) @@ -29,11 +65,11 @@ func processRequest(r *http.Request) (*responseWithHeader, error) { foundInCache := false - savePeakRequest(cacheDigest, r) + rp.savePeakRequest(cacheDigest, r) - cacheBody, ok := lruCache.Get(cacheDigest) + cacheBody, ok := rp.lruCache.Get(cacheDigest) if ok { - cacheEntry := cacheBody.(responseWithHeader) + cacheEntry := cacheBody.(ResponseWithHeader) // if after all attempts we still have no answer, // we try to make the query on our own @@ -42,9 +78,9 @@ func processRequest(r *http.Request) (*responseWithHeader, error) { break } time.Sleep(30 * time.Millisecond) - cacheBody, ok = lruCache.Get(cacheDigest) + cacheBody, ok = rp.lruCache.Get(cacheDigest) if ok && cacheBody != nil { - cacheEntry = cacheBody.(responseWithHeader) + cacheEntry = cacheBody.(ResponseWithHeader) } } if cacheEntry.InProgress { @@ -57,22 +93,22 @@ func processRequest(r *http.Request) (*responseWithHeader, error) { } if !foundInCache { - lruCache.Add(cacheDigest, responseWithHeader{InProgress: true}) + rp.lruCache.Add(cacheDigest, ResponseWithHeader{InProgress: true}) response, err = get(r) if err != nil { return nil, err } if response.StatusCode == 200 || response.StatusCode == 304 || response.StatusCode == 404 { - lruCache.Add(cacheDigest, *response) + rp.lruCache.Add(cacheDigest, *response) } else { log.Printf("REMOVE: %d response for %s from cache\n", response.StatusCode, cacheDigest) - lruCache.Remove(cacheDigest) + rp.lruCache.Remove(cacheDigest) } } return response, nil } -func get(req *http.Request) (*responseWithHeader, error) { +func get(req *http.Request) (*ResponseWithHeader, error) { client := &http.Client{} @@ -106,7 +142,7 @@ func get(req *http.Request) (*responseWithHeader, error) { return nil, err } - return &responseWithHeader{ + return &ResponseWithHeader{ InProgress: false, Expires: time.Now().Add(time.Duration(randInt(1000, 1500)) * time.Second), Body: body, @@ -147,7 +183,7 @@ func dontCache(req *http.Request) bool { // proxy_set_header X-Forwarded-Proto $scheme; // // -func redirectInsecure(req *http.Request) (*responseWithHeader, bool) { +func redirectInsecure(req *http.Request) (*ResponseWithHeader, bool) { if isPlainTextAgent(req.Header.Get("User-Agent")) { return nil, false } @@ -169,7 +205,7 @@ The document has moved `, target)) - return &responseWithHeader{ + return &ResponseWithHeader{ InProgress: false, Expires: time.Now().Add(time.Duration(randInt(1000, 1500)) * time.Second), Body: body, diff --git a/cmd/srv.go b/cmd/srv.go index 0b10e2d..e1d0955 100644 --- a/cmd/srv.go +++ b/cmd/srv.go @@ -9,8 +9,6 @@ import ( "net" "net/http" "time" - - lru "github.com/hashicorp/golang-lru" ) const uplinkSrvAddr = "127.0.0.1:9002" @@ -35,24 +33,7 @@ var plainTextAgents = []string{ "xh", } -var lruCache *lru.Cache - -type responseWithHeader struct { - InProgress bool // true if the request is being processed - Expires time.Time // expiration time of the cache entry - - Body []byte - Header http.Header - StatusCode int // e.g. 200 -} - func init() { - var err error - lruCache, err = lru.New(lruCacheSize) - if err != nil { - panic(err) - } - dialer := &net.Dialer{ Timeout: uplinkTimeout * time.Second, KeepAlive: uplinkTimeout * time.Second, @@ -62,8 +43,6 @@ func init() { http.DefaultTransport.(*http.Transport).DialContext = func(ctx context.Context, network, _ string) (net.Conn, error) { return dialer.DialContext(ctx, network, uplinkSrvAddr) } - - initPeakHandling() } func copyHeader(dst, src http.Header) { @@ -117,6 +96,8 @@ func main() { Conf.Logging.AccessLog, time.Duration(Conf.Logging.Interval)*time.Second) + rp *RequestProcessor + // errs is the servers errors channel. errs chan error = make(chan error, 1) @@ -131,19 +112,28 @@ func main() { }, logLineStart, ) + + err error ) - err := errorsLog.Open() + rp, err = NewRequestProcessor() + if err != nil { + log.Fatalln("log processor initialization:", err) + } + + err = errorsLog.Open() if err != nil { log.Fatalln("errors log:", err) } + rp.Start() + mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { if err := logger.Log(r); err != nil { log.Println(err) } // printStat() - response, err := processRequest(r) + response, err := rp.ProcessRequest(r) if err != nil { log.Println(err) return From 8fd712f790e22f94b26a671c28992369068b8529 Mon Sep 17 00:00:00 2001 From: Igor Chubin Date: Sun, 27 Nov 2022 22:16:19 +0100 Subject: [PATCH 021/105] Add cmd/route.go --- cmd/route.go | 37 +++++++++++++++++++++++++++++++++++++ 1 file changed, 37 insertions(+) create mode 100644 cmd/route.go diff --git a/cmd/route.go b/cmd/route.go new file mode 100644 index 0000000..37cc5cc --- /dev/null +++ b/cmd/route.go @@ -0,0 +1,37 @@ +package main + +import "net/http" + +type Handler interface { + Response(*http.Request) *ResponseWithHeader +} + +type routeFunc func(*http.Request) bool + +type route struct { + routeFunc + Handler +} + +type Router struct { + rt []route +} + +func (r *Router) Route(req *http.Request) Handler { + for _, re := range r.rt { + if re.routeFunc(req) { + return re.Handler + } + } + return nil +} + +func (r *Router) AddPath(path string, handler Handler) { + r.rt = append(r.rt, route{routePath(path), handler}) +} + +func routePath(path string) routeFunc { + return routeFunc(func(req *http.Request) bool { + return req.URL.Path == path + }) +} From ec264850a47560c66ac5f5554933af403a9b8ebe Mon Sep 17 00:00:00 2001 From: Igor Chubin Date: Sun, 27 Nov 2022 22:16:32 +0100 Subject: [PATCH 022/105] Add /:stats support --- cmd/processRequest.go | 35 +++++++++++- cmd/stat.go | 121 +++++++++++++++++++++++++++++------------- 2 files changed, 116 insertions(+), 40 deletions(-) diff --git a/cmd/processRequest.go b/cmd/processRequest.go index da03f34..faba90b 100644 --- a/cmd/processRequest.go +++ b/cmd/processRequest.go @@ -28,6 +28,8 @@ type RequestProcessor struct { peakRequest30 sync.Map peakRequest60 sync.Map lruCache *lru.Cache + stats *Stats + router Router } // NewRequestProcessor returns new RequestProcessor. @@ -37,9 +39,15 @@ func NewRequestProcessor() (*RequestProcessor, error) { return nil, err } - return &RequestProcessor{ + rp := &RequestProcessor{ lruCache: lruCache, - }, nil + stats: NewStats(), + } + + // Initialize routes. + rp.router.AddPath("/:stats", rp.stats) + + return rp, nil } // Start starts async request processor jobs, such as peak handling. @@ -53,11 +61,23 @@ func (rp *RequestProcessor) ProcessRequest(r *http.Request) (*ResponseWithHeader err error ) + rp.stats.Inc("total") + + // Main routing logic. + if rh := rp.router.Route(r); rh != nil { + result := rh.Response(r) + if result != nil { + return result, nil + } + } + if resp, ok := redirectInsecure(r); ok { + rp.stats.Inc("redirects") return resp, nil } if dontCache(r) { + rp.stats.Inc("uncached") return get(r) } @@ -69,6 +89,7 @@ func (rp *RequestProcessor) ProcessRequest(r *http.Request) (*ResponseWithHeader cacheBody, ok := rp.lruCache.Get(cacheDigest) if ok { + rp.stats.Inc("cache1") cacheEntry := cacheBody.(ResponseWithHeader) // if after all attempts we still have no answer, @@ -93,7 +114,17 @@ func (rp *RequestProcessor) ProcessRequest(r *http.Request) (*ResponseWithHeader } if !foundInCache { + // Handling query. + format := r.URL.Query().Get("format") + if len(format) != 0 { + rp.stats.Inc("format") + if format == "j1" { + rp.stats.Inc("format=j1") + } + } + rp.lruCache.Add(cacheDigest, ResponseWithHeader{InProgress: true}) + response, err = get(r) if err != nil { return nil, err diff --git a/cmd/stat.go b/cmd/stat.go index eda4f27..4006247 100644 --- a/cmd/stat.go +++ b/cmd/stat.go @@ -1,40 +1,85 @@ package main -// import ( -// "log" -// "sync" -// "time" -// ) -// -// type safeCounter struct { -// v map[int]int -// mux sync.Mutex -// } -// -// func (c *safeCounter) inc(key int) { -// c.mux.Lock() -// c.v[key]++ -// c.mux.Unlock() -// } -// -// // func (c *safeCounter) val(key int) int { -// // c.mux.Lock() -// // defer c.mux.Unlock() -// // return c.v[key] -// // } -// // -// // func (c *safeCounter) reset(key int) int { -// // c.mux.Lock() -// // defer c.mux.Unlock() -// // result := c.v[key] -// // c.v[key] = 0 -// // return result -// // } -// -// var queriesPerMinute safeCounter -// -// func printStat() { -// _, min, _ := time.Now().Clock() -// queriesPerMinute.inc(min) -// log.Printf("Processed %d requests\n", min) -// } +import ( + "bytes" + "fmt" + "net/http" + "sync" + "time" +) + +// Stats holds processed requests statistics. +type Stats struct { + m sync.Mutex + v map[string]int + startTime time.Time +} + +// NewStats returns new Stats. +func NewStats() *Stats { + return &Stats{ + v: map[string]int{}, + startTime: time.Now(), + } +} + +// Inc key by one. +func (c *Stats) Inc(key string) { + c.m.Lock() + c.v[key]++ + c.m.Unlock() +} + +// Get current key counter value. +func (c *Stats) Get(key string) int { + c.m.Lock() + defer c.m.Unlock() + return c.v[key] +} + +// Reset key counter. +func (c *Stats) Reset(key string) int { + c.m.Lock() + defer c.m.Unlock() + result := c.v[key] + c.v[key] = 0 + return result +} + +// Show returns current statistics formatted as []byte. +func (c *Stats) Show() []byte { + var ( + b bytes.Buffer + ) + + c.m.Lock() + defer c.m.Unlock() + + uptime := time.Since(c.startTime) / time.Second + + fmt.Fprintf(&b, "%-20s: %v\n", "Running since", c.startTime.Format(time.RFC3339)) + fmt.Fprintf(&b, "%-20s: %d\n", "Uptime (min)", uptime/60) + + fmt.Fprintf(&b, "%-20s: %d\n", "Total queries", c.v["total"]) + if uptime != 0 { + fmt.Fprintf(&b, "%-20s: %d\n", "Throughput (QpM)", c.v["total"]*60/int(uptime)) + } + + fmt.Fprintf(&b, "%-20s: %d\n", "Cache L1 queries", c.v["cache1"]) + if c.v["total"] != 0 { + fmt.Fprintf(&b, "%-20s: %d\n", "Cache L1 queries (%)", (100*c.v["cache1"])/c.v["total"]) + } + + fmt.Fprintf(&b, "%-20s: %d\n", "Upstream queries", c.v["total"]-c.v["cache1"]) + fmt.Fprintf(&b, "%-20s: %d\n", "Queries with format", c.v["format"]) + fmt.Fprintf(&b, "%-20s: %d\n", "Queries with format=j1", c.v["format=j1"]) + + return b.Bytes() +} + +func (c *Stats) Response(*http.Request) *ResponseWithHeader { + return &ResponseWithHeader{ + Body: c.Show(), + StatusCode: 200, + } +} From f27bf2d5b30e89aec4447da125ea70693185647e Mon Sep 17 00:00:00 2001 From: Igor Chubin Date: Tue, 29 Nov 2022 21:24:53 +0100 Subject: [PATCH 023/105] Move routing to separate module --- Makefile | 4 +-- cmd/processRequest.go | 37 ++++++++++++------- cmd/route.go | 37 ------------------- cmd/stat.go | 9 ++--- go.mod | 3 +- go.sum | 3 -- internal/routing/routing.go | 71 +++++++++++++++++++++++++++++++++++++ 7 files changed, 104 insertions(+), 60 deletions(-) delete mode 100644 cmd/route.go create mode 100644 internal/routing/routing.go diff --git a/Makefile b/Makefile index 7790552..66956ff 100644 --- a/Makefile +++ b/Makefile @@ -1,2 +1,2 @@ -srv: cmd/*.go - go build -o srv cmd/*.go +srv: cmd/*.go internal/routing/*.go + go build -o srv ./cmd/ diff --git a/cmd/processRequest.go b/cmd/processRequest.go index faba90b..cb2ce69 100644 --- a/cmd/processRequest.go +++ b/cmd/processRequest.go @@ -11,10 +11,12 @@ import ( "sync" "time" + "github.com/chubin/wttr.in/internal/routing" + lru "github.com/hashicorp/golang-lru" ) -type ResponseWithHeader struct { +type responseWithHeader struct { InProgress bool // true if the request is being processed Expires time.Time // expiration time of the cache entry @@ -29,7 +31,7 @@ type RequestProcessor struct { peakRequest60 sync.Map lruCache *lru.Cache stats *Stats - router Router + router routing.Router } // NewRequestProcessor returns new RequestProcessor. @@ -55,9 +57,9 @@ func (rp *RequestProcessor) Start() { rp.startPeakHandling() } -func (rp *RequestProcessor) ProcessRequest(r *http.Request) (*ResponseWithHeader, error) { +func (rp *RequestProcessor) ProcessRequest(r *http.Request) (*responseWithHeader, error) { var ( - response *ResponseWithHeader + response *responseWithHeader err error ) @@ -67,7 +69,7 @@ func (rp *RequestProcessor) ProcessRequest(r *http.Request) (*ResponseWithHeader if rh := rp.router.Route(r); rh != nil { result := rh.Response(r) if result != nil { - return result, nil + return fromCadre(result), nil } } @@ -90,7 +92,7 @@ func (rp *RequestProcessor) ProcessRequest(r *http.Request) (*ResponseWithHeader cacheBody, ok := rp.lruCache.Get(cacheDigest) if ok { rp.stats.Inc("cache1") - cacheEntry := cacheBody.(ResponseWithHeader) + cacheEntry := cacheBody.(responseWithHeader) // if after all attempts we still have no answer, // we try to make the query on our own @@ -101,7 +103,7 @@ func (rp *RequestProcessor) ProcessRequest(r *http.Request) (*ResponseWithHeader time.Sleep(30 * time.Millisecond) cacheBody, ok = rp.lruCache.Get(cacheDigest) if ok && cacheBody != nil { - cacheEntry = cacheBody.(ResponseWithHeader) + cacheEntry = cacheBody.(responseWithHeader) } } if cacheEntry.InProgress { @@ -123,7 +125,7 @@ func (rp *RequestProcessor) ProcessRequest(r *http.Request) (*ResponseWithHeader } } - rp.lruCache.Add(cacheDigest, ResponseWithHeader{InProgress: true}) + rp.lruCache.Add(cacheDigest, responseWithHeader{InProgress: true}) response, err = get(r) if err != nil { @@ -139,7 +141,7 @@ func (rp *RequestProcessor) ProcessRequest(r *http.Request) (*ResponseWithHeader return response, nil } -func get(req *http.Request) (*ResponseWithHeader, error) { +func get(req *http.Request) (*responseWithHeader, error) { client := &http.Client{} @@ -173,7 +175,7 @@ func get(req *http.Request) (*ResponseWithHeader, error) { return nil, err } - return &ResponseWithHeader{ + return &responseWithHeader{ InProgress: false, Expires: time.Now().Add(time.Duration(randInt(1000, 1500)) * time.Second), Body: body, @@ -214,7 +216,7 @@ func dontCache(req *http.Request) bool { // proxy_set_header X-Forwarded-Proto $scheme; // // -func redirectInsecure(req *http.Request) (*ResponseWithHeader, bool) { +func redirectInsecure(req *http.Request) (*responseWithHeader, bool) { if isPlainTextAgent(req.Header.Get("User-Agent")) { return nil, false } @@ -236,7 +238,7 @@ The document has moved `, target)) - return &ResponseWithHeader{ + return &responseWithHeader{ InProgress: false, Expires: time.Now().Add(time.Duration(randInt(1000, 1500)) * time.Second), Body: body, @@ -284,3 +286,14 @@ func ipFromAddr(s string) string { } return s[:pos] } + +// fromCadre converts Cadre into a responseWithHeader. +func fromCadre(cadre *routing.Cadre) *responseWithHeader { + return &responseWithHeader{ + Body: cadre.Body, + Expires: cadre.Expires, + StatusCode: 200, + InProgress: false, + } + +} diff --git a/cmd/route.go b/cmd/route.go deleted file mode 100644 index 37cc5cc..0000000 --- a/cmd/route.go +++ /dev/null @@ -1,37 +0,0 @@ -package main - -import "net/http" - -type Handler interface { - Response(*http.Request) *ResponseWithHeader -} - -type routeFunc func(*http.Request) bool - -type route struct { - routeFunc - Handler -} - -type Router struct { - rt []route -} - -func (r *Router) Route(req *http.Request) Handler { - for _, re := range r.rt { - if re.routeFunc(req) { - return re.Handler - } - } - return nil -} - -func (r *Router) AddPath(path string, handler Handler) { - r.rt = append(r.rt, route{routePath(path), handler}) -} - -func routePath(path string) routeFunc { - return routeFunc(func(req *http.Request) bool { - return req.URL.Path == path - }) -} diff --git a/cmd/stat.go b/cmd/stat.go index 4006247..73e4647 100644 --- a/cmd/stat.go +++ b/cmd/stat.go @@ -6,6 +6,8 @@ import ( "net/http" "sync" "time" + + "github.com/chubin/wttr.in/internal/routing" ) // Stats holds processed requests statistics. @@ -77,9 +79,8 @@ func (c *Stats) Show() []byte { return b.Bytes() } -func (c *Stats) Response(*http.Request) *ResponseWithHeader { - return &ResponseWithHeader{ - Body: c.Show(), - StatusCode: 200, +func (c *Stats) Response(*http.Request) *routing.Cadre { + return &routing.Cadre{ + Body: c.Show(), } } diff --git a/go.mod b/go.mod index 7cf799a..001da54 100644 --- a/go.mod +++ b/go.mod @@ -1,9 +1,8 @@ -module github.com/chubin/wttr.in/v2 +module github.com/chubin/wttr.in go 1.16 require ( github.com/hashicorp/golang-lru v0.6.0 github.com/robfig/cron v1.2.0 - gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/go.sum b/go.sum index fbffd34..d30f05d 100644 --- a/go.sum +++ b/go.sum @@ -2,6 +2,3 @@ github.com/hashicorp/golang-lru v0.6.0 h1:uL2shRDx7RTrOrTCUZEGP/wJUFiUI8QT6E7z5o github.com/hashicorp/golang-lru v0.6.0/go.mod h1:iADmTwqILo4mZ8BN3D2Q6+9jd8WM5uGBxy+E8yxSoD4= github.com/robfig/cron v1.2.0 h1:ZjScXvvxeQ63Dbyxy76Fj3AT3Ut0aKsyd2/tl3DTMuQ= github.com/robfig/cron v1.2.0/go.mod h1:JGuDeoQd7Z6yL4zQhZ3OPEVHB7fL6Ka6skscFHfmt2k= -gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= -gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= -gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/internal/routing/routing.go b/internal/routing/routing.go new file mode 100644 index 0000000..ba59566 --- /dev/null +++ b/internal/routing/routing.go @@ -0,0 +1,71 @@ +package routing + +import ( + "net/http" + "time" +) + +// CadreFormat specifies how the shot data is formatted. +type CadreFormat int + +const ( + // CadreFormatANSI represents Terminal ANSI format. + CadreFormatANSI = iota + + // CadreFormatHTML represents HTML. + CadreFormatHTML + + // CadreFormatPNG represents PNG. + CadreFormatPNG +) + +// Cadre contains result of a query execution. +type Cadre struct { + // Body contains the data of Cadre, formatted as Format. + Body []byte + + // Format of the shot. + Format CadreFormat + + // Expires contains the time of the Cadre expiration, + // or 0 if it does not expire. + Expires time.Time +} + +// Handler can handle queries and return views. +type Handler interface { + Response(*http.Request) *Cadre +} + +type routeFunc func(*http.Request) bool + +type route struct { + routeFunc + Handler +} + +// Router keeps a routing table, and finds queries handlers, based on its rules. +type Router struct { + rt []route +} + +// Route returns a query handler based on its content. +func (r *Router) Route(req *http.Request) Handler { + for _, re := range r.rt { + if re.routeFunc(req) { + return re.Handler + } + } + return nil +} + +// AddPath adds route for a static path. +func (r *Router) AddPath(path string, handler Handler) { + r.rt = append(r.rt, route{routePath(path), handler}) +} + +func routePath(path string) routeFunc { + return routeFunc(func(req *http.Request) bool { + return req.URL.Path == path + }) +} From 7b8c6665e8da9a8dcd7050dbfda98cea068a318e Mon Sep 17 00:00:00 2001 From: Igor Chubin Date: Tue, 29 Nov 2022 21:38:39 +0100 Subject: [PATCH 024/105] Move config to separate package --- cmd/srv.go | 19 +++++++++++-------- {cmd => internal/config}/config.go | 2 +- 2 files changed, 12 insertions(+), 9 deletions(-) rename {cmd => internal/config}/config.go (98%) diff --git a/cmd/srv.go b/cmd/srv.go index e1d0955..0db5a9d 100644 --- a/cmd/srv.go +++ b/cmd/srv.go @@ -9,6 +9,8 @@ import ( "net" "net/http" "time" + + "github.com/chubin/wttr.in/internal/config" ) const uplinkSrvAddr = "127.0.0.1:9002" @@ -67,6 +69,7 @@ func serveHTTP(mux *http.ServeMux, port int, logFile io.Writer, errs chan<- erro func serveHTTPS(mux *http.ServeMux, port int, logFile io.Writer, errs chan<- error) { tlsConfig := &tls.Config{ + // CipherSuites: []uint16{ // tls.TLS_CHACHA20_POLY1305_SHA256, // tls.TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256, @@ -83,7 +86,7 @@ func serveHTTPS(mux *http.ServeMux, port int, logFile io.Writer, errs chan<- err TLSConfig: tlsConfig, Handler: mux, } - errs <- srv.ListenAndServeTLS(Conf.Server.TLSCertFile, Conf.Server.TLSKeyFile) + errs <- srv.ListenAndServeTLS(config.Conf.Server.TLSCertFile, config.Conf.Server.TLSKeyFile) } func main() { @@ -93,8 +96,8 @@ func main() { // logger is optimized requests logger. logger *RequestLogger = NewRequestLogger( - Conf.Logging.AccessLog, - time.Duration(Conf.Logging.Interval)*time.Second) + config.Conf.Logging.AccessLog, + time.Duration(config.Conf.Logging.Interval)*time.Second) rp *RequestProcessor @@ -105,7 +108,7 @@ func main() { numberOfServers int errorsLog *LogSuppressor = NewLogSuppressor( - Conf.Logging.ErrorsLog, + config.Conf.Logging.ErrorsLog, []string{ "error reading preface from client", "TLS handshake error from", @@ -149,12 +152,12 @@ func main() { w.Write(response.Body) }) - if Conf.Server.PortHTTP != 0 { - go serveHTTP(mux, Conf.Server.PortHTTP, errorsLog, errs) + if config.Conf.Server.PortHTTP != 0 { + go serveHTTP(mux, config.Conf.Server.PortHTTP, errorsLog, errs) numberOfServers++ } - if Conf.Server.PortHTTPS != 0 { - go serveHTTPS(mux, Conf.Server.PortHTTPS, errorsLog, errs) + if config.Conf.Server.PortHTTPS != 0 { + go serveHTTPS(mux, config.Conf.Server.PortHTTPS, errorsLog, errs) numberOfServers++ } if numberOfServers == 0 { diff --git a/cmd/config.go b/internal/config/config.go similarity index 98% rename from cmd/config.go rename to internal/config/config.go index bf6cf90..8ccd493 100644 --- a/cmd/config.go +++ b/internal/config/config.go @@ -1,4 +1,4 @@ -package main +package config // Config of the program. type Config struct { From 28f1fd9aae03f0baaacc79cac379772420370888 Mon Sep 17 00:00:00 2001 From: Igor Chubin Date: Fri, 2 Dec 2022 19:47:36 +0100 Subject: [PATCH 025/105] Move global transport to RequestProcessor --- cmd/processRequest.go | 39 ++++++++++++++++++++++++++++----------- cmd/srv.go | 14 -------------- 2 files changed, 28 insertions(+), 25 deletions(-) diff --git a/cmd/processRequest.go b/cmd/processRequest.go index cb2ce69..8080115 100644 --- a/cmd/processRequest.go +++ b/cmd/processRequest.go @@ -1,6 +1,7 @@ package main import ( + "context" "fmt" "io/ioutil" "log" @@ -27,11 +28,12 @@ type responseWithHeader struct { // RequestProcessor handles incoming requests. type RequestProcessor struct { - peakRequest30 sync.Map - peakRequest60 sync.Map - lruCache *lru.Cache - stats *Stats - router routing.Router + peakRequest30 sync.Map + peakRequest60 sync.Map + lruCache *lru.Cache + stats *Stats + router routing.Router + upstreamTransport *http.Transport } // NewRequestProcessor returns new RequestProcessor. @@ -41,9 +43,22 @@ func NewRequestProcessor() (*RequestProcessor, error) { return nil, err } + dialer := &net.Dialer{ + Timeout: uplinkTimeout * time.Second, + KeepAlive: uplinkTimeout * time.Second, + DualStack: true, + } + + transport := &http.Transport{ + DialContext: func(ctx context.Context, network, _ string) (net.Conn, error) { + return dialer.DialContext(ctx, network, uplinkSrvAddr) + }, + } + rp := &RequestProcessor{ - lruCache: lruCache, - stats: NewStats(), + lruCache: lruCache, + stats: NewStats(), + upstreamTransport: transport, } // Initialize routes. @@ -80,7 +95,7 @@ func (rp *RequestProcessor) ProcessRequest(r *http.Request) (*responseWithHeader if dontCache(r) { rp.stats.Inc("uncached") - return get(r) + return get(r, rp.upstreamTransport) } cacheDigest := getCacheDigest(r) @@ -127,7 +142,7 @@ func (rp *RequestProcessor) ProcessRequest(r *http.Request) (*responseWithHeader rp.lruCache.Add(cacheDigest, responseWithHeader{InProgress: true}) - response, err = get(r) + response, err = get(r, rp.upstreamTransport) if err != nil { return nil, err } @@ -141,9 +156,11 @@ func (rp *RequestProcessor) ProcessRequest(r *http.Request) (*responseWithHeader return response, nil } -func get(req *http.Request) (*responseWithHeader, error) { +func get(req *http.Request, transport *http.Transport) (*responseWithHeader, error) { - client := &http.Client{} + client := &http.Client{ + Transport: transport, + } queryURL := fmt.Sprintf("http://%s%s", req.Host, req.RequestURI) diff --git a/cmd/srv.go b/cmd/srv.go index 0db5a9d..28bd2ce 100644 --- a/cmd/srv.go +++ b/cmd/srv.go @@ -1,12 +1,10 @@ package main import ( - "context" "crypto/tls" "fmt" "io" "log" - "net" "net/http" "time" @@ -35,18 +33,6 @@ var plainTextAgents = []string{ "xh", } -func init() { - dialer := &net.Dialer{ - Timeout: uplinkTimeout * time.Second, - KeepAlive: uplinkTimeout * time.Second, - DualStack: true, - } - - http.DefaultTransport.(*http.Transport).DialContext = func(ctx context.Context, network, _ string) (net.Conn, error) { - return dialer.DialContext(ctx, network, uplinkSrvAddr) - } -} - func copyHeader(dst, src http.Header) { for k, vv := range src { for _, v := range vv { From 30c2c85e54e5e84dd010ded3d7dd4fe473218f4f Mon Sep 17 00:00:00 2001 From: Igor Chubin Date: Fri, 2 Dec 2022 20:10:32 +0100 Subject: [PATCH 026/105] Move global consts to config --- cmd/peakHandling.go | 2 +- cmd/processRequest.go | 13 +++++---- cmd/srv.go | 6 +--- internal/config/config.go | 60 ++++++++++++++++++++++++++++++--------- 4 files changed, 57 insertions(+), 24 deletions(-) diff --git a/cmd/peakHandling.go b/cmd/peakHandling.go index 461f7ff..0195c3c 100644 --- a/cmd/peakHandling.go +++ b/cmd/peakHandling.go @@ -62,7 +62,7 @@ func (rp *RequestProcessor) prefetchPeakRequests(peakRequestMap *sync.Map) { return } log.Printf("PREFETCH: Prefetching %d requests\n", peakRequestLen) - sleepBetweenRequests := time.Duration(prefetchInterval*1000/peakRequestLen) * time.Millisecond + sleepBetweenRequests := time.Duration(rp.config.Uplink.PrefetchInterval*1000/peakRequestLen) * time.Millisecond peakRequestMap.Range(func(key interface{}, value interface{}) bool { go func(r http.Request) { rp.prefetchRequest(&r) diff --git a/cmd/processRequest.go b/cmd/processRequest.go index 8080115..a010978 100644 --- a/cmd/processRequest.go +++ b/cmd/processRequest.go @@ -12,6 +12,7 @@ import ( "sync" "time" + "github.com/chubin/wttr.in/internal/config" "github.com/chubin/wttr.in/internal/routing" lru "github.com/hashicorp/golang-lru" @@ -34,24 +35,25 @@ type RequestProcessor struct { stats *Stats router routing.Router upstreamTransport *http.Transport + config *config.Config } // NewRequestProcessor returns new RequestProcessor. -func NewRequestProcessor() (*RequestProcessor, error) { - lruCache, err := lru.New(lruCacheSize) +func NewRequestProcessor(config *config.Config) (*RequestProcessor, error) { + lruCache, err := lru.New(config.Cache.Size) if err != nil { return nil, err } dialer := &net.Dialer{ - Timeout: uplinkTimeout * time.Second, - KeepAlive: uplinkTimeout * time.Second, + Timeout: time.Duration(config.Uplink.Timeout) * time.Second, + KeepAlive: time.Duration(config.Uplink.Timeout) * time.Second, DualStack: true, } transport := &http.Transport{ DialContext: func(ctx context.Context, network, _ string) (net.Conn, error) { - return dialer.DialContext(ctx, network, uplinkSrvAddr) + return dialer.DialContext(ctx, network, config.Uplink.Address) }, } @@ -59,6 +61,7 @@ func NewRequestProcessor() (*RequestProcessor, error) { lruCache: lruCache, stats: NewStats(), upstreamTransport: transport, + config: config, } // Initialize routes. diff --git a/cmd/srv.go b/cmd/srv.go index 28bd2ce..6246eb4 100644 --- a/cmd/srv.go +++ b/cmd/srv.go @@ -11,10 +11,6 @@ import ( "github.com/chubin/wttr.in/internal/config" ) -const uplinkSrvAddr = "127.0.0.1:9002" -const uplinkTimeout = 30 -const prefetchInterval = 300 -const lruCacheSize = 12800 const logLineStart = "LOG_LINE_START " // plainTextAgents contains signatures of the plain-text agents @@ -105,7 +101,7 @@ func main() { err error ) - rp, err = NewRequestProcessor() + rp, err = NewRequestProcessor(config.Conf) if err != nil { log.Fatalln("log processor initialization:", err) } diff --git a/internal/config/config.go b/internal/config/config.go index 8ccd493..8722658 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -2,8 +2,10 @@ package config // Config of the program. type Config struct { + Cache Logging Server + Uplink } // Logging configuration. @@ -37,17 +39,49 @@ type Server struct { TLSKeyFile string } -// Conf contains the current configuration. -var Conf = Config{ - Logging{ - AccessLog: "/wttr.in/log/access.log", - ErrorsLog: "/wttr.in/log/errors.log", - Interval: 300, - }, - Server{ - PortHTTP: 8083, - PortHTTPS: 8084, - TLSCertFile: "/wttr.in/etc/fullchain.pem", - TLSKeyFile: "/wttr.in/etc/privkey.pem", - }, +// Uplink configuration. +type Uplink struct { + // Address contains address of the uplink server in form IP:PORT. + Address string + + // Timeout for upstream queries. + Timeout int + + // PrefetchInterval contains time (in milliseconds) indicating, + // how long the prefetch procedure should take. + PrefetchInterval int } + +// Cache configuration. +type Cache struct { + // Size of the main cache. + Size int +} + +// Default contains the default configuration. +func Default() *Config { + return &Config{ + Cache{ + Size: 12800, + }, + Logging{ + AccessLog: "/wttr.in/log/access.log", + ErrorsLog: "/wttr.in/log/errors.log", + Interval: 300, + }, + Server{ + PortHTTP: 8083, + PortHTTPS: 8084, + TLSCertFile: "/wttr.in/etc/fullchain.pem", + TLSKeyFile: "/wttr.in/etc/privkey.pem", + }, + Uplink{ + Address: "127.0.0.1:9002", + Timeout: 30, + PrefetchInterval: 300, + }, + } +} + +// Conf contains the current configuration +var Conf = Default() From aef41c375e0376f91bbda3fdb9e713ff64eaa365 Mon Sep 17 00:00:00 2001 From: Igor Chubin Date: Fri, 2 Dec 2022 20:10:48 +0100 Subject: [PATCH 027/105] Fix Makefile --- Makefile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Makefile b/Makefile index 66956ff..467424a 100644 --- a/Makefile +++ b/Makefile @@ -1,2 +1,2 @@ -srv: cmd/*.go internal/routing/*.go +srv: cmd/*.go internal/*/*.go go build -o srv ./cmd/ From 0d42c23d5b82655f84d0111ac7eb174577d3fb27 Mon Sep 17 00:00:00 2001 From: Igor Chubin Date: Sat, 3 Dec 2022 15:39:34 +0100 Subject: [PATCH 028/105] Move stats to a separate package --- cmd/processRequest.go | 5 +++-- cmd/stat.go => internal/stats/stats.go | 6 +++--- 2 files changed, 6 insertions(+), 5 deletions(-) rename cmd/stat.go => internal/stats/stats.go (96%) diff --git a/cmd/processRequest.go b/cmd/processRequest.go index a010978..0aa6085 100644 --- a/cmd/processRequest.go +++ b/cmd/processRequest.go @@ -14,6 +14,7 @@ import ( "github.com/chubin/wttr.in/internal/config" "github.com/chubin/wttr.in/internal/routing" + "github.com/chubin/wttr.in/internal/stats" lru "github.com/hashicorp/golang-lru" ) @@ -32,7 +33,7 @@ type RequestProcessor struct { peakRequest30 sync.Map peakRequest60 sync.Map lruCache *lru.Cache - stats *Stats + stats *stats.Stats router routing.Router upstreamTransport *http.Transport config *config.Config @@ -59,7 +60,7 @@ func NewRequestProcessor(config *config.Config) (*RequestProcessor, error) { rp := &RequestProcessor{ lruCache: lruCache, - stats: NewStats(), + stats: stats.New(), upstreamTransport: transport, config: config, } diff --git a/cmd/stat.go b/internal/stats/stats.go similarity index 96% rename from cmd/stat.go rename to internal/stats/stats.go index 73e4647..a030add 100644 --- a/cmd/stat.go +++ b/internal/stats/stats.go @@ -1,4 +1,4 @@ -package main +package stats import ( "bytes" @@ -17,8 +17,8 @@ type Stats struct { startTime time.Time } -// NewStats returns new Stats. -func NewStats() *Stats { +// New returns new Stats. +func New() *Stats { return &Stats{ v: map[string]int{}, startTime: time.Now(), From 12df32b07c5fdf97f7155edbe9df886037953a02 Mon Sep 17 00:00:00 2001 From: Igor Chubin Date: Sat, 3 Dec 2022 16:08:43 +0100 Subject: [PATCH 029/105] Split rest of srv into packages --- cmd/srv.go | 26 +++--------- cmd/log.go => internal/logging/logging.go | 6 ++- .../logging/suppress.go | 2 +- .../processor/peak.go | 2 +- .../processor/processor.go | 41 ++++++++++--------- internal/util/http.go | 25 +++++++++++ 6 files changed, 58 insertions(+), 44 deletions(-) rename cmd/log.go => internal/logging/logging.go (95%) rename cmd/logsuppress.go => internal/logging/suppress.go (98%) rename cmd/peakHandling.go => internal/processor/peak.go (98%) rename cmd/processRequest.go => internal/processor/processor.go (94%) create mode 100644 internal/util/http.go diff --git a/cmd/srv.go b/cmd/srv.go index 6246eb4..e9ae7e1 100644 --- a/cmd/srv.go +++ b/cmd/srv.go @@ -9,26 +9,12 @@ import ( "time" "github.com/chubin/wttr.in/internal/config" + "github.com/chubin/wttr.in/internal/logging" + "github.com/chubin/wttr.in/internal/processor" ) const logLineStart = "LOG_LINE_START " -// plainTextAgents contains signatures of the plain-text agents -var plainTextAgents = []string{ - "curl", - "httpie", - "lwp-request", - "wget", - "python-httpx", - "python-requests", - "openbsd ftp", - "powershell", - "fetch", - "aiohttp", - "http_get", - "xh", -} - func copyHeader(dst, src http.Header) { for k, vv := range src { for _, v := range vv { @@ -77,11 +63,11 @@ func main() { mux *http.ServeMux = http.NewServeMux() // logger is optimized requests logger. - logger *RequestLogger = NewRequestLogger( + logger *logging.RequestLogger = logging.NewRequestLogger( config.Conf.Logging.AccessLog, time.Duration(config.Conf.Logging.Interval)*time.Second) - rp *RequestProcessor + rp *processor.RequestProcessor // errs is the servers errors channel. errs chan error = make(chan error, 1) @@ -89,7 +75,7 @@ func main() { // numberOfServers started. If 0, exit. numberOfServers int - errorsLog *LogSuppressor = NewLogSuppressor( + errorsLog *logging.LogSuppressor = logging.NewLogSuppressor( config.Conf.Logging.ErrorsLog, []string{ "error reading preface from client", @@ -101,7 +87,7 @@ func main() { err error ) - rp, err = NewRequestProcessor(config.Conf) + rp, err = processor.NewRequestProcessor(config.Conf) if err != nil { log.Fatalln("log processor initialization:", err) } diff --git a/cmd/log.go b/internal/logging/logging.go similarity index 95% rename from cmd/log.go rename to internal/logging/logging.go index 3de1b1c..e58b6e1 100644 --- a/cmd/log.go +++ b/internal/logging/logging.go @@ -1,4 +1,4 @@ -package main +package logging import ( "fmt" @@ -6,6 +6,8 @@ import ( "os" "sync" "time" + + "github.com/chubin/wttr.in/internal/util" ) // Logging request. @@ -43,7 +45,7 @@ func NewRequestLogger(filename string, period time.Duration) *RequestLogger { func (rl *RequestLogger) Log(r *http.Request) error { le := logEntry{ Proto: "http", - IP: readUserIP(r), + IP: util.ReadUserIP(r), URI: r.RequestURI, UserAgent: r.Header.Get("User-Agent"), } diff --git a/cmd/logsuppress.go b/internal/logging/suppress.go similarity index 98% rename from cmd/logsuppress.go rename to internal/logging/suppress.go index d8ca9e6..76cecce 100644 --- a/cmd/logsuppress.go +++ b/internal/logging/suppress.go @@ -1,4 +1,4 @@ -package main +package logging import ( "os" diff --git a/cmd/peakHandling.go b/internal/processor/peak.go similarity index 98% rename from cmd/peakHandling.go rename to internal/processor/peak.go index 0195c3c..55f8ae6 100644 --- a/cmd/peakHandling.go +++ b/internal/processor/peak.go @@ -1,4 +1,4 @@ -package main +package processor import ( "log" diff --git a/cmd/processRequest.go b/internal/processor/processor.go similarity index 94% rename from cmd/processRequest.go rename to internal/processor/processor.go index 0aa6085..da829fa 100644 --- a/cmd/processRequest.go +++ b/internal/processor/processor.go @@ -1,4 +1,4 @@ -package main +package processor import ( "context" @@ -12,13 +12,30 @@ import ( "sync" "time" + lru "github.com/hashicorp/golang-lru" + "github.com/chubin/wttr.in/internal/config" "github.com/chubin/wttr.in/internal/routing" "github.com/chubin/wttr.in/internal/stats" - - lru "github.com/hashicorp/golang-lru" + "github.com/chubin/wttr.in/internal/util" ) +// plainTextAgents contains signatures of the plain-text agents. +var plainTextAgents = []string{ + "curl", + "httpie", + "lwp-request", + "wget", + "python-httpx", + "python-requests", + "openbsd ftp", + "powershell", + "fetch", + "aiohttp", + "http_get", + "xh", +} + type responseWithHeader struct { InProgress bool // true if the request is being processed Expires time.Time // expiration time of the cache entry @@ -213,7 +230,7 @@ func getCacheDigest(req *http.Request) string { queryHost := req.Host queryString := req.RequestURI - clientIPAddress := readUserIP(req) + clientIPAddress := util.ReadUserIP(req) lang := req.Header.Get("Accept-Language") @@ -279,22 +296,6 @@ func isPlainTextAgent(userAgent string) bool { return false } -func readUserIP(r *http.Request) string { - IPAddress := r.Header.Get("X-Real-Ip") - if IPAddress == "" { - IPAddress = r.Header.Get("X-Forwarded-For") - } - if IPAddress == "" { - IPAddress = r.RemoteAddr - var err error - IPAddress, _, err = net.SplitHostPort(IPAddress) - if err != nil { - log.Printf("ERROR: userip: %q is not IP:port\n", IPAddress) - } - } - return IPAddress -} - func randInt(min int, max int) int { return min + rand.Intn(max-min) } diff --git a/internal/util/http.go b/internal/util/http.go new file mode 100644 index 0000000..ab3f40b --- /dev/null +++ b/internal/util/http.go @@ -0,0 +1,25 @@ +package util + +import ( + "log" + "net" + "net/http" +) + +// ReadUserIP returns IP address of the client from http.Request, +// taking into account the HTTP headers. +func ReadUserIP(r *http.Request) string { + IPAddress := r.Header.Get("X-Real-Ip") + if IPAddress == "" { + IPAddress = r.Header.Get("X-Forwarded-For") + } + if IPAddress == "" { + IPAddress = r.RemoteAddr + var err error + IPAddress, _, err = net.SplitHostPort(IPAddress) + if err != nil { + log.Printf("ERROR: userip: %q is not IP:port\n", IPAddress) + } + } + return IPAddress +} From fb8f0d248b073ab3650e55cdbb6abdad955c97ca Mon Sep 17 00:00:00 2001 From: Igor Chubin Date: Sat, 3 Dec 2022 16:10:10 +0100 Subject: [PATCH 030/105] Move cmd/srv.go to ./ --- Makefile | 4 ++-- cmd/srv.go => srv.go | 0 2 files changed, 2 insertions(+), 2 deletions(-) rename cmd/srv.go => srv.go (100%) diff --git a/Makefile b/Makefile index 467424a..506fb6a 100644 --- a/Makefile +++ b/Makefile @@ -1,2 +1,2 @@ -srv: cmd/*.go internal/*/*.go - go build -o srv ./cmd/ +srv: srv.go internal/*/*.go + go build -o srv ./ diff --git a/cmd/srv.go b/srv.go similarity index 100% rename from cmd/srv.go rename to srv.go From 2328b29bfe79e16c7faf8b519ad66bb5b5f9e360 Mon Sep 17 00:00:00 2001 From: Igor Chubin Date: Sat, 3 Dec 2022 17:53:28 +0100 Subject: [PATCH 031/105] Add CLI parsing --- go.mod | 2 ++ go.sum | 5 +++++ srv.go | 9 +++++++++ 3 files changed, 16 insertions(+) diff --git a/go.mod b/go.mod index 001da54..1a6c362 100644 --- a/go.mod +++ b/go.mod @@ -3,6 +3,8 @@ module github.com/chubin/wttr.in go 1.16 require ( + github.com/alecthomas/kong v0.7.1 // indirect github.com/hashicorp/golang-lru v0.6.0 github.com/robfig/cron v1.2.0 + gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/go.sum b/go.sum index d30f05d..9fb8621 100644 --- a/go.sum +++ b/go.sum @@ -1,4 +1,9 @@ +github.com/alecthomas/assert/v2 v2.1.0/go.mod h1:b/+1DI2Q6NckYi+3mXyH3wFb8qG37K/DuK80n7WefXA= +github.com/alecthomas/kong v0.7.1 h1:azoTh0IOfwlAX3qN9sHWTxACE2oV8Bg2gAwBsMwDQY4= +github.com/alecthomas/kong v0.7.1/go.mod h1:n1iCIO2xS46oE8ZfYCNDqdR0b0wZNrXAIAqro/2132U= +github.com/alecthomas/repr v0.1.0/go.mod h1:2kn6fqh/zIyPLmm3ugklbEi5hg5wS435eygvNfaDQL8= github.com/hashicorp/golang-lru v0.6.0 h1:uL2shRDx7RTrOrTCUZEGP/wJUFiUI8QT6E7z5o8jga4= github.com/hashicorp/golang-lru v0.6.0/go.mod h1:iADmTwqILo4mZ8BN3D2Q6+9jd8WM5uGBxy+E8yxSoD4= +github.com/hexops/gotextdiff v1.0.3/go.mod h1:pSWU5MAI3yDq+fZBTazCSJysOMbxWL1BSow5/V2vxeg= github.com/robfig/cron v1.2.0 h1:ZjScXvvxeQ63Dbyxy76Fj3AT3Ut0aKsyd2/tl3DTMuQ= github.com/robfig/cron v1.2.0/go.mod h1:JGuDeoQd7Z6yL4zQhZ3OPEVHB7fL6Ka6skscFHfmt2k= diff --git a/srv.go b/srv.go index e9ae7e1..50409f1 100644 --- a/srv.go +++ b/srv.go @@ -2,17 +2,26 @@ package main import ( "crypto/tls" + "errors" "fmt" "io" "log" "net/http" "time" + "github.com/alecthomas/kong" + "github.com/chubin/wttr.in/internal/config" "github.com/chubin/wttr.in/internal/logging" "github.com/chubin/wttr.in/internal/processor" ) +var cli struct { + ConfigCheck bool `name:"config-check" help:"Check configuration"` + ConfigDump bool `name:"config-dump" help:"Dump configuration"` + ConfigFile string `name:"config-file" arg:"" optional:"" help:"Name of configuration file"` +} + const logLineStart = "LOG_LINE_START " func copyHeader(dst, src http.Header) { From 5d86d36b7ece9ca5a01fbd2dde24a3ea3512a64e Mon Sep 17 00:00:00 2001 From: Igor Chubin Date: Sat, 3 Dec 2022 17:53:50 +0100 Subject: [PATCH 032/105] Add config file parsing/dumping --- go.sum | 3 ++ internal/config/config.go | 62 +++++++++++++++++++++++++------- srv.go | 75 ++++++++++++++++++++++++++++----------- 3 files changed, 106 insertions(+), 34 deletions(-) diff --git a/go.sum b/go.sum index 9fb8621..f584782 100644 --- a/go.sum +++ b/go.sum @@ -7,3 +7,6 @@ github.com/hashicorp/golang-lru v0.6.0/go.mod h1:iADmTwqILo4mZ8BN3D2Q6+9jd8WM5uG github.com/hexops/gotextdiff v1.0.3/go.mod h1:pSWU5MAI3yDq+fZBTazCSJysOMbxWL1BSow5/V2vxeg= github.com/robfig/cron v1.2.0 h1:ZjScXvvxeQ63Dbyxy76Fj3AT3Ut0aKsyd2/tl3DTMuQ= github.com/robfig/cron v1.2.0/go.mod h1:JGuDeoQd7Z6yL4zQhZ3OPEVHB7fL6Ka6skscFHfmt2k= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/internal/config/config.go b/internal/config/config.go index 8722658..c6366ea 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -1,5 +1,14 @@ package config +import ( + "log" + "os" + + "gopkg.in/yaml.v3" + + "github.com/chubin/wttr.in/internal/util" +) + // Config of the program. type Config struct { Cache @@ -12,13 +21,13 @@ type Config struct { type Logging struct { // AccessLog path. - AccessLog string + AccessLog string `yaml:"accessLog,omitempty"` // ErrorsLog path. - ErrorsLog string + ErrorsLog string `yaml:"errorsLog,omitempty"` // Interval between access log flushes, in seconds. - Interval int + Interval int `yaml:"interval,omitempty"` } // Server configuration. @@ -26,36 +35,36 @@ type Server struct { // PortHTTP is port where HTTP server must listen. // If 0, HTTP is disabled. - PortHTTP int + PortHTTP int `yaml:"portHTTP,omitempty"` // PortHTTP is port where the HTTPS server must listen. // If 0, HTTPS is disabled. - PortHTTPS int + PortHTTPS int `yaml:"portHTTPS,omitempty"` // TLSCertFile contains path to cert file for TLS Server. - TLSCertFile string + TLSCertFile string `yaml:"tlsCertFile,omitempty"` // TLSCertFile contains path to key file for TLS Server. - TLSKeyFile string + TLSKeyFile string `yaml:"tlsKeyFile,omitempty"` } // Uplink configuration. type Uplink struct { // Address contains address of the uplink server in form IP:PORT. - Address string + Address string `yaml:"address,omitempty"` // Timeout for upstream queries. - Timeout int + Timeout int `yaml:"timeout,omitempty"` // PrefetchInterval contains time (in milliseconds) indicating, // how long the prefetch procedure should take. - PrefetchInterval int + PrefetchInterval int `yaml:"prefetchInterval,omitempty"` } // Cache configuration. type Cache struct { // Size of the main cache. - Size int + Size int `yaml:"size,omitempty"` } // Default contains the default configuration. @@ -83,5 +92,32 @@ func Default() *Config { } } -// Conf contains the current configuration -var Conf = Default() +// Load config from file. +func Load(filename string) (*Config, error) { + var ( + config Config + data []byte + err error + ) + + data, err = os.ReadFile(filename) + if err != nil { + return nil, err + } + + err = util.YamlUnmarshalStrict(data, &config) + if err != nil { + return nil, err + } + + return &config, nil +} + +func (c *Config) Dump() []byte { + data, err := yaml.Marshal(c) + if err != nil { + // should never happen. + log.Fatalln("config.Dump():", err) + } + return data +} diff --git a/srv.go b/srv.go index 50409f1..c39b80e 100644 --- a/srv.go +++ b/srv.go @@ -44,7 +44,7 @@ func serveHTTP(mux *http.ServeMux, port int, logFile io.Writer, errs chan<- erro errs <- srv.ListenAndServe() } -func serveHTTPS(mux *http.ServeMux, port int, logFile io.Writer, errs chan<- error) { +func serveHTTPS(mux *http.ServeMux, port int, certFile, keyFile string, logFile io.Writer, errs chan<- error) { tlsConfig := &tls.Config{ // CipherSuites: []uint16{ @@ -63,18 +63,16 @@ func serveHTTPS(mux *http.ServeMux, port int, logFile io.Writer, errs chan<- err TLSConfig: tlsConfig, Handler: mux, } - errs <- srv.ListenAndServeTLS(config.Conf.Server.TLSCertFile, config.Conf.Server.TLSKeyFile) + errs <- srv.ListenAndServeTLS(certFile, keyFile) } -func main() { +func serve(conf *config.Config) error { var ( // mux is main HTTP/HTTP requests multiplexer. mux *http.ServeMux = http.NewServeMux() // logger is optimized requests logger. - logger *logging.RequestLogger = logging.NewRequestLogger( - config.Conf.Logging.AccessLog, - time.Duration(config.Conf.Logging.Interval)*time.Second) + logger *logging.RequestLogger rp *processor.RequestProcessor @@ -84,19 +82,26 @@ func main() { // numberOfServers started. If 0, exit. numberOfServers int - errorsLog *logging.LogSuppressor = logging.NewLogSuppressor( - config.Conf.Logging.ErrorsLog, - []string{ - "error reading preface from client", - "TLS handshake error from", - }, - logLineStart, - ) + errorsLog *logging.LogSuppressor err error ) - rp, err = processor.NewRequestProcessor(config.Conf) + // logger is optimized requests logger. + logger = logging.NewRequestLogger( + conf.Logging.AccessLog, + time.Duration(conf.Logging.Interval)*time.Second) + + errorsLog = logging.NewLogSuppressor( + conf.Logging.ErrorsLog, + []string{ + "error reading preface from client", + "TLS handshake error from", + }, + logLineStart, + ) + + rp, err = processor.NewRequestProcessor(conf) if err != nil { log.Fatalln("log processor initialization:", err) } @@ -129,17 +134,45 @@ func main() { w.Write(response.Body) }) - if config.Conf.Server.PortHTTP != 0 { - go serveHTTP(mux, config.Conf.Server.PortHTTP, errorsLog, errs) + if conf.Server.PortHTTP != 0 { + go serveHTTP(mux, conf.Server.PortHTTP, errorsLog, errs) numberOfServers++ } - if config.Conf.Server.PortHTTPS != 0 { - go serveHTTPS(mux, config.Conf.Server.PortHTTPS, errorsLog, errs) + if conf.Server.PortHTTPS != 0 { + go serveHTTPS(mux, conf.Server.PortHTTPS, conf.Server.TLSCertFile, conf.Server.TLSKeyFile, errorsLog, errs) numberOfServers++ } if numberOfServers == 0 { - log.Println("no servers configured; exiting") + return errors.New("no servers configured") + } + return <-errs // block until one of the servers writes an error +} + +func main() { + var ( + conf *config.Config + err error + ) + + ctx := kong.Parse(&cli) + + if cli.ConfigFile != "" { + conf, err = config.Load(cli.ConfigFile) + if err != nil { + log.Fatalf("reading config from %s: %s\n", cli.ConfigFile, err) + } + } else { + conf = config.Default() + } + + if cli.ConfigDump { + fmt.Print(string(conf.Dump())) + } + + if cli.ConfigCheck || cli.ConfigDump { return } - log.Fatal(<-errs) // block until one of the servers writes an error + + err = serve(conf) + ctx.FatalIfErrorf(err) } From a6f2844c6722c201b849fe00dbd3efe04096c09e Mon Sep 17 00:00:00 2001 From: Igor Chubin Date: Sun, 4 Dec 2022 10:35:58 +0100 Subject: [PATCH 033/105] Suppress log if needed --- Makefile | 2 ++ internal/logging/logging.go | 35 ++++++++++++++++++++--------------- internal/logging/suppress.go | 12 ++++++++++++ 3 files changed, 34 insertions(+), 15 deletions(-) diff --git a/Makefile b/Makefile index 506fb6a..9674c9f 100644 --- a/Makefile +++ b/Makefile @@ -1,2 +1,4 @@ srv: srv.go internal/*/*.go go build -o srv ./ +test: + go test ./ diff --git a/internal/logging/logging.go b/internal/logging/logging.go index e58b6e1..d1c8e75 100644 --- a/internal/logging/logging.go +++ b/internal/logging/logging.go @@ -32,6 +32,9 @@ type logEntry struct { // NewRequestLogger returns a new RequestLogger for the specified log file. // Flush logging entries after period of time. +// +// If filename is empty, no log will be written, and all logging entries +// will be silently dropped. func NewRequestLogger(filename string, period time.Duration) *RequestLogger { return &RequestLogger{ buf: map[logEntry]int{}, @@ -74,23 +77,25 @@ func (rl *RequestLogger) flush() error { return nil } - // Generate log output. - output := "" - for k, hitsNumber := range rl.buf { - output += fmt.Sprintf("%s %3d %s\n", time.Now().Format(time.RFC3339), hitsNumber, k.String()) - } + if rl.filename != "" { + // Generate log output. + output := "" + for k, hitsNumber := range rl.buf { + output += fmt.Sprintf("%s %3d %s\n", time.Now().Format(time.RFC3339), hitsNumber, k.String()) + } - // Open log file. - f, err := os.OpenFile(rl.filename, os.O_APPEND|os.O_WRONLY|os.O_CREATE, 0600) - if err != nil { - return err - } - defer f.Close() + // Open log file. + f, err := os.OpenFile(rl.filename, os.O_APPEND|os.O_WRONLY|os.O_CREATE, 0600) + if err != nil { + return err + } + defer f.Close() - // Save output to log file. - _, err = f.Write([]byte(output)) - if err != nil { - return err + // Save output to log file. + _, err = f.Write([]byte(output)) + if err != nil { + return err + } } // Flush buffer. diff --git a/internal/logging/suppress.go b/internal/logging/suppress.go index 76cecce..ab217f2 100644 --- a/internal/logging/suppress.go +++ b/internal/logging/suppress.go @@ -19,6 +19,8 @@ type LogSuppressor struct { // NewLogSuppressor creates a new LogSuppressor for specified // filename and lines to be suppressed. +// +// If filename is empty, log entries will be printed to stderr. func NewLogSuppressor(filename string, suppress []string, linePrefix string) *LogSuppressor { return &LogSuppressor{ filename: filename, @@ -29,6 +31,9 @@ func NewLogSuppressor(filename string, suppress []string, linePrefix string) *Lo // Open opens log file. func (ls *LogSuppressor) Open() error { + if ls.filename == "" { + return nil + } var err error ls.logFile, err = os.OpenFile(ls.filename, os.O_APPEND|os.O_WRONLY|os.O_CREATE, 0600) return err @@ -36,6 +41,9 @@ func (ls *LogSuppressor) Open() error { // Close closes log file. func (ls *LogSuppressor) Close() error { + if ls.filename == "" { + return nil + } return ls.logFile.Close() } @@ -46,6 +54,10 @@ func (ls *LogSuppressor) Write(p []byte) (n int, err error) { output string ) + if ls.filename == "" { + return os.Stdin.Write(p) + } + ls.m.Lock() defer ls.m.Unlock() From 074b8b6ec87a6cb5f4c976eaac5aea2fb3e91bd0 Mon Sep 17 00:00:00 2001 From: Igor Chubin Date: Sun, 4 Dec 2022 16:48:04 +0100 Subject: [PATCH 034/105] Add GeoIP info cache access --- internal/geo/ip/ip.go | 99 ++++++++++++++++++++++++++++++++++++++ internal/geo/ip/ip_test.go | 71 +++++++++++++++++++++++++++ 2 files changed, 170 insertions(+) create mode 100644 internal/geo/ip/ip.go create mode 100644 internal/geo/ip/ip_test.go diff --git a/internal/geo/ip/ip.go b/internal/geo/ip/ip.go new file mode 100644 index 0000000..8adc2da --- /dev/null +++ b/internal/geo/ip/ip.go @@ -0,0 +1,99 @@ +package ip + +import ( + "errors" + "os" + "path" + "strconv" + "strings" + + "github.com/chubin/wttr.in/internal/config" +) + +var ( + ErrNotFound = errors.New("cache entry not found") + ErrInvalidCacheEntry = errors.New("invalid cache entry format") +) + +// Location information. +type Location struct { + CountryCode string + Country string + Region string + City string + Latitude float64 + Longitude float64 +} + +// Cache provides access to the IP Geodata cache. +type Cache struct { + config *config.Config +} + +// NewCache returns new cache reader for the specified config. +func NewCache(config *config.Config) *Cache { + return &Cache{ + config: config, + } +} + +// Read returns location information from the cache, if found, +// or ErrNotFound if not found. If the entry is found, but its format +// is invalid, ErrInvalidCacheEntry is returned. +// +// Format: +// +// [CountryCode];Country;Region;City;[Latitude];[Longitude] +// +// Example: +// +// DE;Germany;Free and Hanseatic City of Hamburg;Hamburg;53.5736;9.9782 +// +func (c *Cache) Read(addr string) (*Location, error) { + bytes, err := os.ReadFile(c.cacheFile(addr)) + if err != nil { + return nil, ErrNotFound + } + return parseCacheEntry(string(bytes)) +} + +// cacheFile retuns path to the cache entry for addr. +func (c *Cache) cacheFile(addr string) string { + return path.Join(c.config.Geo.IPCache, addr) +} + +// parseCacheEntry parses the location cache entry s, +// and return location, or error, if the cache entry is invalid. +func parseCacheEntry(s string) (*Location, error) { + var ( + lat float64 = -1000 + long float64 = -1000 + err error + ) + + parts := strings.Split(s, ";") + if len(parts) < 4 { + return nil, ErrInvalidCacheEntry + } + + if len(parts) >= 6 { + lat, err = strconv.ParseFloat(parts[4], 64) + if err != nil { + return nil, ErrInvalidCacheEntry + } + + long, err = strconv.ParseFloat(parts[5], 64) + if err != nil { + return nil, ErrInvalidCacheEntry + } + } + + return &Location{ + CountryCode: parts[0], + Country: parts[1], + Region: parts[2], + City: parts[3], + Latitude: lat, + Longitude: long, + }, nil +} diff --git a/internal/geo/ip/ip_test.go b/internal/geo/ip/ip_test.go new file mode 100644 index 0000000..8921628 --- /dev/null +++ b/internal/geo/ip/ip_test.go @@ -0,0 +1,71 @@ +package ip + +import ( + "testing" + + "github.com/stretchr/testify/require" +) + +func TestParseCacheEntry(t *testing.T) { + tests := []struct { + input string + expected Location + err error + }{ + { + "DE;Germany;Free and Hanseatic City of Hamburg;Hamburg;53.5736;9.9782", + Location{ + CountryCode: "DE", + Country: "Germany", + Region: "Free and Hanseatic City of Hamburg", + City: "Hamburg", + Latitude: 53.5736, + Longitude: 9.9782, + }, + nil, + }, + + { + "ES;Spain;Madrid, Comunidad de;Madrid;40.4165;-3.70256;28223;Orange Espagne SA;orange.es", + Location{ + CountryCode: "ES", + Country: "Spain", + Region: "Madrid, Comunidad de", + City: "Madrid", + Latitude: 40.4165, + Longitude: -3.70256, + }, + nil, + }, + + { + "US;United States of America;California;Mountain View", + Location{ + CountryCode: "US", + Country: "United States of America", + Region: "California", + City: "Mountain View", + Latitude: -1000, + Longitude: -1000, + }, + nil, + }, + + // Invalid entries + { + "DE;Germany;Free and Hanseatic City of Hamburg;Hamburg;53.5736;XXX", + Location{}, + ErrInvalidCacheEntry, + }, + } + + for _, tt := range tests { + result, err := parseCacheEntry(tt.input) + if tt.err == nil { + require.NoError(t, err) + require.Equal(t, *result, tt.expected) + } else { + require.ErrorIs(t, err, tt.err) + } + } +} From 3765dcbfbf4c4eb7df086fb427915f84318bc5c4 Mon Sep 17 00:00:00 2001 From: Igor Chubin Date: Sun, 4 Dec 2022 16:55:14 +0100 Subject: [PATCH 035/105] Count number of queries with known IPs --- internal/config/config.go | 10 ++++++++++ internal/processor/processor.go | 10 ++++++++++ internal/stats/stats.go | 1 + 3 files changed, 21 insertions(+) diff --git a/internal/config/config.go b/internal/config/config.go index c6366ea..de4024d 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -12,6 +12,7 @@ import ( // Config of the program. type Config struct { Cache + Geo Logging Server Uplink @@ -67,12 +68,21 @@ type Cache struct { Size int `yaml:"size,omitempty"` } +// Geo contains geolocation configuration. +type Geo struct { + // IPCache contains the path to the IP Geodata cache. + IPCache string `yaml:"ipCache,omitempty"` +} + // Default contains the default configuration. func Default() *Config { return &Config{ Cache{ Size: 12800, }, + Geo{ + IPCache: "/wttr.in/cache/ip2l", + }, Logging{ AccessLog: "/wttr.in/log/access.log", ErrorsLog: "/wttr.in/log/errors.log", diff --git a/internal/processor/processor.go b/internal/processor/processor.go index da829fa..6448d9a 100644 --- a/internal/processor/processor.go +++ b/internal/processor/processor.go @@ -15,6 +15,7 @@ import ( lru "github.com/hashicorp/golang-lru" "github.com/chubin/wttr.in/internal/config" + geoip "github.com/chubin/wttr.in/internal/geo/ip" "github.com/chubin/wttr.in/internal/routing" "github.com/chubin/wttr.in/internal/stats" "github.com/chubin/wttr.in/internal/util" @@ -54,6 +55,7 @@ type RequestProcessor struct { router routing.Router upstreamTransport *http.Transport config *config.Config + geoIPCache *geoip.Cache } // NewRequestProcessor returns new RequestProcessor. @@ -80,6 +82,7 @@ func NewRequestProcessor(config *config.Config) (*RequestProcessor, error) { stats: stats.New(), upstreamTransport: transport, config: config, + geoIPCache: geoip.NewCache(config), } // Initialize routes. @@ -161,6 +164,13 @@ func (rp *RequestProcessor) ProcessRequest(r *http.Request) (*responseWithHeader } } + // How many IP addresses are known. + ip := util.ReadUserIP(r) + _, err = rp.geoIPCache.Read(ip) + if err == nil { + rp.stats.Inc("geoip") + } + rp.lruCache.Add(cacheDigest, responseWithHeader{InProgress: true}) response, err = get(r, rp.upstreamTransport) diff --git a/internal/stats/stats.go b/internal/stats/stats.go index a030add..220c0ec 100644 --- a/internal/stats/stats.go +++ b/internal/stats/stats.go @@ -75,6 +75,7 @@ func (c *Stats) Show() []byte { fmt.Fprintf(&b, "%-20s: %d\n", "Upstream queries", c.v["total"]-c.v["cache1"]) fmt.Fprintf(&b, "%-20s: %d\n", "Queries with format", c.v["format"]) fmt.Fprintf(&b, "%-20s: %d\n", "Queries with format=j1", c.v["format=j1"]) + fmt.Fprintf(&b, "%-20s: %d\n", "Queries with known IP", c.v["geoip"]) return b.Bytes() } From f9e5e0ecf627e4e3b5872f547ad8ad91b080e4c5 Mon Sep 17 00:00:00 2001 From: Igor Chubin Date: Sun, 4 Dec 2022 16:55:58 +0100 Subject: [PATCH 036/105] Add github.com/stretchr/testify to go.mod --- go.mod | 1 + go.sum | 14 ++++++++++++++ 2 files changed, 15 insertions(+) diff --git a/go.mod b/go.mod index 1a6c362..204b25b 100644 --- a/go.mod +++ b/go.mod @@ -6,5 +6,6 @@ require ( github.com/alecthomas/kong v0.7.1 // indirect github.com/hashicorp/golang-lru v0.6.0 github.com/robfig/cron v1.2.0 + github.com/stretchr/testify v1.8.1 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/go.sum b/go.sum index f584782..efa2f76 100644 --- a/go.sum +++ b/go.sum @@ -2,11 +2,25 @@ github.com/alecthomas/assert/v2 v2.1.0/go.mod h1:b/+1DI2Q6NckYi+3mXyH3wFb8qG37K/ github.com/alecthomas/kong v0.7.1 h1:azoTh0IOfwlAX3qN9sHWTxACE2oV8Bg2gAwBsMwDQY4= github.com/alecthomas/kong v0.7.1/go.mod h1:n1iCIO2xS46oE8ZfYCNDqdR0b0wZNrXAIAqro/2132U= github.com/alecthomas/repr v0.1.0/go.mod h1:2kn6fqh/zIyPLmm3ugklbEi5hg5wS435eygvNfaDQL8= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/hashicorp/golang-lru v0.6.0 h1:uL2shRDx7RTrOrTCUZEGP/wJUFiUI8QT6E7z5o8jga4= github.com/hashicorp/golang-lru v0.6.0/go.mod h1:iADmTwqILo4mZ8BN3D2Q6+9jd8WM5uGBxy+E8yxSoD4= github.com/hexops/gotextdiff v1.0.3/go.mod h1:pSWU5MAI3yDq+fZBTazCSJysOMbxWL1BSow5/V2vxeg= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/robfig/cron v1.2.0 h1:ZjScXvvxeQ63Dbyxy76Fj3AT3Ut0aKsyd2/tl3DTMuQ= github.com/robfig/cron v1.2.0/go.mod h1:JGuDeoQd7Z6yL4zQhZ3OPEVHB7fL6Ka6skscFHfmt2k= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= +github.com/stretchr/objx v0.5.0 h1:1zr/of2m5FGMsad5YfcqgdqdWrIhu+EBEJRhR1U7z/c= +github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= +github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= +github.com/stretchr/testify v1.8.1 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKsk= +github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= From 98358744229904986753f68b26e923d6f6291569 Mon Sep 17 00:00:00 2001 From: Igor Chubin Date: Sun, 4 Dec 2022 16:56:11 +0100 Subject: [PATCH 037/105] Update Makefile --- Makefile | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/Makefile b/Makefile index 9674c9f..94079a8 100644 --- a/Makefile +++ b/Makefile @@ -1,4 +1,5 @@ -srv: srv.go internal/*/*.go +srv: srv.go internal/*/*.go internal/*/*/*.go go build -o srv ./ -test: - go test ./ + +go-test: + go test ./... From 6c0107bf7a37088fd2b1fd2d15f8563f72910159 Mon Sep 17 00:00:00 2001 From: Igor Chubin Date: Sun, 4 Dec 2022 17:37:22 +0100 Subject: [PATCH 038/105] Add temporary support for /:geo-ip-{get,put} With these queries, the IP resolution cache can be centralized under the front end server, and its internal format can be changed. --- internal/geo/ip/ip.go | 66 +++++++++++++++++++++++++++++++++ internal/processor/processor.go | 2 + 2 files changed, 68 insertions(+) diff --git a/internal/geo/ip/ip.go b/internal/geo/ip/ip.go index 8adc2da..d74381f 100644 --- a/internal/geo/ip/ip.go +++ b/internal/geo/ip/ip.go @@ -2,12 +2,17 @@ package ip import ( "errors" + "log" + "net/http" "os" "path" + "regexp" "strconv" "strings" "github.com/chubin/wttr.in/internal/config" + "github.com/chubin/wttr.in/internal/routing" + "github.com/chubin/wttr.in/internal/util" ) var ( @@ -97,3 +102,64 @@ func parseCacheEntry(s string) (*Location, error) { Longitude: long, }, nil } + +// Reponse provides routing interface to the geo cache. +// +// Temporary workaround to switch IP addresses handling to the Go server. +// Handles two queries: +// +// /:geo-ip-put?ip=IP&value=VALUE +// /:geo-ip-get?ip=IP +// +func (c *Cache) Response(r *http.Request) *routing.Cadre { + var ( + respERR = &routing.Cadre{Body: []byte("ERR")} + respOK = &routing.Cadre{Body: []byte("OK")} + ) + + if ip := util.ReadUserIP(r); ip != "127.0.0.1" { + log.Printf("geoIP access from %s rejected\n", ip) + return nil + } + + if r.URL.Path == "/:geo-ip-put" { + ip := r.URL.Query().Get("ip") + value := r.URL.Query().Get("value") + if !validIP4(ip) || value == "" { + log.Printf("invalid geoIP put query: ip='%s' value='%s'\n", ip, value) + return respERR + } + + err := c.putRaw(ip, value) + if err != nil { + return respERR + } + return respOK + } + if r.URL.Path == "/:geo-ip-get" { + ip := r.URL.Query().Get("ip") + if !validIP4(ip) { + return respERR + } + + result, err := c.getRaw(ip) + if err != nil { + return respERR + } + return &routing.Cadre{Body: result} + } + return nil +} + +func (c *Cache) getRaw(addr string) ([]byte, error) { + return os.ReadFile(c.cacheFile(addr)) +} + +func (c *Cache) putRaw(addr, value string) error { + return os.WriteFile(c.cacheFile(addr), []byte(value), 0644) +} + +func validIP4(ipAddress string) bool { + re, _ := regexp.Compile(`^(([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])\.){3}([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])$`) + return re.MatchString(strings.Trim(ipAddress, " ")) +} diff --git a/internal/processor/processor.go b/internal/processor/processor.go index 6448d9a..8a4247c 100644 --- a/internal/processor/processor.go +++ b/internal/processor/processor.go @@ -87,6 +87,8 @@ func NewRequestProcessor(config *config.Config) (*RequestProcessor, error) { // Initialize routes. rp.router.AddPath("/:stats", rp.stats) + rp.router.AddPath("/:geo-ip-get", rp.geoIPCache) + rp.router.AddPath("/:geo-ip-put", rp.geoIPCache) return rp, nil } From bcb3667aef8dea619dde616bb1dca3ec3328a754 Mon Sep 17 00:00:00 2001 From: Igor Chubin Date: Sun, 4 Dec 2022 21:15:43 +0100 Subject: [PATCH 039/105] Add samonzeweb/godb and mattn/go-sqlite3 to go.mod --- go.mod | 9 +++++++++ go.sum | 27 +++++++++++++++++++++++++++ 2 files changed, 36 insertions(+) diff --git a/go.mod b/go.mod index 204b25b..c435a4d 100644 --- a/go.mod +++ b/go.mod @@ -4,8 +4,17 @@ go 1.16 require ( github.com/alecthomas/kong v0.7.1 // indirect + github.com/denisenkom/go-mssqldb v0.0.0-20200910202707-1e08a3fab204 // indirect + github.com/go-sql-driver/mysql v1.5.0 // indirect + github.com/gopherjs/gopherjs v0.0.0-20200217142428-fce0ec30dd00 // indirect github.com/hashicorp/golang-lru v0.6.0 + github.com/lib/pq v1.8.0 // indirect + github.com/mattn/go-sqlite3 v1.14.16 // indirect github.com/robfig/cron v1.2.0 + github.com/samonzeweb/godb v1.0.8 // indirect + github.com/smartystreets/assertions v1.2.0 // indirect + github.com/smartystreets/goconvey v1.6.4 // indirect github.com/stretchr/testify v1.8.1 // indirect + golang.org/x/crypto v0.0.0-20201002170205-7f63de1d35b0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/go.sum b/go.sum index efa2f76..1fa36b2 100644 --- a/go.sum +++ b/go.sum @@ -5,13 +5,31 @@ github.com/alecthomas/repr v0.1.0/go.mod h1:2kn6fqh/zIyPLmm3ugklbEi5hg5wS435eygv github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/denisenkom/go-mssqldb v0.0.0-20200910202707-1e08a3fab204/go.mod h1:xbL0rPBG9cCiLr28tMa8zpbdarY27NDyej4t/EjAShU= +github.com/go-sql-driver/mysql v1.5.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg= +github.com/golang-sql/civil v0.0.0-20190719163853-cb61b32ac6fe/go.mod h1:8vg3r2VgvsThLBIFL93Qb5yWzgyZWhEmBwUJWevAkK0= +github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY= +github.com/gopherjs/gopherjs v0.0.0-20200217142428-fce0ec30dd00/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY= github.com/hashicorp/golang-lru v0.6.0 h1:uL2shRDx7RTrOrTCUZEGP/wJUFiUI8QT6E7z5o8jga4= github.com/hashicorp/golang-lru v0.6.0/go.mod h1:iADmTwqILo4mZ8BN3D2Q6+9jd8WM5uGBxy+E8yxSoD4= github.com/hexops/gotextdiff v1.0.3/go.mod h1:pSWU5MAI3yDq+fZBTazCSJysOMbxWL1BSow5/V2vxeg= +github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU= +github.com/lib/pq v1.8.0/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= +github.com/mattn/go-sqlite3 v1.14.16 h1:yOQRA0RpS5PFz/oikGwBEqvAWhWg5ufRz4ETLjwpU1Y= +github.com/mattn/go-sqlite3 v1.14.16/go.mod h1:2eHXhiwb8IkHr+BDWZGa96P6+rkvnG63S2DGjv9HUNg= +github.com/mattn/go-sqlite3 v2.0.3+incompatible h1:gXHsfypPkaMZrKbD5209QV9jbUTJKjyR5WD3HYQSd+U= +github.com/mattn/go-sqlite3 v2.0.3+incompatible/go.mod h1:FPy6KqzDD04eiIsT53CuJW3U88zkxoIYsOqkbpncsNc= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/robfig/cron v1.2.0 h1:ZjScXvvxeQ63Dbyxy76Fj3AT3Ut0aKsyd2/tl3DTMuQ= github.com/robfig/cron v1.2.0/go.mod h1:JGuDeoQd7Z6yL4zQhZ3OPEVHB7fL6Ka6skscFHfmt2k= +github.com/samonzeweb/godb v1.0.8 h1:WRn6nq0FChYOzh+w8SgpXHUkEhL7W6ZqkCf5Ninx7Uc= +github.com/samonzeweb/godb v1.0.8/go.mod h1:LNDt3CakfBwpRY4AD0y/QPTbj+jB6O17tSxQES0p47o= +github.com/samonzeweb/godb v1.0.15 h1:HyNb8o1w109as9KWE8ih1YIBe8jC4luJ22f1XNacW38= +github.com/samonzeweb/godb v1.0.15/go.mod h1:SxCHqyireDXNrZApknS9lGUEutA43x9eJF632ecbK5Q= +github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc= +github.com/smartystreets/assertions v1.2.0/go.mod h1:tcbTF8ujkAEcZ8TElKY+i30BzYlVhC/LOxJk7iOWnoo= +github.com/smartystreets/goconvey v1.6.4/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= github.com/stretchr/objx v0.5.0 h1:1zr/of2m5FGMsad5YfcqgdqdWrIhu+EBEJRhR1U7z/c= @@ -20,6 +38,15 @@ github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/ github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= github.com/stretchr/testify v1.8.1 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKsk= github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20190325154230-a5d413f7728c/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20201002170205-7f63de1d35b0/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +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= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/tools v0.0.0-20190328211700-ab21143f2384/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= From b54ba3664393b7c8db142417fc0739322f3cc91c Mon Sep 17 00:00:00 2001 From: Igor Chubin Date: Sun, 4 Dec 2022 21:16:23 +0100 Subject: [PATCH 040/105] Add geoip cache converter --- internal/config/config.go | 6 ++- internal/geo/ip/convert.go | 99 ++++++++++++++++++++++++++++++++++++++ internal/geo/ip/ip.go | 18 ++++--- internal/geo/ip/ip_test.go | 10 +++- srv.go | 9 ++++ 5 files changed, 132 insertions(+), 10 deletions(-) create mode 100644 internal/geo/ip/convert.go diff --git a/internal/config/config.go b/internal/config/config.go index de4024d..c7c618b 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -72,6 +72,9 @@ type Cache struct { type Geo struct { // IPCache contains the path to the IP Geodata cache. IPCache string `yaml:"ipCache,omitempty"` + + // IPCacheDB contains the path to the SQLite DB with the IP Geodata cache. + IPCacheDB string `yaml:"ipCacheDB,omitempty"` } // Default contains the default configuration. @@ -81,7 +84,8 @@ func Default() *Config { Size: 12800, }, Geo{ - IPCache: "/wttr.in/cache/ip2l", + IPCache: "/wttr.in/cache/ip2l", + IPCacheDB: "/wttr.in/cache/geoip.db", }, Logging{ AccessLog: "/wttr.in/log/access.log", diff --git a/internal/geo/ip/convert.go b/internal/geo/ip/convert.go new file mode 100644 index 0000000..8582685 --- /dev/null +++ b/internal/geo/ip/convert.go @@ -0,0 +1,99 @@ +package ip + +import ( + "fmt" + "log" + "os" + "path/filepath" + + "github.com/samonzeweb/godb" + "github.com/samonzeweb/godb/adapters/sqlite" +) + +func (c *Cache) ConvertCache() error { + dbfile := c.config.Geo.IPCacheDB + + err := removeDBIfExists(dbfile) + if err != nil { + return err + } + + db, err := godb.Open(sqlite.Adapter, dbfile) + if err != nil { + return err + } + + err = createTable(db, "Location") + if err != nil { + return err + } + + log.Println("listing cache entries...") + files, err := filepath.Glob(filepath.Join(c.config.Geo.IPCache, "*")) + if err != nil { + return err + } + + log.Printf("going to convert %d entries\n", len(files)) + + block := []Location{} + for i, file := range files { + ip := filepath.Base(file) + loc, err := c.Read(ip) + if err != nil { + log.Println("invalid entry for", ip) + continue + } + + block = append(block, *loc) + if i%1000 != 0 || i == 0 { + continue + } + + err = db.BulkInsert(&block).Do() + if err != nil { + return err + } + block = []Location{} + log.Println("converted", i+1, "entries") + } + + // inserting the rest. + err = db.BulkInsert(&block).Do() + if err != nil { + return err + } + + log.Println("converted", len(files), "entries") + + return nil +} + +func createTable(db *godb.DB, tableName string) error { + createTable := fmt.Sprintf( + `create table %s ( + ip text not null primary key, + countryCode text not null, + country text not null, + region text not null, + city text not null, + latitude text not null, + longitude text not null); + `, tableName) + + _, err := db.CurrentDB().Exec(createTable) + return err +} + +func removeDBIfExists(filename string) error { + _, err := os.Stat(filename) + if err != nil { + if !os.IsNotExist(err) { + return err + } + // no db file + return nil + } + + return os.Remove(filename) +} diff --git a/internal/geo/ip/ip.go b/internal/geo/ip/ip.go index d74381f..dd9fbd2 100644 --- a/internal/geo/ip/ip.go +++ b/internal/geo/ip/ip.go @@ -22,12 +22,13 @@ var ( // Location information. type Location struct { - CountryCode string - Country string - Region string - City string - Latitude float64 - Longitude float64 + IP string `db:"ip,key"` + CountryCode string `db:"countryCode"` + Country string `db:"country"` + Region string `db:"region"` + City string `db:"city"` + Latitude float64 `db:"latitude"` + Longitude float64 `db:"longitude"` } // Cache provides access to the IP Geodata cache. @@ -59,7 +60,7 @@ func (c *Cache) Read(addr string) (*Location, error) { if err != nil { return nil, ErrNotFound } - return parseCacheEntry(string(bytes)) + return parseCacheEntry(addr, string(bytes)) } // cacheFile retuns path to the cache entry for addr. @@ -69,7 +70,7 @@ func (c *Cache) cacheFile(addr string) string { // parseCacheEntry parses the location cache entry s, // and return location, or error, if the cache entry is invalid. -func parseCacheEntry(s string) (*Location, error) { +func parseCacheEntry(addr, s string) (*Location, error) { var ( lat float64 = -1000 long float64 = -1000 @@ -94,6 +95,7 @@ func parseCacheEntry(s string) (*Location, error) { } return &Location{ + IP: addr, CountryCode: parts[0], Country: parts[1], Region: parts[2], diff --git a/internal/geo/ip/ip_test.go b/internal/geo/ip/ip_test.go index 8921628..bcad77c 100644 --- a/internal/geo/ip/ip_test.go +++ b/internal/geo/ip/ip_test.go @@ -8,13 +8,16 @@ import ( func TestParseCacheEntry(t *testing.T) { tests := []struct { + addr string input string expected Location err error }{ { + "1.2.3.4", "DE;Germany;Free and Hanseatic City of Hamburg;Hamburg;53.5736;9.9782", Location{ + IP: "1.2.3.4", CountryCode: "DE", Country: "Germany", Region: "Free and Hanseatic City of Hamburg", @@ -26,8 +29,10 @@ func TestParseCacheEntry(t *testing.T) { }, { + "1.2.3.4", "ES;Spain;Madrid, Comunidad de;Madrid;40.4165;-3.70256;28223;Orange Espagne SA;orange.es", Location{ + IP: "1.2.3.4", CountryCode: "ES", Country: "Spain", Region: "Madrid, Comunidad de", @@ -39,8 +44,10 @@ func TestParseCacheEntry(t *testing.T) { }, { + "1.2.3.4", "US;United States of America;California;Mountain View", Location{ + IP: "1.2.3.4", CountryCode: "US", Country: "United States of America", Region: "California", @@ -53,6 +60,7 @@ func TestParseCacheEntry(t *testing.T) { // Invalid entries { + "1.2.3.4", "DE;Germany;Free and Hanseatic City of Hamburg;Hamburg;53.5736;XXX", Location{}, ErrInvalidCacheEntry, @@ -60,7 +68,7 @@ func TestParseCacheEntry(t *testing.T) { } for _, tt := range tests { - result, err := parseCacheEntry(tt.input) + result, err := parseCacheEntry(tt.addr, tt.input) if tt.err == nil { require.NoError(t, err) require.Equal(t, *result, tt.expected) diff --git a/srv.go b/srv.go index c39b80e..7d2a134 100644 --- a/srv.go +++ b/srv.go @@ -12,6 +12,7 @@ import ( "github.com/alecthomas/kong" "github.com/chubin/wttr.in/internal/config" + geoip "github.com/chubin/wttr.in/internal/geo/ip" "github.com/chubin/wttr.in/internal/logging" "github.com/chubin/wttr.in/internal/processor" ) @@ -20,6 +21,8 @@ var cli struct { ConfigCheck bool `name:"config-check" help:"Check configuration"` ConfigDump bool `name:"config-dump" help:"Dump configuration"` ConfigFile string `name:"config-file" arg:"" optional:"" help:"Name of configuration file"` + + ConvertGeoIPCache bool `name:"convert-geo-ip-cache" help:"Convert Geo IP data cache to SQlite"` } const logLineStart = "LOG_LINE_START " @@ -173,6 +176,12 @@ func main() { return } + if cli.ConvertGeoIPCache { + geoIPCache := geoip.NewCache(conf) + ctx.FatalIfErrorf(geoIPCache.ConvertCache()) + return + } + err = serve(conf) ctx.FatalIfErrorf(err) } From 307476764a60bd578f1aed4840e522d52eb129c8 Mon Sep 17 00:00:00 2001 From: Igor Chubin Date: Sun, 4 Dec 2022 21:16:39 +0100 Subject: [PATCH 041/105] Add internal/util/yaml.go --- internal/util/yaml.go | 14 ++++++++++++++ 1 file changed, 14 insertions(+) create mode 100644 internal/util/yaml.go diff --git a/internal/util/yaml.go b/internal/util/yaml.go new file mode 100644 index 0000000..21cd88b --- /dev/null +++ b/internal/util/yaml.go @@ -0,0 +1,14 @@ +package util + +import ( + "bytes" + + "gopkg.in/yaml.v3" +) + +// YamlUnmarshalStrict unmarshals YAML data with an error when unknown fields are present. +func YamlUnmarshalStrict(in []byte, out interface{}) error { + dec := yaml.NewDecoder(bytes.NewReader(in)) + dec.KnownFields(true) + return dec.Decode(out) +} From 4599029329ef5dedb15f6b6b4498325ad88b6eaa Mon Sep 17 00:00:00 2001 From: Igor Chubin Date: Tue, 6 Dec 2022 18:03:24 +0100 Subject: [PATCH 042/105] Ignore local queries in stats --- internal/processor/processor.go | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/internal/processor/processor.go b/internal/processor/processor.go index 8a4247c..94d40ca 100644 --- a/internal/processor/processor.go +++ b/internal/processor/processor.go @@ -104,7 +104,10 @@ func (rp *RequestProcessor) ProcessRequest(r *http.Request) (*responseWithHeader err error ) - rp.stats.Inc("total") + ip := util.ReadUserIP(r) + if ip != "127.0.0.1" { + rp.stats.Inc("total") + } // Main routing logic. if rh := rp.router.Route(r); rh != nil { @@ -167,7 +170,6 @@ func (rp *RequestProcessor) ProcessRequest(r *http.Request) (*responseWithHeader } // How many IP addresses are known. - ip := util.ReadUserIP(r) _, err = rp.geoIPCache.Read(ip) if err == nil { rp.stats.Inc("geoip") From a27541a25b04611413eb8fb825ad1d2c6d12706f Mon Sep 17 00:00:00 2001 From: Igor Chubin Date: Tue, 6 Dec 2022 18:52:26 +0100 Subject: [PATCH 043/105] Add support for geoip cache db reading --- internal/config/config.go | 4 +++ internal/geo/ip/ip.go | 54 +++++++++++++++++++++++++++------ internal/processor/processor.go | 7 ++++- internal/types/types.go | 8 +++++ srv.go | 5 ++- 5 files changed, 67 insertions(+), 11 deletions(-) create mode 100644 internal/types/types.go diff --git a/internal/config/config.go b/internal/config/config.go index c7c618b..bccc6f9 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -6,6 +6,7 @@ import ( "gopkg.in/yaml.v3" + "github.com/chubin/wttr.in/internal/types" "github.com/chubin/wttr.in/internal/util" ) @@ -75,6 +76,8 @@ type Geo struct { // IPCacheDB contains the path to the SQLite DB with the IP Geodata cache. IPCacheDB string `yaml:"ipCacheDB,omitempty"` + + CacheType types.CacheType `yaml:"cacheType,omitempty"` } // Default contains the default configuration. @@ -86,6 +89,7 @@ func Default() *Config { Geo{ IPCache: "/wttr.in/cache/ip2l", IPCacheDB: "/wttr.in/cache/geoip.db", + CacheType: types.CacheTypeFiles, }, Logging{ AccessLog: "/wttr.in/log/access.log", diff --git a/internal/geo/ip/ip.go b/internal/geo/ip/ip.go index dd9fbd2..064ab17 100644 --- a/internal/geo/ip/ip.go +++ b/internal/geo/ip/ip.go @@ -2,6 +2,7 @@ package ip import ( "errors" + "fmt" "log" "net/http" "os" @@ -12,7 +13,10 @@ import ( "github.com/chubin/wttr.in/internal/config" "github.com/chubin/wttr.in/internal/routing" + "github.com/chubin/wttr.in/internal/types" "github.com/chubin/wttr.in/internal/util" + "github.com/samonzeweb/godb" + "github.com/samonzeweb/godb/adapters/sqlite" ) var ( @@ -31,16 +35,34 @@ type Location struct { Longitude float64 `db:"longitude"` } +func (l *Location) String() string { + if l.Latitude == -1000 { + return fmt.Sprintf( + "%s;%s;%s;%s", + l.CountryCode, l.CountryCode, l.Region, l.City) + } + return fmt.Sprintf( + "%s;%s;%s;%s;%v;%v", + l.CountryCode, l.CountryCode, l.Region, l.City, l.Latitude, l.Longitude) +} + // Cache provides access to the IP Geodata cache. type Cache struct { config *config.Config + db *godb.DB } // NewCache returns new cache reader for the specified config. -func NewCache(config *config.Config) *Cache { +func NewCache(config *config.Config) (*Cache, error) { + db, err := godb.Open(sqlite.Adapter, config.Geo.IPCacheDB) + if err != nil { + return nil, err + } + return &Cache{ config: config, - } + db: db, + }, nil } // Read returns location information from the cache, if found, @@ -56,6 +78,13 @@ func NewCache(config *config.Config) *Cache { // DE;Germany;Free and Hanseatic City of Hamburg;Hamburg;53.5736;9.9782 // func (c *Cache) Read(addr string) (*Location, error) { + if c.config.Geo.CacheType == types.CacheTypeDB { + return c.readFromCacheDB(addr) + } + return c.readFromCacheFile(addr) +} + +func (c *Cache) readFromCacheFile(addr string) (*Location, error) { bytes, err := os.ReadFile(c.cacheFile(addr)) if err != nil { return nil, ErrNotFound @@ -63,6 +92,17 @@ func (c *Cache) Read(addr string) (*Location, error) { return parseCacheEntry(addr, string(bytes)) } +func (c *Cache) readFromCacheDB(addr string) (*Location, error) { + result := Location{} + err := c.db.Select(&result). + Where("IP = ?", addr). + Do() + if err != nil { + return nil, err + } + return &result, nil +} + // cacheFile retuns path to the cache entry for addr. func (c *Cache) cacheFile(addr string) string { return path.Join(c.config.Geo.IPCache, addr) @@ -144,19 +184,15 @@ func (c *Cache) Response(r *http.Request) *routing.Cadre { return respERR } - result, err := c.getRaw(ip) - if err != nil { + result, err := c.Read(ip) + if result == nil || err != nil { return respERR } - return &routing.Cadre{Body: result} + return &routing.Cadre{Body: []byte(result.String())} } return nil } -func (c *Cache) getRaw(addr string) ([]byte, error) { - return os.ReadFile(c.cacheFile(addr)) -} - func (c *Cache) putRaw(addr, value string) error { return os.WriteFile(c.cacheFile(addr), []byte(value), 0644) } diff --git a/internal/processor/processor.go b/internal/processor/processor.go index 94d40ca..2d507fc 100644 --- a/internal/processor/processor.go +++ b/internal/processor/processor.go @@ -77,12 +77,17 @@ func NewRequestProcessor(config *config.Config) (*RequestProcessor, error) { }, } + geoCache, err := geoip.NewCache(config) + if err != nil { + return nil, err + } + rp := &RequestProcessor{ lruCache: lruCache, stats: stats.New(), upstreamTransport: transport, config: config, - geoIPCache: geoip.NewCache(config), + geoIPCache: geoCache, } // Initialize routes. diff --git a/internal/types/types.go b/internal/types/types.go new file mode 100644 index 0000000..d28d0c6 --- /dev/null +++ b/internal/types/types.go @@ -0,0 +1,8 @@ +package types + +type CacheType string + +const ( + CacheTypeDB = "db" + CacheTypeFiles = "files" +) diff --git a/srv.go b/srv.go index 7d2a134..53f82b3 100644 --- a/srv.go +++ b/srv.go @@ -177,7 +177,10 @@ func main() { } if cli.ConvertGeoIPCache { - geoIPCache := geoip.NewCache(conf) + geoIPCache, err := geoip.NewCache(conf) + if err != nil { + ctx.FatalIfErrorf(err) + } ctx.FatalIfErrorf(geoIPCache.ConvertCache()) return } From 4855adeaf645b18705b1aa6a0226d50dc41ba1b5 Mon Sep 17 00:00:00 2001 From: Igor Chubin Date: Tue, 6 Dec 2022 19:37:57 +0100 Subject: [PATCH 044/105] Add support for geoip cache db writing --- internal/geo/ip/ip.go | 48 +++++++++++++++++++++++++++++++------- internal/geo/ip/ip_test.go | 2 +- 2 files changed, 41 insertions(+), 9 deletions(-) diff --git a/internal/geo/ip/ip.go b/internal/geo/ip/ip.go index 064ab17..96ababf 100644 --- a/internal/geo/ip/ip.go +++ b/internal/geo/ip/ip.go @@ -59,6 +59,9 @@ func NewCache(config *config.Config) (*Cache, error) { return nil, err } + // Needed for "upsert" implementation in Put() + db.UseErrorParser() + return &Cache{ config: config, db: db, @@ -89,7 +92,7 @@ func (c *Cache) readFromCacheFile(addr string) (*Location, error) { if err != nil { return nil, ErrNotFound } - return parseCacheEntry(addr, string(bytes)) + return NewLocationFromString(addr, string(bytes)) } func (c *Cache) readFromCacheDB(addr string) (*Location, error) { @@ -103,14 +106,42 @@ func (c *Cache) readFromCacheDB(addr string) (*Location, error) { return &result, nil } +func (c *Cache) Put(addr string, loc *Location) error { + if c.config.Geo.CacheType == types.CacheTypeDB { + return c.putToCacheDB(addr, loc) + } + return c.putToCacheFile(addr, loc) +} + +func (c *Cache) putToCacheDB(addr string, loc *Location) error { + err := c.db.Insert(loc).Do() + // it should work like this: + // + // target := dberror.UniqueConstraint{} + // if errors.As(err, &target) { + // + // See: https://github.com/samonzeweb/godb/pull/23 + // + // But for some reason it does not work, + // so the dirty hack is used: + if strings.Contains(fmt.Sprint(err), "UNIQUE constraint failed") { + return c.db.Update(loc).Do() + } + return err +} + +func (c *Cache) putToCacheFile(addr string, loc *Location) error { + return os.WriteFile(c.cacheFile(addr), []byte(loc.String()), 0644) +} + // cacheFile retuns path to the cache entry for addr. func (c *Cache) cacheFile(addr string) string { return path.Join(c.config.Geo.IPCache, addr) } -// parseCacheEntry parses the location cache entry s, +// NewLocationFromString parses the location cache entry s, // and return location, or error, if the cache entry is invalid. -func parseCacheEntry(addr, s string) (*Location, error) { +func NewLocationFromString(addr, s string) (*Location, error) { var ( lat float64 = -1000 long float64 = -1000 @@ -172,7 +203,12 @@ func (c *Cache) Response(r *http.Request) *routing.Cadre { return respERR } - err := c.putRaw(ip, value) + location, err := NewLocationFromString(ip, value) + if err != nil { + return respERR + } + + err = c.Put(ip, location) if err != nil { return respERR } @@ -193,10 +229,6 @@ func (c *Cache) Response(r *http.Request) *routing.Cadre { return nil } -func (c *Cache) putRaw(addr, value string) error { - return os.WriteFile(c.cacheFile(addr), []byte(value), 0644) -} - func validIP4(ipAddress string) bool { re, _ := regexp.Compile(`^(([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])\.){3}([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])$`) return re.MatchString(strings.Trim(ipAddress, " ")) diff --git a/internal/geo/ip/ip_test.go b/internal/geo/ip/ip_test.go index bcad77c..7e98f00 100644 --- a/internal/geo/ip/ip_test.go +++ b/internal/geo/ip/ip_test.go @@ -68,7 +68,7 @@ func TestParseCacheEntry(t *testing.T) { } for _, tt := range tests { - result, err := parseCacheEntry(tt.addr, tt.input) + result, err := NewLocationFromString(tt.addr, tt.input) if tt.err == nil { require.NoError(t, err) require.Equal(t, *result, tt.expected) From b8a7991cb6b46bf86285b569effcd4c5f4652e77 Mon Sep 17 00:00:00 2001 From: Igor Chubin Date: Wed, 7 Dec 2022 20:10:38 +0100 Subject: [PATCH 045/105] Switch to GeoIP Cache DB --- internal/config/config.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/internal/config/config.go b/internal/config/config.go index bccc6f9..16ee9eb 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -89,7 +89,7 @@ func Default() *Config { Geo{ IPCache: "/wttr.in/cache/ip2l", IPCacheDB: "/wttr.in/cache/geoip.db", - CacheType: types.CacheTypeFiles, + CacheType: types.CacheTypeDB, }, Logging{ AccessLog: "/wttr.in/log/access.log", From ca54ff4d2d99d6f14bc2c0fbbcc36d378d9eb00e Mon Sep 17 00:00:00 2001 From: Lucas Larson Date: Fri, 9 Dec 2022 13:27:59 -0500 Subject: [PATCH 046/105] fix spelling (#822) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This is a pull request to fix #822, where > the words “ultraviolet” and “until” are misspelled `/lib/fields.py`: > > - https://github.com/chubin/wttr.in/blob/dabfc55c8be1d793e355c7023f3345533ee16be3/lib/fields.py#L33 > - https://github.com/chubin/wttr.in/blob/dabfc55c8be1d793e355c7023f3345533ee16be3/lib/fields.py#L94 > - https://github.com/chubin/wttr.in/blob/dabfc55c8be1d793e355c7023f3345533ee16be3/lib/fields.py#L97 > - https://github.com/chubin/wttr.in/blob/8dc0e08f5e38ff1cc7aca8377019750c0284ce9d/lib/fields.py#L100 > - https://github.com/chubin/wttr.in/blob/dabfc55c8be1d793e355c7023f3345533ee16be3/lib/fields.py#L103 Signed-off-by: Lucas Larson --- lib/fields.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/lib/fields.py b/lib/fields.py index 3576ee5..17d8f4e 100644 --- a/lib/fields.py +++ b/lib/fields.py @@ -30,7 +30,7 @@ DESCRIPTION = { "Temperature in Fahrenheit", "temperature_fahrenheit"), "uvIndex": ( - "Ultaviolet Radiation Index", + "Ultraviolet Radiation Index", "uv_index"), "visibility": ( "Visible Distance in Kilometres", @@ -91,15 +91,15 @@ DESCRIPTION = { # astronomy fields with time "moonrise": ( - "Minutes since start of the day untill the moon appears above the horizon", + "Minutes since start of the day until the moon appears above the horizon", "astronomy_moonrise_min"), "moonset": ( - "Minutes since start of the day untill the moon disappears below the horizon", + "Minutes since start of the day until the moon disappears below the horizon", "astronomy_moonset_min"), "sunrise": ( - "Minutes since start of the day untill the sun appears above the horizon", + "Minutes since start of the day until the sun appears above the horizon", "astronomy_sunrise_min"), "sunset": ( - "Minutes since start of the day untill the moon disappears below the horizon", + "Minutes since start of the day until the moon disappears below the horizon", "astronomy_sunset_min"), } From 2ce4c28c34065e057cf33db7b3c4b84252eb98d3 Mon Sep 17 00:00:00 2001 From: Igor Chubin Date: Fri, 9 Dec 2022 21:02:10 +0100 Subject: [PATCH 047/105] Add Nominatim queries resolution initial support --- internal/geo/location/location.go | 49 ++++++++++++++++++++++ internal/geo/location/nominatim.go | 66 ++++++++++++++++++++++++++++++ 2 files changed, 115 insertions(+) create mode 100644 internal/geo/location/location.go create mode 100644 internal/geo/location/nominatim.go diff --git a/internal/geo/location/location.go b/internal/geo/location/location.go new file mode 100644 index 0000000..86fde7f --- /dev/null +++ b/internal/geo/location/location.go @@ -0,0 +1,49 @@ +package location + +import "github.com/chubin/wttr.in/internal/config" + +type Location struct { + Name string + Fullname string `json:"display_name"` + Lat string + Lon string +} + +type Provider interface { + Query(location string) (*Location, error) +} + +type Searcher struct { + providers []Provider +} + +// NewSearcher returns a new Searcher for the specified config. +func NewSearcher(config *config.Config) *Searcher { + providers := []Provider{} + for _, p := range config.Geo.Nominatim { + providers = append(providers, NewNominatim(p.Name, p.URL, p.Token)) + } + + return &Searcher{ + providers: providers, + } +} + +// Search makes queries through all known providers, +// and returns response, as soon as it is not nil. +// If all responses were nil, the last response is returned. +func (s *Searcher) Search(location string) (*Location, error) { + var ( + err error + result *Location + ) + + for _, p := range s.providers { + result, err = p.Query(location) + if result != nil && err == nil { + return result, nil + } + } + + return result, err +} diff --git a/internal/geo/location/nominatim.go b/internal/geo/location/nominatim.go new file mode 100644 index 0000000..fbb8a7f --- /dev/null +++ b/internal/geo/location/nominatim.go @@ -0,0 +1,66 @@ +package location + +import ( + "encoding/json" + "fmt" + "io/ioutil" + "log" + "net/http" + "net/url" +) + +type Nominatim struct { + name string + url string + token string +} + +func NewNominatim(name, url, token string) *Nominatim { + return &Nominatim{ + name: name, + url: url, + token: token, + } +} + +func (n *Nominatim) Query(location string) (*Location, error) { + var ( + result []Location + + errResponse struct { + Error string + } + ) + + urlws := fmt.Sprintf( + "%s?q=%s&format=json&accept-language=native&limit=1&key=%s", + n.url, url.QueryEscape(location), n.token) + + log.Println(urlws) + resp, err := http.Get(urlws) + if err != nil { + return nil, fmt.Errorf("%s: %w", n.name, err) + } + + body, err := ioutil.ReadAll(resp.Body) + if err != nil { + return nil, fmt.Errorf("%s: %w", n.name, err) + } + + err = json.Unmarshal(body, &errResponse) + if err == nil && errResponse.Error != "" { + return nil, fmt.Errorf("%s: %s", n.name, errResponse.Error) + } + + err = json.Unmarshal(body, &result) + if err != nil { + return nil, fmt.Errorf("%s: %w", n.name, err) + } + + if len(result) != 1 { + return nil, fmt.Errorf("%s: invalid response", n.name) + } + + return &result[0], nil + +} From caecca05cf1f7e3f17ee59018a14c099c19148bf Mon Sep 17 00:00:00 2001 From: Igor Chubin Date: Fri, 9 Dec 2022 21:02:47 +0100 Subject: [PATCH 048/105] Add Nominatim configuration --- internal/config/config.go | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/internal/config/config.go b/internal/config/config.go index 16ee9eb..611c35b 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -78,6 +78,16 @@ type Geo struct { IPCacheDB string `yaml:"ipCacheDB,omitempty"` CacheType types.CacheType `yaml:"cacheType,omitempty"` + + Nominatim []Nominatim +} + +type Nominatim struct { + Name string + + URL string + + Token string } // Default contains the default configuration. @@ -90,6 +100,13 @@ func Default() *Config { IPCache: "/wttr.in/cache/ip2l", IPCacheDB: "/wttr.in/cache/geoip.db", CacheType: types.CacheTypeDB, + Nominatim: []Nominatim{ + { + Name: "locationiq", + URL: "https://eu1.locationiq.com/v1/search", + Token: os.Getenv("NOMINATIM_LOCATIONIQ"), + }, + }, }, Logging{ AccessLog: "/wttr.in/log/access.log", From 1510ce6a88790493e902a5fb31d2ee8ce8b3588d Mon Sep 17 00:00:00 2001 From: Igor Chubin Date: Fri, 9 Dec 2022 21:03:10 +0100 Subject: [PATCH 049/105] Add new option: --geo-resolve for geo queries --- srv.go | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/srv.go b/srv.go index 53f82b3..f47b10d 100644 --- a/srv.go +++ b/srv.go @@ -13,6 +13,7 @@ import ( "github.com/chubin/wttr.in/internal/config" geoip "github.com/chubin/wttr.in/internal/geo/ip" + geoloc "github.com/chubin/wttr.in/internal/geo/location" "github.com/chubin/wttr.in/internal/logging" "github.com/chubin/wttr.in/internal/processor" ) @@ -20,7 +21,9 @@ import ( var cli struct { ConfigCheck bool `name:"config-check" help:"Check configuration"` ConfigDump bool `name:"config-dump" help:"Dump configuration"` - ConfigFile string `name:"config-file" arg:"" optional:"" help:"Name of configuration file"` + GeoResolve string `name:"geo-resolve" help:"Resolve location"` + + ConfigFile string `name:"config-file" arg:"" optional:"" help:"Name of configuration file"` ConvertGeoIPCache bool `name:"convert-geo-ip-cache" help:"Convert Geo IP data cache to SQlite"` } @@ -185,6 +188,16 @@ func main() { return } + if cli.GeoResolve != "" { + sr := geoloc.NewSearcher(conf) + loc, err := sr.Search(cli.GeoResolve) + ctx.FatalIfErrorf(err) + if loc != nil { + fmt.Println(*loc) + + } + } + err = serve(conf) ctx.FatalIfErrorf(err) } From 1bcdd45f34cde1f150c9116bc2362d4e4af0c6fa Mon Sep 17 00:00:00 2001 From: Igor Chubin Date: Sun, 11 Dec 2022 09:36:11 +0100 Subject: [PATCH 050/105] Add geoloc cache converter --- go.mod | 1 + go.sum | 2 ++ internal/config/config.go | 19 +++++++++++++++---- internal/geo/ip/convert.go | 31 ++++++++----------------------- internal/geo/ip/ip.go | 22 ++++++++-------------- internal/geo/ip/ip_test.go | 4 +++- internal/geo/location/location.go | 27 ++++++++++++++++++++++----- internal/types/types.go | 7 +++++++ srv.go | 12 +++++++++++- 9 files changed, 77 insertions(+), 48 deletions(-) diff --git a/go.mod b/go.mod index c435a4d..bf7549e 100644 --- a/go.mod +++ b/go.mod @@ -15,6 +15,7 @@ require ( github.com/smartystreets/assertions v1.2.0 // indirect github.com/smartystreets/goconvey v1.6.4 // indirect github.com/stretchr/testify v1.8.1 // indirect + github.com/zsefvlol/timezonemapper v1.0.0 // indirect golang.org/x/crypto v0.0.0-20201002170205-7f63de1d35b0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/go.sum b/go.sum index 1fa36b2..084eb3f 100644 --- a/go.sum +++ b/go.sum @@ -38,6 +38,8 @@ github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/ github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= github.com/stretchr/testify v1.8.1 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKsk= github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= +github.com/zsefvlol/timezonemapper v1.0.0 h1:HXqkOzf01gXYh2nDQcDSROikFgMaximnhE8BY9SyF6E= +github.com/zsefvlol/timezonemapper v1.0.0/go.mod h1:cVUCOLEmc/VvOMusEhpd2G/UBtadL26ZVz2syODXDoQ= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20190325154230-a5d413f7728c/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20201002170205-7f63de1d35b0/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= diff --git a/internal/config/config.go b/internal/config/config.go index 611c35b..bac275c 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -77,7 +77,15 @@ type Geo struct { // IPCacheDB contains the path to the SQLite DB with the IP Geodata cache. IPCacheDB string `yaml:"ipCacheDB,omitempty"` - CacheType types.CacheType `yaml:"cacheType,omitempty"` + IPCacheType types.CacheType `yaml:"cacheType,omitempty"` + + // LocationCache contains the path to the Location Geodata cache. + LocationCache string `yaml:"locationCache,omitempty"` + + // LocationCacheDB contains the path to the SQLite DB with the Location Geodata cache. + LocationCacheDB string `yaml:"locationCacheDB,omitempty"` + + LocationCacheType types.CacheType `yaml:"locationCacheType,omitempty"` Nominatim []Nominatim } @@ -97,9 +105,12 @@ func Default() *Config { Size: 12800, }, Geo{ - IPCache: "/wttr.in/cache/ip2l", - IPCacheDB: "/wttr.in/cache/geoip.db", - CacheType: types.CacheTypeDB, + IPCache: "/wttr.in/cache/ip2l", + IPCacheDB: "/wttr.in/cache/geoip.db", + IPCacheType: types.CacheTypeDB, + LocationCache: "/wttr.in/cache/loc", + LocationCacheDB: "/wttr.in/cache/geoloc.db", + LocationCacheType: types.CacheTypeFiles, Nominatim: []Nominatim{ { Name: "locationiq", diff --git a/internal/geo/ip/convert.go b/internal/geo/ip/convert.go index 8582685..962d546 100644 --- a/internal/geo/ip/convert.go +++ b/internal/geo/ip/convert.go @@ -3,17 +3,18 @@ package ip import ( "fmt" "log" - "os" "path/filepath" "github.com/samonzeweb/godb" "github.com/samonzeweb/godb/adapters/sqlite" + + "github.com/chubin/wttr.in/internal/util" ) func (c *Cache) ConvertCache() error { dbfile := c.config.Geo.IPCacheDB - err := removeDBIfExists(dbfile) + err := util.RemoveFileIfExists(dbfile) if err != nil { return err } @@ -29,7 +30,7 @@ func (c *Cache) ConvertCache() error { } log.Println("listing cache entries...") - files, err := filepath.Glob(filepath.Join(c.config.Geo.IPCache, "*")) + files, err := filepath.Glob(filepath.Join(c.config.Geo.LocationCache, "*")) if err != nil { return err } @@ -72,28 +73,12 @@ func (c *Cache) ConvertCache() error { func createTable(db *godb.DB, tableName string) error { createTable := fmt.Sprintf( `create table %s ( - ip text not null primary key, - countryCode text not null, - country text not null, - region text not null, - city text not null, - latitude text not null, - longitude text not null); + name text not null primary key, + fullName text not null, + lat text not null, + long text not null); `, tableName) _, err := db.CurrentDB().Exec(createTable) return err } - -func removeDBIfExists(filename string) error { - _, err := os.Stat(filename) - if err != nil { - if !os.IsNotExist(err) { - return err - } - // no db file - return nil - } - - return os.Remove(filename) -} diff --git a/internal/geo/ip/ip.go b/internal/geo/ip/ip.go index 96ababf..b20336c 100644 --- a/internal/geo/ip/ip.go +++ b/internal/geo/ip/ip.go @@ -1,7 +1,6 @@ package ip import ( - "errors" "fmt" "log" "net/http" @@ -19,11 +18,6 @@ import ( "github.com/samonzeweb/godb/adapters/sqlite" ) -var ( - ErrNotFound = errors.New("cache entry not found") - ErrInvalidCacheEntry = errors.New("invalid cache entry format") -) - // Location information. type Location struct { IP string `db:"ip,key"` @@ -69,8 +63,8 @@ func NewCache(config *config.Config) (*Cache, error) { } // Read returns location information from the cache, if found, -// or ErrNotFound if not found. If the entry is found, but its format -// is invalid, ErrInvalidCacheEntry is returned. +// or types.ErrNotFound if not found. If the entry is found, but its format +// is invalid, types.ErrInvalidCacheEntry is returned. // // Format: // @@ -81,7 +75,7 @@ func NewCache(config *config.Config) (*Cache, error) { // DE;Germany;Free and Hanseatic City of Hamburg;Hamburg;53.5736;9.9782 // func (c *Cache) Read(addr string) (*Location, error) { - if c.config.Geo.CacheType == types.CacheTypeDB { + if c.config.Geo.IPCacheType == types.CacheTypeDB { return c.readFromCacheDB(addr) } return c.readFromCacheFile(addr) @@ -90,7 +84,7 @@ func (c *Cache) Read(addr string) (*Location, error) { func (c *Cache) readFromCacheFile(addr string) (*Location, error) { bytes, err := os.ReadFile(c.cacheFile(addr)) if err != nil { - return nil, ErrNotFound + return nil, types.ErrNotFound } return NewLocationFromString(addr, string(bytes)) } @@ -107,7 +101,7 @@ func (c *Cache) readFromCacheDB(addr string) (*Location, error) { } func (c *Cache) Put(addr string, loc *Location) error { - if c.config.Geo.CacheType == types.CacheTypeDB { + if c.config.Geo.IPCacheType == types.CacheTypeDB { return c.putToCacheDB(addr, loc) } return c.putToCacheFile(addr, loc) @@ -150,18 +144,18 @@ func NewLocationFromString(addr, s string) (*Location, error) { parts := strings.Split(s, ";") if len(parts) < 4 { - return nil, ErrInvalidCacheEntry + return nil, types.ErrInvalidCacheEntry } if len(parts) >= 6 { lat, err = strconv.ParseFloat(parts[4], 64) if err != nil { - return nil, ErrInvalidCacheEntry + return nil, types.ErrInvalidCacheEntry } long, err = strconv.ParseFloat(parts[5], 64) if err != nil { - return nil, ErrInvalidCacheEntry + return nil, types.ErrInvalidCacheEntry } } diff --git a/internal/geo/ip/ip_test.go b/internal/geo/ip/ip_test.go index 7e98f00..dcd4cb7 100644 --- a/internal/geo/ip/ip_test.go +++ b/internal/geo/ip/ip_test.go @@ -4,6 +4,8 @@ import ( "testing" "github.com/stretchr/testify/require" + + "github.com/chubin/wttr.in/internal/types" ) func TestParseCacheEntry(t *testing.T) { @@ -63,7 +65,7 @@ func TestParseCacheEntry(t *testing.T) { "1.2.3.4", "DE;Germany;Free and Hanseatic City of Hamburg;Hamburg;53.5736;XXX", Location{}, - ErrInvalidCacheEntry, + types.ErrInvalidCacheEntry, }, } diff --git a/internal/geo/location/location.go b/internal/geo/location/location.go index 86fde7f..c07cbbc 100644 --- a/internal/geo/location/location.go +++ b/internal/geo/location/location.go @@ -1,12 +1,29 @@ package location -import "github.com/chubin/wttr.in/internal/config" +import ( + "encoding/json" + "log" + + "github.com/chubin/wttr.in/internal/config" +) type Location struct { - Name string - Fullname string `json:"display_name"` - Lat string - Lon string + Name string `db:"name,key"` + Fullname string `db:"displayName" json:"display_name"` + Lat string `db:"lat"` + Lon string `db:"lon"` + Timezone string `db:"timezone"` +} + +// String returns string represenation of location +func (l *Location) String() string { + bytes, err := json.Marshal(l) + if err != nil { + // should never happen + log.Fatalln(err) + } + + return string(bytes) } type Provider interface { diff --git a/internal/types/types.go b/internal/types/types.go index d28d0c6..24f0f6d 100644 --- a/internal/types/types.go +++ b/internal/types/types.go @@ -1,8 +1,15 @@ package types +import "errors" + type CacheType string const ( CacheTypeDB = "db" CacheTypeFiles = "files" ) + +var ( + ErrNotFound = errors.New("cache entry not found") + ErrInvalidCacheEntry = errors.New("invalid cache entry format") +) diff --git a/srv.go b/srv.go index f47b10d..a8c3739 100644 --- a/srv.go +++ b/srv.go @@ -25,7 +25,8 @@ var cli struct { ConfigFile string `name:"config-file" arg:"" optional:"" help:"Name of configuration file"` - ConvertGeoIPCache bool `name:"convert-geo-ip-cache" help:"Convert Geo IP data cache to SQlite"` + ConvertGeoIPCache bool `name:"convert-geo-ip-cache" help:"Convert Geo IP data cache to SQlite"` + ConvertGeoLocationCache bool `name:"convert-geo-location-cache" help:"Convert Geo Location data cache to SQlite"` } const logLineStart = "LOG_LINE_START " @@ -188,6 +189,15 @@ func main() { return } + if cli.ConvertGeoLocationCache { + geoLocCache, err := geoloc.NewCache(conf) + if err != nil { + ctx.FatalIfErrorf(err) + } + ctx.FatalIfErrorf(geoLocCache.ConvertCache()) + return + } + if cli.GeoResolve != "" { sr := geoloc.NewSearcher(conf) loc, err := sr.Search(cli.GeoResolve) From 53b074af9354fbdd483a1800bfa4b1777dcb7b39 Mon Sep 17 00:00:00 2001 From: Igor Chubin Date: Sun, 11 Dec 2022 14:27:27 +0100 Subject: [PATCH 051/105] Add internal/geo/location/ --- internal/geo/location/cache.go | 142 +++++++++++++++++++++++++++++++ internal/geo/location/convert.go | 120 ++++++++++++++++++++++++++ 2 files changed, 262 insertions(+) create mode 100644 internal/geo/location/cache.go create mode 100644 internal/geo/location/convert.go diff --git a/internal/geo/location/cache.go b/internal/geo/location/cache.go new file mode 100644 index 0000000..8530c2c --- /dev/null +++ b/internal/geo/location/cache.go @@ -0,0 +1,142 @@ +package location + +import ( + "encoding/json" + "fmt" + "os" + "path" + "strings" + + "github.com/samonzeweb/godb" + "github.com/samonzeweb/godb/adapters/sqlite" + "github.com/zsefvlol/timezonemapper" + + "github.com/chubin/wttr.in/internal/config" + "github.com/chubin/wttr.in/internal/types" +) + +// Cache is an implemenation of DB/file-based cache. +// +// At the moment, it is an implementation for the location cache, +// but it should be generalized to cache everything. +type Cache struct { + config *config.Config + db *godb.DB + indexField string + filesCacheDir string +} + +// NewCache returns new cache reader for the specified config. +func NewCache(config *config.Config) (*Cache, error) { + var ( + db *godb.DB + err error + ) + + if config.Geo.LocationCacheType == types.CacheTypeDB { + db, err = godb.Open(sqlite.Adapter, config.Geo.IPCacheDB) + if err != nil { + return nil, err + } + + // Needed for "upsert" implementation in Put() + db.UseErrorParser() + } + + return &Cache{ + config: config, + db: db, + indexField: "name", + filesCacheDir: config.Geo.LocationCache, + }, nil +} + +// Read returns location information from the cache, if found, +// or types.ErrNotFound if not found. If the entry is found, but its format +// is invalid, types.ErrInvalidCacheEntry is returned. +func (c *Cache) Read(addr string) (*Location, error) { + if c.config.Geo.LocationCacheType == types.CacheTypeFiles { + return c.readFromCacheFile(addr) + } + + return c.readFromCacheDB(addr) +} + +func (c *Cache) readFromCacheFile(name string) (*Location, error) { + var ( + fileLoc = struct { + Latitude float64 `json:"latitude"` + Longitude float64 `json:"longitude"` + Timezone string `json:"timezone"` + Address string `json:"address"` + }{} + location Location + ) + + bytes, err := os.ReadFile(c.cacheFile(name)) + if err != nil { + return nil, types.ErrNotFound + } + err = json.Unmarshal(bytes, &fileLoc) + if err != nil { + return nil, err + } + + // normalize name + name = strings.TrimSpace( + strings.TrimRight( + strings.TrimLeft(name, `"`), `"`)) + + timezone := fileLoc.Timezone + if timezone == "" { + timezone = timezonemapper.LatLngToTimezoneString(fileLoc.Latitude, fileLoc.Longitude) + } + + location = Location{ + Name: name, + Lat: fmt.Sprint(fileLoc.Latitude), + Lon: fmt.Sprint(fileLoc.Longitude), + Timezone: timezone, + Fullname: fileLoc.Address, + } + + return &location, nil +} + +func (c *Cache) readFromCacheDB(addr string) (*Location, error) { + result := Location{} + err := c.db.Select(&result). + Where(c.indexField+" = ?", addr). + Do() + if err != nil { + return nil, err + } + + return &result, nil +} + +func (c *Cache) Put(addr string, loc *Location) error { + if c.config.Geo.IPCacheType == types.CacheTypeDB { + return c.putToCacheDB(loc) + } + + return c.putToCacheFile(addr, loc) +} + +func (c *Cache) putToCacheDB(loc *Location) error { + err := c.db.Insert(loc).Do() + if strings.Contains(fmt.Sprint(err), "UNIQUE constraint failed") { + return c.db.Update(loc).Do() + } + + return err +} + +func (c *Cache) putToCacheFile(addr string, loc fmt.Stringer) error { + return os.WriteFile(c.cacheFile(addr), []byte(loc.String()), 0o600) +} + +// cacheFile returns path to the cache entry for addr. +func (c *Cache) cacheFile(item string) string { + return path.Join(c.filesCacheDir, item) +} diff --git a/internal/geo/location/convert.go b/internal/geo/location/convert.go new file mode 100644 index 0000000..bda2765 --- /dev/null +++ b/internal/geo/location/convert.go @@ -0,0 +1,120 @@ +package location + +import ( + "fmt" + "log" + "os" + "path/filepath" + "strings" + + "github.com/samonzeweb/godb" + "github.com/samonzeweb/godb/adapters/sqlite" +) + +//nolint:funlen,cyclop +func (c *Cache) ConvertCache() error { + var ( + dbfile = c.config.Geo.LocationCacheDB + tableName = "Location" + cacheFiles = c.filesCacheDir + known = map[string]bool{} + ) + + err := removeDBIfExists(dbfile) + if err != nil { + return err + } + + db, err := godb.Open(sqlite.Adapter, dbfile) + if err != nil { + return err + } + + err = createTable(db, tableName) + if err != nil { + return err + } + + log.Println("listing cache entries...") + files, err := filepath.Glob(filepath.Join(cacheFiles, "*")) + if err != nil { + return err + } + + log.Printf("going to convert %d entries\n", len(files)) + + block := []Location{} + for i, file := range files { + ip := filepath.Base(file) + loc, err := c.Read(ip) + if err != nil { + log.Println("invalid entry for", ip) + + continue + } + + // Skip duplicates. + if known[loc.Name] { + log.Println("skipping", loc.Name) + + continue + } + known[loc.Name] = true + + // Skip some invalid names. + if strings.Contains(loc.Name, "\n") { + continue + } + + block = append(block, *loc) + if i%1000 != 0 || i == 0 { + continue + } + + log.Println("going to insert new entries") + err = db.BulkInsert(&block).Do() + if err != nil { + return err + } + block = []Location{} + log.Println("converted", i+1, "entries") + } + + // inserting the rest. + err = db.BulkInsert(&block).Do() + if err != nil { + return err + } + + log.Println("converted", len(files), "entries") + + return nil +} + +func createTable(db *godb.DB, tableName string) error { + createTable := fmt.Sprintf( + `create table %s ( + name text not null primary key, + displayName text not null, + lat text not null, + lon text not null, + timezone text not null); + `, tableName) + + _, err := db.CurrentDB().Exec(createTable) + + return err +} + +func removeDBIfExists(filename string) error { + _, err := os.Stat(filename) + if err != nil { + if !os.IsNotExist(err) { + return err + } + // no db file + return nil + } + + return os.Remove(filename) +} From aec889e65e5db2a53e374d818c0af73a7d7b747e Mon Sep 17 00:00:00 2001 From: Igor Chubin Date: Sun, 11 Dec 2022 14:27:46 +0100 Subject: [PATCH 052/105] Add internal/util/files.go --- internal/util/files.go | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) create mode 100644 internal/util/files.go diff --git a/internal/util/files.go b/internal/util/files.go new file mode 100644 index 0000000..d656624 --- /dev/null +++ b/internal/util/files.go @@ -0,0 +1,18 @@ +package util + +import "os" + +// RemoveFileIfExists removes filename if exists, or does nothing if the file +// is not there. Returns an error, if it occurred during deletion. +func RemoveFileIfExists(filename string) error { + _, err := os.Stat(filename) + if err != nil { + if !os.IsNotExist(err) { + return err + } + // no db file + return nil + } + + return os.Remove(filename) +} From aa3600a01127acb5d4982c65e1f77564c4887004 Mon Sep 17 00:00:00 2001 From: Igor Chubin Date: Sun, 11 Dec 2022 14:28:34 +0100 Subject: [PATCH 053/105] Fix linter checks --- internal/config/config.go | 11 +++--- internal/geo/ip/convert.go | 4 +++ internal/geo/ip/ip.go | 37 ++++++++++++++------ internal/geo/ip/ip_test.go | 5 ++- internal/geo/location/location.go | 5 +-- internal/geo/location/nominatim.go | 8 +++-- internal/logging/logging.go | 4 ++- internal/logging/suppress.go | 23 ++++++------ internal/processor/peak.go | 35 +++++++++++++------ internal/processor/processor.go | 56 ++++++++++++++++-------------- internal/routing/routing.go | 1 + internal/stats/stats.go | 8 +++-- internal/types/errors.go | 9 +++++ internal/types/types.go | 7 ---- internal/util/http.go | 1 + internal/util/yaml.go | 1 + srv.go | 36 ++++++++++++------- 17 files changed, 158 insertions(+), 93 deletions(-) create mode 100644 internal/types/errors.go diff --git a/internal/config/config.go b/internal/config/config.go index bac275c..e33a6ea 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -21,7 +21,6 @@ type Config struct { // Logging configuration. type Logging struct { - // AccessLog path. AccessLog string `yaml:"accessLog,omitempty"` @@ -34,14 +33,13 @@ type Logging struct { // Server configuration. type Server struct { - // PortHTTP is port where HTTP server must listen. // If 0, HTTP is disabled. - PortHTTP int `yaml:"portHTTP,omitempty"` + PortHTTP int `yaml:"portHttp,omitempty"` // PortHTTP is port where the HTTPS server must listen. // If 0, HTTPS is disabled. - PortHTTPS int `yaml:"portHTTPS,omitempty"` + PortHTTPS int `yaml:"portHttps,omitempty"` // TLSCertFile contains path to cert file for TLS Server. TLSCertFile string `yaml:"tlsCertFile,omitempty"` @@ -75,7 +73,7 @@ type Geo struct { IPCache string `yaml:"ipCache,omitempty"` // IPCacheDB contains the path to the SQLite DB with the IP Geodata cache. - IPCacheDB string `yaml:"ipCacheDB,omitempty"` + IPCacheDB string `yaml:"ipCacheDb,omitempty"` IPCacheType types.CacheType `yaml:"cacheType,omitempty"` @@ -83,7 +81,7 @@ type Geo struct { LocationCache string `yaml:"locationCache,omitempty"` // LocationCacheDB contains the path to the SQLite DB with the Location Geodata cache. - LocationCacheDB string `yaml:"locationCacheDB,omitempty"` + LocationCacheDB string `yaml:"locationCacheDb,omitempty"` LocationCacheType types.CacheType `yaml:"locationCacheType,omitempty"` @@ -165,5 +163,6 @@ func (c *Config) Dump() []byte { // should never happen. log.Fatalln("config.Dump():", err) } + return data } diff --git a/internal/geo/ip/convert.go b/internal/geo/ip/convert.go index 962d546..e8cbd10 100644 --- a/internal/geo/ip/convert.go +++ b/internal/geo/ip/convert.go @@ -11,6 +11,7 @@ import ( "github.com/chubin/wttr.in/internal/util" ) +//nolint:cyclop func (c *Cache) ConvertCache() error { dbfile := c.config.Geo.IPCacheDB @@ -43,10 +44,12 @@ func (c *Cache) ConvertCache() error { loc, err := c.Read(ip) if err != nil { log.Println("invalid entry for", ip) + continue } block = append(block, *loc) + if i%1000 != 0 || i == 0 { continue } @@ -80,5 +83,6 @@ func createTable(db *godb.DB, tableName string) error { `, tableName) _, err := db.CurrentDB().Exec(createTable) + return err } diff --git a/internal/geo/ip/ip.go b/internal/geo/ip/ip.go index b20336c..08cb7f1 100644 --- a/internal/geo/ip/ip.go +++ b/internal/geo/ip/ip.go @@ -35,6 +35,7 @@ func (l *Location) String() string { "%s;%s;%s;%s", l.CountryCode, l.CountryCode, l.Region, l.City) } + return fmt.Sprintf( "%s;%s;%s;%s;%v;%v", l.CountryCode, l.CountryCode, l.Region, l.City, l.Latitude, l.Longitude) @@ -68,16 +69,18 @@ func NewCache(config *config.Config) (*Cache, error) { // // Format: // -// [CountryCode];Country;Region;City;[Latitude];[Longitude] +// [CountryCode];Country;Region;City;[Latitude];[Longitude] // // Example: // -// DE;Germany;Free and Hanseatic City of Hamburg;Hamburg;53.5736;9.9782 +// DE;Germany;Free and Hanseatic City of Hamburg;Hamburg;53.5736;9.9782 // + func (c *Cache) Read(addr string) (*Location, error) { if c.config.Geo.IPCacheType == types.CacheTypeDB { return c.readFromCacheDB(addr) } + return c.readFromCacheFile(addr) } @@ -86,6 +89,7 @@ func (c *Cache) readFromCacheFile(addr string) (*Location, error) { if err != nil { return nil, types.ErrNotFound } + return NewLocationFromString(addr, string(bytes)) } @@ -97,17 +101,19 @@ func (c *Cache) readFromCacheDB(addr string) (*Location, error) { if err != nil { return nil, err } + return &result, nil } func (c *Cache) Put(addr string, loc *Location) error { if c.config.Geo.IPCacheType == types.CacheTypeDB { - return c.putToCacheDB(addr, loc) + return c.putToCacheDB(loc) } + return c.putToCacheFile(addr, loc) } -func (c *Cache) putToCacheDB(addr string, loc *Location) error { +func (c *Cache) putToCacheDB(loc *Location) error { err := c.db.Insert(loc).Do() // it should work like this: // @@ -121,14 +127,15 @@ func (c *Cache) putToCacheDB(addr string, loc *Location) error { if strings.Contains(fmt.Sprint(err), "UNIQUE constraint failed") { return c.db.Update(loc).Do() } + return err } -func (c *Cache) putToCacheFile(addr string, loc *Location) error { - return os.WriteFile(c.cacheFile(addr), []byte(loc.String()), 0644) +func (c *Cache) putToCacheFile(addr string, loc fmt.Stringer) error { + return os.WriteFile(c.cacheFile(addr), []byte(loc.String()), 0o600) } -// cacheFile retuns path to the cache entry for addr. +// cacheFile returns path to the cache entry for addr. func (c *Cache) cacheFile(addr string) string { return path.Join(c.config.Geo.IPCache, addr) } @@ -170,14 +177,15 @@ func NewLocationFromString(addr, s string) (*Location, error) { }, nil } -// Reponse provides routing interface to the geo cache. +// Response provides routing interface to the geo cache. // // Temporary workaround to switch IP addresses handling to the Go server. // Handles two queries: // -// /:geo-ip-put?ip=IP&value=VALUE -// /:geo-ip-get?ip=IP +// - /:geo-ip-put?ip=IP&value=VALUE +// - /:geo-ip-get?ip=IP // +//nolint:cyclop func (c *Cache) Response(r *http.Request) *routing.Cadre { var ( respERR = &routing.Cadre{Body: []byte("ERR")} @@ -186,6 +194,7 @@ func (c *Cache) Response(r *http.Request) *routing.Cadre { if ip := util.ReadUserIP(r); ip != "127.0.0.1" { log.Printf("geoIP access from %s rejected\n", ip) + return nil } @@ -194,6 +203,7 @@ func (c *Cache) Response(r *http.Request) *routing.Cadre { value := r.URL.Query().Get("value") if !validIP4(ip) || value == "" { log.Printf("invalid geoIP put query: ip='%s' value='%s'\n", ip, value) + return respERR } @@ -206,6 +216,7 @@ func (c *Cache) Response(r *http.Request) *routing.Cadre { if err != nil { return respERR } + return respOK } if r.URL.Path == "/:geo-ip-get" { @@ -218,12 +229,16 @@ func (c *Cache) Response(r *http.Request) *routing.Cadre { if result == nil || err != nil { return respERR } + return &routing.Cadre{Body: []byte(result.String())} } + return nil } func validIP4(ipAddress string) bool { - re, _ := regexp.Compile(`^(([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])\.){3}([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])$`) + re := regexp.MustCompile( + `^(([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])\.){3}([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])$`) + return re.MatchString(strings.Trim(ipAddress, " ")) } diff --git a/internal/geo/ip/ip_test.go b/internal/geo/ip/ip_test.go index dcd4cb7..91a9213 100644 --- a/internal/geo/ip/ip_test.go +++ b/internal/geo/ip/ip_test.go @@ -1,14 +1,17 @@ -package ip +package ip_test import ( "testing" "github.com/stretchr/testify/require" + . "github.com/chubin/wttr.in/internal/geo/ip" "github.com/chubin/wttr.in/internal/types" ) +//nolint:funlen func TestParseCacheEntry(t *testing.T) { + t.Parallel() tests := []struct { addr string input string diff --git a/internal/geo/location/location.go b/internal/geo/location/location.go index c07cbbc..f33b20d 100644 --- a/internal/geo/location/location.go +++ b/internal/geo/location/location.go @@ -9,13 +9,14 @@ import ( type Location struct { Name string `db:"name,key"` - Fullname string `db:"displayName" json:"display_name"` Lat string `db:"lat"` Lon string `db:"lon"` Timezone string `db:"timezone"` + //nolint:tagliatelle + Fullname string `db:"displayName" json:"display_name"` } -// String returns string represenation of location +// String returns string representation of location. func (l *Location) String() string { bytes, err := json.Marshal(l) if err != nil { diff --git a/internal/geo/location/nominatim.go b/internal/geo/location/nominatim.go index fbb8a7f..e16eef7 100644 --- a/internal/geo/location/nominatim.go +++ b/internal/geo/location/nominatim.go @@ -7,6 +7,8 @@ import ( "log" "net/http" "net/url" + + "github.com/chubin/wttr.in/internal/types" ) type Nominatim struct { @@ -41,6 +43,7 @@ func (n *Nominatim) Query(location string) (*Location, error) { if err != nil { return nil, fmt.Errorf("%s: %w", n.name, err) } + defer resp.Body.Close() body, err := ioutil.ReadAll(resp.Body) if err != nil { @@ -49,7 +52,7 @@ func (n *Nominatim) Query(location string) (*Location, error) { err = json.Unmarshal(body, &errResponse) if err == nil && errResponse.Error != "" { - return nil, fmt.Errorf("%s: %s", n.name, errResponse.Error) + return nil, fmt.Errorf("%w: %s: %s", types.ErrUpstream, n.name, errResponse.Error) } err = json.Unmarshal(body, &result) @@ -58,9 +61,8 @@ func (n *Nominatim) Query(location string) (*Location, error) { } if len(result) != 1 { - return nil, fmt.Errorf("%s: invalid response", n.name) + return nil, fmt.Errorf("%w: %s: invalid response", types.ErrUpstream, n.name) } return &result[0], nil - } diff --git a/internal/logging/logging.go b/internal/logging/logging.go index d1c8e75..992c479 100644 --- a/internal/logging/logging.go +++ b/internal/logging/logging.go @@ -63,6 +63,7 @@ func (rl *RequestLogger) Log(r *http.Request) error { if time.Since(rl.lastFlush) > rl.period { return rl.flush() } + return nil } @@ -85,7 +86,8 @@ func (rl *RequestLogger) flush() error { } // Open log file. - f, err := os.OpenFile(rl.filename, os.O_APPEND|os.O_WRONLY|os.O_CREATE, 0600) + //nolint:nosnakecase + f, err := os.OpenFile(rl.filename, os.O_APPEND|os.O_WRONLY|os.O_CREATE, 0o600) if err != nil { return err } diff --git a/internal/logging/suppress.go b/internal/logging/suppress.go index ab217f2..fd1507e 100644 --- a/internal/logging/suppress.go +++ b/internal/logging/suppress.go @@ -31,11 +31,15 @@ func NewLogSuppressor(filename string, suppress []string, linePrefix string) *Lo // Open opens log file. func (ls *LogSuppressor) Open() error { + var err error + if ls.filename == "" { return nil } - var err error - ls.logFile, err = os.OpenFile(ls.filename, os.O_APPEND|os.O_WRONLY|os.O_CREATE, 0600) + + //nolint:nosnakecase + ls.logFile, err = os.OpenFile(ls.filename, os.O_APPEND|os.O_WRONLY|os.O_CREATE, 0o600) + return err } @@ -44,15 +48,14 @@ func (ls *LogSuppressor) Close() error { if ls.filename == "" { return nil } + return ls.logFile.Close() } // Write writes p to log, and returns number f bytes written. // Implements io.Writer interface. -func (ls *LogSuppressor) Write(p []byte) (n int, err error) { - var ( - output string - ) +func (ls *LogSuppressor) Write(p []byte) (int, error) { + var output string if ls.filename == "" { return os.Stdin.Write(p) @@ -63,19 +66,19 @@ func (ls *LogSuppressor) Write(p []byte) (n int, err error) { lines := strings.Split(string(p), ls.linePrefix) for _, line := range lines { - if (func() bool { + if (func(line string) bool { for _, suppress := range ls.suppress { if strings.Contains(line, suppress) { return true } } + return false - })() { + })(line) { continue } output += line } - n, err = ls.logFile.Write([]byte(output)) - return n, err + return ls.logFile.Write([]byte(output)) } diff --git a/internal/processor/peak.go b/internal/processor/peak.go index 55f8ae6..f29767f 100644 --- a/internal/processor/peak.go +++ b/internal/processor/peak.go @@ -9,38 +9,49 @@ import ( "github.com/robfig/cron" ) -func (rp *RequestProcessor) startPeakHandling() { +func (rp *RequestProcessor) startPeakHandling() error { + var err error + c := cron.New() // cronTime := fmt.Sprintf("%d,%d * * * *", 30-prefetchInterval/60, 60-prefetchInterval/60) - c.AddFunc( + err = c.AddFunc( "24 * * * *", func() { rp.prefetchPeakRequests(&rp.peakRequest30) }, ) - c.AddFunc( + if err != nil { + return err + } + + err = c.AddFunc( "54 * * * *", func() { rp.prefetchPeakRequests(&rp.peakRequest60) }, ) + if err != nil { + return err + } + c.Start() + + return nil } func (rp *RequestProcessor) savePeakRequest(cacheDigest string, r *http.Request) { - _, min, _ := time.Now().Clock() - if min == 30 { + if _, min, _ := time.Now().Clock(); min == 30 { rp.peakRequest30.Store(cacheDigest, *r) } else if min == 0 { rp.peakRequest60.Store(cacheDigest, *r) } } -func (rp *RequestProcessor) prefetchRequest(r *http.Request) { - rp.ProcessRequest(r) +func (rp *RequestProcessor) prefetchRequest(r *http.Request) error { + _, err := rp.ProcessRequest(r) + + return err } func syncMapLen(sm *sync.Map) int { count := 0 - f := func(key, value interface{}) bool { - // Not really certain about this part, don't know for sure // if this is a good check for an entry's existence if key == "" { @@ -65,10 +76,14 @@ func (rp *RequestProcessor) prefetchPeakRequests(peakRequestMap *sync.Map) { sleepBetweenRequests := time.Duration(rp.config.Uplink.PrefetchInterval*1000/peakRequestLen) * time.Millisecond peakRequestMap.Range(func(key interface{}, value interface{}) bool { go func(r http.Request) { - rp.prefetchRequest(&r) + err := rp.prefetchRequest(&r) + if err != nil { + log.Println("prefetch request:", err) + } }(value.(http.Request)) peakRequestMap.Delete(key) time.Sleep(sleepBetweenRequests) + return true }) } diff --git a/internal/processor/processor.go b/internal/processor/processor.go index 2d507fc..cf3c329 100644 --- a/internal/processor/processor.go +++ b/internal/processor/processor.go @@ -22,19 +22,21 @@ import ( ) // plainTextAgents contains signatures of the plain-text agents. -var plainTextAgents = []string{ - "curl", - "httpie", - "lwp-request", - "wget", - "python-httpx", - "python-requests", - "openbsd ftp", - "powershell", - "fetch", - "aiohttp", - "http_get", - "xh", +func plainTextAgents() []string { + return []string{ + "curl", + "httpie", + "lwp-request", + "wget", + "python-httpx", + "python-requests", + "openbsd ftp", + "powershell", + "fetch", + "aiohttp", + "http_get", + "xh", + } } type responseWithHeader struct { @@ -99,8 +101,8 @@ func NewRequestProcessor(config *config.Config) (*RequestProcessor, error) { } // Start starts async request processor jobs, such as peak handling. -func (rp *RequestProcessor) Start() { - rp.startPeakHandling() +func (rp *RequestProcessor) Start() error { + return rp.startPeakHandling() } func (rp *RequestProcessor) ProcessRequest(r *http.Request) (*responseWithHeader, error) { @@ -124,11 +126,13 @@ func (rp *RequestProcessor) ProcessRequest(r *http.Request) (*responseWithHeader if resp, ok := redirectInsecure(r); ok { rp.stats.Inc("redirects") + return resp, nil } if dontCache(r) { rp.stats.Inc("uncached") + return get(r, rp.upstreamTransport) } @@ -193,11 +197,11 @@ func (rp *RequestProcessor) ProcessRequest(r *http.Request) (*responseWithHeader rp.lruCache.Remove(cacheDigest) } } + return response, nil } func get(req *http.Request, transport *http.Transport) (*responseWithHeader, error) { - client := &http.Client{ Transport: transport, } @@ -226,6 +230,7 @@ func get(req *http.Request, transport *http.Transport) (*responseWithHeader, err if err != nil { return nil, err } + defer res.Body.Close() body, err := ioutil.ReadAll(res.Body) if err != nil { @@ -241,9 +246,8 @@ func get(req *http.Request, transport *http.Transport) (*responseWithHeader, err }, nil } -// implementation of the cache.get_signature of original wttr.in +// getCacheDigest is an implementation of the cache.get_signature of original wttr.in. func getCacheDigest(req *http.Request) string { - userAgent := req.Header.Get("User-Agent") queryHost := req.Host @@ -256,11 +260,11 @@ func getCacheDigest(req *http.Request) string { return fmt.Sprintf("%s:%s%s:%s:%s", userAgent, queryHost, queryString, clientIPAddress, lang) } -// return true if request should not be cached +// dontCache returns true if req should not be cached. func dontCache(req *http.Request) bool { - // dont cache cyclic requests loc := strings.Split(req.RequestURI, "?")[0] + return strings.Contains(loc, ":") } @@ -269,10 +273,7 @@ func dontCache(req *http.Request) bool { // // Insecure queries are marked by the frontend web server // with X-Forwarded-Proto header: -// -// proxy_set_header X-Forwarded-Proto $scheme; -// -// +// `proxy_set_header X-Forwarded-Proto $scheme;`. func redirectInsecure(req *http.Request) (*responseWithHeader, bool) { if isPlainTextAgent(req.Header.Get("User-Agent")) { return nil, false @@ -304,14 +305,15 @@ The document has moved }, true } -// isPlainTextAgent returns true if userAgent is a plain-text agent +// isPlainTextAgent returns true if userAgent is a plain-text agent. func isPlainTextAgent(userAgent string) bool { userAgentLower := strings.ToLower(userAgent) - for _, signature := range plainTextAgents { + for _, signature := range plainTextAgents() { if strings.Contains(userAgentLower, signature) { return true } } + return false } @@ -325,6 +327,7 @@ func ipFromAddr(s string) string { if pos == -1 { return s } + return s[:pos] } @@ -336,5 +339,4 @@ func fromCadre(cadre *routing.Cadre) *responseWithHeader { StatusCode: 200, InProgress: false, } - } diff --git a/internal/routing/routing.go b/internal/routing/routing.go index ba59566..589fd5e 100644 --- a/internal/routing/routing.go +++ b/internal/routing/routing.go @@ -56,6 +56,7 @@ func (r *Router) Route(req *http.Request) Handler { return re.Handler } } + return nil } diff --git a/internal/stats/stats.go b/internal/stats/stats.go index 220c0ec..104bf45 100644 --- a/internal/stats/stats.go +++ b/internal/stats/stats.go @@ -36,6 +36,7 @@ func (c *Stats) Inc(key string) { func (c *Stats) Get(key string) int { c.m.Lock() defer c.m.Unlock() + return c.v[key] } @@ -45,14 +46,13 @@ func (c *Stats) Reset(key string) int { defer c.m.Unlock() result := c.v[key] c.v[key] = 0 + return result } // Show returns current statistics formatted as []byte. func (c *Stats) Show() []byte { - var ( - b bytes.Buffer - ) + var b bytes.Buffer c.m.Lock() defer c.m.Unlock() @@ -63,11 +63,13 @@ func (c *Stats) Show() []byte { fmt.Fprintf(&b, "%-20s: %d\n", "Uptime (min)", uptime/60) fmt.Fprintf(&b, "%-20s: %d\n", "Total queries", c.v["total"]) + if uptime != 0 { fmt.Fprintf(&b, "%-20s: %d\n", "Throughput (QpM)", c.v["total"]*60/int(uptime)) } fmt.Fprintf(&b, "%-20s: %d\n", "Cache L1 queries", c.v["cache1"]) + if c.v["total"] != 0 { fmt.Fprintf(&b, "%-20s: %d\n", "Cache L1 queries (%)", (100*c.v["cache1"])/c.v["total"]) } diff --git a/internal/types/errors.go b/internal/types/errors.go new file mode 100644 index 0000000..89d438d --- /dev/null +++ b/internal/types/errors.go @@ -0,0 +1,9 @@ +package types + +import "errors" + +var ( + ErrNotFound = errors.New("cache entry not found") + ErrInvalidCacheEntry = errors.New("invalid cache entry format") + ErrUpstream = errors.New("upstream error") +) diff --git a/internal/types/types.go b/internal/types/types.go index 24f0f6d..d28d0c6 100644 --- a/internal/types/types.go +++ b/internal/types/types.go @@ -1,15 +1,8 @@ package types -import "errors" - type CacheType string const ( CacheTypeDB = "db" CacheTypeFiles = "files" ) - -var ( - ErrNotFound = errors.New("cache entry not found") - ErrInvalidCacheEntry = errors.New("invalid cache entry format") -) diff --git a/internal/util/http.go b/internal/util/http.go index ab3f40b..e1b45d2 100644 --- a/internal/util/http.go +++ b/internal/util/http.go @@ -21,5 +21,6 @@ func ReadUserIP(r *http.Request) string { log.Printf("ERROR: userip: %q is not IP:port\n", IPAddress) } } + return IPAddress } diff --git a/internal/util/yaml.go b/internal/util/yaml.go index 21cd88b..1a71b85 100644 --- a/internal/util/yaml.go +++ b/internal/util/yaml.go @@ -10,5 +10,6 @@ import ( func YamlUnmarshalStrict(in []byte, out interface{}) error { dec := yaml.NewDecoder(bytes.NewReader(in)) dec.KnownFields(true) + return dec.Decode(out) } diff --git a/srv.go b/srv.go index a8c3739..65c1c66 100644 --- a/srv.go +++ b/srv.go @@ -18,15 +18,15 @@ import ( "github.com/chubin/wttr.in/internal/processor" ) +//nolint:gochecknoglobals var cli struct { - ConfigCheck bool `name:"config-check" help:"Check configuration"` - ConfigDump bool `name:"config-dump" help:"Dump configuration"` - GeoResolve string `name:"geo-resolve" help:"Resolve location"` - ConfigFile string `name:"config-file" arg:"" optional:"" help:"Name of configuration file"` - ConvertGeoIPCache bool `name:"convert-geo-ip-cache" help:"Convert Geo IP data cache to SQlite"` - ConvertGeoLocationCache bool `name:"convert-geo-location-cache" help:"Convert Geo Location data cache to SQlite"` + ConfigCheck bool `name:"config-check" help:"Check configuration"` + ConfigDump bool `name:"config-dump" help:"Dump configuration"` + ConvertGeoIPCache bool `name:"convert-geo-ip-cache" help:"Convert Geo IP data cache to SQlite"` + ConvertGeoLocationCache bool `name:"convert-geo-location-cache" help:"Convert Geo Location data cache to SQlite"` + GeoResolve string `name:"geo-resolve" help:"Resolve location"` } const logLineStart = "LOG_LINE_START " @@ -84,7 +84,7 @@ func serve(conf *config.Config) error { rp *processor.RequestProcessor // errs is the servers errors channel. - errs chan error = make(chan error, 1) + errs = make(chan error, 1) // numberOfServers started. If 0, exit. numberOfServers int @@ -110,15 +110,18 @@ func serve(conf *config.Config) error { rp, err = processor.NewRequestProcessor(conf) if err != nil { - log.Fatalln("log processor initialization:", err) + return fmt.Errorf("log processor initialization: %w", err) } err = errorsLog.Open() if err != nil { - log.Fatalln("errors log:", err) + return err } - rp.Start() + err = rp.Start() + if err != nil { + return err + } mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { if err := logger.Log(r); err != nil { @@ -128,17 +131,22 @@ func serve(conf *config.Config) error { response, err := rp.ProcessRequest(r) if err != nil { log.Println(err) + return } if response.StatusCode == 0 { log.Println("status code 0", response) + return } copyHeader(w.Header(), response.Header) w.Header().Set("Access-Control-Allow-Origin", "*") w.WriteHeader(response.StatusCode) - w.Write(response.Body) + _, err = w.Write(response.Body) + if err != nil { + log.Println(err) + } }) if conf.Server.PortHTTP != 0 { @@ -152,6 +160,7 @@ func serve(conf *config.Config) error { if numberOfServers == 0 { return errors.New("no servers configured") } + return <-errs // block until one of the servers writes an error } @@ -185,7 +194,9 @@ func main() { if err != nil { ctx.FatalIfErrorf(err) } + ctx.FatalIfErrorf(geoIPCache.ConvertCache()) + return } @@ -194,7 +205,9 @@ func main() { if err != nil { ctx.FatalIfErrorf(err) } + ctx.FatalIfErrorf(geoLocCache.ConvertCache()) + return } @@ -204,7 +217,6 @@ func main() { ctx.FatalIfErrorf(err) if loc != nil { fmt.Println(*loc) - } } From bb4474b0cf378f7c52939b733929ad2f9d45a593 Mon Sep 17 00:00:00 2001 From: Igor Chubin Date: Sun, 11 Dec 2022 14:28:58 +0100 Subject: [PATCH 054/105] Add linter configuration --- .golangci.yaml | 40 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 40 insertions(+) create mode 100644 .golangci.yaml diff --git a/.golangci.yaml b/.golangci.yaml new file mode 100644 index 0000000..293faec --- /dev/null +++ b/.golangci.yaml @@ -0,0 +1,40 @@ +run: + skip-dirs: + - pkg/curlator +linters: + enable-all: true + disable: + - wsl + - wrapcheck + - varnamelen + - gci + - exhaustivestruct + - exhaustruct + - gomnd + - gofmt + + # to be fixed: + - gosec + - noctx + - funlen + - nestif + - forbidigo + - funlen + - interfacer + - revive + - cyclop + - goerr113 + - forcetypeassert + - gocognit + - golint + - stylecheck + - ireturn + + # deprecated: + - scopelint + - deadcode + - varcheck + - maligned + - ifshort + - nosnakecase + - structcheck From fca62e63c3549a529086a08580a5cf43cf63e140 Mon Sep 17 00:00:00 2001 From: Igor Chubin Date: Sun, 11 Dec 2022 14:29:17 +0100 Subject: [PATCH 055/105] Add Makefile target: lint --- Makefile | 3 +++ 1 file changed, 3 insertions(+) diff --git a/Makefile b/Makefile index 94079a8..dc4616e 100644 --- a/Makefile +++ b/Makefile @@ -3,3 +3,6 @@ srv: srv.go internal/*/*.go internal/*/*/*.go go-test: go test ./... + +lint: + golangci-lint run ./... From 173b501a2d339b64a7a766f0721b5375092f17c2 Mon Sep 17 00:00:00 2001 From: Igor Chubin Date: Sun, 11 Dec 2022 14:50:06 +0100 Subject: [PATCH 056/105] Fix linter findings for: forbidigo, funlen, forcetypeassert, stylecheck --- .golangci.yaml | 8 ++--- internal/processor/peak.go | 9 ++++- internal/processor/processor.go | 64 ++++++++++++++++++--------------- srv.go | 8 +++-- 4 files changed, 52 insertions(+), 37 deletions(-) diff --git a/.golangci.yaml b/.golangci.yaml index 293faec..a6260b8 100644 --- a/.golangci.yaml +++ b/.golangci.yaml @@ -14,21 +14,16 @@ linters: - gofmt # to be fixed: + - ireturn - gosec - noctx - funlen - nestif - - forbidigo - - funlen - interfacer - revive - cyclop - goerr113 - - forcetypeassert - gocognit - - golint - - stylecheck - - ireturn # deprecated: - scopelint @@ -38,3 +33,4 @@ linters: - ifshort - nosnakecase - structcheck + - golint diff --git a/internal/processor/peak.go b/internal/processor/peak.go index f29767f..c03beb2 100644 --- a/internal/processor/peak.go +++ b/internal/processor/peak.go @@ -75,12 +75,19 @@ func (rp *RequestProcessor) prefetchPeakRequests(peakRequestMap *sync.Map) { log.Printf("PREFETCH: Prefetching %d requests\n", peakRequestLen) sleepBetweenRequests := time.Duration(rp.config.Uplink.PrefetchInterval*1000/peakRequestLen) * time.Millisecond peakRequestMap.Range(func(key interface{}, value interface{}) bool { + req, ok := value.(http.Request) + if !ok { + log.Println("missing value for:", key) + + return true + } + go func(r http.Request) { err := rp.prefetchRequest(&r) if err != nil { log.Println("prefetch request:", err) } - }(value.(http.Request)) + }(req) peakRequestMap.Delete(key) time.Sleep(sleepBetweenRequests) diff --git a/internal/processor/processor.go b/internal/processor/processor.go index cf3c329..1c97a50 100644 --- a/internal/processor/processor.go +++ b/internal/processor/processor.go @@ -107,8 +107,9 @@ func (rp *RequestProcessor) Start() error { func (rp *RequestProcessor) ProcessRequest(r *http.Request) (*responseWithHeader, error) { var ( - response *responseWithHeader - err error + response *responseWithHeader + cacheEntry responseWithHeader + err error ) ip := util.ReadUserIP(r) @@ -143,9 +144,11 @@ func (rp *RequestProcessor) ProcessRequest(r *http.Request) (*responseWithHeader rp.savePeakRequest(cacheDigest, r) cacheBody, ok := rp.lruCache.Get(cacheDigest) + if ok { + cacheEntry, ok = cacheBody.(responseWithHeader) + } if ok { rp.stats.Inc("cache1") - cacheEntry := cacheBody.(responseWithHeader) // if after all attempts we still have no answer, // we try to make the query on our own @@ -156,7 +159,9 @@ func (rp *RequestProcessor) ProcessRequest(r *http.Request) (*responseWithHeader time.Sleep(30 * time.Millisecond) cacheBody, ok = rp.lruCache.Get(cacheDigest) if ok && cacheBody != nil { - cacheEntry = cacheBody.(responseWithHeader) + if v, ok := cacheBody.(responseWithHeader); ok { + cacheEntry = v + } } } if cacheEntry.InProgress { @@ -168,34 +173,37 @@ func (rp *RequestProcessor) ProcessRequest(r *http.Request) (*responseWithHeader } } - if !foundInCache { - // Handling query. - format := r.URL.Query().Get("format") - if len(format) != 0 { - rp.stats.Inc("format") - if format == "j1" { - rp.stats.Inc("format=j1") - } - } + if foundInCache { + return response, nil + } - // How many IP addresses are known. - _, err = rp.geoIPCache.Read(ip) - if err == nil { - rp.stats.Inc("geoip") + // Response was not found in cache. + // Starting real handling. + format := r.URL.Query().Get("format") + if len(format) != 0 { + rp.stats.Inc("format") + if format == "j1" { + rp.stats.Inc("format=j1") } + } - rp.lruCache.Add(cacheDigest, responseWithHeader{InProgress: true}) + // How many IP addresses are known. + _, err = rp.geoIPCache.Read(ip) + if err == nil { + rp.stats.Inc("geoip") + } - response, err = get(r, rp.upstreamTransport) - if err != nil { - return nil, err - } - if response.StatusCode == 200 || response.StatusCode == 304 || response.StatusCode == 404 { - rp.lruCache.Add(cacheDigest, *response) - } else { - log.Printf("REMOVE: %d response for %s from cache\n", response.StatusCode, cacheDigest) - rp.lruCache.Remove(cacheDigest) - } + rp.lruCache.Add(cacheDigest, responseWithHeader{InProgress: true}) + + response, err = get(r, rp.upstreamTransport) + if err != nil { + return nil, err + } + if response.StatusCode == 200 || response.StatusCode == 304 || response.StatusCode == 404 { + rp.lruCache.Add(cacheDigest, *response) + } else { + log.Printf("REMOVE: %d response for %s from cache\n", response.StatusCode, cacheDigest) + rp.lruCache.Remove(cacheDigest) } return response, nil diff --git a/srv.go b/srv.go index 65c1c66..22b26d7 100644 --- a/srv.go +++ b/srv.go @@ -76,7 +76,7 @@ func serveHTTPS(mux *http.ServeMux, port int, certFile, keyFile string, logFile func serve(conf *config.Config) error { var ( // mux is main HTTP/HTTP requests multiplexer. - mux *http.ServeMux = http.NewServeMux() + mux = http.NewServeMux() // logger is optimized requests logger. logger *logging.RequestLogger @@ -182,10 +182,13 @@ func main() { } if cli.ConfigDump { + //nolint:forbidigo fmt.Print(string(conf.Dump())) + + return } - if cli.ConfigCheck || cli.ConfigDump { + if cli.ConfigCheck { return } @@ -216,6 +219,7 @@ func main() { loc, err := sr.Search(cli.GeoResolve) ctx.FatalIfErrorf(err) if loc != nil { + //nolint:forbidigo fmt.Println(*loc) } } From 635ac451c09a1310b5e607d366fcf3bc16418a96 Mon Sep 17 00:00:00 2001 From: Igor Chubin Date: Sun, 11 Dec 2022 14:58:40 +0100 Subject: [PATCH 057/105] Fix linter findings for: goerr113 --- .golangci.yaml | 3 +-- internal/types/errors.go | 3 +++ srv.go | 4 ++-- 3 files changed, 6 insertions(+), 4 deletions(-) diff --git a/.golangci.yaml b/.golangci.yaml index a6260b8..e516efd 100644 --- a/.golangci.yaml +++ b/.golangci.yaml @@ -19,11 +19,10 @@ linters: - noctx - funlen - nestif + - gocognit - interfacer - revive - cyclop - - goerr113 - - gocognit # deprecated: - scopelint diff --git a/internal/types/errors.go b/internal/types/errors.go index 89d438d..62c36f1 100644 --- a/internal/types/errors.go +++ b/internal/types/errors.go @@ -6,4 +6,7 @@ var ( ErrNotFound = errors.New("cache entry not found") ErrInvalidCacheEntry = errors.New("invalid cache entry format") ErrUpstream = errors.New("upstream error") + + // ErrNoServersConfigured means that there are no servers to run. + ErrNoServersConfigured = errors.New("no servers configured") ) diff --git a/srv.go b/srv.go index 22b26d7..8f4e88d 100644 --- a/srv.go +++ b/srv.go @@ -2,7 +2,6 @@ package main import ( "crypto/tls" - "errors" "fmt" "io" "log" @@ -16,6 +15,7 @@ import ( geoloc "github.com/chubin/wttr.in/internal/geo/location" "github.com/chubin/wttr.in/internal/logging" "github.com/chubin/wttr.in/internal/processor" + "github.com/chubin/wttr.in/internal/types" ) //nolint:gochecknoglobals @@ -158,7 +158,7 @@ func serve(conf *config.Config) error { numberOfServers++ } if numberOfServers == 0 { - return errors.New("no servers configured") + return types.ErrNoServersConfigured } return <-errs // block until one of the servers writes an error From 04f064460c2537971baeaec50c50b64ceda6ab6a Mon Sep 17 00:00:00 2001 From: Igor Chubin Date: Sat, 17 Dec 2022 20:49:48 +0100 Subject: [PATCH 058/105] Fix linter findings for: funlen, nestif, gocognit, revive, cyclop --- .golangci.yaml | 5 -- internal/processor/peak.go | 2 + internal/processor/processor.go | 115 ++++++++++++++++++------------- srv.go | 117 ++++++++++++++++---------------- 4 files changed, 130 insertions(+), 109 deletions(-) diff --git a/.golangci.yaml b/.golangci.yaml index e516efd..7057a7c 100644 --- a/.golangci.yaml +++ b/.golangci.yaml @@ -17,12 +17,7 @@ linters: - ireturn - gosec - noctx - - funlen - - nestif - - gocognit - interfacer - - revive - - cyclop # deprecated: - scopelint diff --git a/internal/processor/peak.go b/internal/processor/peak.go index c03beb2..05752df 100644 --- a/internal/processor/peak.go +++ b/internal/processor/peak.go @@ -35,6 +35,8 @@ func (rp *RequestProcessor) startPeakHandling() error { return nil } +// registerPeakRequest registers requests coming in the peak time. +// Such requests can be prefetched afterwards just before the peak time comes. func (rp *RequestProcessor) savePeakRequest(cacheDigest string, r *http.Request) { if _, min, _ := time.Now().Clock(); min == 30 { rp.peakRequest30.Store(cacheDigest, *r) diff --git a/internal/processor/processor.go b/internal/processor/processor.go index 1c97a50..5623c5c 100644 --- a/internal/processor/processor.go +++ b/internal/processor/processor.go @@ -39,7 +39,7 @@ func plainTextAgents() []string { } } -type responseWithHeader struct { +type ResponseWithHeader struct { InProgress bool // true if the request is being processed Expires time.Time // expiration time of the cache entry @@ -105,14 +105,12 @@ func (rp *RequestProcessor) Start() error { return rp.startPeakHandling() } -func (rp *RequestProcessor) ProcessRequest(r *http.Request) (*responseWithHeader, error) { +func (rp *RequestProcessor) ProcessRequest(r *http.Request) (*ResponseWithHeader, error) { var ( - response *responseWithHeader - cacheEntry responseWithHeader - err error + response *ResponseWithHeader + ip = util.ReadUserIP(r) ) - ip := util.ReadUserIP(r) if ip != "127.0.0.1" { rp.stats.Inc("total") } @@ -137,46 +135,68 @@ func (rp *RequestProcessor) ProcessRequest(r *http.Request) (*responseWithHeader return get(r, rp.upstreamTransport) } + // processing cached request cacheDigest := getCacheDigest(r) - foundInCache := false - rp.savePeakRequest(cacheDigest, r) - cacheBody, ok := rp.lruCache.Get(cacheDigest) - if ok { - cacheEntry, ok = cacheBody.(responseWithHeader) - } - if ok { - rp.stats.Inc("cache1") - - // if after all attempts we still have no answer, - // we try to make the query on our own - for attempts := 0; attempts < 300; attempts++ { - if !ok || !cacheEntry.InProgress { - break - } - time.Sleep(30 * time.Millisecond) - cacheBody, ok = rp.lruCache.Get(cacheDigest) - if ok && cacheBody != nil { - if v, ok := cacheBody.(responseWithHeader); ok { - cacheEntry = v - } - } - } - if cacheEntry.InProgress { - log.Printf("TIMEOUT: %s\n", cacheDigest) - } - if ok && !cacheEntry.InProgress && cacheEntry.Expires.After(time.Now()) { - response = &cacheEntry - foundInCache = true - } - } - - if foundInCache { + response = rp.processRequestFromCache(r) + if response != nil { return response, nil } + return rp.processUncachedRequest(r) +} + +// processRequestFromCache processes requests using the cache. +// If no entry in cache found, nil is returned. +func (rp *RequestProcessor) processRequestFromCache(r *http.Request) *ResponseWithHeader { + var ( + cacheEntry ResponseWithHeader + cacheDigest = getCacheDigest(r) + ok bool + ) + + cacheBody, _ := rp.lruCache.Get(cacheDigest) + cacheEntry, ok = cacheBody.(ResponseWithHeader) + if !ok { + return nil + } + + // if after all attempts we still have no answer, + // we try to make the query on our own + for attempts := 0; attempts < 300; attempts++ { + if !ok || !cacheEntry.InProgress { + break + } + time.Sleep(30 * time.Millisecond) + cacheBody, _ = rp.lruCache.Get(cacheDigest) + v, ok := cacheBody.(ResponseWithHeader) + if ok { + cacheEntry = v + } + } + if cacheEntry.InProgress { + log.Printf("TIMEOUT: %s\n", cacheDigest) + } + if ok && !cacheEntry.InProgress && cacheEntry.Expires.After(time.Now()) { + rp.stats.Inc("cache1") + + return &cacheEntry + } + + return nil +} + +// processUncachedRequest processes requests that were not found in the cache. +func (rp *RequestProcessor) processUncachedRequest(r *http.Request) (*ResponseWithHeader, error) { + var ( + cacheDigest = getCacheDigest(r) + ip = util.ReadUserIP(r) + response *ResponseWithHeader + err error + ) + // Response was not found in cache. // Starting real handling. format := r.URL.Query().Get("format") @@ -187,13 +207,14 @@ func (rp *RequestProcessor) ProcessRequest(r *http.Request) (*responseWithHeader } } - // How many IP addresses are known. + // Count, how many IP addresses are known. _, err = rp.geoIPCache.Read(ip) if err == nil { rp.stats.Inc("geoip") } - rp.lruCache.Add(cacheDigest, responseWithHeader{InProgress: true}) + // Indicate, that the request is being handled. + rp.lruCache.Add(cacheDigest, ResponseWithHeader{InProgress: true}) response, err = get(r, rp.upstreamTransport) if err != nil { @@ -209,7 +230,7 @@ func (rp *RequestProcessor) ProcessRequest(r *http.Request) (*responseWithHeader return response, nil } -func get(req *http.Request, transport *http.Transport) (*responseWithHeader, error) { +func get(req *http.Request, transport *http.Transport) (*ResponseWithHeader, error) { client := &http.Client{ Transport: transport, } @@ -245,7 +266,7 @@ func get(req *http.Request, transport *http.Transport) (*responseWithHeader, err return nil, err } - return &responseWithHeader{ + return &ResponseWithHeader{ InProgress: false, Expires: time.Now().Add(time.Duration(randInt(1000, 1500)) * time.Second), Body: body, @@ -282,7 +303,7 @@ func dontCache(req *http.Request) bool { // Insecure queries are marked by the frontend web server // with X-Forwarded-Proto header: // `proxy_set_header X-Forwarded-Proto $scheme;`. -func redirectInsecure(req *http.Request) (*responseWithHeader, bool) { +func redirectInsecure(req *http.Request) (*ResponseWithHeader, bool) { if isPlainTextAgent(req.Header.Get("User-Agent")) { return nil, false } @@ -304,7 +325,7 @@ The document has moved `, target)) - return &responseWithHeader{ + return &ResponseWithHeader{ InProgress: false, Expires: time.Now().Add(time.Duration(randInt(1000, 1500)) * time.Second), Body: body, @@ -340,8 +361,8 @@ func ipFromAddr(s string) string { } // fromCadre converts Cadre into a responseWithHeader. -func fromCadre(cadre *routing.Cadre) *responseWithHeader { - return &responseWithHeader{ +func fromCadre(cadre *routing.Cadre) *ResponseWithHeader { + return &ResponseWithHeader{ Body: cadre.Body, Expires: cadre.Expires, StatusCode: 200, diff --git a/srv.go b/srv.go index 8f4e88d..25c11a1 100644 --- a/srv.go +++ b/srv.go @@ -79,7 +79,9 @@ func serve(conf *config.Config) error { mux = http.NewServeMux() // logger is optimized requests logger. - logger *logging.RequestLogger + logger = logging.NewRequestLogger( + conf.Logging.AccessLog, + time.Duration(conf.Logging.Interval)*time.Second) rp *processor.RequestProcessor @@ -89,25 +91,18 @@ func serve(conf *config.Config) error { // numberOfServers started. If 0, exit. numberOfServers int - errorsLog *logging.LogSuppressor + errorsLog = logging.NewLogSuppressor( + conf.Logging.ErrorsLog, + []string{ + "error reading preface from client", + "TLS handshake error from", + }, + logLineStart, + ) err error ) - // logger is optimized requests logger. - logger = logging.NewRequestLogger( - conf.Logging.AccessLog, - time.Duration(conf.Logging.Interval)*time.Second) - - errorsLog = logging.NewLogSuppressor( - conf.Logging.ErrorsLog, - []string{ - "error reading preface from client", - "TLS handshake error from", - }, - logLineStart, - ) - rp, err = processor.NewRequestProcessor(conf) if err != nil { return fmt.Errorf("log processor initialization: %w", err) @@ -123,11 +118,32 @@ func serve(conf *config.Config) error { return err } - mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { + mux.HandleFunc("/", mainHandler(rp, logger)) + + if conf.Server.PortHTTP != 0 { + go serveHTTP(mux, conf.Server.PortHTTP, errorsLog, errs) + numberOfServers++ + } + if conf.Server.PortHTTPS != 0 { + go serveHTTPS(mux, conf.Server.PortHTTPS, conf.Server.TLSCertFile, conf.Server.TLSKeyFile, errorsLog, errs) + numberOfServers++ + } + if numberOfServers == 0 { + return types.ErrNoServersConfigured + } + + return <-errs // block until one of the servers writes an error +} + +func mainHandler( + rp *processor.RequestProcessor, + logger *logging.RequestLogger, +) func(http.ResponseWriter, *http.Request) { + return func(w http.ResponseWriter, r *http.Request) { if err := logger.Log(r); err != nil { log.Println(err) } - // printStat() + response, err := rp.ProcessRequest(r) if err != nil { log.Println(err) @@ -147,21 +163,7 @@ func serve(conf *config.Config) error { if err != nil { log.Println(err) } - }) - - if conf.Server.PortHTTP != 0 { - go serveHTTP(mux, conf.Server.PortHTTP, errorsLog, errs) - numberOfServers++ } - if conf.Server.PortHTTPS != 0 { - go serveHTTPS(mux, conf.Server.PortHTTPS, conf.Server.TLSCertFile, conf.Server.TLSKeyFile, errorsLog, errs) - numberOfServers++ - } - if numberOfServers == 0 { - return types.ErrNoServersConfigured - } - - return <-errs // block until one of the servers writes an error } func main() { @@ -192,29 +194,12 @@ func main() { return } - if cli.ConvertGeoIPCache { - geoIPCache, err := geoip.NewCache(conf) - if err != nil { - ctx.FatalIfErrorf(err) - } - - ctx.FatalIfErrorf(geoIPCache.ConvertCache()) - - return - } - - if cli.ConvertGeoLocationCache { - geoLocCache, err := geoloc.NewCache(conf) - if err != nil { - ctx.FatalIfErrorf(err) - } - - ctx.FatalIfErrorf(geoLocCache.ConvertCache()) - - return - } - - if cli.GeoResolve != "" { + switch { + case cli.ConvertGeoIPCache: + ctx.FatalIfErrorf(convertGeoIPCache(conf)) + case cli.ConvertGeoLocationCache: + ctx.FatalIfErrorf(convertGeoLocationCache(conf)) + case cli.GeoResolve != "": sr := geoloc.NewSearcher(conf) loc, err := sr.Search(cli.GeoResolve) ctx.FatalIfErrorf(err) @@ -222,8 +207,26 @@ func main() { //nolint:forbidigo fmt.Println(*loc) } + default: + err = serve(conf) + ctx.FatalIfErrorf(err) + } +} + +func convertGeoIPCache(conf *config.Config) error { + geoIPCache, err := geoip.NewCache(conf) + if err != nil { + return err } - err = serve(conf) - ctx.FatalIfErrorf(err) + return geoIPCache.ConvertCache() +} + +func convertGeoLocationCache(conf *config.Config) error { + geoLocCache, err := geoloc.NewCache(conf) + if err != nil { + return err + } + + return geoLocCache.ConvertCache() } From 08794675a7d6d75898b6bbbdac583d0c0d1cac20 Mon Sep 17 00:00:00 2001 From: Igor Chubin Date: Sun, 18 Dec 2022 15:44:06 +0100 Subject: [PATCH 059/105] Add github.com/sirupsen/logrus to dependencies --- go.mod | 1 + go.sum | 5 +++++ 2 files changed, 6 insertions(+) diff --git a/go.mod b/go.mod index bf7549e..5b2ca40 100644 --- a/go.mod +++ b/go.mod @@ -12,6 +12,7 @@ require ( github.com/mattn/go-sqlite3 v1.14.16 // indirect github.com/robfig/cron v1.2.0 github.com/samonzeweb/godb v1.0.8 // indirect + github.com/sirupsen/logrus v1.9.0 // indirect github.com/smartystreets/assertions v1.2.0 // indirect github.com/smartystreets/goconvey v1.6.4 // indirect github.com/stretchr/testify v1.8.1 // indirect diff --git a/go.sum b/go.sum index 084eb3f..81f1c89 100644 --- a/go.sum +++ b/go.sum @@ -27,6 +27,8 @@ github.com/samonzeweb/godb v1.0.8 h1:WRn6nq0FChYOzh+w8SgpXHUkEhL7W6ZqkCf5Ninx7Uc github.com/samonzeweb/godb v1.0.8/go.mod h1:LNDt3CakfBwpRY4AD0y/QPTbj+jB6O17tSxQES0p47o= github.com/samonzeweb/godb v1.0.15 h1:HyNb8o1w109as9KWE8ih1YIBe8jC4luJ22f1XNacW38= github.com/samonzeweb/godb v1.0.15/go.mod h1:SxCHqyireDXNrZApknS9lGUEutA43x9eJF632ecbK5Q= +github.com/sirupsen/logrus v1.9.0 h1:trlNQbNUG3OdDrDil03MCb1H2o9nJ1x4/5LYw7byDE0= +github.com/sirupsen/logrus v1.9.0/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc= github.com/smartystreets/assertions v1.2.0/go.mod h1:tcbTF8ujkAEcZ8TElKY+i30BzYlVhC/LOxJk7iOWnoo= github.com/smartystreets/goconvey v1.6.4/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA= @@ -34,6 +36,7 @@ github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+ github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= github.com/stretchr/objx v0.5.0 h1:1zr/of2m5FGMsad5YfcqgdqdWrIhu+EBEJRhR1U7z/c= github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= +github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= github.com/stretchr/testify v1.8.1 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKsk= @@ -47,6 +50,8 @@ golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 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= +golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8 h1:0A+M6Uqn+Eje4kHMK80dtF3JCXC4ykBgQG4Fe06QRhQ= +golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/tools v0.0.0-20190328211700-ab21143f2384/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= From c398d9204d10fed995fd3ea6916f3576be0e1b9a Mon Sep 17 00:00:00 2001 From: Igor Chubin Date: Sun, 18 Dec 2022 15:44:20 +0100 Subject: [PATCH 060/105] Implement location resolution interface --- internal/geo/location/cache.go | 78 +++++++++++++++++++++++++++++- internal/geo/location/location.go | 41 ---------------- internal/geo/location/nominatim.go | 5 +- internal/geo/location/response.go | 41 ++++++++++++++++ internal/geo/location/search.go | 42 ++++++++++++++++ internal/processor/processor.go | 9 ++++ srv.go | 19 ++++++-- 7 files changed, 188 insertions(+), 47 deletions(-) create mode 100644 internal/geo/location/response.go create mode 100644 internal/geo/location/search.go diff --git a/internal/geo/location/cache.go b/internal/geo/location/cache.go index 8530c2c..7aca700 100644 --- a/internal/geo/location/cache.go +++ b/internal/geo/location/cache.go @@ -2,13 +2,16 @@ package location import ( "encoding/json" + "errors" "fmt" "os" "path" + "strconv" "strings" "github.com/samonzeweb/godb" "github.com/samonzeweb/godb/adapters/sqlite" + log "github.com/sirupsen/logrus" "github.com/zsefvlol/timezonemapper" "github.com/chubin/wttr.in/internal/config" @@ -22,6 +25,7 @@ import ( type Cache struct { config *config.Config db *godb.DB + searcher *Searcher indexField string filesCacheDir string } @@ -34,11 +38,14 @@ func NewCache(config *config.Config) (*Cache, error) { ) if config.Geo.LocationCacheType == types.CacheTypeDB { - db, err = godb.Open(sqlite.Adapter, config.Geo.IPCacheDB) + log.Debugln("using db for location cache") + db, err = godb.Open(sqlite.Adapter, config.Geo.LocationCacheDB) if err != nil { return nil, err } + log.Debugln("db file:", config.Geo.LocationCacheDB) + // Needed for "upsert" implementation in Put() db.UseErrorParser() } @@ -48,9 +55,39 @@ func NewCache(config *config.Config) (*Cache, error) { db: db, indexField: "name", filesCacheDir: config.Geo.LocationCache, + searcher: NewSearcher(config), }, nil } +// Resolve returns location information for specified location. +// If the information is found in the cache, it is returned. +// If it is not found, the external service is queried, +// and the result is stored in the cache. +func (c *Cache) Resolve(location string) (*Location, error) { + location = normalizeLocationName(location) + + loc, err := c.Read(location) + if !errors.Is(err, types.ErrNotFound) { + return loc, err + } + + log.Debugln("geo/location: not found in cache:", location) + loc, err = c.searcher.Search(location) + if err != nil { + return nil, err + } + + loc.Name = location + loc.Timezone = latLngToTimezoneString(loc.Lat, loc.Lon) + + err = c.Put(location, loc) + if err != nil { + return nil, err + } + + return loc, nil +} + // Read returns location information from the cache, if found, // or types.ErrNotFound if not found. If the entry is found, but its format // is invalid, types.ErrInvalidCacheEntry is returned. @@ -108,6 +145,11 @@ func (c *Cache) readFromCacheDB(addr string) (*Location, error) { err := c.db.Select(&result). Where(c.indexField+" = ?", addr). Do() + + if strings.Contains(fmt.Sprint(err), "no rows in result set") { + return nil, types.ErrNotFound + } + if err != nil { return nil, err } @@ -116,6 +158,7 @@ func (c *Cache) readFromCacheDB(addr string) (*Location, error) { } func (c *Cache) Put(addr string, loc *Location) error { + log.Infoln("geo/location: storing in cache:", loc) if c.config.Geo.IPCacheType == types.CacheTypeDB { return c.putToCacheDB(loc) } @@ -140,3 +183,36 @@ func (c *Cache) putToCacheFile(addr string, loc fmt.Stringer) error { func (c *Cache) cacheFile(item string) string { return path.Join(c.filesCacheDir, item) } + +// normalizeLocationName converts name into the standard location form +// with the following steps: +// - remove excessive spaces, +// - remove quotes, +// - convert to lover case. +func normalizeLocationName(name string) string { + name = strings.ReplaceAll(name, `"`, " ") + name = strings.ReplaceAll(name, `'`, " ") + name = strings.TrimSpace(name) + name = strings.Join(strings.Fields(name), " ") + + return strings.ToLower(name) +} + +// latLngToTimezoneString returns timezone for lat, lon, +// or an empty string if they are invalid. +func latLngToTimezoneString(lat, lon string) string { + latFloat, err := strconv.ParseFloat(lat, 64) + if err != nil { + log.Errorln("geoloc: latLngToTimezoneString:", err) + + return "" + } + lonFloat, err := strconv.ParseFloat(lon, 64) + if err != nil { + log.Errorln("geoloc: latLngToTimezoneString:", err) + + return "" + } + + return timezonemapper.LatLngToTimezoneString(latFloat, lonFloat) +} diff --git a/internal/geo/location/location.go b/internal/geo/location/location.go index f33b20d..0338b40 100644 --- a/internal/geo/location/location.go +++ b/internal/geo/location/location.go @@ -3,8 +3,6 @@ package location import ( "encoding/json" "log" - - "github.com/chubin/wttr.in/internal/config" ) type Location struct { @@ -26,42 +24,3 @@ func (l *Location) String() string { return string(bytes) } - -type Provider interface { - Query(location string) (*Location, error) -} - -type Searcher struct { - providers []Provider -} - -// NewSearcher returns a new Searcher for the specified config. -func NewSearcher(config *config.Config) *Searcher { - providers := []Provider{} - for _, p := range config.Geo.Nominatim { - providers = append(providers, NewNominatim(p.Name, p.URL, p.Token)) - } - - return &Searcher{ - providers: providers, - } -} - -// Search makes queries through all known providers, -// and returns response, as soon as it is not nil. -// If all responses were nil, the last response is returned. -func (s *Searcher) Search(location string) (*Location, error) { - var ( - err error - result *Location - ) - - for _, p := range s.providers { - result, err = p.Query(location) - if result != nil && err == nil { - return result, nil - } - } - - return result, err -} diff --git a/internal/geo/location/nominatim.go b/internal/geo/location/nominatim.go index e16eef7..6b3e752 100644 --- a/internal/geo/location/nominatim.go +++ b/internal/geo/location/nominatim.go @@ -4,11 +4,11 @@ import ( "encoding/json" "fmt" "io/ioutil" - "log" "net/http" "net/url" "github.com/chubin/wttr.in/internal/types" + log "github.com/sirupsen/logrus" ) type Nominatim struct { @@ -38,7 +38,7 @@ func (n *Nominatim) Query(location string) (*Location, error) { "%s?q=%s&format=json&accept-language=native&limit=1&key=%s", n.url, url.QueryEscape(location), n.token) - log.Println(urlws) + log.Debugln("nominatim:", urlws) resp, err := http.Get(urlws) if err != nil { return nil, fmt.Errorf("%s: %w", n.name, err) @@ -55,6 +55,7 @@ func (n *Nominatim) Query(location string) (*Location, error) { return nil, fmt.Errorf("%w: %s: %s", types.ErrUpstream, n.name, errResponse.Error) } + log.Debugln("nominatim: response: ", string(body)) err = json.Unmarshal(body, &result) if err != nil { return nil, fmt.Errorf("%s: %w", n.name, err) diff --git a/internal/geo/location/response.go b/internal/geo/location/response.go new file mode 100644 index 0000000..6ebc6fa --- /dev/null +++ b/internal/geo/location/response.go @@ -0,0 +1,41 @@ +package location + +import ( + "encoding/json" + "fmt" + "net/http" + + "github.com/chubin/wttr.in/internal/routing" +) + +// Response provides routing interface to the geo cache. +func (c *Cache) Response(r *http.Request) *routing.Cadre { + var ( + locationName = r.URL.Query().Get("location") + loc *Location + bytes []byte + err error + ) + + if locationName == "" { + return errorResponse("location is not specified") + } + + loc, err = c.Resolve(locationName) + if err != nil { + return errorResponse(fmt.Sprint(err)) + } + + bytes, err = json.Marshal(loc) + if err != nil { + return errorResponse(fmt.Sprint(err)) + } + + return &routing.Cadre{Body: bytes} +} + +func errorResponse(s string) *routing.Cadre { + return &routing.Cadre{Body: []byte( + fmt.Sprintf(`{"error": %q}`, s), + )} +} diff --git a/internal/geo/location/search.go b/internal/geo/location/search.go new file mode 100644 index 0000000..c05cdb0 --- /dev/null +++ b/internal/geo/location/search.go @@ -0,0 +1,42 @@ +package location + +import "github.com/chubin/wttr.in/internal/config" + +type Provider interface { + Query(location string) (*Location, error) +} + +type Searcher struct { + providers []Provider +} + +// NewSearcher returns a new Searcher for the specified config. +func NewSearcher(config *config.Config) *Searcher { + providers := []Provider{} + for _, p := range config.Geo.Nominatim { + providers = append(providers, NewNominatim(p.Name, p.URL, p.Token)) + } + + return &Searcher{ + providers: providers, + } +} + +// Search makes queries through all known providers, +// and returns response, as soon as it is not nil. +// If all responses were nil, the last response is returned. +func (s *Searcher) Search(location string) (*Location, error) { + var ( + err error + result *Location + ) + + for _, p := range s.providers { + result, err = p.Query(location) + if result != nil && err == nil { + return result, nil + } + } + + return result, err +} diff --git a/internal/processor/processor.go b/internal/processor/processor.go index 5623c5c..90abea7 100644 --- a/internal/processor/processor.go +++ b/internal/processor/processor.go @@ -16,6 +16,7 @@ import ( "github.com/chubin/wttr.in/internal/config" geoip "github.com/chubin/wttr.in/internal/geo/ip" + geoloc "github.com/chubin/wttr.in/internal/geo/location" "github.com/chubin/wttr.in/internal/routing" "github.com/chubin/wttr.in/internal/stats" "github.com/chubin/wttr.in/internal/util" @@ -58,6 +59,7 @@ type RequestProcessor struct { upstreamTransport *http.Transport config *config.Config geoIPCache *geoip.Cache + geoLocation *geoloc.Cache } // NewRequestProcessor returns new RequestProcessor. @@ -84,18 +86,25 @@ func NewRequestProcessor(config *config.Config) (*RequestProcessor, error) { return nil, err } + geoLocation, err := geoloc.NewCache(config) + if err != nil { + return nil, err + } + rp := &RequestProcessor{ lruCache: lruCache, stats: stats.New(), upstreamTransport: transport, config: config, geoIPCache: geoCache, + geoLocation: geoLocation, } // Initialize routes. rp.router.AddPath("/:stats", rp.stats) rp.router.AddPath("/:geo-ip-get", rp.geoIPCache) rp.router.AddPath("/:geo-ip-put", rp.geoIPCache) + rp.router.AddPath("/:geo-location", rp.geoLocation) return rp, nil } diff --git a/srv.go b/srv.go index 25c11a1..62afa87 100644 --- a/srv.go +++ b/srv.go @@ -4,11 +4,12 @@ import ( "crypto/tls" "fmt" "io" - "log" + stdlog "log" "net/http" "time" "github.com/alecthomas/kong" + log "github.com/sirupsen/logrus" "github.com/chubin/wttr.in/internal/config" geoip "github.com/chubin/wttr.in/internal/geo/ip" @@ -27,6 +28,7 @@ var cli struct { ConvertGeoIPCache bool `name:"convert-geo-ip-cache" help:"Convert Geo IP data cache to SQlite"` ConvertGeoLocationCache bool `name:"convert-geo-location-cache" help:"Convert Geo Location data cache to SQlite"` GeoResolve string `name:"geo-resolve" help:"Resolve location"` + LogLevel string `name:"log-level" short:"l" help:"Show log messages with level" default:"info"` } const logLineStart = "LOG_LINE_START " @@ -42,7 +44,7 @@ func copyHeader(dst, src http.Header) { func serveHTTP(mux *http.ServeMux, port int, logFile io.Writer, errs chan<- error) { srv := &http.Server{ Addr: fmt.Sprintf(":%d", port), - ErrorLog: log.New(logFile, logLineStart, log.LstdFlags), + ErrorLog: stdlog.New(logFile, logLineStart, stdlog.LstdFlags), ReadTimeout: 5 * time.Second, WriteTimeout: 10 * time.Second, IdleTimeout: 1 * time.Second, @@ -63,7 +65,7 @@ func serveHTTPS(mux *http.ServeMux, port int, certFile, keyFile string, logFile } srv := &http.Server{ Addr: fmt.Sprintf(":%d", port), - ErrorLog: log.New(logFile, logLineStart, log.LstdFlags), + ErrorLog: stdlog.New(logFile, logLineStart, stdlog.LstdFlags), ReadTimeout: 5 * time.Second, WriteTimeout: 20 * time.Second, IdleTimeout: 1 * time.Second, @@ -173,6 +175,7 @@ func main() { ) ctx := kong.Parse(&cli) + ctx.FatalIfErrorf(setLogLevel(cli.LogLevel)) if cli.ConfigFile != "" { conf, err = config.Load(cli.ConfigFile) @@ -230,3 +233,13 @@ func convertGeoLocationCache(conf *config.Config) error { return geoLocCache.ConvertCache() } + +func setLogLevel(logLevel string) error { + parsedLevel, err := log.ParseLevel(logLevel) + if err != nil { + return err + } + log.SetLevel(parsedLevel) + + return nil +} From 0e2e39774e44b31b049796d763af43cf4040a5a6 Mon Sep 17 00:00:00 2001 From: Igor Chubin Date: Sun, 18 Dec 2022 15:46:13 +0100 Subject: [PATCH 061/105] Use db-based location cache by default --- internal/config/config.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/internal/config/config.go b/internal/config/config.go index e33a6ea..60c8386 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -75,7 +75,7 @@ type Geo struct { // IPCacheDB contains the path to the SQLite DB with the IP Geodata cache. IPCacheDB string `yaml:"ipCacheDb,omitempty"` - IPCacheType types.CacheType `yaml:"cacheType,omitempty"` + IPCacheType types.CacheType `yaml:"ipCacheType,omitempty"` // LocationCache contains the path to the Location Geodata cache. LocationCache string `yaml:"locationCache,omitempty"` @@ -108,7 +108,7 @@ func Default() *Config { IPCacheType: types.CacheTypeDB, LocationCache: "/wttr.in/cache/loc", LocationCacheDB: "/wttr.in/cache/geoloc.db", - LocationCacheType: types.CacheTypeFiles, + LocationCacheType: types.CacheTypeDB, Nominatim: []Nominatim{ { Name: "locationiq", From d91c6da43efa25839142cc13c7ca48f72543e419 Mon Sep 17 00:00:00 2001 From: Igor Chubin Date: Sun, 18 Dec 2022 16:24:55 +0100 Subject: [PATCH 062/105] Suppress more HTTP log messages --- srv.go | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/srv.go b/srv.go index 62afa87..97fdbb6 100644 --- a/srv.go +++ b/srv.go @@ -33,6 +33,15 @@ var cli struct { const logLineStart = "LOG_LINE_START " +func suppressMessages() []string { + return []string{ + "error reading preface from client", + "TLS handshake error from", + "URL query contains semicolon, which is no longer a supported separator", + "connection error: PROTOCOL_ERROR", + } +} + func copyHeader(dst, src http.Header) { for k, vv := range src { for _, v := range vv { @@ -95,10 +104,7 @@ func serve(conf *config.Config) error { errorsLog = logging.NewLogSuppressor( conf.Logging.ErrorsLog, - []string{ - "error reading preface from client", - "TLS handshake error from", - }, + suppressMessages(), logLineStart, ) From c9fc3aa74a9aa9157599f55b5049e08faeab3acb Mon Sep 17 00:00:00 2001 From: Igor Chubin Date: Sun, 18 Dec 2022 16:26:30 +0100 Subject: [PATCH 063/105] Rename Location to Address in geo/ip --- internal/geo/ip/convert.go | 8 ++++---- internal/geo/ip/ip.go | 28 ++++++++++++++-------------- internal/geo/ip/ip_test.go | 12 ++++++------ 3 files changed, 24 insertions(+), 24 deletions(-) diff --git a/internal/geo/ip/convert.go b/internal/geo/ip/convert.go index e8cbd10..bca972a 100644 --- a/internal/geo/ip/convert.go +++ b/internal/geo/ip/convert.go @@ -25,20 +25,20 @@ func (c *Cache) ConvertCache() error { return err } - err = createTable(db, "Location") + err = createTable(db, "Address") if err != nil { return err } log.Println("listing cache entries...") - files, err := filepath.Glob(filepath.Join(c.config.Geo.LocationCache, "*")) + files, err := filepath.Glob(filepath.Join(c.config.Geo.IPCache, "*")) if err != nil { return err } log.Printf("going to convert %d entries\n", len(files)) - block := []Location{} + block := []Address{} for i, file := range files { ip := filepath.Base(file) loc, err := c.Read(ip) @@ -58,7 +58,7 @@ func (c *Cache) ConvertCache() error { if err != nil { return err } - block = []Location{} + block = []Address{} log.Println("converted", i+1, "entries") } diff --git a/internal/geo/ip/ip.go b/internal/geo/ip/ip.go index 08cb7f1..91f3450 100644 --- a/internal/geo/ip/ip.go +++ b/internal/geo/ip/ip.go @@ -18,8 +18,8 @@ import ( "github.com/samonzeweb/godb/adapters/sqlite" ) -// Location information. -type Location struct { +// Address information. +type Address struct { IP string `db:"ip,key"` CountryCode string `db:"countryCode"` Country string `db:"country"` @@ -29,7 +29,7 @@ type Location struct { Longitude float64 `db:"longitude"` } -func (l *Location) String() string { +func (l *Address) String() string { if l.Latitude == -1000 { return fmt.Sprintf( "%s;%s;%s;%s", @@ -76,7 +76,7 @@ func NewCache(config *config.Config) (*Cache, error) { // DE;Germany;Free and Hanseatic City of Hamburg;Hamburg;53.5736;9.9782 // -func (c *Cache) Read(addr string) (*Location, error) { +func (c *Cache) Read(addr string) (*Address, error) { if c.config.Geo.IPCacheType == types.CacheTypeDB { return c.readFromCacheDB(addr) } @@ -84,17 +84,17 @@ func (c *Cache) Read(addr string) (*Location, error) { return c.readFromCacheFile(addr) } -func (c *Cache) readFromCacheFile(addr string) (*Location, error) { +func (c *Cache) readFromCacheFile(addr string) (*Address, error) { bytes, err := os.ReadFile(c.cacheFile(addr)) if err != nil { return nil, types.ErrNotFound } - return NewLocationFromString(addr, string(bytes)) + return NewAddressFromString(addr, string(bytes)) } -func (c *Cache) readFromCacheDB(addr string) (*Location, error) { - result := Location{} +func (c *Cache) readFromCacheDB(addr string) (*Address, error) { + result := Address{} err := c.db.Select(&result). Where("IP = ?", addr). Do() @@ -105,7 +105,7 @@ func (c *Cache) readFromCacheDB(addr string) (*Location, error) { return &result, nil } -func (c *Cache) Put(addr string, loc *Location) error { +func (c *Cache) Put(addr string, loc *Address) error { if c.config.Geo.IPCacheType == types.CacheTypeDB { return c.putToCacheDB(loc) } @@ -113,7 +113,7 @@ func (c *Cache) Put(addr string, loc *Location) error { return c.putToCacheFile(addr, loc) } -func (c *Cache) putToCacheDB(loc *Location) error { +func (c *Cache) putToCacheDB(loc *Address) error { err := c.db.Insert(loc).Do() // it should work like this: // @@ -140,9 +140,9 @@ func (c *Cache) cacheFile(addr string) string { return path.Join(c.config.Geo.IPCache, addr) } -// NewLocationFromString parses the location cache entry s, +// NewAddressFromString parses the location cache entry s, // and return location, or error, if the cache entry is invalid. -func NewLocationFromString(addr, s string) (*Location, error) { +func NewAddressFromString(addr, s string) (*Address, error) { var ( lat float64 = -1000 long float64 = -1000 @@ -166,7 +166,7 @@ func NewLocationFromString(addr, s string) (*Location, error) { } } - return &Location{ + return &Address{ IP: addr, CountryCode: parts[0], Country: parts[1], @@ -207,7 +207,7 @@ func (c *Cache) Response(r *http.Request) *routing.Cadre { return respERR } - location, err := NewLocationFromString(ip, value) + location, err := NewAddressFromString(ip, value) if err != nil { return respERR } diff --git a/internal/geo/ip/ip_test.go b/internal/geo/ip/ip_test.go index 91a9213..9ea2576 100644 --- a/internal/geo/ip/ip_test.go +++ b/internal/geo/ip/ip_test.go @@ -15,13 +15,13 @@ func TestParseCacheEntry(t *testing.T) { tests := []struct { addr string input string - expected Location + expected Address err error }{ { "1.2.3.4", "DE;Germany;Free and Hanseatic City of Hamburg;Hamburg;53.5736;9.9782", - Location{ + Address{ IP: "1.2.3.4", CountryCode: "DE", Country: "Germany", @@ -36,7 +36,7 @@ func TestParseCacheEntry(t *testing.T) { { "1.2.3.4", "ES;Spain;Madrid, Comunidad de;Madrid;40.4165;-3.70256;28223;Orange Espagne SA;orange.es", - Location{ + Address{ IP: "1.2.3.4", CountryCode: "ES", Country: "Spain", @@ -51,7 +51,7 @@ func TestParseCacheEntry(t *testing.T) { { "1.2.3.4", "US;United States of America;California;Mountain View", - Location{ + Address{ IP: "1.2.3.4", CountryCode: "US", Country: "United States of America", @@ -67,13 +67,13 @@ func TestParseCacheEntry(t *testing.T) { { "1.2.3.4", "DE;Germany;Free and Hanseatic City of Hamburg;Hamburg;53.5736;XXX", - Location{}, + Address{}, types.ErrInvalidCacheEntry, }, } for _, tt := range tests { - result, err := NewLocationFromString(tt.addr, tt.input) + result, err := NewAddressFromString(tt.addr, tt.input) if tt.err == nil { require.NoError(t, err) require.Equal(t, *result, tt.expected) From 45900bc565fb6c9e9ef1a5f99c545d5a734a311f Mon Sep 17 00:00:00 2001 From: Igor Chubin Date: Sun, 18 Dec 2022 16:27:12 +0100 Subject: [PATCH 064/105] Prepend function name when doing logging in readFromCacheDB --- internal/geo/location/cache.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/internal/geo/location/cache.go b/internal/geo/location/cache.go index 7aca700..a68dd0e 100644 --- a/internal/geo/location/cache.go +++ b/internal/geo/location/cache.go @@ -151,7 +151,7 @@ func (c *Cache) readFromCacheDB(addr string) (*Location, error) { } if err != nil { - return nil, err + return nil, fmt.Errorf("readFromCacheDB: %w", err) } return &result, nil From 2e67874e047539777cc81e78a318ce6bcde05e4f Mon Sep 17 00:00:00 2001 From: Igor Chubin Date: Thu, 22 Dec 2022 18:07:01 +0100 Subject: [PATCH 065/105] Add NominatimLocation --- internal/geo/ip/ip.go | 4 ++-- internal/geo/location/location.go | 11 +++++------ internal/geo/location/nominatim.go | 18 ++++++++++++++++-- 3 files changed, 23 insertions(+), 10 deletions(-) diff --git a/internal/geo/ip/ip.go b/internal/geo/ip/ip.go index 91f3450..c494bd4 100644 --- a/internal/geo/ip/ip.go +++ b/internal/geo/ip/ip.go @@ -33,12 +33,12 @@ func (l *Address) String() string { if l.Latitude == -1000 { return fmt.Sprintf( "%s;%s;%s;%s", - l.CountryCode, l.CountryCode, l.Region, l.City) + l.CountryCode, l.Country, l.Region, l.City) } return fmt.Sprintf( "%s;%s;%s;%s;%v;%v", - l.CountryCode, l.CountryCode, l.Region, l.City, l.Latitude, l.Longitude) + l.CountryCode, l.Country, l.Region, l.City, l.Latitude, l.Longitude) } // Cache provides access to the IP Geodata cache. diff --git a/internal/geo/location/location.go b/internal/geo/location/location.go index 0338b40..7cffd00 100644 --- a/internal/geo/location/location.go +++ b/internal/geo/location/location.go @@ -6,12 +6,11 @@ import ( ) type Location struct { - Name string `db:"name,key"` - Lat string `db:"lat"` - Lon string `db:"lon"` - Timezone string `db:"timezone"` - //nolint:tagliatelle - Fullname string `db:"displayName" json:"display_name"` + Name string `db:"name,key" json:"name"` + Lat string `db:"lat" json:"latitude"` + Lon string `db:"lon" json:"longitude"` + Timezone string `db:"timezone" json:"timezone"` + Fullname string `db:"displayName" json:"address"` } // String returns string representation of location. diff --git a/internal/geo/location/nominatim.go b/internal/geo/location/nominatim.go index 6b3e752..2927761 100644 --- a/internal/geo/location/nominatim.go +++ b/internal/geo/location/nominatim.go @@ -17,6 +17,14 @@ type Nominatim struct { token string } +type NominatimLocation struct { + Name string `db:"name,key"` + Lat string `db:"lat"` + Lon string `db:"lon"` + //nolint:tagliatelle + Fullname string `db:"displayName" json:"display_name"` +} + func NewNominatim(name, url, token string) *Nominatim { return &Nominatim{ name: name, @@ -27,7 +35,7 @@ func NewNominatim(name, url, token string) *Nominatim { func (n *Nominatim) Query(location string) (*Location, error) { var ( - result []Location + result []NominatimLocation errResponse struct { Error string @@ -65,5 +73,11 @@ func (n *Nominatim) Query(location string) (*Location, error) { return nil, fmt.Errorf("%w: %s: invalid response", types.ErrUpstream, n.name) } - return &result[0], nil + nl := &result[0] + + return &Location{ + Lat: nl.Lat, + Lon: nl.Lon, + Fullname: nl.Fullname, + }, nil } From 302b00ee7d5f10ecb4926376e104ce92078aa1b2 Mon Sep 17 00:00:00 2001 From: Igor Chubin Date: Thu, 22 Dec 2022 22:33:03 +0100 Subject: [PATCH 066/105] Add opencage support --- internal/config/config.go | 11 ++++ internal/geo/location/nominatim.go | 62 +++++++++---------- internal/geo/location/nominatim_locationiq.go | 39 ++++++++++++ internal/geo/location/nominatim_opencage.go | 42 +++++++++++++ internal/geo/location/search.go | 2 +- internal/types/errors.go | 2 + 6 files changed, 123 insertions(+), 35 deletions(-) create mode 100644 internal/geo/location/nominatim_locationiq.go create mode 100644 internal/geo/location/nominatim_opencage.go diff --git a/internal/config/config.go b/internal/config/config.go index 60c8386..23c90d1 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -91,6 +91,10 @@ type Geo struct { type Nominatim struct { Name string + // Type describes the type of the location service. + // Supported types: iq. + Type string + URL string Token string @@ -112,9 +116,16 @@ func Default() *Config { Nominatim: []Nominatim{ { Name: "locationiq", + Type: "iq", URL: "https://eu1.locationiq.com/v1/search", Token: os.Getenv("NOMINATIM_LOCATIONIQ"), }, + { + Name: "opencage", + Type: "opencage", + URL: "https://api.opencagedata.com/geocode/v1/json", + Token: os.Getenv("NOMINATIM_OPENCAGE"), + }, }, }, Logging{ diff --git a/internal/geo/location/nominatim.go b/internal/geo/location/nominatim.go index 2927761..a1b214f 100644 --- a/internal/geo/location/nominatim.go +++ b/internal/geo/location/nominatim.go @@ -5,7 +5,6 @@ import ( "fmt" "io/ioutil" "net/http" - "net/url" "github.com/chubin/wttr.in/internal/types" log "github.com/sirupsen/logrus" @@ -15,69 +14,64 @@ type Nominatim struct { name string url string token string + typ string } -type NominatimLocation struct { - Name string `db:"name,key"` - Lat string `db:"lat"` - Lon string `db:"lon"` - //nolint:tagliatelle - Fullname string `db:"displayName" json:"display_name"` +type locationQuerier interface { + Query(*Nominatim, string) (*Location, error) } -func NewNominatim(name, url, token string) *Nominatim { +func NewNominatim(name, typ, url, token string) *Nominatim { return &Nominatim{ name: name, url: url, token: token, + typ: typ, } } func (n *Nominatim) Query(location string) (*Location, error) { - var ( - result []NominatimLocation + var data locationQuerier - errResponse struct { - Error string - } - ) + switch n.typ { + case "iq": + data = &locationIQ{} + case "opencage": + data = &locationOpenCage{} + default: + return nil, fmt.Errorf("%s: %w", n.name, types.ErrUnknownLocationService) + } - urlws := fmt.Sprintf( - "%s?q=%s&format=json&accept-language=native&limit=1&key=%s", - n.url, url.QueryEscape(location), n.token) + return data.Query(n, location) +} - log.Debugln("nominatim:", urlws) - resp, err := http.Get(urlws) +func makeQuery(url string, result interface{}) error { + var errResponse struct { + Error string + } + + log.Debugln("nominatim:", url) + resp, err := http.Get(url) if err != nil { - return nil, fmt.Errorf("%s: %w", n.name, err) + return err } defer resp.Body.Close() body, err := ioutil.ReadAll(resp.Body) if err != nil { - return nil, fmt.Errorf("%s: %w", n.name, err) + return err } err = json.Unmarshal(body, &errResponse) if err == nil && errResponse.Error != "" { - return nil, fmt.Errorf("%w: %s: %s", types.ErrUpstream, n.name, errResponse.Error) + return fmt.Errorf("%w: %s", types.ErrUpstream, errResponse.Error) } log.Debugln("nominatim: response: ", string(body)) err = json.Unmarshal(body, &result) if err != nil { - return nil, fmt.Errorf("%s: %w", n.name, err) + return err } - if len(result) != 1 { - return nil, fmt.Errorf("%w: %s: invalid response", types.ErrUpstream, n.name) - } - - nl := &result[0] - - return &Location{ - Lat: nl.Lat, - Lon: nl.Lon, - Fullname: nl.Fullname, - }, nil + return nil } diff --git a/internal/geo/location/nominatim_locationiq.go b/internal/geo/location/nominatim_locationiq.go new file mode 100644 index 0000000..0a8d5fb --- /dev/null +++ b/internal/geo/location/nominatim_locationiq.go @@ -0,0 +1,39 @@ +package location + +import ( + "fmt" + "net/url" + + "github.com/chubin/wttr.in/internal/types" +) + +type locationIQ []struct { + Name string `db:"name,key"` + Lat string `db:"lat"` + Lon string `db:"lon"` + //nolint:tagliatelle + Fullname string `db:"displayName" json:"display_name"` +} + +func (data *locationIQ) Query(n *Nominatim, location string) (*Location, error) { + url := fmt.Sprintf( + "%s?q=%s&format=json&language=native&limit=1&key=%s", + n.url, url.QueryEscape(location), n.token) + + err := makeQuery(url, data) + if err != nil { + return nil, fmt.Errorf("%s: %w", n.name, err) + } + + if len(*data) != 1 { + return nil, fmt.Errorf("%w: %s: invalid response", types.ErrUpstream, n.name) + } + + nl := &(*data)[0] + + return &Location{ + Lat: nl.Lat, + Lon: nl.Lon, + Fullname: nl.Fullname, + }, nil +} diff --git a/internal/geo/location/nominatim_opencage.go b/internal/geo/location/nominatim_opencage.go new file mode 100644 index 0000000..577accd --- /dev/null +++ b/internal/geo/location/nominatim_opencage.go @@ -0,0 +1,42 @@ +package location + +import ( + "fmt" + "net/url" + + "github.com/chubin/wttr.in/internal/types" +) + +type locationOpenCage struct { + Results []struct { + Name string `db:"name,key"` + Geometry struct { + Lat float64 `db:"lat"` + Lng float64 `db:"lng"` + } + Fullname string `json:"formatted"` + } `json:"results"` +} + +func (data *locationOpenCage) Query(n *Nominatim, location string) (*Location, error) { + url := fmt.Sprintf( + "%s?q=%s&language=native&limit=1&key=%s", + n.url, url.QueryEscape(location), n.token) + + err := makeQuery(url, data) + if err != nil { + return nil, fmt.Errorf("%s: %w", n.name, err) + } + + if len(data.Results) != 1 { + return nil, fmt.Errorf("%w: %s: invalid response", types.ErrUpstream, n.name) + } + + nl := data.Results[0] + + return &Location{ + Lat: fmt.Sprint(nl.Geometry.Lat), + Lon: fmt.Sprint(nl.Geometry.Lng), + Fullname: nl.Fullname, + }, nil +} diff --git a/internal/geo/location/search.go b/internal/geo/location/search.go index c05cdb0..2efefeb 100644 --- a/internal/geo/location/search.go +++ b/internal/geo/location/search.go @@ -14,7 +14,7 @@ type Searcher struct { func NewSearcher(config *config.Config) *Searcher { providers := []Provider{} for _, p := range config.Geo.Nominatim { - providers = append(providers, NewNominatim(p.Name, p.URL, p.Token)) + providers = append(providers, NewNominatim(p.Name, p.Type, p.URL, p.Token)) } return &Searcher{ diff --git a/internal/types/errors.go b/internal/types/errors.go index 62c36f1..e081774 100644 --- a/internal/types/errors.go +++ b/internal/types/errors.go @@ -9,4 +9,6 @@ var ( // ErrNoServersConfigured means that there are no servers to run. ErrNoServersConfigured = errors.New("no servers configured") + + ErrUnknownLocationService = errors.New("unknown location service") ) From 54b5bfc64d0a0f1570a9eca3a6555e4f8d22f8de Mon Sep 17 00:00:00 2001 From: Igor Chubin Date: Fri, 23 Dec 2022 14:08:39 +0100 Subject: [PATCH 067/105] Link statically --- Makefile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Makefile b/Makefile index dc4616e..2d7a03f 100644 --- a/Makefile +++ b/Makefile @@ -1,5 +1,5 @@ srv: srv.go internal/*/*.go internal/*/*/*.go - go build -o srv ./ + go build -o srv -ldflags '-w -linkmode external -extldflags "-static"' ./ go-test: go test ./... From 38e2ddd69bf370975800f3cb1a96d9598785a3b4 Mon Sep 17 00:00:00 2001 From: Igor Chubin Date: Fri, 23 Dec 2022 14:09:17 +0100 Subject: [PATCH 068/105] Keep existing db when converting geocache --- internal/geo/location/convert.go | 23 +++++++++++++++-------- srv.go | 2 +- 2 files changed, 16 insertions(+), 9 deletions(-) diff --git a/internal/geo/location/convert.go b/internal/geo/location/convert.go index bda2765..11343f6 100644 --- a/internal/geo/location/convert.go +++ b/internal/geo/location/convert.go @@ -11,8 +11,11 @@ import ( "github.com/samonzeweb/godb/adapters/sqlite" ) -//nolint:funlen,cyclop -func (c *Cache) ConvertCache() error { +// ConvertCache converts files-based cache into the DB-based cache. +// If reset is true, the DB cache is created from scratch. +// +//nolint:funlen,cyclop,gocognit +func (c *Cache) ConvertCache(reset bool) error { var ( dbfile = c.config.Geo.LocationCacheDB tableName = "Location" @@ -20,9 +23,11 @@ func (c *Cache) ConvertCache() error { known = map[string]bool{} ) - err := removeDBIfExists(dbfile) - if err != nil { - return err + if reset { + err := removeDBIfExists(dbfile) + if err != nil { + return err + } } db, err := godb.Open(sqlite.Adapter, dbfile) @@ -30,9 +35,11 @@ func (c *Cache) ConvertCache() error { return err } - err = createTable(db, tableName) - if err != nil { - return err + if reset { + err = createTable(db, tableName) + if err != nil { + return err + } } log.Println("listing cache entries...") diff --git a/srv.go b/srv.go index 97fdbb6..e730841 100644 --- a/srv.go +++ b/srv.go @@ -237,7 +237,7 @@ func convertGeoLocationCache(conf *config.Config) error { return err } - return geoLocCache.ConvertCache() + return geoLocCache.ConvertCache(false) } func setLogLevel(logLevel string) error { From ecc9479719209274b7f0eff9128815ad30486773 Mon Sep 17 00:00:00 2001 From: Igor Chubin Date: Fri, 23 Dec 2022 14:45:59 +0100 Subject: [PATCH 069/105] Skip existing entries in db when converting --- internal/geo/location/convert.go | 20 +++++++++++++++++++- 1 file changed, 19 insertions(+), 1 deletion(-) diff --git a/internal/geo/location/convert.go b/internal/geo/location/convert.go index 11343f6..7219cb3 100644 --- a/internal/geo/location/convert.go +++ b/internal/geo/location/convert.go @@ -1,6 +1,8 @@ package location import ( + "database/sql" + "errors" "fmt" "log" "os" @@ -14,7 +16,7 @@ import ( // ConvertCache converts files-based cache into the DB-based cache. // If reset is true, the DB cache is created from scratch. // -//nolint:funlen,cyclop,gocognit +//nolint:funlen,cyclop func (c *Cache) ConvertCache(reset bool) error { var ( dbfile = c.config.Geo.LocationCacheDB @@ -60,12 +62,28 @@ func (c *Cache) ConvertCache(reset bool) error { continue } + // Skip too long location names. + if len(loc.Name) > 25 { + continue + } + // Skip duplicates. if known[loc.Name] { log.Println("skipping", loc.Name) continue } + + singleLocation := Location{} + err = db.Select(&singleLocation). + Where("name = ?", loc.Name). + Do() + if !errors.Is(err, sql.ErrNoRows) { + log.Println("found in db:", loc.Name) + + continue + } + known[loc.Name] = true // Skip some invalid names. From 7f9eb91ecc9f35d7ef5162cba844cb49b2187d16 Mon Sep 17 00:00:00 2001 From: Igor Chubin Date: Fri, 23 Dec 2022 14:53:16 +0100 Subject: [PATCH 070/105] Print geolocation errors --- internal/geo/location/response.go | 3 +++ 1 file changed, 3 insertions(+) diff --git a/internal/geo/location/response.go b/internal/geo/location/response.go index 6ebc6fa..b926fca 100644 --- a/internal/geo/location/response.go +++ b/internal/geo/location/response.go @@ -3,6 +3,7 @@ package location import ( "encoding/json" "fmt" + "log" "net/http" "github.com/chubin/wttr.in/internal/routing" @@ -23,6 +24,8 @@ func (c *Cache) Response(r *http.Request) *routing.Cadre { loc, err = c.Resolve(locationName) if err != nil { + log.Println("geo/location error:", locationName) + return errorResponse(fmt.Sprint(err)) } From c94cc933dd98f76ea4f80a382b623f351cbd0393 Mon Sep 17 00:00:00 2001 From: Igor Chubin Date: Fri, 23 Dec 2022 16:23:08 +0100 Subject: [PATCH 071/105] Move we-lang to internal/view/v1 --- {share/we-lang => internal/view/v1}/api.go | 0 {share/we-lang => internal/view/v1}/format.go | 0 {share/we-lang => internal/view/v1}/go.mod | 0 {share/we-lang => internal/view/v1}/go.sum | 0 {share/we-lang => internal/view/v1}/icons.go | 0 {share/we-lang => internal/view/v1}/locale.go | 0 {share/we-lang => internal/view/v1}/main.go | 0 {share/we-lang => internal/view/v1}/view1.go | 0 8 files changed, 0 insertions(+), 0 deletions(-) rename {share/we-lang => internal/view/v1}/api.go (100%) rename {share/we-lang => internal/view/v1}/format.go (100%) rename {share/we-lang => internal/view/v1}/go.mod (100%) rename {share/we-lang => internal/view/v1}/go.sum (100%) rename {share/we-lang => internal/view/v1}/icons.go (100%) rename {share/we-lang => internal/view/v1}/locale.go (100%) rename {share/we-lang => internal/view/v1}/main.go (100%) rename {share/we-lang => internal/view/v1}/view1.go (100%) diff --git a/share/we-lang/api.go b/internal/view/v1/api.go similarity index 100% rename from share/we-lang/api.go rename to internal/view/v1/api.go diff --git a/share/we-lang/format.go b/internal/view/v1/format.go similarity index 100% rename from share/we-lang/format.go rename to internal/view/v1/format.go diff --git a/share/we-lang/go.mod b/internal/view/v1/go.mod similarity index 100% rename from share/we-lang/go.mod rename to internal/view/v1/go.mod diff --git a/share/we-lang/go.sum b/internal/view/v1/go.sum similarity index 100% rename from share/we-lang/go.sum rename to internal/view/v1/go.sum diff --git a/share/we-lang/icons.go b/internal/view/v1/icons.go similarity index 100% rename from share/we-lang/icons.go rename to internal/view/v1/icons.go diff --git a/share/we-lang/locale.go b/internal/view/v1/locale.go similarity index 100% rename from share/we-lang/locale.go rename to internal/view/v1/locale.go diff --git a/share/we-lang/main.go b/internal/view/v1/main.go similarity index 100% rename from share/we-lang/main.go rename to internal/view/v1/main.go diff --git a/share/we-lang/view1.go b/internal/view/v1/view1.go similarity index 100% rename from share/we-lang/view1.go rename to internal/view/v1/view1.go From 25b321015b525762fb044c7efbc0f5fa40c33d6f Mon Sep 17 00:00:00 2001 From: Igor Chubin Date: Fri, 23 Dec 2022 16:24:31 +0100 Subject: [PATCH 072/105] Rename internal/view/v1/main.go -> internal/view/v1/cmd.go --- internal/view/v1/{main.go => cmd.go} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename internal/view/v1/{main.go => cmd.go} (100%) diff --git a/internal/view/v1/main.go b/internal/view/v1/cmd.go similarity index 100% rename from internal/view/v1/main.go rename to internal/view/v1/cmd.go From afd335cfa745798bea2acfbb224e54f47d7e58ad Mon Sep 17 00:00:00 2001 From: Igor Chubin Date: Fri, 23 Dec 2022 16:25:26 +0100 Subject: [PATCH 073/105] Set v1 package --- internal/view/v1/api.go | 2 +- internal/view/v1/cmd.go | 2 +- internal/view/v1/format.go | 2 +- internal/view/v1/icons.go | 2 +- internal/view/v1/locale.go | 2 +- internal/view/v1/view1.go | 2 +- 6 files changed, 6 insertions(+), 6 deletions(-) diff --git a/internal/view/v1/api.go b/internal/view/v1/api.go index cb5f99c..b5f609e 100644 --- a/internal/view/v1/api.go +++ b/internal/view/v1/api.go @@ -1,4 +1,4 @@ -package main +package v1 import ( "bytes" diff --git a/internal/view/v1/cmd.go b/internal/view/v1/cmd.go index b1a1f50..83d5f09 100644 --- a/internal/view/v1/cmd.go +++ b/internal/view/v1/cmd.go @@ -1,7 +1,7 @@ // This code represents wttr.in view v1. // It is based on wego (github.com/schachmat/wego) from which it diverged back in 2016. -package main +package v1 import ( _ "crypto/sha512" diff --git a/internal/view/v1/format.go b/internal/view/v1/format.go index 041903a..a3544d8 100644 --- a/internal/view/v1/format.go +++ b/internal/view/v1/format.go @@ -1,4 +1,4 @@ -package main +package v1 import ( "fmt" diff --git a/internal/view/v1/icons.go b/internal/view/v1/icons.go index 6468161..5dbe96b 100644 --- a/internal/view/v1/icons.go +++ b/internal/view/v1/icons.go @@ -1,4 +1,4 @@ -package main +package v1 var ( iconUnknown = []string{ diff --git a/internal/view/v1/locale.go b/internal/view/v1/locale.go index 2560af6..2987ea2 100644 --- a/internal/view/v1/locale.go +++ b/internal/view/v1/locale.go @@ -1,4 +1,4 @@ -package main +package v1 var ( locale = map[string]string{ diff --git a/internal/view/v1/view1.go b/internal/view/v1/view1.go index 357fea5..0f3cdbc 100644 --- a/internal/view/v1/view1.go +++ b/internal/view/v1/view1.go @@ -1,4 +1,4 @@ -package main +package v1 import ( "math" From 602ee55a8bd1a64f00389ed7c2d5806209141ca1 Mon Sep 17 00:00:00 2001 From: Igor Chubin Date: Fri, 23 Dec 2022 16:32:51 +0100 Subject: [PATCH 074/105] Remove internal/view/v1/go.{mod,sum} --- internal/view/v1/go.mod | 9 --------- internal/view/v1/go.sum | 13 ------------- 2 files changed, 22 deletions(-) delete mode 100644 internal/view/v1/go.mod delete mode 100644 internal/view/v1/go.sum diff --git a/internal/view/v1/go.mod b/internal/view/v1/go.mod deleted file mode 100644 index c0e0988..0000000 --- a/internal/view/v1/go.mod +++ /dev/null @@ -1,9 +0,0 @@ -module github.com/chubin/wttr.in/v2 - -go 1.15 - -require ( - github.com/klauspost/lctime v0.1.0 - github.com/mattn/go-colorable v0.1.11 - github.com/mattn/go-runewidth v0.0.13 -) diff --git a/internal/view/v1/go.sum b/internal/view/v1/go.sum deleted file mode 100644 index c9c1b70..0000000 --- a/internal/view/v1/go.sum +++ /dev/null @@ -1,13 +0,0 @@ -github.com/klauspost/lctime v0.1.0 h1:nINsuFc860M9cyYhT6vfg6U1USh7kiVBj/s/2b04U70= -github.com/klauspost/lctime v0.1.0/go.mod h1:OwdMhr8tbQvusAsnilqkkgDQqivWlqyg0w5cfXkLiDk= -github.com/mattn/go-colorable v0.1.11 h1:nQ+aFkoE2TMGc0b68U2OKSexC+eq46+XwZzWXHRmPYs= -github.com/mattn/go-colorable v0.1.11/go.mod h1:u5H1YNBxpqRaxsYJYSkiCWKzEfiAb1Gb520KVy5xxl4= -github.com/mattn/go-isatty v0.0.14 h1:yVuAays6BHfxijgZPzw+3Zlu5yQgKGP2/hcQbHb7S9Y= -github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94= -github.com/mattn/go-runewidth v0.0.13 h1:lTGmDsbAYt5DmK6OnoV7EuIF1wEIFAcxld6ypU4OSgU= -github.com/mattn/go-runewidth v0.0.13/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= -github.com/rivo/uniseg v0.2.0 h1:S1pD9weZBuJdFmowNwbpi7BJ8TNftyUImj/0WQi72jY= -github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= -golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6 h1:foEbQz/B0Oz6YIqu/69kfXPYeFQAuuMYFkjaqXzl5Wo= -golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= From cfe5b0761eca8557a844f8243e4c19fb5596b8bf Mon Sep 17 00:00:00 2001 From: Igor Chubin Date: Fri, 23 Dec 2022 16:33:28 +0100 Subject: [PATCH 075/105] Add v1 Go dependencies --- go.mod | 3 +++ go.sum | 12 ++++++++++++ internal/view/v1/cmd.go | 6 +++--- srv.go | 3 +++ 4 files changed, 21 insertions(+), 3 deletions(-) diff --git a/go.mod b/go.mod index 5b2ca40..0626030 100644 --- a/go.mod +++ b/go.mod @@ -8,7 +8,10 @@ require ( github.com/go-sql-driver/mysql v1.5.0 // indirect github.com/gopherjs/gopherjs v0.0.0-20200217142428-fce0ec30dd00 // indirect github.com/hashicorp/golang-lru v0.6.0 + github.com/klauspost/lctime v0.1.0 // indirect github.com/lib/pq v1.8.0 // indirect + github.com/mattn/go-colorable v0.1.13 // indirect + github.com/mattn/go-runewidth v0.0.14 // indirect github.com/mattn/go-sqlite3 v1.14.16 // indirect github.com/robfig/cron v1.2.0 github.com/samonzeweb/godb v1.0.8 // indirect diff --git a/go.sum b/go.sum index 81f1c89..422eb44 100644 --- a/go.sum +++ b/go.sum @@ -14,13 +14,23 @@ github.com/hashicorp/golang-lru v0.6.0 h1:uL2shRDx7RTrOrTCUZEGP/wJUFiUI8QT6E7z5o github.com/hashicorp/golang-lru v0.6.0/go.mod h1:iADmTwqILo4mZ8BN3D2Q6+9jd8WM5uGBxy+E8yxSoD4= github.com/hexops/gotextdiff v1.0.3/go.mod h1:pSWU5MAI3yDq+fZBTazCSJysOMbxWL1BSow5/V2vxeg= github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU= +github.com/klauspost/lctime v0.1.0 h1:nINsuFc860M9cyYhT6vfg6U1USh7kiVBj/s/2b04U70= +github.com/klauspost/lctime v0.1.0/go.mod h1:OwdMhr8tbQvusAsnilqkkgDQqivWlqyg0w5cfXkLiDk= github.com/lib/pq v1.8.0/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= +github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= +github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= +github.com/mattn/go-isatty v0.0.16 h1:bq3VjFmv/sOjHtdEhmkEV4x1AJtvUvOJ2PFAZ5+peKQ= +github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= +github.com/mattn/go-runewidth v0.0.14 h1:+xnbZSEeDbOIg5/mE6JF0w6n9duR1l3/WmbinWVwUuU= +github.com/mattn/go-runewidth v0.0.14/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= github.com/mattn/go-sqlite3 v1.14.16 h1:yOQRA0RpS5PFz/oikGwBEqvAWhWg5ufRz4ETLjwpU1Y= github.com/mattn/go-sqlite3 v1.14.16/go.mod h1:2eHXhiwb8IkHr+BDWZGa96P6+rkvnG63S2DGjv9HUNg= github.com/mattn/go-sqlite3 v2.0.3+incompatible h1:gXHsfypPkaMZrKbD5209QV9jbUTJKjyR5WD3HYQSd+U= github.com/mattn/go-sqlite3 v2.0.3+incompatible/go.mod h1:FPy6KqzDD04eiIsT53CuJW3U88zkxoIYsOqkbpncsNc= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/rivo/uniseg v0.2.0 h1:S1pD9weZBuJdFmowNwbpi7BJ8TNftyUImj/0WQi72jY= +github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= github.com/robfig/cron v1.2.0 h1:ZjScXvvxeQ63Dbyxy76Fj3AT3Ut0aKsyd2/tl3DTMuQ= github.com/robfig/cron v1.2.0/go.mod h1:JGuDeoQd7Z6yL4zQhZ3OPEVHB7fL6Ka6skscFHfmt2k= github.com/samonzeweb/godb v1.0.8 h1:WRn6nq0FChYOzh+w8SgpXHUkEhL7W6ZqkCf5Ninx7Uc= @@ -52,6 +62,8 @@ golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5h golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8 h1:0A+M6Uqn+Eje4kHMK80dtF3JCXC4ykBgQG4Fe06QRhQ= golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab h1:2QkjZIsXupsJbJIdSjjUOgWK3aEtzyuh2mPt3l/CkeU= +golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/tools v0.0.0-20190328211700-ab21143f2384/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= diff --git a/internal/view/v1/cmd.go b/internal/view/v1/cmd.go index 83d5f09..29a0526 100644 --- a/internal/view/v1/cmd.go +++ b/internal/view/v1/cmd.go @@ -20,7 +20,7 @@ import ( "github.com/mattn/go-runewidth" ) -type configuration struct { +type Configuration struct { APIKey string City string Numdays int @@ -36,7 +36,7 @@ type configuration struct { var ( ansiEsc *regexp.Regexp - config configuration + config Configuration configpath string debug bool ) @@ -98,7 +98,7 @@ func init() { ansiEsc = regexp.MustCompile("\033.*?m") } -func main() { +func Cmd() { flag.Parse() r := getDataFromAPI() diff --git a/srv.go b/srv.go index e730841..582199c 100644 --- a/srv.go +++ b/srv.go @@ -17,6 +17,7 @@ import ( "github.com/chubin/wttr.in/internal/logging" "github.com/chubin/wttr.in/internal/processor" "github.com/chubin/wttr.in/internal/types" + "github.com/chubin/wttr.in/internal/view/v1" ) //nolint:gochecknoglobals @@ -29,6 +30,8 @@ var cli struct { ConvertGeoLocationCache bool `name:"convert-geo-location-cache" help:"Convert Geo Location data cache to SQlite"` GeoResolve string `name:"geo-resolve" help:"Resolve location"` LogLevel string `name:"log-level" short:"l" help:"Show log messages with level" default:"info"` + + V1 struct v1.Configuration } const logLineStart = "LOG_LINE_START " From 0bf476bd41e4900cde66aacaecfdead4719fc79f Mon Sep 17 00:00:00 2001 From: Igor Chubin Date: Fri, 23 Dec 2022 16:35:30 +0100 Subject: [PATCH 076/105] Run gofumpt for view/v1 --- internal/view/v1/cmd.go | 2 +- internal/view/v1/format.go | 46 +++++++++++++++--------------- internal/view/v1/icons.go | 57 +++++++++++++++++++++++++------------- internal/view/v1/locale.go | 26 ++++++++--------- internal/view/v1/view1.go | 10 +++---- srv.go | 4 +-- 6 files changed, 81 insertions(+), 64 deletions(-) diff --git a/internal/view/v1/cmd.go b/internal/view/v1/cmd.go index 29a0526..d9008e0 100644 --- a/internal/view/v1/cmd.go +++ b/internal/view/v1/cmd.go @@ -58,7 +58,7 @@ func configload() error { func configsave() error { j, err := json.MarshalIndent(config, "", "\t") if err == nil { - return ioutil.WriteFile(configpath, j, 0600) + return ioutil.WriteFile(configpath, j, 0o600) } return err } diff --git a/internal/view/v1/format.go b/internal/view/v1/format.go index a3544d8..3636e2a 100644 --- a/internal/view/v1/format.go +++ b/internal/view/v1/format.go @@ -8,30 +8,28 @@ import ( "github.com/mattn/go-runewidth" ) -var ( - windDir = map[string]string{ - "N": "\033[1m↓\033[0m", - "NNE": "\033[1m↓\033[0m", - "NE": "\033[1m↙\033[0m", - "ENE": "\033[1m↙\033[0m", - "E": "\033[1m←\033[0m", - "ESE": "\033[1m←\033[0m", - "SE": "\033[1m↖\033[0m", - "SSE": "\033[1m↖\033[0m", - "S": "\033[1m↑\033[0m", - "SSW": "\033[1m↑\033[0m", - "SW": "\033[1m↗\033[0m", - "WSW": "\033[1m↗\033[0m", - "W": "\033[1m→\033[0m", - "WNW": "\033[1m→\033[0m", - "NW": "\033[1m↘\033[0m", - "NNW": "\033[1m↘\033[0m", - } -) +var windDir = map[string]string{ + "N": "\033[1m↓\033[0m", + "NNE": "\033[1m↓\033[0m", + "NE": "\033[1m↙\033[0m", + "ENE": "\033[1m↙\033[0m", + "E": "\033[1m←\033[0m", + "ESE": "\033[1m←\033[0m", + "SE": "\033[1m↖\033[0m", + "SSE": "\033[1m↖\033[0m", + "S": "\033[1m↑\033[0m", + "SSW": "\033[1m↑\033[0m", + "SW": "\033[1m↗\033[0m", + "WSW": "\033[1m↗\033[0m", + "W": "\033[1m→\033[0m", + "WNW": "\033[1m→\033[0m", + "NW": "\033[1m↘\033[0m", + "NNW": "\033[1m↘\033[0m", +} func formatTemp(c cond) string { color := func(temp int, explicitPlus bool) string { - var col = 0 + col := 0 if !config.Inverse { // Extemely cold temperature must be shown with violet // because dark blue is too dark @@ -193,7 +191,7 @@ func formatWind(c cond) string { return spd } color := func(spd int) string { - var col = 46 + col := 46 switch spd { case 1, 2, 3: col = 82 @@ -286,7 +284,7 @@ func formatCond(cur []string, c cond, current bool) (ret []string) { icon[i] = strings.Replace(icon[i], "38;5;251", "38;5;238", -1) } } - //desc := fmt.Sprintf("%-15.15v", c.WeatherDesc[0].Value) + // desc := fmt.Sprintf("%-15.15v", c.WeatherDesc[0].Value) desc := c.WeatherDesc[0].Value if config.RightToLeft { for runewidth.StringWidth(desc) < 15 { @@ -325,7 +323,7 @@ func formatCond(cur []string, c cond, current bool) (ret []string) { } else { if lastRune, size := utf8.DecodeLastRuneInString(desc); lastRune != ' ' { desc = desc[:len(desc)-size] + "…" - //for numberOfSpaces < runewidth.StringWidth(fmt.Sprintf("%c", lastRune)) - 1 { + // for numberOfSpaces < runewidth.StringWidth(fmt.Sprintf("%c", lastRune)) - 1 { for runewidth.StringWidth(desc) < 15 { desc = desc + " " } diff --git a/internal/view/v1/icons.go b/internal/view/v1/icons.go index 5dbe96b..08017fe 100644 --- a/internal/view/v1/icons.go +++ b/internal/view/v1/icons.go @@ -6,133 +6,152 @@ var ( " __) ", " ( ", " `-’ ", - " • "} + " • ", + } iconSunny = []string{ "\033[38;5;226m \\ / \033[0m", "\033[38;5;226m .-. \033[0m", "\033[38;5;226m ― ( ) ― \033[0m", "\033[38;5;226m `-’ \033[0m", - "\033[38;5;226m / \\ \033[0m"} + "\033[38;5;226m / \\ \033[0m", + } iconPartlyCloudy = []string{ "\033[38;5;226m \\ /\033[0m ", "\033[38;5;226m _ /\"\"\033[38;5;250m.-. \033[0m", "\033[38;5;226m \\_\033[38;5;250m( ). \033[0m", "\033[38;5;226m /\033[38;5;250m(___(__) \033[0m", - " "} + " ", + } iconCloudy = []string{ " ", "\033[38;5;250m .--. \033[0m", "\033[38;5;250m .-( ). \033[0m", "\033[38;5;250m (___.__)__) \033[0m", - " "} + " ", + } iconVeryCloudy = []string{ " ", "\033[38;5;240;1m .--. \033[0m", "\033[38;5;240;1m .-( ). \033[0m", "\033[38;5;240;1m (___.__)__) \033[0m", - " "} + " ", + } iconLightShowers = []string{ "\033[38;5;226m _`/\"\"\033[38;5;250m.-. \033[0m", "\033[38;5;226m ,\\_\033[38;5;250m( ). \033[0m", "\033[38;5;226m /\033[38;5;250m(___(__) \033[0m", "\033[38;5;111m ‘ ‘ ‘ ‘ \033[0m", - "\033[38;5;111m ‘ ‘ ‘ ‘ \033[0m"} + "\033[38;5;111m ‘ ‘ ‘ ‘ \033[0m", + } iconHeavyShowers = []string{ "\033[38;5;226m _`/\"\"\033[38;5;240;1m.-. \033[0m", "\033[38;5;226m ,\\_\033[38;5;240;1m( ). \033[0m", "\033[38;5;226m /\033[38;5;240;1m(___(__) \033[0m", "\033[38;5;21;1m ‚‘‚‘‚‘‚‘ \033[0m", - "\033[38;5;21;1m ‚’‚’‚’‚’ \033[0m"} + "\033[38;5;21;1m ‚’‚’‚’‚’ \033[0m", + } iconLightSnowShowers = []string{ "\033[38;5;226m _`/\"\"\033[38;5;250m.-. \033[0m", "\033[38;5;226m ,\\_\033[38;5;250m( ). \033[0m", "\033[38;5;226m /\033[38;5;250m(___(__) \033[0m", "\033[38;5;255m * * * \033[0m", - "\033[38;5;255m * * * \033[0m"} + "\033[38;5;255m * * * \033[0m", + } iconHeavySnowShowers = []string{ "\033[38;5;226m _`/\"\"\033[38;5;240;1m.-. \033[0m", "\033[38;5;226m ,\\_\033[38;5;240;1m( ). \033[0m", "\033[38;5;226m /\033[38;5;240;1m(___(__) \033[0m", "\033[38;5;255;1m * * * * \033[0m", - "\033[38;5;255;1m * * * * \033[0m"} + "\033[38;5;255;1m * * * * \033[0m", + } iconLightSleetShowers = []string{ "\033[38;5;226m _`/\"\"\033[38;5;250m.-. \033[0m", "\033[38;5;226m ,\\_\033[38;5;250m( ). \033[0m", "\033[38;5;226m /\033[38;5;250m(___(__) \033[0m", "\033[38;5;111m ‘ \033[38;5;255m*\033[38;5;111m ‘ \033[38;5;255m* \033[0m", - "\033[38;5;255m *\033[38;5;111m ‘ \033[38;5;255m*\033[38;5;111m ‘ \033[0m"} + "\033[38;5;255m *\033[38;5;111m ‘ \033[38;5;255m*\033[38;5;111m ‘ \033[0m", + } iconThunderyShowers = []string{ "\033[38;5;226m _`/\"\"\033[38;5;250m.-. \033[0m", "\033[38;5;226m ,\\_\033[38;5;250m( ). \033[0m", "\033[38;5;226m /\033[38;5;250m(___(__) \033[0m", "\033[38;5;228;5m ⚡\033[38;5;111;25m‘‘\033[38;5;228;5m⚡\033[38;5;111;25m‘‘ \033[0m", - "\033[38;5;111m ‘ ‘ ‘ ‘ \033[0m"} + "\033[38;5;111m ‘ ‘ ‘ ‘ \033[0m", + } iconThunderyHeavyRain = []string{ "\033[38;5;240;1m .-. \033[0m", "\033[38;5;240;1m ( ). \033[0m", "\033[38;5;240;1m (___(__) \033[0m", "\033[38;5;21;1m ‚‘\033[38;5;228;5m⚡\033[38;5;21;25m‘‚\033[38;5;228;5m⚡\033[38;5;21;25m‚‘ \033[0m", - "\033[38;5;21;1m ‚’‚’\033[38;5;228;5m⚡\033[38;5;21;25m’‚’ \033[0m"} + "\033[38;5;21;1m ‚’‚’\033[38;5;228;5m⚡\033[38;5;21;25m’‚’ \033[0m", + } iconThunderySnowShowers = []string{ "\033[38;5;226m _`/\"\"\033[38;5;250m.-. \033[0m", "\033[38;5;226m ,\\_\033[38;5;250m( ). \033[0m", "\033[38;5;226m /\033[38;5;250m(___(__) \033[0m", "\033[38;5;255m *\033[38;5;228;5m⚡\033[38;5;255;25m*\033[38;5;228;5m⚡\033[38;5;255;25m* \033[0m", - "\033[38;5;255m * * * \033[0m"} + "\033[38;5;255m * * * \033[0m", + } iconLightRain = []string{ "\033[38;5;250m .-. \033[0m", "\033[38;5;250m ( ). \033[0m", "\033[38;5;250m (___(__) \033[0m", "\033[38;5;111m ‘ ‘ ‘ ‘ \033[0m", - "\033[38;5;111m ‘ ‘ ‘ ‘ \033[0m"} + "\033[38;5;111m ‘ ‘ ‘ ‘ \033[0m", + } iconHeavyRain = []string{ "\033[38;5;240;1m .-. \033[0m", "\033[38;5;240;1m ( ). \033[0m", "\033[38;5;240;1m (___(__) \033[0m", "\033[38;5;21;1m ‚‘‚‘‚‘‚‘ \033[0m", - "\033[38;5;21;1m ‚’‚’‚’‚’ \033[0m"} + "\033[38;5;21;1m ‚’‚’‚’‚’ \033[0m", + } iconLightSnow = []string{ "\033[38;5;250m .-. \033[0m", "\033[38;5;250m ( ). \033[0m", "\033[38;5;250m (___(__) \033[0m", "\033[38;5;255m * * * \033[0m", - "\033[38;5;255m * * * \033[0m"} + "\033[38;5;255m * * * \033[0m", + } iconHeavySnow = []string{ "\033[38;5;240;1m .-. \033[0m", "\033[38;5;240;1m ( ). \033[0m", "\033[38;5;240;1m (___(__) \033[0m", "\033[38;5;255;1m * * * * \033[0m", - "\033[38;5;255;1m * * * * \033[0m"} + "\033[38;5;255;1m * * * * \033[0m", + } iconLightSleet = []string{ "\033[38;5;250m .-. \033[0m", "\033[38;5;250m ( ). \033[0m", "\033[38;5;250m (___(__) \033[0m", "\033[38;5;111m ‘ \033[38;5;255m*\033[38;5;111m ‘ \033[38;5;255m* \033[0m", - "\033[38;5;255m *\033[38;5;111m ‘ \033[38;5;255m*\033[38;5;111m ‘ \033[0m"} + "\033[38;5;255m *\033[38;5;111m ‘ \033[38;5;255m*\033[38;5;111m ‘ \033[0m", + } iconFog = []string{ " ", "\033[38;5;251m _ - _ - _ - \033[0m", "\033[38;5;251m _ - _ - _ \033[0m", "\033[38;5;251m _ - _ - _ - \033[0m", - " "} + " ", + } codes = map[int][]string{ 113: iconSunny, diff --git a/internal/view/v1/locale.go b/internal/view/v1/locale.go index 2987ea2..412e3d8 100644 --- a/internal/view/v1/locale.go +++ b/internal/view/v1/locale.go @@ -229,65 +229,65 @@ var ( } localizedRain = map[string]map[bool]string{ - "en": map[bool]string{ + "en": { false: "mm", true: "in", }, - "be": map[bool]string{ + "be": { false: "мм", true: "in", }, - "ru": map[bool]string{ + "ru": { false: "мм", true: "in", }, - "uk": map[bool]string{ + "uk": { false: "мм", true: "in", }, } localizedVis = map[string]map[bool]string{ - "en": map[bool]string{ + "en": { false: "km", true: "mi", }, - "be": map[bool]string{ + "be": { false: "км", true: "mi", }, - "ru": map[bool]string{ + "ru": { false: "км", true: "mi", }, - "uk": map[bool]string{ + "uk": { false: "км", true: "mi", }, } localizedWind = map[string]map[int]string{ - "en": map[int]string{ + "en": { 0: "km/h", 1: "mph", 2: "m/s", }, - "be": map[int]string{ + "be": { 0: "км/г", 1: "mph", 2: "м/c", }, - "ru": map[int]string{ + "ru": { 0: "км/ч", 1: "mph", 2: "м/c", }, - "tr": map[int]string{ + "tr": { 0: "km/sa", 1: "mph", 2: "m/s", }, - "uk": map[int]string{ + "uk": { 0: "км/год", 1: "mph", 2: "м/c", diff --git a/internal/view/v1/view1.go b/internal/view/v1/view1.go index 0f3cdbc..4607bec 100644 --- a/internal/view/v1/view1.go +++ b/internal/view/v1/view1.go @@ -7,9 +7,7 @@ import ( "github.com/klauspost/lctime" ) -var ( - slotTimes = [slotcount]int{9 * 60, 12 * 60, 18 * 60, 22 * 60} -) +var slotTimes = [slotcount]int{9 * 60, 12 * 60, 18 * 60, 22 * 60} func printDay(w weather) (ret []string) { hourly := w.Hourly @@ -97,7 +95,8 @@ func printDay(w weather) (ret []string) { " ┌─────────────┐ ", "┌───────────────────────" + dateFmt + "───────────────────────┐", names, - "├──────────────────────────────┼──────────────────────────────┤"}, + "├──────────────────────────────┼──────────────────────────────┤", + }, ret...) return append(ret, @@ -118,7 +117,8 @@ func printDay(w weather) (ret []string) { " ┌─────────────┐ ", "┌──────────────────────────────┬───────────────────────" + dateFmt + "───────────────────────┬──────────────────────────────┐", names, - "├──────────────────────────────┼──────────────────────────────┼──────────────────────────────┼──────────────────────────────┤"}, + "├──────────────────────────────┼──────────────────────────────┼──────────────────────────────┼──────────────────────────────┤", + }, ret...) return append(ret, diff --git a/srv.go b/srv.go index 582199c..92754e5 100644 --- a/srv.go +++ b/srv.go @@ -17,7 +17,7 @@ import ( "github.com/chubin/wttr.in/internal/logging" "github.com/chubin/wttr.in/internal/processor" "github.com/chubin/wttr.in/internal/types" - "github.com/chubin/wttr.in/internal/view/v1" + v1 "github.com/chubin/wttr.in/internal/view/v1" ) //nolint:gochecknoglobals @@ -31,7 +31,7 @@ var cli struct { GeoResolve string `name:"geo-resolve" help:"Resolve location"` LogLevel string `name:"log-level" short:"l" help:"Show log messages with level" default:"info"` - V1 struct v1.Configuration + V1 v1.Configuration } const logLineStart = "LOG_LINE_START " From bebbc32353eb7dec3ece69d8223b370e23072220 Mon Sep 17 00:00:00 2001 From: Igor Chubin Date: Fri, 23 Dec 2022 16:55:14 +0100 Subject: [PATCH 077/105] Fix nlreturn findings --- internal/view/v1/api.go | 4 ++++ internal/view/v1/cmd.go | 2 ++ internal/view/v1/format.go | 16 ++++++++++++++++ internal/view/v1/locale.go | 3 +++ internal/view/v1/view1.go | 2 ++ 5 files changed, 27 insertions(+) diff --git a/internal/view/v1/api.go b/internal/view/v1/api.go index b5f609e..d993652 100644 --- a/internal/view/v1/api.go +++ b/internal/view/v1/api.go @@ -99,7 +99,9 @@ func getDataFromAPI() (ret resp) { if debug { var out bytes.Buffer + json.Indent(&out, body, "", " ") + out.WriteTo(os.Stderr) fmt.Print("\n\n") } @@ -113,6 +115,7 @@ func getDataFromAPI() (ret resp) { log.Println(err) } } + return } @@ -172,5 +175,6 @@ func unmarshalLang(body []byte, r *resp) error { if err := json.NewDecoder(&buf).Decode(r); err != nil { return err } + return nil } diff --git a/internal/view/v1/cmd.go b/internal/view/v1/cmd.go index d9008e0..ac94209 100644 --- a/internal/view/v1/cmd.go +++ b/internal/view/v1/cmd.go @@ -52,6 +52,7 @@ func configload() error { if err == nil { return json.Unmarshal(b, &config) } + return err } @@ -60,6 +61,7 @@ func configsave() error { if err == nil { return ioutil.WriteFile(configpath, j, 0o600) } + return err } diff --git a/internal/view/v1/format.go b/internal/view/v1/format.go index 3636e2a..3e51e56 100644 --- a/internal/view/v1/format.go +++ b/internal/view/v1/format.go @@ -131,6 +131,7 @@ func formatTemp(c cond) string { if explicitPlus { return fmt.Sprintf("\033[38;5;%03dm+%d\033[0m", col, temp) } + return fmt.Sprintf("\033[38;5;%03dm%d\033[0m", col, temp) } t := c.TempC @@ -158,6 +159,7 @@ func formatTemp(c cond) string { if explicitPlus1 { explicitPlus2 = false } + return pad( fmt.Sprintf("%s(%s) °%s", color(t, explicitPlus1), @@ -188,6 +190,7 @@ func formatWind(c cond) string { spd = (spd * 1000) / 1609 } } + return spd } color := func(spd int) string { @@ -230,6 +233,7 @@ func formatWind(c cond) string { } } + hyphen := " - " // if (config.Lang == "sl") { // hyphen = "-" @@ -241,6 +245,7 @@ func formatWind(c cond) string { if windInRightUnits(c.WindGustKmph) > windInRightUnits(c.WindspeedKmph) { return pad(fmt.Sprintf("%s %s%s%s %s", windDir[c.Winddir16Point], cWindspeedKmph, hyphen, cWindGustKmph, unitWindString), 15) } + return pad(fmt.Sprintf("%s %s %s", windDir[c.Winddir16Point], cWindspeedKmph, unitWindString), 15) } @@ -248,6 +253,7 @@ func formatVisibility(c cond) string { if config.Imperial { c.VisibleDistKM = (c.VisibleDistKM * 621) / 1000 } + return pad(fmt.Sprintf("%d %s", c.VisibleDistKM, unitVis(config.Imperial, config.Lang)), 15) } @@ -262,7 +268,9 @@ func formatRain(c cond) string { rainUnit, unitRain(config.Imperial, config.Lang), c.ChanceOfRain), 15) + } + return pad(fmt.Sprintf("%.1f %s", rainUnit, unitRain(config.Imperial, config.Lang)), 15) } @@ -334,7 +342,9 @@ func formatCond(cur []string, c cond, current bool) (ret []string) { ret = append(ret, fmt.Sprintf("%v %v %v", cur[0], desc, icon[0]), fmt.Sprintf("%v %v %v", cur[1], formatTemp(c), icon[1]), fmt.Sprintf("%v %v %v", cur[2], formatWind(c), icon[2]), fmt.Sprintf("%v %v %v", cur[3], formatVisibility(c), icon[3]), fmt.Sprintf("%v %v %v", cur[4], formatRain(c), icon[4])) } else { ret = append(ret, fmt.Sprintf("%v %v %v", cur[0], icon[0], desc), fmt.Sprintf("%v %v %v", cur[1], icon[1], formatTemp(c)), fmt.Sprintf("%v %v %v", cur[2], icon[2], formatWind(c)), fmt.Sprintf("%v %v %v", cur[3], icon[3], formatVisibility(c)), fmt.Sprintf("%v %v %v", cur[4], icon[4], formatRain(c))) + } + return } @@ -348,7 +358,9 @@ func justifyCenter(s string, width int) string { s = " " + s appendSide = 1 } + } + return s } @@ -356,7 +368,9 @@ func reverse(s string) string { r := []rune(s) for i, j := 0, len(r)-1; i < len(r)/2; i, j = i+1, j-1 { r[i], r[j] = r[j], r[i] + } + return string(r) } @@ -379,6 +393,8 @@ func pad(s string, mustLen int) (ret string) { } else { ret = fmt.Sprintf("%s%s%s", toks[0], esc, pad(toks[1], mustLen-tokLen)) } + } + return } diff --git a/internal/view/v1/locale.go b/internal/view/v1/locale.go index 412e3d8..04267ca 100644 --- a/internal/view/v1/locale.go +++ b/internal/view/v1/locale.go @@ -300,6 +300,7 @@ func unitWind(unit int, lang string) string { if !ok { translation = localizedWind["en"] } + return translation[unit] } @@ -308,6 +309,7 @@ func unitVis(unit bool, lang string) string { if !ok { translation = localizedVis["en"] } + return translation[unit] } @@ -316,5 +318,6 @@ func unitRain(unit bool, lang string) string { if !ok { translation = localizedRain["en"] } + return translation[unit] } diff --git a/internal/view/v1/view1.go b/internal/view/v1/view1.go index 4607bec..d4f5615 100644 --- a/internal/view/v1/view1.go +++ b/internal/view/v1/view1.go @@ -49,8 +49,10 @@ func printDay(w weather) (ret []string) { // dateFmt := "┤ " + d.Format("Mon 02. Jan") + " ├" if val, ok := locale[config.Lang]; ok { + lctime.SetLocale(val) } else { + lctime.SetLocale("en_US") } dateName := "" From 8cdf491cb5649c027a2aaa06a10bc6904a17b91a Mon Sep 17 00:00:00 2001 From: Igor Chubin Date: Fri, 23 Dec 2022 16:59:36 +0100 Subject: [PATCH 078/105] Fix whitespace findings --- internal/view/v1/format.go | 5 ----- internal/view/v1/view1.go | 4 ---- 2 files changed, 9 deletions(-) diff --git a/internal/view/v1/format.go b/internal/view/v1/format.go index 3e51e56..7a4edb2 100644 --- a/internal/view/v1/format.go +++ b/internal/view/v1/format.go @@ -268,7 +268,6 @@ func formatRain(c cond) string { rainUnit, unitRain(config.Imperial, config.Lang), c.ChanceOfRain), 15) - } return pad(fmt.Sprintf("%.1f %s", rainUnit, unitRain(config.Imperial, config.Lang)), 15) @@ -342,7 +341,6 @@ func formatCond(cur []string, c cond, current bool) (ret []string) { ret = append(ret, fmt.Sprintf("%v %v %v", cur[0], desc, icon[0]), fmt.Sprintf("%v %v %v", cur[1], formatTemp(c), icon[1]), fmt.Sprintf("%v %v %v", cur[2], formatWind(c), icon[2]), fmt.Sprintf("%v %v %v", cur[3], formatVisibility(c), icon[3]), fmt.Sprintf("%v %v %v", cur[4], formatRain(c), icon[4])) } else { ret = append(ret, fmt.Sprintf("%v %v %v", cur[0], icon[0], desc), fmt.Sprintf("%v %v %v", cur[1], icon[1], formatTemp(c)), fmt.Sprintf("%v %v %v", cur[2], icon[2], formatWind(c)), fmt.Sprintf("%v %v %v", cur[3], icon[3], formatVisibility(c)), fmt.Sprintf("%v %v %v", cur[4], icon[4], formatRain(c))) - } return @@ -358,7 +356,6 @@ func justifyCenter(s string, width int) string { s = " " + s appendSide = 1 } - } return s @@ -368,7 +365,6 @@ func reverse(s string) string { r := []rune(s) for i, j := 0, len(r)-1; i < len(r)/2; i, j = i+1, j-1 { r[i], r[j] = r[j], r[i] - } return string(r) @@ -393,7 +389,6 @@ func pad(s string, mustLen int) (ret string) { } else { ret = fmt.Sprintf("%s%s%s", toks[0], esc, pad(toks[1], mustLen-tokLen)) } - } return diff --git a/internal/view/v1/view1.go b/internal/view/v1/view1.go index d4f5615..3a5aac0 100644 --- a/internal/view/v1/view1.go +++ b/internal/view/v1/view1.go @@ -49,10 +49,8 @@ func printDay(w weather) (ret []string) { // dateFmt := "┤ " + d.Format("Mon 02. Jan") + " ├" if val, ok := locale[config.Lang]; ok { - lctime.SetLocale(val) } else { - lctime.SetLocale("en_US") } dateName := "" @@ -89,7 +87,6 @@ func printDay(w weather) (ret []string) { trans = t } if config.Narrow { - names := "│ " + justifyCenter(trans[1], 16) + "└──────┬──────┘" + justifyCenter(trans[3], 16) + " │" @@ -103,7 +100,6 @@ func printDay(w weather) (ret []string) { return append(ret, "└──────────────────────────────┴──────────────────────────────┘") - } names := "" From ba82bbd09674fc89cd55210210f6c21c30ef0fab Mon Sep 17 00:00:00 2001 From: Igor Chubin Date: Fri, 23 Dec 2022 17:02:39 +0100 Subject: [PATCH 079/105] Fix wastedassign findings --- internal/view/v1/format.go | 6 ++---- internal/view/v1/view1.go | 8 ++++++-- 2 files changed, 8 insertions(+), 6 deletions(-) diff --git a/internal/view/v1/format.go b/internal/view/v1/format.go index 7a4edb2..d44978e 100644 --- a/internal/view/v1/format.go +++ b/internal/view/v1/format.go @@ -29,7 +29,7 @@ var windDir = map[string]string{ func formatTemp(c cond) string { color := func(temp int, explicitPlus bool) string { - col := 0 + var col int if !config.Inverse { // Extemely cold temperature must be shown with violet // because dark blue is too dark @@ -233,12 +233,10 @@ func formatWind(c cond) string { } } - - hyphen := " - " // if (config.Lang == "sl") { // hyphen = "-" // } - hyphen = "-" + hyphen := "-" cWindGustKmph := color(c.WindGustKmph) cWindspeedKmph := color(c.WindspeedKmph) diff --git a/internal/view/v1/view1.go b/internal/view/v1/view1.go index 3a5aac0..9cc98e6 100644 --- a/internal/view/v1/view1.go +++ b/internal/view/v1/view1.go @@ -10,6 +10,11 @@ import ( var slotTimes = [slotcount]int{9 * 60, 12 * 60, 18 * 60, 22 * 60} func printDay(w weather) (ret []string) { + var ( + dateName string + names string + ) + hourly := w.Hourly ret = make([]string, 5) for i := range ret { @@ -53,7 +58,7 @@ func printDay(w weather) (ret []string) { } else { lctime.SetLocale("en_US") } - dateName := "" + if config.RightToLeft { dow := lctime.Strftime("%a", d) day := lctime.Strftime("%d", d) @@ -102,7 +107,6 @@ func printDay(w weather) (ret []string) { "└──────────────────────────────┴──────────────────────────────┘") } - names := "" if config.RightToLeft { names = "│" + justifyCenter(trans[3], 29) + "│ " + justifyCenter(trans[2], 16) + "└──────┬──────┘" + justifyCenter(trans[1], 16) + " │" + justifyCenter(trans[0], 29) + "│" From d86f03f2b7604fc591d9fb722532bdfec54a8866 Mon Sep 17 00:00:00 2001 From: Igor Chubin Date: Fri, 23 Dec 2022 17:15:17 +0100 Subject: [PATCH 080/105] v1: Move icons to a function --- internal/view/v1/api.go | 2 + internal/view/v1/format.go | 2 +- internal/view/v1/icons.go | 406 +++++++++++++++++++------------------ 3 files changed, 208 insertions(+), 202 deletions(-) diff --git a/internal/view/v1/api.go b/internal/view/v1/api.go index d993652..f39ac08 100644 --- a/internal/view/v1/api.go +++ b/internal/view/v1/api.go @@ -14,6 +14,7 @@ import ( "strings" ) +//nolint:tagliatelle type cond struct { ChanceOfRain string `json:"chanceofrain"` FeelsLikeC int `json:",string"` @@ -49,6 +50,7 @@ type loc struct { Type string `json:"type"` } +//nolint:tagliatelle type resp struct { Data struct { Cur []cond `json:"current_condition"` diff --git a/internal/view/v1/format.go b/internal/view/v1/format.go index d44978e..035acf5 100644 --- a/internal/view/v1/format.go +++ b/internal/view/v1/format.go @@ -274,7 +274,7 @@ func formatRain(c cond) string { func formatCond(cur []string, c cond, current bool) (ret []string) { var icon []string if i, ok := codes[c.WeatherCode]; !ok { - icon = iconUnknown + icon = getIcon("iconUnknown") } else { icon = i } diff --git a/internal/view/v1/icons.go b/internal/view/v1/icons.go index 08017fe..0fef24c 100644 --- a/internal/view/v1/icons.go +++ b/internal/view/v1/icons.go @@ -1,206 +1,210 @@ package v1 -var ( - iconUnknown = []string{ - " .-. ", - " __) ", - " ( ", - " `-’ ", - " • ", +func getIcon(name string) []string { + icon := map[string][]string{ + "iconUnknown": { + " .-. ", + " __) ", + " ( ", + " `-’ ", + " • ", + }, + + "iconSunny": { + "\033[38;5;226m \\ / \033[0m", + "\033[38;5;226m .-. \033[0m", + "\033[38;5;226m ― ( ) ― \033[0m", + "\033[38;5;226m `-’ \033[0m", + "\033[38;5;226m / \\ \033[0m", + }, + + "iconPartlyCloudy": { + "\033[38;5;226m \\ /\033[0m ", + "\033[38;5;226m _ /\"\"\033[38;5;250m.-. \033[0m", + "\033[38;5;226m \\_\033[38;5;250m( ). \033[0m", + "\033[38;5;226m /\033[38;5;250m(___(__) \033[0m", + " ", + }, + + "iconCloudy": { + " ", + "\033[38;5;250m .--. \033[0m", + "\033[38;5;250m .-( ). \033[0m", + "\033[38;5;250m (___.__)__) \033[0m", + " ", + }, + + "iconVeryCloudy": { + " ", + "\033[38;5;240;1m .--. \033[0m", + "\033[38;5;240;1m .-( ). \033[0m", + "\033[38;5;240;1m (___.__)__) \033[0m", + " ", + }, + + "iconLightShowers": { + "\033[38;5;226m _`/\"\"\033[38;5;250m.-. \033[0m", + "\033[38;5;226m ,\\_\033[38;5;250m( ). \033[0m", + "\033[38;5;226m /\033[38;5;250m(___(__) \033[0m", + "\033[38;5;111m ‘ ‘ ‘ ‘ \033[0m", + "\033[38;5;111m ‘ ‘ ‘ ‘ \033[0m", + }, + + "iconHeavyShowers": { + "\033[38;5;226m _`/\"\"\033[38;5;240;1m.-. \033[0m", + "\033[38;5;226m ,\\_\033[38;5;240;1m( ). \033[0m", + "\033[38;5;226m /\033[38;5;240;1m(___(__) \033[0m", + "\033[38;5;21;1m ‚‘‚‘‚‘‚‘ \033[0m", + "\033[38;5;21;1m ‚’‚’‚’‚’ \033[0m", + }, + + "iconLightSnowShowers": { + "\033[38;5;226m _`/\"\"\033[38;5;250m.-. \033[0m", + "\033[38;5;226m ,\\_\033[38;5;250m( ). \033[0m", + "\033[38;5;226m /\033[38;5;250m(___(__) \033[0m", + "\033[38;5;255m * * * \033[0m", + "\033[38;5;255m * * * \033[0m", + }, + + "iconHeavySnowShowers": { + "\033[38;5;226m _`/\"\"\033[38;5;240;1m.-. \033[0m", + "\033[38;5;226m ,\\_\033[38;5;240;1m( ). \033[0m", + "\033[38;5;226m /\033[38;5;240;1m(___(__) \033[0m", + "\033[38;5;255;1m * * * * \033[0m", + "\033[38;5;255;1m * * * * \033[0m", + }, + + "iconLightSleetShowers": { + "\033[38;5;226m _`/\"\"\033[38;5;250m.-. \033[0m", + "\033[38;5;226m ,\\_\033[38;5;250m( ). \033[0m", + "\033[38;5;226m /\033[38;5;250m(___(__) \033[0m", + "\033[38;5;111m ‘ \033[38;5;255m*\033[38;5;111m ‘ \033[38;5;255m* \033[0m", + "\033[38;5;255m *\033[38;5;111m ‘ \033[38;5;255m*\033[38;5;111m ‘ \033[0m", + }, + + "iconThunderyShowers": { + "\033[38;5;226m _`/\"\"\033[38;5;250m.-. \033[0m", + "\033[38;5;226m ,\\_\033[38;5;250m( ). \033[0m", + "\033[38;5;226m /\033[38;5;250m(___(__) \033[0m", + "\033[38;5;228;5m ⚡\033[38;5;111;25m‘‘\033[38;5;228;5m⚡\033[38;5;111;25m‘‘ \033[0m", + "\033[38;5;111m ‘ ‘ ‘ ‘ \033[0m", + }, + + "iconThunderyHeavyRain": { + "\033[38;5;240;1m .-. \033[0m", + "\033[38;5;240;1m ( ). \033[0m", + "\033[38;5;240;1m (___(__) \033[0m", + "\033[38;5;21;1m ‚‘\033[38;5;228;5m⚡\033[38;5;21;25m‘‚\033[38;5;228;5m⚡\033[38;5;21;25m‚‘ \033[0m", + "\033[38;5;21;1m ‚’‚’\033[38;5;228;5m⚡\033[38;5;21;25m’‚’ \033[0m", + }, + + "iconThunderySnowShowers": { + "\033[38;5;226m _`/\"\"\033[38;5;250m.-. \033[0m", + "\033[38;5;226m ,\\_\033[38;5;250m( ). \033[0m", + "\033[38;5;226m /\033[38;5;250m(___(__) \033[0m", + "\033[38;5;255m *\033[38;5;228;5m⚡\033[38;5;255;25m*\033[38;5;228;5m⚡\033[38;5;255;25m* \033[0m", + "\033[38;5;255m * * * \033[0m", + }, + + "iconLightRain": { + "\033[38;5;250m .-. \033[0m", + "\033[38;5;250m ( ). \033[0m", + "\033[38;5;250m (___(__) \033[0m", + "\033[38;5;111m ‘ ‘ ‘ ‘ \033[0m", + "\033[38;5;111m ‘ ‘ ‘ ‘ \033[0m", + }, + + "iconHeavyRain": { + "\033[38;5;240;1m .-. \033[0m", + "\033[38;5;240;1m ( ). \033[0m", + "\033[38;5;240;1m (___(__) \033[0m", + "\033[38;5;21;1m ‚‘‚‘‚‘‚‘ \033[0m", + "\033[38;5;21;1m ‚’‚’‚’‚’ \033[0m", + }, + + "iconLightSnow": { + "\033[38;5;250m .-. \033[0m", + "\033[38;5;250m ( ). \033[0m", + "\033[38;5;250m (___(__) \033[0m", + "\033[38;5;255m * * * \033[0m", + "\033[38;5;255m * * * \033[0m", + }, + + "iconHeavySnow": { + "\033[38;5;240;1m .-. \033[0m", + "\033[38;5;240;1m ( ). \033[0m", + "\033[38;5;240;1m (___(__) \033[0m", + "\033[38;5;255;1m * * * * \033[0m", + "\033[38;5;255;1m * * * * \033[0m", + }, + + "iconLightSleet": { + "\033[38;5;250m .-. \033[0m", + "\033[38;5;250m ( ). \033[0m", + "\033[38;5;250m (___(__) \033[0m", + "\033[38;5;111m ‘ \033[38;5;255m*\033[38;5;111m ‘ \033[38;5;255m* \033[0m", + "\033[38;5;255m *\033[38;5;111m ‘ \033[38;5;255m*\033[38;5;111m ‘ \033[0m", + }, + + "iconFog": { + " ", + "\033[38;5;251m _ - _ - _ - \033[0m", + "\033[38;5;251m _ - _ - _ \033[0m", + "\033[38;5;251m _ - _ - _ - \033[0m", + " ", + }, } - iconSunny = []string{ - "\033[38;5;226m \\ / \033[0m", - "\033[38;5;226m .-. \033[0m", - "\033[38;5;226m ― ( ) ― \033[0m", - "\033[38;5;226m `-’ \033[0m", - "\033[38;5;226m / \\ \033[0m", - } + return icon[name] +} - iconPartlyCloudy = []string{ - "\033[38;5;226m \\ /\033[0m ", - "\033[38;5;226m _ /\"\"\033[38;5;250m.-. \033[0m", - "\033[38;5;226m \\_\033[38;5;250m( ). \033[0m", - "\033[38;5;226m /\033[38;5;250m(___(__) \033[0m", - " ", - } - - iconCloudy = []string{ - " ", - "\033[38;5;250m .--. \033[0m", - "\033[38;5;250m .-( ). \033[0m", - "\033[38;5;250m (___.__)__) \033[0m", - " ", - } - - iconVeryCloudy = []string{ - " ", - "\033[38;5;240;1m .--. \033[0m", - "\033[38;5;240;1m .-( ). \033[0m", - "\033[38;5;240;1m (___.__)__) \033[0m", - " ", - } - - iconLightShowers = []string{ - "\033[38;5;226m _`/\"\"\033[38;5;250m.-. \033[0m", - "\033[38;5;226m ,\\_\033[38;5;250m( ). \033[0m", - "\033[38;5;226m /\033[38;5;250m(___(__) \033[0m", - "\033[38;5;111m ‘ ‘ ‘ ‘ \033[0m", - "\033[38;5;111m ‘ ‘ ‘ ‘ \033[0m", - } - - iconHeavyShowers = []string{ - "\033[38;5;226m _`/\"\"\033[38;5;240;1m.-. \033[0m", - "\033[38;5;226m ,\\_\033[38;5;240;1m( ). \033[0m", - "\033[38;5;226m /\033[38;5;240;1m(___(__) \033[0m", - "\033[38;5;21;1m ‚‘‚‘‚‘‚‘ \033[0m", - "\033[38;5;21;1m ‚’‚’‚’‚’ \033[0m", - } - - iconLightSnowShowers = []string{ - "\033[38;5;226m _`/\"\"\033[38;5;250m.-. \033[0m", - "\033[38;5;226m ,\\_\033[38;5;250m( ). \033[0m", - "\033[38;5;226m /\033[38;5;250m(___(__) \033[0m", - "\033[38;5;255m * * * \033[0m", - "\033[38;5;255m * * * \033[0m", - } - - iconHeavySnowShowers = []string{ - "\033[38;5;226m _`/\"\"\033[38;5;240;1m.-. \033[0m", - "\033[38;5;226m ,\\_\033[38;5;240;1m( ). \033[0m", - "\033[38;5;226m /\033[38;5;240;1m(___(__) \033[0m", - "\033[38;5;255;1m * * * * \033[0m", - "\033[38;5;255;1m * * * * \033[0m", - } - - iconLightSleetShowers = []string{ - "\033[38;5;226m _`/\"\"\033[38;5;250m.-. \033[0m", - "\033[38;5;226m ,\\_\033[38;5;250m( ). \033[0m", - "\033[38;5;226m /\033[38;5;250m(___(__) \033[0m", - "\033[38;5;111m ‘ \033[38;5;255m*\033[38;5;111m ‘ \033[38;5;255m* \033[0m", - "\033[38;5;255m *\033[38;5;111m ‘ \033[38;5;255m*\033[38;5;111m ‘ \033[0m", - } - - iconThunderyShowers = []string{ - "\033[38;5;226m _`/\"\"\033[38;5;250m.-. \033[0m", - "\033[38;5;226m ,\\_\033[38;5;250m( ). \033[0m", - "\033[38;5;226m /\033[38;5;250m(___(__) \033[0m", - "\033[38;5;228;5m ⚡\033[38;5;111;25m‘‘\033[38;5;228;5m⚡\033[38;5;111;25m‘‘ \033[0m", - "\033[38;5;111m ‘ ‘ ‘ ‘ \033[0m", - } - - iconThunderyHeavyRain = []string{ - "\033[38;5;240;1m .-. \033[0m", - "\033[38;5;240;1m ( ). \033[0m", - "\033[38;5;240;1m (___(__) \033[0m", - "\033[38;5;21;1m ‚‘\033[38;5;228;5m⚡\033[38;5;21;25m‘‚\033[38;5;228;5m⚡\033[38;5;21;25m‚‘ \033[0m", - "\033[38;5;21;1m ‚’‚’\033[38;5;228;5m⚡\033[38;5;21;25m’‚’ \033[0m", - } - - iconThunderySnowShowers = []string{ - "\033[38;5;226m _`/\"\"\033[38;5;250m.-. \033[0m", - "\033[38;5;226m ,\\_\033[38;5;250m( ). \033[0m", - "\033[38;5;226m /\033[38;5;250m(___(__) \033[0m", - "\033[38;5;255m *\033[38;5;228;5m⚡\033[38;5;255;25m*\033[38;5;228;5m⚡\033[38;5;255;25m* \033[0m", - "\033[38;5;255m * * * \033[0m", - } - - iconLightRain = []string{ - "\033[38;5;250m .-. \033[0m", - "\033[38;5;250m ( ). \033[0m", - "\033[38;5;250m (___(__) \033[0m", - "\033[38;5;111m ‘ ‘ ‘ ‘ \033[0m", - "\033[38;5;111m ‘ ‘ ‘ ‘ \033[0m", - } - - iconHeavyRain = []string{ - "\033[38;5;240;1m .-. \033[0m", - "\033[38;5;240;1m ( ). \033[0m", - "\033[38;5;240;1m (___(__) \033[0m", - "\033[38;5;21;1m ‚‘‚‘‚‘‚‘ \033[0m", - "\033[38;5;21;1m ‚’‚’‚’‚’ \033[0m", - } - - iconLightSnow = []string{ - "\033[38;5;250m .-. \033[0m", - "\033[38;5;250m ( ). \033[0m", - "\033[38;5;250m (___(__) \033[0m", - "\033[38;5;255m * * * \033[0m", - "\033[38;5;255m * * * \033[0m", - } - - iconHeavySnow = []string{ - "\033[38;5;240;1m .-. \033[0m", - "\033[38;5;240;1m ( ). \033[0m", - "\033[38;5;240;1m (___(__) \033[0m", - "\033[38;5;255;1m * * * * \033[0m", - "\033[38;5;255;1m * * * * \033[0m", - } - - iconLightSleet = []string{ - "\033[38;5;250m .-. \033[0m", - "\033[38;5;250m ( ). \033[0m", - "\033[38;5;250m (___(__) \033[0m", - "\033[38;5;111m ‘ \033[38;5;255m*\033[38;5;111m ‘ \033[38;5;255m* \033[0m", - "\033[38;5;255m *\033[38;5;111m ‘ \033[38;5;255m*\033[38;5;111m ‘ \033[0m", - } - - iconFog = []string{ - " ", - "\033[38;5;251m _ - _ - _ - \033[0m", - "\033[38;5;251m _ - _ - _ \033[0m", - "\033[38;5;251m _ - _ - _ - \033[0m", - " ", - } - - codes = map[int][]string{ - 113: iconSunny, - 116: iconPartlyCloudy, - 119: iconCloudy, - 122: iconVeryCloudy, - 143: iconFog, - 176: iconLightShowers, - 179: iconLightSleetShowers, - 182: iconLightSleet, - 185: iconLightSleet, - 200: iconThunderyShowers, - 227: iconLightSnow, - 230: iconHeavySnow, - 248: iconFog, - 260: iconFog, - 263: iconLightShowers, - 266: iconLightRain, - 281: iconLightSleet, - 284: iconLightSleet, - 293: iconLightRain, - 296: iconLightRain, - 299: iconHeavyShowers, - 302: iconHeavyRain, - 305: iconHeavyShowers, - 308: iconHeavyRain, - 311: iconLightSleet, - 314: iconLightSleet, - 317: iconLightSleet, - 320: iconLightSnow, - 323: iconLightSnowShowers, - 326: iconLightSnowShowers, - 329: iconHeavySnow, - 332: iconHeavySnow, - 335: iconHeavySnowShowers, - 338: iconHeavySnow, - 350: iconLightSleet, - 353: iconLightShowers, - 356: iconHeavyShowers, - 359: iconHeavyRain, - 362: iconLightSleetShowers, - 365: iconLightSleetShowers, - 368: iconLightSnowShowers, - 371: iconHeavySnowShowers, - 374: iconLightSleetShowers, - 377: iconLightSleet, - 386: iconThunderyShowers, - 389: iconThunderyHeavyRain, - 392: iconThunderySnowShowers, - 395: iconHeavySnowShowers, - } -) +var codes = map[int][]string{ + 113: getIcon("iconSunny"), + 116: getIcon("iconPartlyCloudy"), + 119: getIcon("iconCloudy"), + 122: getIcon("iconVeryCloudy"), + 143: getIcon("iconFog"), + 176: getIcon("iconLightShowers"), + 179: getIcon("iconLightSleetShowers"), + 182: getIcon("iconLightSleet"), + 185: getIcon("iconLightSleet"), + 200: getIcon("iconThunderyShowers"), + 227: getIcon("iconLightSnow"), + 230: getIcon("iconHeavySnow"), + 248: getIcon("iconFog"), + 260: getIcon("iconFog"), + 263: getIcon("iconLightShowers"), + 266: getIcon("iconLightRain"), + 281: getIcon("iconLightSleet"), + 284: getIcon("iconLightSleet"), + 293: getIcon("iconLightRain"), + 296: getIcon("iconLightRain"), + 299: getIcon("iconHeavyShowers"), + 302: getIcon("iconHeavyRain"), + 305: getIcon("iconHeavyShowers"), + 308: getIcon("iconHeavyRain"), + 311: getIcon("iconLightSleet"), + 314: getIcon("iconLightSleet"), + 317: getIcon("iconLightSleet"), + 320: getIcon("iconLightSnow"), + 323: getIcon("iconLightSnowShowers"), + 326: getIcon("iconLightSnowShowers"), + 329: getIcon("iconHeavySnow"), + 332: getIcon("iconHeavySnow"), + 335: getIcon("iconHeavySnowShowers"), + 338: getIcon("iconHeavySnow"), + 350: getIcon("iconLightSleet"), + 353: getIcon("iconLightShowers"), + 356: getIcon("iconHeavyShowers"), + 359: getIcon("iconHeavyRain"), + 362: getIcon("iconLightSleetShowers"), + 365: getIcon("iconLightSleetShowers"), + 368: getIcon("iconLightSnowShowers"), + 371: getIcon("iconHeavySnowShowers"), + 374: getIcon("iconLightSleetShowers"), + 377: getIcon("iconLightSleet"), + 386: getIcon("iconThunderyShowers"), + 389: getIcon("iconThunderyHeavyRain"), + 392: getIcon("iconThunderySnowShowers"), + 395: getIcon("iconHeavySnowShowers"), +} From 5b80bf417b1aeba2f52ff4eadce9b1820d41480b Mon Sep 17 00:00:00 2001 From: Igor Chubin Date: Fri, 23 Dec 2022 20:05:30 +0100 Subject: [PATCH 081/105] Fix lll findings --- internal/view/v1/format.go | 35 +++++++++++++++++++---------------- internal/view/v1/view1.go | 2 ++ 2 files changed, 21 insertions(+), 16 deletions(-) diff --git a/internal/view/v1/format.go b/internal/view/v1/format.go index 035acf5..5c83b85 100644 --- a/internal/view/v1/format.go +++ b/internal/view/v1/format.go @@ -31,7 +31,7 @@ func formatTemp(c cond) string { color := func(temp int, explicitPlus bool) string { var col int if !config.Inverse { - // Extemely cold temperature must be shown with violet + // Extremely cold temperature must be shown with violet // because dark blue is too dark col = 165 switch temp { @@ -167,17 +167,6 @@ func formatTemp(c cond) string { unitTemp[config.Imperial]), 15) } - // if c.FeelsLikeC < t { - // if c.FeelsLikeC < 0 && t > 0 { - // explicitPlus = true - // } - // return pad(fmt.Sprintf("%s%s%s °%s", color(c.FeelsLikeC, false), hyphen, color(t, explicitPlus), unitTemp[config.Imperial]), 15) - // } else if c.FeelsLikeC > t { - // if t < 0 && c.FeelsLikeC > 0 { - // explicitPlus = true - // } - // return pad(fmt.Sprintf("%s%s%s °%s", color(t, false), hyphen, color(c.FeelsLikeC, explicitPlus), unitTemp[config.Imperial]), 15) - // } return pad(fmt.Sprintf("%s °%s", color(c.FeelsLikeC, false), unitTemp[config.Imperial]), 15) } @@ -241,7 +230,9 @@ func formatWind(c cond) string { cWindGustKmph := color(c.WindGustKmph) cWindspeedKmph := color(c.WindspeedKmph) if windInRightUnits(c.WindGustKmph) > windInRightUnits(c.WindspeedKmph) { - return pad(fmt.Sprintf("%s %s%s%s %s", windDir[c.Winddir16Point], cWindspeedKmph, hyphen, cWindGustKmph, unitWindString), 15) + return pad( + fmt.Sprintf("%s %s%s%s %s", windDir[c.Winddir16Point], cWindspeedKmph, hyphen, cWindGustKmph, unitWindString), + 15) } return pad(fmt.Sprintf("%s %s %s", windDir[c.Winddir16Point], cWindspeedKmph, unitWindString), 15) @@ -258,7 +249,7 @@ func formatVisibility(c cond) string { func formatRain(c cond) string { rainUnit := float32(c.PrecipMM) if config.Imperial { - rainUnit = float32(c.PrecipMM) * 0.039 + rainUnit = c.PrecipMM * 0.039 } if c.ChanceOfRain != "" { return pad(fmt.Sprintf( @@ -336,9 +327,21 @@ func formatCond(cur []string, c cond, current bool) (ret []string) { } } if config.RightToLeft { - ret = append(ret, fmt.Sprintf("%v %v %v", cur[0], desc, icon[0]), fmt.Sprintf("%v %v %v", cur[1], formatTemp(c), icon[1]), fmt.Sprintf("%v %v %v", cur[2], formatWind(c), icon[2]), fmt.Sprintf("%v %v %v", cur[3], formatVisibility(c), icon[3]), fmt.Sprintf("%v %v %v", cur[4], formatRain(c), icon[4])) + ret = append( + ret, + fmt.Sprintf("%v %v %v", cur[0], desc, icon[0]), + fmt.Sprintf("%v %v %v", cur[1], formatTemp(c), icon[1]), + fmt.Sprintf("%v %v %v", cur[2], formatWind(c), icon[2]), + fmt.Sprintf("%v %v %v", cur[3], formatVisibility(c), icon[3]), + fmt.Sprintf("%v %v %v", cur[4], formatRain(c), icon[4])) } else { - ret = append(ret, fmt.Sprintf("%v %v %v", cur[0], icon[0], desc), fmt.Sprintf("%v %v %v", cur[1], icon[1], formatTemp(c)), fmt.Sprintf("%v %v %v", cur[2], icon[2], formatWind(c)), fmt.Sprintf("%v %v %v", cur[3], icon[3], formatVisibility(c)), fmt.Sprintf("%v %v %v", cur[4], icon[4], formatRain(c))) + ret = append( + ret, + fmt.Sprintf("%v %v %v", cur[0], icon[0], desc), + fmt.Sprintf("%v %v %v", cur[1], icon[1], formatTemp(c)), + fmt.Sprintf("%v %v %v", cur[2], icon[2], formatWind(c)), + fmt.Sprintf("%v %v %v", cur[3], icon[3], formatVisibility(c)), + fmt.Sprintf("%v %v %v", cur[4], icon[4], formatRain(c))) } return diff --git a/internal/view/v1/view1.go b/internal/view/v1/view1.go index 9cc98e6..3d352b6 100644 --- a/internal/view/v1/view1.go +++ b/internal/view/v1/view1.go @@ -115,6 +115,7 @@ func printDay(w weather) (ret []string) { "└──────┬──────┘" + justifyCenter(trans[2], 16) + " │" + justifyCenter(trans[3], 29) + "│" } + //nolint:lll ret = append([]string{ " ┌─────────────┐ ", "┌──────────────────────────────┬───────────────────────" + dateFmt + "───────────────────────┬──────────────────────────────┐", @@ -123,6 +124,7 @@ func printDay(w weather) (ret []string) { }, ret...) + //nolint:lll return append(ret, "└──────────────────────────────┴──────────────────────────────┴──────────────────────────────┴──────────────────────────────┘") } From 232637db116350a06daed0f709ea2515acdc20e7 Mon Sep 17 00:00:00 2001 From: Igor Chubin Date: Fri, 23 Dec 2022 20:13:13 +0100 Subject: [PATCH 082/105] Fix nonamedreturns findings --- internal/view/v1/api.go | 9 ++++++--- internal/view/v1/format.go | 18 ++++++++++++------ internal/view/v1/locale.go | 1 + internal/view/v1/view1.go | 3 ++- 4 files changed, 21 insertions(+), 10 deletions(-) diff --git a/internal/view/v1/api.go b/internal/view/v1/api.go index f39ac08..ffa3739 100644 --- a/internal/view/v1/api.go +++ b/internal/view/v1/api.go @@ -60,8 +60,11 @@ type resp struct { } `json:"data"` } -func getDataFromAPI() (ret resp) { - var params []string +func getDataFromAPI() resp { + var ( + ret resp + params []string + ) if len(config.APIKey) == 0 { log.Fatal("No API key specified. Setup instructions are in the README.") @@ -118,7 +121,7 @@ func getDataFromAPI() (ret resp) { } } - return + return ret } func unmarshalLang(body []byte, r *resp) error { diff --git a/internal/view/v1/format.go b/internal/view/v1/format.go index 5c83b85..e9db3a7 100644 --- a/internal/view/v1/format.go +++ b/internal/view/v1/format.go @@ -167,6 +167,7 @@ func formatTemp(c cond) string { unitTemp[config.Imperial]), 15) } + return pad(fmt.Sprintf("%s °%s", color(c.FeelsLikeC, false), unitTemp[config.Imperial]), 15) } @@ -247,7 +248,7 @@ func formatVisibility(c cond) string { } func formatRain(c cond) string { - rainUnit := float32(c.PrecipMM) + rainUnit := c.PrecipMM if config.Imperial { rainUnit = c.PrecipMM * 0.039 } @@ -262,8 +263,12 @@ func formatRain(c cond) string { return pad(fmt.Sprintf("%.1f %s", rainUnit, unitRain(config.Imperial, config.Lang)), 15) } -func formatCond(cur []string, c cond, current bool) (ret []string) { - var icon []string +func formatCond(cur []string, c cond, current bool) []string { + var ( + ret []string + icon []string + ) + if i, ok := codes[c.WeatherCode]; !ok { icon = getIcon("iconUnknown") } else { @@ -344,7 +349,7 @@ func formatCond(cur []string, c cond, current bool) (ret []string) { fmt.Sprintf("%v %v %v", cur[4], icon[4], formatRain(c))) } - return + return ret } func justifyCenter(s string, width int) string { @@ -371,7 +376,8 @@ func reverse(s string) string { return string(r) } -func pad(s string, mustLen int) (ret string) { +func pad(s string, mustLen int) string { + var ret string ret = s realLen := utf8.RuneCountInString(ansiEsc.ReplaceAllLiteralString(s, "")) delta := mustLen - realLen @@ -392,5 +398,5 @@ func pad(s string, mustLen int) (ret string) { } } - return + return ret } diff --git a/internal/view/v1/locale.go b/internal/view/v1/locale.go index 04267ca..4369a31 100644 --- a/internal/view/v1/locale.go +++ b/internal/view/v1/locale.go @@ -148,6 +148,7 @@ var ( "mg": "Vinavina toetr'andro hoan'ny:", } + //nolint:misspell daytimeTranslation = map[string][]string{ "af": {"Oggend", "Middag", "Vroegaand", "Laatnag"}, "am": {"ጠዋት", "ከሰዓት በኋላ", "ምሽት", "ሌሊት"}, diff --git a/internal/view/v1/view1.go b/internal/view/v1/view1.go index 3d352b6..4db2f82 100644 --- a/internal/view/v1/view1.go +++ b/internal/view/v1/view1.go @@ -9,8 +9,9 @@ import ( var slotTimes = [slotcount]int{9 * 60, 12 * 60, 18 * 60, 22 * 60} -func printDay(w weather) (ret []string) { +func printDay(w weather) []string { var ( + ret []string dateName string names string ) From 919089727709956abaf646be76dcba9fff9cb15f Mon Sep 17 00:00:00 2001 From: Igor Chubin Date: Fri, 23 Dec 2022 20:55:30 +0100 Subject: [PATCH 083/105] Fix gochecknoglobals findings --- internal/view/v1/api.go | 34 +++++----- internal/view/v1/cmd.go | 85 ++++++++++++------------ internal/view/v1/format.go | 128 +++++++++++++++++++------------------ internal/view/v1/icons.go | 100 +++++++++++++++-------------- internal/view/v1/locale.go | 44 ++++++++----- internal/view/v1/view1.go | 30 +++++---- 6 files changed, 221 insertions(+), 200 deletions(-) diff --git a/internal/view/v1/api.go b/internal/view/v1/api.go index ffa3739..66ab184 100644 --- a/internal/view/v1/api.go +++ b/internal/view/v1/api.go @@ -60,35 +60,35 @@ type resp struct { } `json:"data"` } -func getDataFromAPI() resp { +func (g *global) getDataFromAPI() resp { var ( ret resp params []string ) - if len(config.APIKey) == 0 { + if len(g.config.APIKey) == 0 { log.Fatal("No API key specified. Setup instructions are in the README.") } - params = append(params, "key="+config.APIKey) + params = append(params, "key="+g.config.APIKey) // non-flag shortcut arguments will overwrite possible flag arguments for _, arg := range flag.Args() { if v, err := strconv.Atoi(arg); err == nil && len(arg) == 1 { - config.Numdays = v + g.config.Numdays = v } else { - config.City = arg + g.config.City = arg } } - if len(config.City) > 0 { - params = append(params, "q="+url.QueryEscape(config.City)) + if len(g.config.City) > 0 { + params = append(params, "q="+url.QueryEscape(g.config.City)) } - params = append(params, "format=json", "num_of_days="+strconv.Itoa(config.Numdays), "tp=3") - if config.Lang != "" { - params = append(params, "lang="+config.Lang) + params = append(params, "format=json", "num_of_days="+strconv.Itoa(g.config.Numdays), "tp=3") + if g.config.Lang != "" { + params = append(params, "lang="+g.config.Lang) } - if debug { + if g.debug { fmt.Fprintln(os.Stderr, params) } @@ -102,7 +102,7 @@ func getDataFromAPI() resp { log.Fatal(err) } - if debug { + if g.debug { var out bytes.Buffer json.Indent(&out, body, "", " ") @@ -111,12 +111,12 @@ func getDataFromAPI() resp { fmt.Print("\n\n") } - if config.Lang == "" { + if g.config.Lang == "" { if err = json.Unmarshal(body, &ret); err != nil { log.Println(err) } } else { - if err = unmarshalLang(body, &ret); err != nil { + if err = g.unmarshalLang(body, &ret); err != nil { log.Println(err) } } @@ -124,7 +124,7 @@ func getDataFromAPI() resp { return ret } -func unmarshalLang(body []byte, r *resp) error { +func (g *global) unmarshalLang(body []byte, r *resp) error { var rv map[string]interface{} if err := json.Unmarshal(body, &rv); err != nil { return err @@ -136,7 +136,7 @@ func unmarshalLang(body []byte, r *resp) error { if !ok { continue } - langs, ok := cc["lang_"+config.Lang].([]interface{}) + langs, ok := cc["lang_"+g.config.Lang].([]interface{}) if !ok || len(langs) == 0 { continue } @@ -159,7 +159,7 @@ func unmarshalLang(body []byte, r *resp) error { if !ok { continue } - langs, ok := h["lang_"+config.Lang].([]interface{}) + langs, ok := h["lang_"+g.config.Lang].([]interface{}) if !ok || len(langs) == 0 { continue } diff --git a/internal/view/v1/cmd.go b/internal/view/v1/cmd.go index ac94209..f86c589 100644 --- a/internal/view/v1/cmd.go +++ b/internal/view/v1/cmd.go @@ -34,12 +34,12 @@ type Configuration struct { RightToLeft bool } -var ( +type global struct { ansiEsc *regexp.Regexp config Configuration configpath string debug bool -) +} const ( wuri = "http://127.0.0.1:5001/premium/v1/weather.ashx?" @@ -47,63 +47,66 @@ const ( slotcount = 4 ) -func configload() error { - b, err := ioutil.ReadFile(configpath) +func (g *global) configload() error { + b, err := ioutil.ReadFile(g.configpath) if err == nil { - return json.Unmarshal(b, &config) + return json.Unmarshal(b, &g.config) } return err } -func configsave() error { - j, err := json.MarshalIndent(config, "", "\t") +func (g *global) configsave() error { + j, err := json.MarshalIndent(g.config, "", "\t") if err == nil { - return ioutil.WriteFile(configpath, j, 0o600) + return ioutil.WriteFile(g.configpath, j, 0o600) } return err } -func init() { - flag.IntVar(&config.Numdays, "days", 3, "Number of days of weather forecast to be displayed") - flag.StringVar(&config.Lang, "lang", "en", "Language of the report") - flag.StringVar(&config.City, "city", "New York", "City to be queried") - flag.BoolVar(&debug, "debug", false, "Print out raw json response for debugging purposes") - flag.BoolVar(&config.Imperial, "imperial", false, "Use imperial units") - flag.BoolVar(&config.Inverse, "inverse", false, "Use inverted colors") - flag.BoolVar(&config.Narrow, "narrow", false, "Narrow output (two columns)") - flag.StringVar(&config.LocationName, "location_name", "", "Location name (used in the caption)") - flag.BoolVar(&config.WindMS, "wind_in_ms", false, "Show wind speed in m/s") - flag.BoolVar(&config.RightToLeft, "right_to_left", false, "Right to left script") - configpath = os.Getenv("WEGORC") - if configpath == "" { +func (g *global) init() { + flag.IntVar(&g.config.Numdays, "days", 3, "Number of days of weather forecast to be displayed") + flag.StringVar(&g.config.Lang, "lang", "en", "Language of the report") + flag.StringVar(&g.config.City, "city", "New York", "City to be queried") + flag.BoolVar(&g.debug, "debug", false, "Print out raw json response for debugging purposes") + flag.BoolVar(&g.config.Imperial, "imperial", false, "Use imperial units") + flag.BoolVar(&g.config.Inverse, "inverse", false, "Use inverted colors") + flag.BoolVar(&g.config.Narrow, "narrow", false, "Narrow output (two columns)") + flag.StringVar(&g.config.LocationName, "location_name", "", "Location name (used in the caption)") + flag.BoolVar(&g.config.WindMS, "wind_in_ms", false, "Show wind speed in m/s") + flag.BoolVar(&g.config.RightToLeft, "right_to_left", false, "Right to left script") + g.configpath = os.Getenv("WEGORC") + if g.configpath == "" { usr, err := user.Current() if err != nil { log.Fatalf("%v\nYou can set the environment variable WEGORC to point to your config file as a workaround.", err) } - configpath = path.Join(usr.HomeDir, ".wegorc") + g.configpath = path.Join(usr.HomeDir, ".wegorc") } - config.APIKey = "" - config.Imperial = false - config.Lang = "en" - err := configload() + g.config.APIKey = "" + g.config.Imperial = false + g.config.Lang = "en" + err := g.configload() if _, ok := err.(*os.PathError); ok { - log.Printf("No config file found. Creating %s ...", configpath) - if err2 := configsave(); err2 != nil { + log.Printf("No config file found. Creating %s ...", g.configpath) + if err2 := g.configsave(); err2 != nil { log.Fatal(err2) } } else if err != nil { - log.Fatalf("could not parse %v: %v", configpath, err) + log.Fatalf("could not parse %v: %v", g.configpath, err) } - ansiEsc = regexp.MustCompile("\033.*?m") + g.ansiEsc = regexp.MustCompile("\033.*?m") } func Cmd() { + g := global{} + g.init() + flag.Parse() - r := getDataFromAPI() + r := g.getDataFromAPI() if r.Data.Req == nil || len(r.Data.Req) < 1 { if r.Data.Err != nil && len(r.Data.Err) >= 1 { @@ -112,16 +115,16 @@ func Cmd() { log.Fatal("Malformed response.") } locationName := r.Data.Req[0].Query - if config.LocationName != "" { - locationName = config.LocationName + if g.config.LocationName != "" { + locationName = g.config.LocationName } - if config.Lang == "he" || config.Lang == "ar" || config.Lang == "fa" { - config.RightToLeft = true + if g.config.Lang == "he" || g.config.Lang == "ar" || g.config.Lang == "fa" { + g.config.RightToLeft = true } - if caption, ok := localizedCaption[config.Lang]; !ok { + if caption, ok := localizedCaption()[g.config.Lang]; !ok { fmt.Printf("Weather report: %s\n\n", locationName) } else { - if config.RightToLeft { + if g.config.RightToLeft { caption = locationName + " " + caption space := strings.Repeat(" ", 125-runewidth.StringWidth(caption)) fmt.Printf("%s%s\n\n", space, caption) @@ -134,9 +137,9 @@ func Cmd() { if r.Data.Cur == nil || len(r.Data.Cur) < 1 { log.Fatal("No weather data available.") } - out := formatCond(make([]string, 5), r.Data.Cur[0], true) + out := g.formatCond(make([]string, 5), r.Data.Cur[0], true) for _, val := range out { - if config.RightToLeft { + if g.config.RightToLeft { fmt.Fprint(stdout, strings.Repeat(" ", 94)) } else { fmt.Fprint(stdout, " ") @@ -144,14 +147,14 @@ func Cmd() { fmt.Fprintln(stdout, val) } - if config.Numdays == 0 { + if g.config.Numdays == 0 { return } if r.Data.Weather == nil { log.Fatal("No detailed weather forecast available.") } for _, d := range r.Data.Weather { - for _, val := range printDay(d) { + for _, val := range g.printDay(d) { fmt.Fprintln(stdout, val) } } diff --git a/internal/view/v1/format.go b/internal/view/v1/format.go index e9db3a7..d5e36dc 100644 --- a/internal/view/v1/format.go +++ b/internal/view/v1/format.go @@ -8,29 +8,31 @@ import ( "github.com/mattn/go-runewidth" ) -var windDir = map[string]string{ - "N": "\033[1m↓\033[0m", - "NNE": "\033[1m↓\033[0m", - "NE": "\033[1m↙\033[0m", - "ENE": "\033[1m↙\033[0m", - "E": "\033[1m←\033[0m", - "ESE": "\033[1m←\033[0m", - "SE": "\033[1m↖\033[0m", - "SSE": "\033[1m↖\033[0m", - "S": "\033[1m↑\033[0m", - "SSW": "\033[1m↑\033[0m", - "SW": "\033[1m↗\033[0m", - "WSW": "\033[1m↗\033[0m", - "W": "\033[1m→\033[0m", - "WNW": "\033[1m→\033[0m", - "NW": "\033[1m↘\033[0m", - "NNW": "\033[1m↘\033[0m", +func windDir() map[string]string { + return map[string]string{ + "N": "\033[1m↓\033[0m", + "NNE": "\033[1m↓\033[0m", + "NE": "\033[1m↙\033[0m", + "ENE": "\033[1m↙\033[0m", + "E": "\033[1m←\033[0m", + "ESE": "\033[1m←\033[0m", + "SE": "\033[1m↖\033[0m", + "SSE": "\033[1m↖\033[0m", + "S": "\033[1m↑\033[0m", + "SSW": "\033[1m↑\033[0m", + "SW": "\033[1m↗\033[0m", + "WSW": "\033[1m↗\033[0m", + "W": "\033[1m→\033[0m", + "WNW": "\033[1m→\033[0m", + "NW": "\033[1m↘\033[0m", + "NNW": "\033[1m↘\033[0m", + } } -func formatTemp(c cond) string { +func (g *global) formatTemp(c cond) string { color := func(temp int, explicitPlus bool) string { var col int - if !config.Inverse { + if !g.config.Inverse { // Extremely cold temperature must be shown with violet // because dark blue is too dark col = 165 @@ -125,7 +127,7 @@ func formatTemp(c cond) string { } } } - if config.Imperial { + if g.config.Imperial { temp = (temp*18 + 320) / 10 } if explicitPlus { @@ -160,23 +162,23 @@ func formatTemp(c cond) string { explicitPlus2 = false } - return pad( + return g.pad( fmt.Sprintf("%s(%s) °%s", color(t, explicitPlus1), color(c.FeelsLikeC, explicitPlus2), - unitTemp[config.Imperial]), + unitTemp()[g.config.Imperial]), 15) } - return pad(fmt.Sprintf("%s °%s", color(c.FeelsLikeC, false), unitTemp[config.Imperial]), 15) + return g.pad(fmt.Sprintf("%s °%s", color(c.FeelsLikeC, false), unitTemp()[g.config.Imperial]), 15) } -func formatWind(c cond) string { +func (g *global) formatWind(c cond) string { windInRightUnits := func(spd int) int { - if config.WindMS { + if g.config.WindMS { spd = (spd * 1000) / 3600 } else { - if config.Imperial { + if g.config.Imperial { spd = (spd * 1000) / 1609 } } @@ -214,12 +216,12 @@ func formatWind(c cond) string { return fmt.Sprintf("\033[38;5;%03dm%d\033[0m", col, spd) } - unitWindString := unitWind(0, config.Lang) - if config.WindMS { - unitWindString = unitWind(2, config.Lang) + unitWindString := unitWind(0, g.config.Lang) + if g.config.WindMS { + unitWindString = unitWind(2, g.config.Lang) } else { - if config.Imperial { - unitWindString = unitWind(1, config.Lang) + if g.config.Imperial { + unitWindString = unitWind(1, g.config.Lang) } } @@ -231,50 +233,50 @@ func formatWind(c cond) string { cWindGustKmph := color(c.WindGustKmph) cWindspeedKmph := color(c.WindspeedKmph) if windInRightUnits(c.WindGustKmph) > windInRightUnits(c.WindspeedKmph) { - return pad( - fmt.Sprintf("%s %s%s%s %s", windDir[c.Winddir16Point], cWindspeedKmph, hyphen, cWindGustKmph, unitWindString), + return g.pad( + fmt.Sprintf("%s %s%s%s %s", windDir()[c.Winddir16Point], cWindspeedKmph, hyphen, cWindGustKmph, unitWindString), 15) } - return pad(fmt.Sprintf("%s %s %s", windDir[c.Winddir16Point], cWindspeedKmph, unitWindString), 15) + return g.pad(fmt.Sprintf("%s %s %s", windDir()[c.Winddir16Point], cWindspeedKmph, unitWindString), 15) } -func formatVisibility(c cond) string { - if config.Imperial { +func (g *global) formatVisibility(c cond) string { + if g.config.Imperial { c.VisibleDistKM = (c.VisibleDistKM * 621) / 1000 } - return pad(fmt.Sprintf("%d %s", c.VisibleDistKM, unitVis(config.Imperial, config.Lang)), 15) + return g.pad(fmt.Sprintf("%d %s", c.VisibleDistKM, unitVis(g.config.Imperial, g.config.Lang)), 15) } -func formatRain(c cond) string { +func (g *global) formatRain(c cond) string { rainUnit := c.PrecipMM - if config.Imperial { + if g.config.Imperial { rainUnit = c.PrecipMM * 0.039 } if c.ChanceOfRain != "" { - return pad(fmt.Sprintf( + return g.pad(fmt.Sprintf( "%.1f %s | %s%%", rainUnit, - unitRain(config.Imperial, config.Lang), + unitRain(g.config.Imperial, g.config.Lang), c.ChanceOfRain), 15) } - return pad(fmt.Sprintf("%.1f %s", rainUnit, unitRain(config.Imperial, config.Lang)), 15) + return g.pad(fmt.Sprintf("%.1f %s", rainUnit, unitRain(g.config.Imperial, g.config.Lang)), 15) } -func formatCond(cur []string, c cond, current bool) []string { +func (g *global) formatCond(cur []string, c cond, current bool) []string { var ( ret []string icon []string ) - if i, ok := codes[c.WeatherCode]; !ok { + if i, ok := codes()[c.WeatherCode]; !ok { icon = getIcon("iconUnknown") } else { icon = i } - if config.Inverse { + if g.config.Inverse { // inverting colors for i := range icon { icon[i] = strings.Replace(icon[i], "38;5;226", "38;5;94", -1) @@ -287,7 +289,7 @@ func formatCond(cur []string, c cond, current bool) []string { } // desc := fmt.Sprintf("%-15.15v", c.WeatherDesc[0].Value) desc := c.WeatherDesc[0].Value - if config.RightToLeft { + if g.config.RightToLeft { for runewidth.StringWidth(desc) < 15 { desc = " " + desc } @@ -305,7 +307,7 @@ func formatCond(cur []string, c cond, current bool) []string { } } if current { - if config.RightToLeft { + if g.config.RightToLeft { desc = c.WeatherDesc[0].Value if runewidth.StringWidth(desc) < 15 { desc = strings.Repeat(" ", 15-runewidth.StringWidth(desc)) + desc @@ -314,7 +316,7 @@ func formatCond(cur []string, c cond, current bool) []string { desc = c.WeatherDesc[0].Value } } else { - if config.RightToLeft { + if g.config.RightToLeft { if frstRune, size := utf8.DecodeRuneInString(desc); frstRune != ' ' { desc = "…" + desc[size:] for runewidth.StringWidth(desc) < 15 { @@ -331,22 +333,22 @@ func formatCond(cur []string, c cond, current bool) []string { } } } - if config.RightToLeft { + if g.config.RightToLeft { ret = append( ret, fmt.Sprintf("%v %v %v", cur[0], desc, icon[0]), - fmt.Sprintf("%v %v %v", cur[1], formatTemp(c), icon[1]), - fmt.Sprintf("%v %v %v", cur[2], formatWind(c), icon[2]), - fmt.Sprintf("%v %v %v", cur[3], formatVisibility(c), icon[3]), - fmt.Sprintf("%v %v %v", cur[4], formatRain(c), icon[4])) + fmt.Sprintf("%v %v %v", cur[1], g.formatTemp(c), icon[1]), + fmt.Sprintf("%v %v %v", cur[2], g.formatWind(c), icon[2]), + fmt.Sprintf("%v %v %v", cur[3], g.formatVisibility(c), icon[3]), + fmt.Sprintf("%v %v %v", cur[4], g.formatRain(c), icon[4])) } else { ret = append( ret, fmt.Sprintf("%v %v %v", cur[0], icon[0], desc), - fmt.Sprintf("%v %v %v", cur[1], icon[1], formatTemp(c)), - fmt.Sprintf("%v %v %v", cur[2], icon[2], formatWind(c)), - fmt.Sprintf("%v %v %v", cur[3], icon[3], formatVisibility(c)), - fmt.Sprintf("%v %v %v", cur[4], icon[4], formatRain(c))) + fmt.Sprintf("%v %v %v", cur[1], icon[1], g.formatTemp(c)), + fmt.Sprintf("%v %v %v", cur[2], icon[2], g.formatWind(c)), + fmt.Sprintf("%v %v %v", cur[3], icon[3], g.formatVisibility(c)), + fmt.Sprintf("%v %v %v", cur[4], icon[4], g.formatRain(c))) } return ret @@ -376,25 +378,25 @@ func reverse(s string) string { return string(r) } -func pad(s string, mustLen int) string { +func (g *global) pad(s string, mustLen int) string { var ret string ret = s - realLen := utf8.RuneCountInString(ansiEsc.ReplaceAllLiteralString(s, "")) + realLen := utf8.RuneCountInString(g.ansiEsc.ReplaceAllLiteralString(s, "")) delta := mustLen - realLen if delta > 0 { - if config.RightToLeft { + if g.config.RightToLeft { ret = strings.Repeat(" ", delta) + ret + "\033[0m" } else { ret += "\033[0m" + strings.Repeat(" ", delta) } } else if delta < 0 { - toks := ansiEsc.Split(s, 2) + toks := g.ansiEsc.Split(s, 2) tokLen := utf8.RuneCountInString(toks[0]) - esc := ansiEsc.FindString(s) + esc := g.ansiEsc.FindString(s) if tokLen > mustLen { ret = fmt.Sprintf("%.*s\033[0m", mustLen, toks[0]) } else { - ret = fmt.Sprintf("%s%s%s", toks[0], esc, pad(toks[1], mustLen-tokLen)) + ret = fmt.Sprintf("%s%s%s", toks[0], esc, g.pad(toks[1], mustLen-tokLen)) } } diff --git a/internal/view/v1/icons.go b/internal/view/v1/icons.go index 0fef24c..d92eb63 100644 --- a/internal/view/v1/icons.go +++ b/internal/view/v1/icons.go @@ -158,53 +158,55 @@ func getIcon(name string) []string { return icon[name] } -var codes = map[int][]string{ - 113: getIcon("iconSunny"), - 116: getIcon("iconPartlyCloudy"), - 119: getIcon("iconCloudy"), - 122: getIcon("iconVeryCloudy"), - 143: getIcon("iconFog"), - 176: getIcon("iconLightShowers"), - 179: getIcon("iconLightSleetShowers"), - 182: getIcon("iconLightSleet"), - 185: getIcon("iconLightSleet"), - 200: getIcon("iconThunderyShowers"), - 227: getIcon("iconLightSnow"), - 230: getIcon("iconHeavySnow"), - 248: getIcon("iconFog"), - 260: getIcon("iconFog"), - 263: getIcon("iconLightShowers"), - 266: getIcon("iconLightRain"), - 281: getIcon("iconLightSleet"), - 284: getIcon("iconLightSleet"), - 293: getIcon("iconLightRain"), - 296: getIcon("iconLightRain"), - 299: getIcon("iconHeavyShowers"), - 302: getIcon("iconHeavyRain"), - 305: getIcon("iconHeavyShowers"), - 308: getIcon("iconHeavyRain"), - 311: getIcon("iconLightSleet"), - 314: getIcon("iconLightSleet"), - 317: getIcon("iconLightSleet"), - 320: getIcon("iconLightSnow"), - 323: getIcon("iconLightSnowShowers"), - 326: getIcon("iconLightSnowShowers"), - 329: getIcon("iconHeavySnow"), - 332: getIcon("iconHeavySnow"), - 335: getIcon("iconHeavySnowShowers"), - 338: getIcon("iconHeavySnow"), - 350: getIcon("iconLightSleet"), - 353: getIcon("iconLightShowers"), - 356: getIcon("iconHeavyShowers"), - 359: getIcon("iconHeavyRain"), - 362: getIcon("iconLightSleetShowers"), - 365: getIcon("iconLightSleetShowers"), - 368: getIcon("iconLightSnowShowers"), - 371: getIcon("iconHeavySnowShowers"), - 374: getIcon("iconLightSleetShowers"), - 377: getIcon("iconLightSleet"), - 386: getIcon("iconThunderyShowers"), - 389: getIcon("iconThunderyHeavyRain"), - 392: getIcon("iconThunderySnowShowers"), - 395: getIcon("iconHeavySnowShowers"), +func codes() map[int][]string { + return map[int][]string{ + 113: getIcon("iconSunny"), + 116: getIcon("iconPartlyCloudy"), + 119: getIcon("iconCloudy"), + 122: getIcon("iconVeryCloudy"), + 143: getIcon("iconFog"), + 176: getIcon("iconLightShowers"), + 179: getIcon("iconLightSleetShowers"), + 182: getIcon("iconLightSleet"), + 185: getIcon("iconLightSleet"), + 200: getIcon("iconThunderyShowers"), + 227: getIcon("iconLightSnow"), + 230: getIcon("iconHeavySnow"), + 248: getIcon("iconFog"), + 260: getIcon("iconFog"), + 263: getIcon("iconLightShowers"), + 266: getIcon("iconLightRain"), + 281: getIcon("iconLightSleet"), + 284: getIcon("iconLightSleet"), + 293: getIcon("iconLightRain"), + 296: getIcon("iconLightRain"), + 299: getIcon("iconHeavyShowers"), + 302: getIcon("iconHeavyRain"), + 305: getIcon("iconHeavyShowers"), + 308: getIcon("iconHeavyRain"), + 311: getIcon("iconLightSleet"), + 314: getIcon("iconLightSleet"), + 317: getIcon("iconLightSleet"), + 320: getIcon("iconLightSnow"), + 323: getIcon("iconLightSnowShowers"), + 326: getIcon("iconLightSnowShowers"), + 329: getIcon("iconHeavySnow"), + 332: getIcon("iconHeavySnow"), + 335: getIcon("iconHeavySnowShowers"), + 338: getIcon("iconHeavySnow"), + 350: getIcon("iconLightSleet"), + 353: getIcon("iconLightShowers"), + 356: getIcon("iconHeavyShowers"), + 359: getIcon("iconHeavyRain"), + 362: getIcon("iconLightSleetShowers"), + 365: getIcon("iconLightSleetShowers"), + 368: getIcon("iconLightSnowShowers"), + 371: getIcon("iconHeavySnowShowers"), + 374: getIcon("iconLightSleetShowers"), + 377: getIcon("iconLightSleet"), + 386: getIcon("iconThunderyShowers"), + 389: getIcon("iconThunderyHeavyRain"), + 392: getIcon("iconThunderySnowShowers"), + 395: getIcon("iconHeavySnowShowers"), + } } diff --git a/internal/view/v1/locale.go b/internal/view/v1/locale.go index 4369a31..b40219d 100644 --- a/internal/view/v1/locale.go +++ b/internal/view/v1/locale.go @@ -1,7 +1,7 @@ package v1 -var ( - locale = map[string]string{ +func locale() map[string]string { + return map[string]string{ "af": "af_ZA", "am": "am_ET", "ar": "ar_TN", @@ -73,8 +73,10 @@ var ( "zh": "zh_CN", "zu": "zu_ZA", } +} - localizedCaption = map[string]string{ +func localizedCaption() map[string]string { + return map[string]string{ "af": "Weer verslag vir:", "am": "የአየር ሁኔታ ዘገባ ለ ፥", "ar": "تقرير حالة ألطقس", @@ -147,9 +149,11 @@ var ( "zh-tw": "天氣預報:", "mg": "Vinavina toetr'andro hoan'ny:", } +} - //nolint:misspell - daytimeTranslation = map[string][]string{ +//nolint:misspell +func daytimeTranslation() map[string][]string { + return map[string][]string{ "af": {"Oggend", "Middag", "Vroegaand", "Laatnag"}, "am": {"ጠዋት", "ከሰዓት በኋላ", "ምሽት", "ሌሊት"}, "ar": {"ﺎﻠﻠﻴﻟ", "ﺎﻠﻤﺳﺍﺀ", "ﺎﻠﻈﻫﺭ", "ﺎﻠﺼﺑﺎﺣ"}, @@ -223,13 +227,17 @@ var ( "zu": {"Morning", "Noon", "Evening", "Night"}, "mg": {"Maraina", "Tolakandro", "Ariva", "Alina"}, } +} - unitTemp = map[bool]string{ +func unitTemp() map[bool]string { + return map[bool]string{ false: "C", true: "F", } +} - localizedRain = map[string]map[bool]string{ +func localizedRain() map[string]map[bool]string { + return map[string]map[bool]string{ "en": { false: "mm", true: "in", @@ -247,8 +255,10 @@ var ( true: "in", }, } +} - localizedVis = map[string]map[bool]string{ +func localizedVis() map[string]map[bool]string { + return map[string]map[bool]string{ "en": { false: "km", true: "mi", @@ -266,8 +276,10 @@ var ( true: "mi", }, } +} - localizedWind = map[string]map[int]string{ +func localizedWind() map[string]map[int]string { + return map[string]map[int]string{ "en": { 0: "km/h", 1: "mph", @@ -294,30 +306,30 @@ var ( 2: "м/c", }, } -) +} func unitWind(unit int, lang string) string { - translation, ok := localizedWind[lang] + translation, ok := localizedWind()[lang] if !ok { - translation = localizedWind["en"] + translation = localizedWind()["en"] } return translation[unit] } func unitVis(unit bool, lang string) string { - translation, ok := localizedVis[lang] + translation, ok := localizedVis()[lang] if !ok { - translation = localizedVis["en"] + translation = localizedVis()["en"] } return translation[unit] } func unitRain(unit bool, lang string) string { - translation, ok := localizedRain[lang] + translation, ok := localizedRain()[lang] if !ok { - translation = localizedRain["en"] + translation = localizedRain()["en"] } return translation[unit] diff --git a/internal/view/v1/view1.go b/internal/view/v1/view1.go index 4db2f82..af002aa 100644 --- a/internal/view/v1/view1.go +++ b/internal/view/v1/view1.go @@ -7,9 +7,11 @@ import ( "github.com/klauspost/lctime" ) -var slotTimes = [slotcount]int{9 * 60, 12 * 60, 18 * 60, 22 * 60} +func slotTimes() []int { + return []int{9 * 60, 12 * 60, 18 * 60, 22 * 60} +} -func printDay(w weather) []string { +func (g *global) printDay(w weather) []string { var ( ret []string dateName string @@ -27,25 +29,25 @@ func printDay(w weather) []string { for _, h := range hourly { c := int(math.Mod(float64(h.Time), 100)) + 60*(h.Time/100) for i, s := range slots { - if math.Abs(float64(c-slotTimes[i])) < math.Abs(float64(s.Time-slotTimes[i])) { + if math.Abs(float64(c-slotTimes()[i])) < math.Abs(float64(s.Time-slotTimes()[i])) { h.Time = c slots[i] = h } } } - if config.RightToLeft { + if g.config.RightToLeft { slots[0], slots[3] = slots[3], slots[0] slots[1], slots[2] = slots[2], slots[1] } for i, s := range slots { - if config.Narrow { + if g.config.Narrow { if i == 0 || i == 2 { continue } } - ret = formatCond(ret, s, false) + ret = g.formatCond(ret, s, false) for i := range ret { ret[i] = ret[i] + "│" } @@ -54,23 +56,23 @@ func printDay(w weather) []string { d, _ := time.Parse("2006-01-02", w.Date) // dateFmt := "┤ " + d.Format("Mon 02. Jan") + " ├" - if val, ok := locale[config.Lang]; ok { + if val, ok := locale()[g.config.Lang]; ok { lctime.SetLocale(val) } else { lctime.SetLocale("en_US") } - if config.RightToLeft { + if g.config.RightToLeft { dow := lctime.Strftime("%a", d) day := lctime.Strftime("%d", d) month := lctime.Strftime("%b", d) dateName = reverse(month) + " " + day + " " + reverse(dow) } else { dateName = lctime.Strftime("%a %d %b", d) - if config.Lang == "ko" { + if g.config.Lang == "ko" { dateName = lctime.Strftime("%b %d일 %a", d) } - if config.Lang == "zh" || config.Lang == "zh-tw" || config.Lang == "zh-cn" { + if g.config.Lang == "zh" || g.config.Lang == "zh-tw" || g.config.Lang == "zh-cn" { dateName = lctime.Strftime("%b%d日%A", d) } } @@ -88,11 +90,11 @@ func printDay(w weather) []string { dateFmt := "┤" + justifyCenter(dateName, 12) + "├" - trans := daytimeTranslation["en"] - if t, ok := daytimeTranslation[config.Lang]; ok { + trans := daytimeTranslation()["en"] + if t, ok := daytimeTranslation()[g.config.Lang]; ok { trans = t } - if config.Narrow { + if g.config.Narrow { names := "│ " + justifyCenter(trans[1], 16) + "└──────┬──────┘" + justifyCenter(trans[3], 16) + " │" @@ -108,7 +110,7 @@ func printDay(w weather) []string { "└──────────────────────────────┴──────────────────────────────┘") } - if config.RightToLeft { + if g.config.RightToLeft { names = "│" + justifyCenter(trans[3], 29) + "│ " + justifyCenter(trans[2], 16) + "└──────┬──────┘" + justifyCenter(trans[1], 16) + " │" + justifyCenter(trans[0], 29) + "│" } else { From 91e52efa31dbf7e89c889462f5a5a63880d58d55 Mon Sep 17 00:00:00 2001 From: Igor Chubin Date: Sat, 24 Dec 2022 18:10:20 +0100 Subject: [PATCH 084/105] Fix gocritic findings --- internal/view/v1/api.go | 26 ++++++++++++++++---------- internal/view/v1/cmd.go | 17 +++++++++++++---- internal/view/v1/format.go | 28 ++++++++++++---------------- internal/view/v1/view1.go | 20 ++++++++++++++------ 4 files changed, 55 insertions(+), 36 deletions(-) diff --git a/internal/view/v1/api.go b/internal/view/v1/api.go index 66ab184..5c40ab6 100644 --- a/internal/view/v1/api.go +++ b/internal/view/v1/api.go @@ -6,7 +6,6 @@ import ( "flag" "fmt" "io/ioutil" - "log" "net/http" "net/url" "os" @@ -60,14 +59,14 @@ type resp struct { } `json:"data"` } -func (g *global) getDataFromAPI() resp { +func (g *global) getDataFromAPI() (*resp, error) { var ( ret resp params []string ) if len(g.config.APIKey) == 0 { - log.Fatal("No API key specified. Setup instructions are in the README.") + return nil, fmt.Errorf("No API key specified. Setup instructions are in the README.") } params = append(params, "key="+g.config.APIKey) @@ -94,34 +93,41 @@ func (g *global) getDataFromAPI() resp { res, err := http.Get(wuri + strings.Join(params, "&")) if err != nil { - log.Fatal(err) + return nil, err } defer res.Body.Close() body, err := ioutil.ReadAll(res.Body) if err != nil { - log.Fatal(err) + return nil, err } if g.debug { var out bytes.Buffer - json.Indent(&out, body, "", " ") + err := json.Indent(&out, body, "", " ") + if err != nil { + return nil, err + } + + _, err = out.WriteTo(os.Stderr) + if err != nil { + return nil, err + } - out.WriteTo(os.Stderr) fmt.Print("\n\n") } if g.config.Lang == "" { if err = json.Unmarshal(body, &ret); err != nil { - log.Println(err) + return nil, err } } else { if err = g.unmarshalLang(body, &ret); err != nil { - log.Println(err) + return nil, err } } - return ret + return &ret, nil } func (g *global) unmarshalLang(body []byte, r *resp) error { diff --git a/internal/view/v1/cmd.go b/internal/view/v1/cmd.go index f86c589..4d5a5c2 100644 --- a/internal/view/v1/cmd.go +++ b/internal/view/v1/cmd.go @@ -100,13 +100,16 @@ func (g *global) init() { g.ansiEsc = regexp.MustCompile("\033.*?m") } -func Cmd() { +func Cmd() error { g := global{} g.init() flag.Parse() - r := g.getDataFromAPI() + r, err := g.getDataFromAPI() + if err != nil { + return err + } if r.Data.Req == nil || len(r.Data.Req) < 1 { if r.Data.Err != nil && len(r.Data.Err) >= 1 { @@ -148,14 +151,20 @@ func Cmd() { } if g.config.Numdays == 0 { - return + return nil } if r.Data.Weather == nil { log.Fatal("No detailed weather forecast available.") } for _, d := range r.Data.Weather { - for _, val := range g.printDay(d) { + lines, err := g.printDay(d) + if err != nil { + return err + } + for _, val := range lines { fmt.Fprintln(stdout, val) } } + + return nil } diff --git a/internal/view/v1/format.go b/internal/view/v1/format.go index d5e36dc..dcfa6b8 100644 --- a/internal/view/v1/format.go +++ b/internal/view/v1/format.go @@ -177,10 +177,8 @@ func (g *global) formatWind(c cond) string { windInRightUnits := func(spd int) int { if g.config.WindMS { spd = (spd * 1000) / 3600 - } else { - if g.config.Imperial { - spd = (spd * 1000) / 1609 - } + } else if g.config.Imperial { + spd = (spd * 1000) / 1609 } return spd @@ -219,10 +217,8 @@ func (g *global) formatWind(c cond) string { unitWindString := unitWind(0, g.config.Lang) if g.config.WindMS { unitWindString = unitWind(2, g.config.Lang) - } else { - if g.config.Imperial { - unitWindString = unitWind(1, g.config.Lang) - } + } else if g.config.Imperial { + unitWindString = unitWind(1, g.config.Lang) } // if (config.Lang == "sl") { @@ -279,12 +275,12 @@ func (g *global) formatCond(cur []string, c cond, current bool) []string { if g.config.Inverse { // inverting colors for i := range icon { - icon[i] = strings.Replace(icon[i], "38;5;226", "38;5;94", -1) - icon[i] = strings.Replace(icon[i], "38;5;250", "38;5;243", -1) - icon[i] = strings.Replace(icon[i], "38;5;21", "38;5;18", -1) - icon[i] = strings.Replace(icon[i], "38;5;255", "38;5;245", -1) - icon[i] = strings.Replace(icon[i], "38;5;111", "38;5;63", -1) - icon[i] = strings.Replace(icon[i], "38;5;251", "38;5;238", -1) + icon[i] = strings.ReplaceAll(icon[i], "38;5;226", "38;5;94") + icon[i] = strings.ReplaceAll(icon[i], "38;5;250", "38;5;243") + icon[i] = strings.ReplaceAll(icon[i], "38;5;21", "38;5;18") + icon[i] = strings.ReplaceAll(icon[i], "38;5;255", "38;5;245") + icon[i] = strings.ReplaceAll(icon[i], "38;5;111", "38;5;63") + icon[i] = strings.ReplaceAll(icon[i], "38;5;251", "38;5;238") } } // desc := fmt.Sprintf("%-15.15v", c.WeatherDesc[0].Value) @@ -328,7 +324,7 @@ func (g *global) formatCond(cur []string, c cond, current bool) []string { desc = desc[:len(desc)-size] + "…" // for numberOfSpaces < runewidth.StringWidth(fmt.Sprintf("%c", lastRune)) - 1 { for runewidth.StringWidth(desc) < 15 { - desc = desc + " " + desc += " " } } } @@ -358,7 +354,7 @@ func justifyCenter(s string, width int) string { appendSide := 0 for runewidth.StringWidth(s) <= width { if appendSide == 1 { - s = s + " " + s += " " appendSide = 0 } else { s = " " + s diff --git a/internal/view/v1/view1.go b/internal/view/v1/view1.go index af002aa..4fd0c19 100644 --- a/internal/view/v1/view1.go +++ b/internal/view/v1/view1.go @@ -11,7 +11,7 @@ func slotTimes() []int { return []int{9 * 60, 12 * 60, 18 * 60, 22 * 60} } -func (g *global) printDay(w weather) []string { +func (g *global) printDay(w weather) ([]string, error) { var ( ret []string dateName string @@ -49,7 +49,7 @@ func (g *global) printDay(w weather) []string { } ret = g.formatCond(ret, s, false) for i := range ret { - ret[i] = ret[i] + "│" + ret[i] += "│" } } @@ -57,9 +57,15 @@ func (g *global) printDay(w weather) []string { // dateFmt := "┤ " + d.Format("Mon 02. Jan") + " ├" if val, ok := locale()[g.config.Lang]; ok { - lctime.SetLocale(val) + err := lctime.SetLocale(val) + if err != nil { + return nil, err + } } else { - lctime.SetLocale("en_US") + err := lctime.SetLocale("en_US") + if err != nil { + return nil, err + } } if g.config.RightToLeft { @@ -107,7 +113,8 @@ func (g *global) printDay(w weather) []string { ret...) return append(ret, - "└──────────────────────────────┴──────────────────────────────┘") + "└──────────────────────────────┴──────────────────────────────┘"), + nil } if g.config.RightToLeft { @@ -129,5 +136,6 @@ func (g *global) printDay(w weather) []string { //nolint:lll return append(ret, - "└──────────────────────────────┴──────────────────────────────┴──────────────────────────────┴──────────────────────────────┘") + "└──────────────────────────────┴──────────────────────────────┴──────────────────────────────┴──────────────────────────────┘"), + nil } From 53b29709b1fb300f2fd431c2c620af4d04fe4045 Mon Sep 17 00:00:00 2001 From: Igor Chubin Date: Sat, 24 Dec 2022 18:11:42 +0100 Subject: [PATCH 085/105] Fix revive findings --- internal/view/v1/api.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/internal/view/v1/api.go b/internal/view/v1/api.go index 5c40ab6..5c68abc 100644 --- a/internal/view/v1/api.go +++ b/internal/view/v1/api.go @@ -66,7 +66,7 @@ func (g *global) getDataFromAPI() (*resp, error) { ) if len(g.config.APIKey) == 0 { - return nil, fmt.Errorf("No API key specified. Setup instructions are in the README.") + return nil, fmt.Errorf("no API key specified. Setup instructions are in the README") } params = append(params, "key="+g.config.APIKey) From 5671b3312662105b9ee5713357a4d5d3ee9f2951 Mon Sep 17 00:00:00 2001 From: Igor Chubin Date: Sat, 24 Dec 2022 18:48:21 +0100 Subject: [PATCH 086/105] Disable leftover linter findings for view/v1 --- internal/view/v1/api.go | 1 + internal/view/v1/cmd.go | 6 ++- internal/view/v1/format.go | 92 +++++++++++++++++++------------------- internal/view/v1/icons.go | 1 + internal/view/v1/locale.go | 4 +- internal/view/v1/view1.go | 15 +------ 6 files changed, 57 insertions(+), 62 deletions(-) diff --git a/internal/view/v1/api.go b/internal/view/v1/api.go index 5c68abc..84fcb64 100644 --- a/internal/view/v1/api.go +++ b/internal/view/v1/api.go @@ -1,3 +1,4 @@ +//nolint:forbidigo,funlen,nestif,goerr113,gocognit,cyclop package v1 import ( diff --git a/internal/view/v1/cmd.go b/internal/view/v1/cmd.go index 4d5a5c2..631107c 100644 --- a/internal/view/v1/cmd.go +++ b/internal/view/v1/cmd.go @@ -1,11 +1,12 @@ // This code represents wttr.in view v1. // It is based on wego (github.com/schachmat/wego) from which it diverged back in 2016. +//nolint:forbidigo,funlen,gocognit,cyclop package v1 import ( - _ "crypto/sha512" "encoding/json" + "errors" "flag" "fmt" "io/ioutil" @@ -88,7 +89,8 @@ func (g *global) init() { g.config.Imperial = false g.config.Lang = "en" err := g.configload() - if _, ok := err.(*os.PathError); ok { + var pathError *os.PathError + if errors.Is(err, pathError) { log.Printf("No config file found. Creating %s ...", g.configpath) if err2 := g.configsave(); err2 != nil { log.Fatal(err2) diff --git a/internal/view/v1/format.go b/internal/view/v1/format.go index dcfa6b8..07951ce 100644 --- a/internal/view/v1/format.go +++ b/internal/view/v1/format.go @@ -1,3 +1,4 @@ +//nolint:funlen,nestif,cyclop,gocognit,gocyclo package v1 import ( @@ -32,6 +33,7 @@ func windDir() map[string]string { func (g *global) formatTemp(c cond) string { color := func(temp int, explicitPlus bool) string { var col int + //nolint:dupl if !g.config.Inverse { // Extremely cold temperature must be shown with violet // because dark blue is too dark @@ -174,46 +176,6 @@ func (g *global) formatTemp(c cond) string { } func (g *global) formatWind(c cond) string { - windInRightUnits := func(spd int) int { - if g.config.WindMS { - spd = (spd * 1000) / 3600 - } else if g.config.Imperial { - spd = (spd * 1000) / 1609 - } - - return spd - } - color := func(spd int) string { - col := 46 - switch spd { - case 1, 2, 3: - col = 82 - case 4, 5, 6: - col = 118 - case 7, 8, 9: - col = 154 - case 10, 11, 12: - col = 190 - case 13, 14, 15: - col = 226 - case 16, 17, 18, 19: - col = 220 - case 20, 21, 22, 23: - col = 214 - case 24, 25, 26, 27: - col = 208 - case 28, 29, 30, 31: - col = 202 - default: - if spd > 0 { - col = 196 - } - } - spd = windInRightUnits(spd) - - return fmt.Sprintf("\033[38;5;%03dm%d\033[0m", col, spd) - } - unitWindString := unitWind(0, g.config.Lang) if g.config.WindMS { unitWindString = unitWind(2, g.config.Lang) @@ -221,14 +183,12 @@ func (g *global) formatWind(c cond) string { unitWindString = unitWind(1, g.config.Lang) } - // if (config.Lang == "sl") { - // hyphen = "-" - // } hyphen := "-" - cWindGustKmph := color(c.WindGustKmph) - cWindspeedKmph := color(c.WindspeedKmph) - if windInRightUnits(c.WindGustKmph) > windInRightUnits(c.WindspeedKmph) { + cWindGustKmph := speedToColor(c.WindGustKmph, windInRightUnits(c.WindGustKmph, g.config.WindMS, g.config.Imperial)) + cWindspeedKmph := speedToColor(c.WindspeedKmph, windInRightUnits(c.WindspeedKmph, g.config.WindMS, g.config.Imperial)) + if windInRightUnits(c.WindGustKmph, g.config.WindMS, g.config.Imperial) > + windInRightUnits(c.WindspeedKmph, g.config.WindMS, g.config.Imperial) { return g.pad( fmt.Sprintf("%s %s%s%s %s", windDir()[c.Winddir16Point], cWindspeedKmph, hyphen, cWindGustKmph, unitWindString), 15) @@ -237,6 +197,46 @@ func (g *global) formatWind(c cond) string { return g.pad(fmt.Sprintf("%s %s %s", windDir()[c.Winddir16Point], cWindspeedKmph, unitWindString), 15) } +func windInRightUnits(spd int, windMS, imperial bool) int { + if windMS { + spd = (spd * 1000) / 3600 + } else if imperial { + spd = (spd * 1000) / 1609 + } + + return spd +} + +func speedToColor(spd, spdConverted int) string { + col := 46 + switch spd { + case 1, 2, 3: + col = 82 + case 4, 5, 6: + col = 118 + case 7, 8, 9: + col = 154 + case 10, 11, 12: + col = 190 + case 13, 14, 15: + col = 226 + case 16, 17, 18, 19: + col = 220 + case 20, 21, 22, 23: + col = 214 + case 24, 25, 26, 27: + col = 208 + case 28, 29, 30, 31: + col = 202 + default: + if spd > 0 { + col = 196 + } + } + + return fmt.Sprintf("\033[38;5;%03dm%d\033[0m", col, spdConverted) +} + func (g *global) formatVisibility(c cond) string { if g.config.Imperial { c.VisibleDistKM = (c.VisibleDistKM * 621) / 1000 diff --git a/internal/view/v1/icons.go b/internal/view/v1/icons.go index d92eb63..c1f8ce2 100644 --- a/internal/view/v1/icons.go +++ b/internal/view/v1/icons.go @@ -1,5 +1,6 @@ package v1 +//nolint:funlen func getIcon(name string) []string { icon := map[string][]string{ "iconUnknown": { diff --git a/internal/view/v1/locale.go b/internal/view/v1/locale.go index b40219d..3ce1b81 100644 --- a/internal/view/v1/locale.go +++ b/internal/view/v1/locale.go @@ -1,5 +1,6 @@ package v1 +//nolint:funlen func locale() map[string]string { return map[string]string{ "af": "af_ZA", @@ -75,6 +76,7 @@ func locale() map[string]string { } } +//nolint:funlen func localizedCaption() map[string]string { return map[string]string{ "af": "Weer verslag vir:", @@ -151,7 +153,7 @@ func localizedCaption() map[string]string { } } -//nolint:misspell +//nolint:misspell,funlen func daytimeTranslation() map[string][]string { return map[string][]string{ "af": {"Oggend", "Middag", "Vroegaand", "Laatnag"}, diff --git a/internal/view/v1/view1.go b/internal/view/v1/view1.go index 4fd0c19..d839659 100644 --- a/internal/view/v1/view1.go +++ b/internal/view/v1/view1.go @@ -11,15 +11,15 @@ func slotTimes() []int { return []int{9 * 60, 12 * 60, 18 * 60, 22 * 60} } +//nolint:funlen,gocognit,cyclop func (g *global) printDay(w weather) ([]string, error) { var ( - ret []string + ret = []string{} dateName string names string ) hourly := w.Hourly - ret = make([]string, 5) for i := range ret { ret[i] = "│" } @@ -82,17 +82,6 @@ func (g *global) printDay(w weather) ([]string, error) { dateName = lctime.Strftime("%b%d日%A", d) } } - // appendSide := 0 - // // for utf8.RuneCountInString(dateName) <= dateWidth { - // for runewidth.StringWidth(dateName) <= dateWidth { - // if appendSide == 1 { - // dateName = dateName + " " - // appendSide = 0 - // } else { - // dateName = " " + dateName - // appendSide = 1 - // } - // } dateFmt := "┤" + justifyCenter(dateName, 12) + "├" From f2670a1a48e9dd6b571505471c7b9e7fa8aa0880 Mon Sep 17 00:00:00 2001 From: PyDeps <109138844+PyDeps@users.noreply.github.com> Date: Thu, 30 Mar 2023 20:00:24 +0800 Subject: [PATCH 087/105] Fix Python dependency API risk issue --- requirements.txt | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/requirements.txt b/requirements.txt index 33d5abe..ad24997 100644 --- a/requirements.txt +++ b/requirements.txt @@ -6,21 +6,21 @@ gevent dnspython pylint cyrtranslit -astral +astral>=2.0,<=2.2 timezonefinder==2.1.2 pytz pyte -python-dateutil +python-dateutil>=2.5.0,<=2.8.1 diagram pyjq scipy numpy pillow babel -pylru +pylru>=1.0.7,<=1.2.1 pysocks supervisor numba -emoji +emoji>=1.6.0,<=1.7.0 grapheme pycountry From c545b9fe801f0f7442a89eff3973271e3af6ec3c Mon Sep 17 00:00:00 2001 From: Alexander Skinner Hassan Date: Fri, 21 Apr 2023 14:19:27 -0500 Subject: [PATCH 088/105] added pressure units in mmHg --- lib/metno.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/lib/metno.py b/lib/metno.py index 18aae75..046500c 100755 --- a/lib/metno.py +++ b/lib/metno.py @@ -177,6 +177,8 @@ def hpa_to_mb(hpa): def hpa_to_in(hpa): return round(hpa * 0.02953, 2) +def hpa_to_mmHg(hpa): + return round(hpa * 0.75006157584566 , 3) def group_hours_to_days(lat, lng, hourlies, days_to_return): tf = timezonefinder.TimezoneFinder() @@ -345,6 +347,7 @@ def _convert_hour(hour): "visibility": 'not yet implemented', # str(details['vis_km']), "visibilityMiles": 'not yet implemented', # str(details['vis_miles']), "pressure": str(hpa_to_mb(details['air_pressure_at_sea_level'])), + "pressure_mmHg": str(hpa_to_mmHg(details['air_pressure_at_sea_level'])), "pressureInches": str(hpa_to_in(details['air_pressure_at_sea_level'])), "cloudcover": 'not yet implemented', # Convert from cloud_area_fraction?? str(details['cloud']), # metno doesn't have FeelsLikeC, but we-lang.go is using it in calcs, From 390e81a0ab197a4dd023102e3e6351896cc4e09f Mon Sep 17 00:00:00 2001 From: joshdutchik <77994722+jdutchik@users.noreply.github.com> Date: Sun, 23 Apr 2023 18:27:56 -0500 Subject: [PATCH 089/105] 339 Bug Fix for Invalid Domain Name --- lib/location.py | 17 +++++++++++------ 1 file changed, 11 insertions(+), 6 deletions(-) diff --git a/lib/location.py b/lib/location.py index 7b36e79..a0cbfa0 100644 --- a/lib/location.py +++ b/lib/location.py @@ -391,12 +391,17 @@ def location_processing(location, ip_addr): if location and location.lstrip('~ ').startswith('@'): try: - location, region, country = _get_location( - socket.gethostbyname( - location.lstrip('~ ')[1:])) - location = '~' + location - location = _fully_qualified_location(location, region, country) - hide_full_address = not force_show_full_address + if (location.lstrip('~ ')[1:] == ""): + location, region, country = NOT_FOUND_LOCATION, None, None + + else: + location, region, country = _get_location( + socket.gethostbyname( + location.lstrip('~ ')[1:])) + location = '~' + location + location = _fully_qualified_location(location, region, country) + hide_full_address = not force_show_full_address + except: location, region, country = NOT_FOUND_LOCATION, None, None From 9a579eacfdbeca5e9f4ff9e115b13a84391f08c7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Helleu?= Date: Wed, 24 May 2023 19:43:34 +0200 Subject: [PATCH 090/105] Improve WeeChat commands Changes: - use /mute command to remove changes on option - use /item command to create a custom item - refresh item in the alias - add spacer in status bar so the item is right-aligned - run command /wttr immediately --- README.md | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index 52e4878..c0aa6c9 100644 --- a/README.md +++ b/README.md @@ -227,17 +227,19 @@ set -g status-right "$WEATHER ..." ``` ![wttr.in in tmux status bar](https://wttr.in/files/example-tmux-status-line.png) -### Weechat +### WeeChat -To embed in to an IRC ([Weechat](https://github.com/weechat/weechat)) client's existing status bar: +To embed in to an IRC ([WeeChat](https://github.com/weechat/weechat)) client's existing status bar: ``` -/alias add wttr /exec -pipe "/set plugins.var.python.text_item.wttr all" url:wttr.in/Montreal?format=%l:+%c+%f+%h+%p+%P+%m+%w+%S+%s +/alias add wttr /exec -pipe "/mute /set plugins.var.wttr" url:wttr.in/Montreal?format=%l:+%c+%f+%h+%p+%P+%m+%w+%S+%s;/wait 3 /item refresh wttr /trigger add wttr timer 60000;0;0 "" "" "/wttr" -/eval /set weechat.bar.status.items ${weechat.bar.status.items},wttr +/item add wttr "" "${plugins.var.wttr}" +/eval /set weechat.bar.status.items ${weechat.bar.status.items},spacer,wttr /eval /set weechat.startup.command_after_plugins ${weechat.startup.command_after_plugins};/wttr +/wttr ``` -![wttr.in in weechat status bar](https://i.imgur.com/IyvbxjL.png) +![wttr.in in WeeChat status bar](https://i.imgur.com/XkYiRU7.png) ### conky From dd6337fb605e07db45187425ec8c132090375f05 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Helleu?= Date: Wed, 24 May 2023 19:44:09 +0200 Subject: [PATCH 091/105] Fix description of conky image --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index c0aa6c9..7064823 100644 --- a/README.md +++ b/README.md @@ -252,7 +252,7 @@ ${texeci 1800 curl wttr.in/kyiv_0pq_lang=uk.png ${image $HOME/.config/conky/out.png -p 0,0} ``` -![wttr.in in weechat status bar](https://user-images.githubusercontent.com/3875145/172178453-9e9ed9e3-9815-426a-9a21-afdd6e279fc8.png) +![wttr.in in conky](https://user-images.githubusercontent.com/3875145/172178453-9e9ed9e3-9815-426a-9a21-afdd6e279fc8.png) ### Emojis support From 779fed36a78f90ad01b4704b39c7683661788e35 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Helleu?= Date: Wed, 24 May 2023 19:44:57 +0200 Subject: [PATCH 092/105] Remove trailing whitespace --- README.md | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index 7064823..407d1b6 100644 --- a/README.md +++ b/README.md @@ -21,10 +21,10 @@ You can access the service from a shell or from a Web browser like this: Weather for City: Paris, France \ / Clear - .-. 10 – 11 °C - ― ( ) ― ↑ 11 km/h - `-’ 10 km - / \ 0.0 mm + .-. 10 – 11 °C + ― ( ) ― ↑ 11 km/h + `-’ 10 km + / \ 0.0 mm Here is an actual weather report for your location (it's live!): @@ -247,7 +247,7 @@ To embed in to an IRC ([WeeChat](https://github.com/weechat/weechat)) client's e Conky usage example: ``` -${texeci 1800 curl wttr.in/kyiv_0pq_lang=uk.png +${texeci 1800 curl wttr.in/kyiv_0pq_lang=uk.png | convert - -transparent black $HOME/.config/conky/out.png} ${image $HOME/.config/conky/out.png -p 0,0} ``` @@ -445,7 +445,7 @@ Most of these values are self-explanatory, aside from `weatherCode`. The `weathe ### Prometheus Metrics Output -The [Prometheus](https://github.com/prometheus/prometheus) Metrics format is a feature providing access to *wttr.in* data through an easy-to-parse format for monitoring systems, without requiring the user to create a complex script to reinterpret wttr.in's graphical output. +The [Prometheus](https://github.com/prometheus/prometheus) Metrics format is a feature providing access to *wttr.in* data through an easy-to-parse format for monitoring systems, without requiring the user to create a complex script to reinterpret wttr.in's graphical output. To fetch information in Prometheus format, use the following syntax: From 3aea9c6b418ea0ccdfc44e6509cb7d5480a45dc5 Mon Sep 17 00:00:00 2001 From: alvinramoutar Date: Thu, 6 Jul 2023 20:10:06 -0400 Subject: [PATCH 093/105] set application stage image to alpine:3.16 --- Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Dockerfile b/Dockerfile index 2f326b5..7ae75da 100644 --- a/Dockerfile +++ b/Dockerfile @@ -13,7 +13,7 @@ RUN go get -u github.com/mattn/go-colorable && \ cd /app && CGO_ENABLED=0 go build . # Application stage -FROM alpine:3 +FROM alpine:3.16 WORKDIR /app From 9d23087ca4b4df6cf141c313ea3f22d2612c5c65 Mon Sep 17 00:00:00 2001 From: mataha Date: Wed, 9 Aug 2023 18:28:09 +0200 Subject: [PATCH 094/105] Introduce 'dumb' mode for terminals without fallback glyphs Windows' native terminal, `conhost.exe`, lacks font fallback support. This causes some of the characters used in `wttr.in`'s terminal output to not be displayed at all - placeholders are supplied in their place. This PR provides a mode that, when enabled, translates missing glyphs to ones available on every platform without (*I believe*) making any compromises regarding their meaning; see translation table below. | Character | C-UCP | Replacements | R-UCPs | | :--------------- | :----: | :------------------------------------ | :----: | | NORTH WEST ARROW | U+2196 | BOX DRAWINGS LIGHT ARC DOWN AND LEFT | U+256E | | NORTH EAST ARROW | U+2197 | BOX DRAWINGS LIGHT ARC DOWN AND RIGHT | U+256D | | SOUTH EAST ARROW | U+2198 | BOX DRAWINGS LIGHT ARC UP AND RIGHT | U+2570 | | SOUTH WEST ARROW | U+2199 | BOX DRAWINGS LIGHT ARC UP AND LEFT | U+256F | | HIGH VOLTAGE SIGN | U+26A1 | BOX DRAWINGS LIGHT DOWN AND RIGHT + UP AND LEFT | U+250C + U+2518 | --- lib/globals.py | 8 ++++++++ lib/parse_query.py | 2 ++ lib/view/moon.py | 3 +++ lib/view/v2.py | 4 +++- lib/view/wttr.py | 7 +++++-- lib/wttr_srv.py | 4 +++- 6 files changed, 24 insertions(+), 4 deletions(-) diff --git a/lib/globals.py b/lib/globals.py index ccf177d..d54184a 100644 --- a/lib/globals.py +++ b/lib/globals.py @@ -94,6 +94,14 @@ PLAIN_TEXT_AGENTS = [ PLAIN_TEXT_PAGES = [':help', ':bash.function', ':translation', ':iterm2'] +TRANSLATION_TABLE = str.maketrans({ + '\u2196': '\u256E', # '↖' -> '╮' + '\u2197': '\u256D', # '↗' -> '╭' + '\u2198': '\u2570', # '↘' -> '╰' + '\u2199': '\u256F', # '↙' -> '╯' + '\u26A1': '\u250C\u2518' +}) + _IPLOCATION_ORDER = os.environ.get( "WTTR_IPLOCATION_ORDER", 'geoip,ip2location,ipinfo') diff --git a/lib/parse_query.py b/lib/parse_query.py index e598a2b..8eed795 100644 --- a/lib/parse_query.py +++ b/lib/parse_query.py @@ -79,6 +79,8 @@ def parse_query(args): return result if 'A' in q: result['force-ansi'] = True + if 'd' in q: + result['dumb'] = True if 'n' in q: result['narrow'] = True if 'm' in q: diff --git a/lib/view/moon.py b/lib/view/moon.py index 05398a1..eec7676 100644 --- a/lib/view/moon.py +++ b/lib/view/moon.py @@ -44,6 +44,9 @@ def get_moon(parsed_query): if parsed_query.get('no-terminal', False): stdout = globals.remove_ansi(stdout) + if parsed_query.get('dumb', False): + stdout = stdout.translate(globals.TRANSLATION_TABLE) + if html: p = Popen( ["bash", globals.ANSI2HTML, "--palette=solarized", "--bg=dark"], diff --git a/lib/view/v2.py b/lib/view/v2.py index ac401e7..298780a 100644 --- a/lib/view/v2.py +++ b/lib/view/v2.py @@ -38,7 +38,7 @@ from astral import moon, sun from scipy.interpolate import interp1d from babel.dates import format_datetime -from globals import WWO_KEY, remove_ansi +from globals import WWO_KEY, TRANSLATION_TABLE, remove_ansi import constants import translations import parse_query @@ -638,6 +638,8 @@ def main(query, parsed_query, data): output += textual_information(data_parsed, geo_data, parsed_query) if parsed_query.get('no-terminal', False): output = remove_ansi(output) + if parsed_query.get('dumb', False): + output = output.translate(TRANSLATION_TABLE) return output if __name__ == '__main__': diff --git a/lib/view/wttr.py b/lib/view/wttr.py index 72222d3..0c3694f 100644 --- a/lib/view/wttr.py +++ b/lib/view/wttr.py @@ -13,8 +13,8 @@ from gevent.subprocess import Popen, PIPE sys.path.insert(0, "..") from translations import get_message, SUPPORTED_LANGS -from globals import WEGO, NOT_FOUND_LOCATION, DEFAULT_LOCATION, ANSI2HTML, \ - error, remove_ansi +from globals import WEGO, TRANSLATION_TABLE, NOT_FOUND_LOCATION, \ + DEFAULT_LOCATION, ANSI2HTML, error, remove_ansi def get_wetter(parsed_query): @@ -126,6 +126,9 @@ def _wego_postprocessing(location, parsed_query, stdout): if parsed_query.get('no-city', False): stdout = "\n".join(stdout.splitlines()[2:]) + "\n" + if parsed_query.get('dumb', False): + stdout = stdout.translate(TRANSLATION_TABLE) + if full_address \ and parsed_query.get('format', 'txt') != 'png' \ and (not parsed_query.get('no-city') diff --git a/lib/wttr_srv.py b/lib/wttr_srv.py index e7fe484..11f7a8a 100644 --- a/lib/wttr_srv.py +++ b/lib/wttr_srv.py @@ -17,7 +17,7 @@ import fmt.png import parse_query from translations import get_message, FULL_TRANSLATION, PARTIAL_TRANSLATION, SUPPORTED_LANGS from buttons import add_buttons -from globals import get_help_file, remove_ansi, \ +from globals import get_help_file, remove_ansi, TRANSLATION_TABLE, \ BASH_FUNCTION_FILE, TRANSLATION_FILE, LOG_FILE, \ NOT_FOUND_LOCATION, \ MALFORMED_RESPONSE_HTML_PAGE, \ @@ -239,6 +239,8 @@ def _response(parsed_query, query, fast_mode=False): message = get_message('FOLLOW_ME', parsed_query['lang']) if parsed_query.get('no-terminal', False): message = remove_ansi(message) + if parsed_query.get('dumb', False): + message = message.translate(TRANSLATION_TABLE) output += '\n' + message + '\n' return cache.store(cache_signature, output) From 7acd951c600514b6ef0a7afd239470b08df768ed Mon Sep 17 00:00:00 2001 From: Felix | D1strict Date: Wed, 27 Sep 2023 18:28:35 +0200 Subject: [PATCH 095/105] Fix unusual translation in german lang Fixes https://github.com/chubin/wttr.in/issues/920 --- share/we-lang/locale.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/share/we-lang/locale.go b/share/we-lang/locale.go index 2560af6..0d51088 100644 --- a/share/we-lang/locale.go +++ b/share/we-lang/locale.go @@ -161,7 +161,7 @@ var ( "ca": {"Matí", "Dia", "Tarda", "Nit"}, "cy": {"Bore", "Dydd", "Hwyr", "Nos"}, "da": {"Morgen", "Middag", "Aften", "Nat"}, - "de": {"Früh", "Mittag", "Abend", "Nacht"}, + "de": {"Morgen", "Mittag", "Abend", "Nacht"}, "el": {"Πρωί", "Μεσημέρι", "Απόγευμα", "Βράδυ"}, "en": {"Morning", "Noon", "Evening", "Night"}, "eo": {"Mateno", "Tago", "Vespero", "Nokto"}, From a886adcc3458ec421b0f8b0268e8d28c602152c1 Mon Sep 17 00:00:00 2001 From: Igor Chubin Date: Mon, 16 Oct 2023 08:29:25 +0200 Subject: [PATCH 096/105] Disable PNG queries --- Makefile | 1 + go.mod | 1 + go.sum | 8 ++++++++ srv.go | 16 ++++++++++++++-- 4 files changed, 24 insertions(+), 2 deletions(-) diff --git a/Makefile b/Makefile index 2d7a03f..cbecb05 100644 --- a/Makefile +++ b/Makefile @@ -1,5 +1,6 @@ srv: srv.go internal/*/*.go internal/*/*/*.go go build -o srv -ldflags '-w -linkmode external -extldflags "-static"' ./ + #go build -o srv ./ go-test: go test ./... diff --git a/go.mod b/go.mod index 0626030..32a5da1 100644 --- a/go.mod +++ b/go.mod @@ -8,6 +8,7 @@ require ( github.com/go-sql-driver/mysql v1.5.0 // indirect github.com/gopherjs/gopherjs v0.0.0-20200217142428-fce0ec30dd00 // indirect github.com/hashicorp/golang-lru v0.6.0 + github.com/itchyny/gojq v0.12.11 // indirect github.com/klauspost/lctime v0.1.0 // indirect github.com/lib/pq v1.8.0 // indirect github.com/mattn/go-colorable v0.1.13 // indirect diff --git a/go.sum b/go.sum index 422eb44..90a0e04 100644 --- a/go.sum +++ b/go.sum @@ -8,11 +8,16 @@ github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSs github.com/denisenkom/go-mssqldb v0.0.0-20200910202707-1e08a3fab204/go.mod h1:xbL0rPBG9cCiLr28tMa8zpbdarY27NDyej4t/EjAShU= github.com/go-sql-driver/mysql v1.5.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg= github.com/golang-sql/civil v0.0.0-20190719163853-cb61b32ac6fe/go.mod h1:8vg3r2VgvsThLBIFL93Qb5yWzgyZWhEmBwUJWevAkK0= +github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY= github.com/gopherjs/gopherjs v0.0.0-20200217142428-fce0ec30dd00/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY= github.com/hashicorp/golang-lru v0.6.0 h1:uL2shRDx7RTrOrTCUZEGP/wJUFiUI8QT6E7z5o8jga4= github.com/hashicorp/golang-lru v0.6.0/go.mod h1:iADmTwqILo4mZ8BN3D2Q6+9jd8WM5uGBxy+E8yxSoD4= github.com/hexops/gotextdiff v1.0.3/go.mod h1:pSWU5MAI3yDq+fZBTazCSJysOMbxWL1BSow5/V2vxeg= +github.com/itchyny/gojq v0.12.11 h1:YhLueoHhHiN4mkfM+3AyJV6EPcCxKZsOnYf+aVSwaQw= +github.com/itchyny/gojq v0.12.11/go.mod h1:o3FT8Gkbg/geT4pLI0tF3hvip5F3Y/uskjRz9OYa38g= +github.com/itchyny/timefmt-go v0.1.5 h1:G0INE2la8S6ru/ZI5JecgyzbbJNs5lG1RcBqa7Jm6GE= +github.com/itchyny/timefmt-go v0.1.5/go.mod h1:nEP7L+2YmAbT2kZ2HfSs1d8Xtw9LY8D2stDBckWakZ8= github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU= github.com/klauspost/lctime v0.1.0 h1:nINsuFc860M9cyYhT6vfg6U1USh7kiVBj/s/2b04U70= github.com/klauspost/lctime v0.1.0/go.mod h1:OwdMhr8tbQvusAsnilqkkgDQqivWlqyg0w5cfXkLiDk= @@ -64,8 +69,11 @@ golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8 h1:0A+M6Uqn+Eje4kHMK80dtF3JC golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab h1:2QkjZIsXupsJbJIdSjjUOgWK3aEtzyuh2mPt3l/CkeU= golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.2.0 h1:ljd4t30dBnAvMZaQCevtY0xLLD0A+bRZXbgLMLU1F/A= +golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/tools v0.0.0-20190328211700-ab21143f2384/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= diff --git a/srv.go b/srv.go index 92754e5..a4defe2 100644 --- a/srv.go +++ b/srv.go @@ -6,6 +6,7 @@ import ( "io" stdlog "log" "net/http" + "strings" "time" "github.com/alecthomas/kong" @@ -17,7 +18,7 @@ import ( "github.com/chubin/wttr.in/internal/logging" "github.com/chubin/wttr.in/internal/processor" "github.com/chubin/wttr.in/internal/types" - v1 "github.com/chubin/wttr.in/internal/view/v1" + // v1 "github.com/chubin/wttr.in/internal/view/v1" ) //nolint:gochecknoglobals @@ -31,7 +32,7 @@ var cli struct { GeoResolve string `name:"geo-resolve" help:"Resolve location"` LogLevel string `name:"log-level" short:"l" help:"Show log messages with level" default:"info"` - V1 v1.Configuration + // V1 v1.Configuration } const logLineStart = "LOG_LINE_START " @@ -155,6 +156,12 @@ func mainHandler( log.Println(err) } + if checkURLForPNG(r) { + w.Write([]byte("PNG support temporary disabled")) + + return + } + response, err := rp.ProcessRequest(r) if err != nil { log.Println(err) @@ -252,3 +259,8 @@ func setLogLevel(logLevel string) error { return nil } + +func checkURLForPNG(r *http.Request) bool { + url := r.URL.String() + return strings.Contains(url, ".png") +} From 893dc74519254eca7fb8e315441d40d1abd036cb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?U=C4=9Fur=20=C3=96zcan?= Date: Fri, 20 Oct 2023 11:30:10 +0300 Subject: [PATCH 097/105] added png file --- San_Francisco.png | Bin 0 -> 64936 bytes 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 San_Francisco.png diff --git a/San_Francisco.png b/San_Francisco.png new file mode 100644 index 0000000000000000000000000000000000000000..17c2280b5ab333c1ad971590164b848617880ae4 GIT binary patch literal 64936 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 literal 0 HcmV?d00001 From c59f25d5bbb6f400389c74ce6baf901f746068d1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?U=C4=9Fur=20=C3=96zcan?= Date: Fri, 20 Oct 2023 11:34:49 +0300 Subject: [PATCH 098/105] Update README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 52e4878..d9babc9 100644 --- a/README.md +++ b/README.md @@ -29,7 +29,7 @@ You can access the service from a shell or from a Web browser like this: Here is an actual weather report for your location (it's live!): -![Weather Report](https://wttr.in/San-Francisco.png?) +![Weather Report](San_Francisco.png) (It's not your actual location - GitHub's CDN hides your real IP address with its own IP address, but it's still a live weather report in your language.) From 1d2352e0f2aee4940c34e70eb9d20a378b8f4fc9 Mon Sep 17 00:00:00 2001 From: Bruce Weirdan Date: Sat, 16 Sep 2023 22:22:41 +0200 Subject: [PATCH 099/105] Add `nushell` agent --- internal/processor/processor.go | 1 + lib/globals.py | 1 + 2 files changed, 2 insertions(+) diff --git a/internal/processor/processor.go b/internal/processor/processor.go index 90abea7..613e5b8 100644 --- a/internal/processor/processor.go +++ b/internal/processor/processor.go @@ -37,6 +37,7 @@ func plainTextAgents() []string { "aiohttp", "http_get", "xh", + "nushell", } } diff --git a/lib/globals.py b/lib/globals.py index ccf177d..6928f17 100644 --- a/lib/globals.py +++ b/lib/globals.py @@ -90,6 +90,7 @@ PLAIN_TEXT_AGENTS = [ "aiohttp", "http_get", "xh", + "nushell", ] PLAIN_TEXT_PAGES = [':help', ':bash.function', ':translation', ':iterm2'] From 104d7bcc7d2c42f62101f5a14d5e3125e04cac2d Mon Sep 17 00:00:00 2001 From: Archit-Kohli <118905854+Archit-Kohli@users.noreply.github.com> Date: Mon, 30 Oct 2023 07:49:20 +0530 Subject: [PATCH 100/105] Update README.md Removed confusing live example statement --- README.md | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/README.md b/README.md index 24e366a..519234e 100644 --- a/README.md +++ b/README.md @@ -27,13 +27,10 @@ You can access the service from a shell or from a Web browser like this: / \ 0.0 mm -Here is an actual weather report for your location (it's live!): +Here is an example weather report: ![Weather Report](San_Francisco.png) -(It's not your actual location - GitHub's CDN hides your real IP address with its own IP address, -but it's still a live weather report in your language.) - Or in PowerShell: ```PowerShell From c2b29e136c1fb166d8210e2e870ff3d7e9db6ef5 Mon Sep 17 00:00:00 2001 From: mataha Date: Fri, 3 Nov 2023 19:37:42 +0100 Subject: [PATCH 101/105] Update documentation regarding fallback glyphs mode Added in #906. Tips for Windows users were removed as they are no longer necessary (and haven't been for a while). --- README.md | 20 +++++--------------- share/help.txt | 1 + 2 files changed, 6 insertions(+), 15 deletions(-) diff --git a/README.md b/README.md index 7b93588..8071c59 100644 --- a/README.md +++ b/README.md @@ -11,7 +11,7 @@ intended to demonstrate the power of the console-oriented services, You can see it running here: [wttr.in](https://wttr.in). -[Documentation](https://wttr.in/:help) | [Usage](https://github.com/chubin/wttr.in#usage) | [One-line output](https://github.com/chubin/wttr.in#one-line-output) | [Data-rich output format](https://github.com/chubin/wttr.in#data-rich-output-format-v2) | [Map view](https://github.com/chubin/wttr.in#map-view-v3) | [Output formats](https://github.com/chubin/wttr.in#different-output-formats) | [Moon phases](https://github.com/chubin/wttr.in#moon-phases) | [Internationalization](https://github.com/chubin/wttr.in#internationalization-and-localization) | [Windows issues](https://github.com/chubin/wttr.in#windows-users) | [Installation](https://github.com/chubin/wttr.in#installation) +[Documentation](https://wttr.in/:help) | [Usage](https://github.com/chubin/wttr.in#usage) | [One-line output](https://github.com/chubin/wttr.in#one-line-output) | [Data-rich output format](https://github.com/chubin/wttr.in#data-rich-output-format-v2) | [Map view](https://github.com/chubin/wttr.in#map-view-v3) | [Output formats](https://github.com/chubin/wttr.in#different-output-formats) | [Moon phases](https://github.com/chubin/wttr.in#moon-phases) | [Internationalization](https://github.com/chubin/wttr.in#internationalization-and-localization) | [Installation](https://github.com/chubin/wttr.in#installation) ## Usage @@ -75,7 +75,6 @@ To get detailed information online, you can access the [/:help](https://wttr.in/ $ curl wttr.in/:help - ### Weather Units By default the USCS units are used for the queries from the USA and the metric system for the rest of the world. @@ -109,6 +108,10 @@ To force plain text, which disables colors: $ curl wttr.in/?T +To restrict output to glyphs available in standard console fonts (e.g. Consolas and Lucida Console): + + $ curl wttr.in/?d + The PNG format can be forced by adding `.png` to the end of the query: $ wget wttr.in/Paris.png @@ -548,19 +551,6 @@ in your language. ![Queries to wttr.in in various languages](https://pbs.twimg.com/media/C7hShiDXQAES6z1.jpg) -## Windows Users - -There are currently two Windows related issues that prevent the examples found on this page from working exactly as expected out of the box. Until Microsoft fixes the issues, there are a few workarounds. To circumvent both issues you may use a shell such as `bash` on the [Windows Subsystem for Linux (WSL)](https://docs.microsoft.com/en-us/windows/wsl/install-win10) or read on for alternative solutions. - -### Garbage characters in the output -There is a limitation of the current Win32 version of `curl`. Until the [Win32 curl issue](https://github.com/chubin/wttr.in/issues/18#issuecomment-474145551) is resolved and rolled out in a future Windows release, it is recommended that you use Powershell’s `Invoke-Web-Request` command instead: -- `(Invoke-WebRequest https://wttr.in).Content` - -### Missing or double wide diagonal wind direction characters -The second issue is regarding the width of the diagonal arrow glyphs that some Windows Terminal Applications such as the default `conhost.exe` use. At the time of writing this, `ConEmu.exe`, `ConEmu64.exe` and Terminal Applications built on top of ConEmu such as Cmder (`cmder.exe`) use these double-wide glyphs by default. The result is the same with all of these programs, either a missing character for certain wind directions or a broken table in the output or both. Some third-party Terminal Applications have addressed the wind direction glyph issue but that fix depends on the font and the Terminal Application you are using. -One way to display the diagonal wind direction glyphs in your Terminal Application is to use [Windows Terminal](https://www.microsoft.com/en-us/p/windows-terminal-preview/9n0dx20hk701?activetab=pivot:overviewtab) which is currently available in the [Microsoft Store](https://www.microsoft.com/en-us/p/windows-terminal-preview/9n0dx20hk701?activetab=pivot:overviewtab). Windows Terminal is currently a preview release and will be rolled out as the default Terminal Application in an upcoming release. If your output is still skewed after using Windows Terminal then try maximizing the terminal window. -Another way you can display the diagonal wind direction is to swap out the problematic characters with forward and backward slashes as shown [here](https://github.com/chubin/wttr.in/issues/18#issuecomment-405640892). - ## Installation To install the application: diff --git a/share/help.txt b/share/help.txt index 7159118..ebfaaa0 100644 --- a/share/help.txt +++ b/share/help.txt @@ -30,6 +30,7 @@ View options: 1 # current weather + today's forecast 2 # current weather + today's + tomorrow's forecast A # ignore User-Agent and force ANSI output format (terminal) + d # restrict output to standard console font glyphs F # do not show the "Follow" line n # narrow version (only day and night) q # quiet version (no "Weather report" text) From 3f4f06c3cb629f7c821a9d4c7dde007a4c884bd5 Mon Sep 17 00:00:00 2001 From: Igor Chubin Date: Sun, 12 Nov 2023 15:53:48 +0100 Subject: [PATCH 102/105] Add initial colors mapping --- internal/fmt/png/colors.go | 774 +++++++++++++++++++++++++++++++++++++ 1 file changed, 774 insertions(+) create mode 100644 internal/fmt/png/colors.go diff --git a/internal/fmt/png/colors.go b/internal/fmt/png/colors.go new file mode 100644 index 0000000..d62b168 --- /dev/null +++ b/internal/fmt/png/colors.go @@ -0,0 +1,774 @@ +package main + +// Source: https://www.ditig.com/downloads/256-colors.json + +var ansiColorsDB = [][3]float64{ + { + 0, 0, 0, + }, + { + 128, 0, 0, + }, + { + 0, 128, 0, + }, + { + 128, 128, 0, + }, + { + 0, 0, 128, + }, + { + 128, 0, 128, + }, + { + 0, 128, 128, + }, + { + 192, 192, 192, + }, + { + 128, 128, 128, + }, + { + 255, 0, 0, + }, + { + 0, 255, 0, + }, + { + 255, 255, 0, + }, + { + 0, 0, 255, + }, + { + 255, 0, 255, + }, + { + 0, 255, 255, + }, + { + 255, 255, 255, + }, + { + 0, 0, 0, + }, + { + 0, 0, 95, + }, + { + 0, 0, 135, + }, + { + 0, 0, 175, + }, + { + 0, 0, 215, + }, + { + 0, 0, 255, + }, + { + 0, 95, 0, + }, + { + 0, 95, 95, + }, + { + 0, 95, 135, + }, + { + 0, 95, 175, + }, + { + 0, 95, 215, + }, + { + 0, 95, 255, + }, + { + 0, 135, 0, + }, + { + 0, 135, 95, + }, + { + 0, 135, 135, + }, + { + 0, 135, 175, + }, + { + 0, 135, 215, + }, + { + 0, 135, 255, + }, + { + 0, 175, 0, + }, + { + 0, 175, 95, + }, + { + 0, 175, 135, + }, + { + 0, 175, 175, + }, + { + 0, 175, 215, + }, + { + 0, 175, 255, + }, + { + 0, 215, 0, + }, + { + 0, 215, 95, + }, + { + 0, 215, 135, + }, + { + 0, 215, 175, + }, + { + 0, 215, 215, + }, + { + 0, 215, 255, + }, + { + 0, 255, 0, + }, + { + 0, 255, 95, + }, + { + 0, 255, 135, + }, + { + 0, 255, 175, + }, + { + 0, 255, 215, + }, + { + 0, 255, 255, + }, + { + 95, 0, 0, + }, + { + 95, 0, 95, + }, + { + 95, 0, 135, + }, + { + 95, 0, 175, + }, + { + 95, 0, 215, + }, + { + 95, 0, 255, + }, + { + 95, 95, 0, + }, + { + 95, 95, 95, + }, + { + 95, 95, 135, + }, + { + 95, 95, 175, + }, + { + 95, 95, 215, + }, + { + 95, 95, 255, + }, + { + 95, 135, 0, + }, + { + 95, 135, 95, + }, + { + 95, 135, 135, + }, + { + 95, 135, 175, + }, + { + 95, 135, 215, + }, + { + 95, 135, 255, + }, + { + 95, 175, 0, + }, + { + 95, 175, 95, + }, + { + 95, 175, 135, + }, + { + 95, 175, 175, + }, + { + 95, 175, 215, + }, + { + 95, 175, 255, + }, + { + 95, 215, 0, + }, + { + 95, 215, 95, + }, + { + 95, 215, 135, + }, + { + 95, 215, 175, + }, + { + 95, 215, 215, + }, + { + 95, 215, 255, + }, + { + 95, 255, 0, + }, + { + 95, 255, 95, + }, + { + 95, 255, 135, + }, + { + 95, 255, 175, + }, + { + 95, 255, 215, + }, + { + 95, 255, 255, + }, + { + 135, 0, 0, + }, + { + 135, 0, 95, + }, + { + 135, 0, 135, + }, + { + 135, 0, 175, + }, + { + 135, 0, 215, + }, + { + 135, 0, 255, + }, + { + 135, 95, 0, + }, + { + 135, 95, 95, + }, + { + 135, 95, 135, + }, + { + 135, 95, 175, + }, + { + 135, 95, 215, + }, + { + 135, 95, 255, + }, + { + 135, 135, 0, + }, + { + 135, 135, 95, + }, + { + 135, 135, 135, + }, + { + 135, 135, 175, + }, + { + 135, 135, 215, + }, + { + 135, 135, 255, + }, + { + 135, 175, 0, + }, + { + 135, 175, 95, + }, + { + 135, 175, 135, + }, + { + 135, 175, 175, + }, + { + 135, 175, 215, + }, + { + 135, 175, 255, + }, + { + 135, 215, 0, + }, + { + 135, 215, 95, + }, + { + 135, 215, 135, + }, + { + 135, 215, 175, + }, + { + 135, 215, 215, + }, + { + 135, 215, 255, + }, + { + 135, 255, 0, + }, + { + 135, 255, 95, + }, + { + 135, 255, 135, + }, + { + 135, 255, 175, + }, + { + 135, 255, 215, + }, + { + 135, 255, 255, + }, + { + 175, 0, 0, + }, + { + 175, 0, 95, + }, + { + 175, 0, 135, + }, + { + 175, 0, 175, + }, + { + 175, 0, 215, + }, + { + 175, 0, 255, + }, + { + 175, 95, 0, + }, + { + 175, 95, 95, + }, + { + 175, 95, 135, + }, + { + 175, 95, 175, + }, + { + 175, 95, 215, + }, + { + 175, 95, 255, + }, + { + 175, 135, 0, + }, + { + 175, 135, 95, + }, + { + 175, 135, 135, + }, + { + 175, 135, 175, + }, + { + 175, 135, 215, + }, + { + 175, 135, 255, + }, + { + 175, 175, 0, + }, + { + 175, 175, 95, + }, + { + 175, 175, 135, + }, + { + 175, 175, 175, + }, + { + 175, 175, 215, + }, + { + 175, 175, 255, + }, + { + 175, 215, 0, + }, + { + 175, 215, 95, + }, + { + 175, 215, 135, + }, + { + 175, 215, 175, + }, + { + 175, 215, 215, + }, + { + 175, 215, 255, + }, + { + 175, 255, 0, + }, + { + 175, 255, 95, + }, + { + 175, 255, 135, + }, + { + 175, 255, 175, + }, + { + 175, 255, 215, + }, + { + 175, 255, 255, + }, + { + 215, 0, 0, + }, + { + 215, 0, 95, + }, + { + 215, 0, 135, + }, + { + 215, 0, 175, + }, + { + 215, 0, 215, + }, + { + 215, 0, 255, + }, + { + 215, 95, 0, + }, + { + 215, 95, 95, + }, + { + 215, 95, 135, + }, + { + 215, 95, 175, + }, + { + 215, 95, 215, + }, + { + 215, 95, 255, + }, + { + 215, 135, 0, + }, + { + 215, 135, 95, + }, + { + 215, 135, 135, + }, + { + 215, 135, 175, + }, + { + 215, 135, 215, + }, + { + 215, 135, 255, + }, + { + 215, 175, 0, + }, + { + 215, 175, 95, + }, + { + 215, 175, 135, + }, + { + 215, 175, 175, + }, + { + 215, 175, 215, + }, + { + 215, 175, 255, + }, + { + 215, 215, 0, + }, + { + 215, 215, 95, + }, + { + 215, 215, 135, + }, + { + 215, 215, 175, + }, + { + 215, 215, 215, + }, + { + 215, 215, 255, + }, + { + 215, 255, 0, + }, + { + 215, 255, 95, + }, + { + 215, 255, 135, + }, + { + 215, 255, 175, + }, + { + 215, 255, 215, + }, + { + 215, 255, 255, + }, + { + 255, 0, 0, + }, + { + 255, 0, 95, + }, + { + 255, 0, 135, + }, + { + 255, 0, 175, + }, + { + 255, 0, 215, + }, + { + 255, 0, 255, + }, + { + 255, 95, 0, + }, + { + 255, 95, 95, + }, + { + 255, 95, 135, + }, + { + 255, 95, 175, + }, + { + 255, 95, 215, + }, + { + 255, 95, 255, + }, + { + 255, 135, 0, + }, + { + 255, 135, 95, + }, + { + 255, 135, 135, + }, + { + 255, 135, 175, + }, + { + 255, 135, 215, + }, + { + 255, 135, 255, + }, + { + 255, 175, 0, + }, + { + 255, 175, 95, + }, + { + 255, 175, 135, + }, + { + 255, 175, 175, + }, + { + 255, 175, 215, + }, + { + 255, 175, 255, + }, + { + 255, 215, 0, + }, + { + 255, 215, 95, + }, + { + 255, 215, 135, + }, + { + 255, 215, 175, + }, + { + 255, 215, 215, + }, + { + 255, 215, 255, + }, + { + 255, 255, 0, + }, + { + 255, 255, 95, + }, + { + 255, 255, 135, + }, + { + 255, 255, 175, + }, + { + 255, 255, 215, + }, + { + 255, 255, 255, + }, + { + 8, 8, 8, + }, + { + 18, 18, 18, + }, + { + 28, 28, 28, + }, + { + 38, 38, 38, + }, + { + 48, 48, 48, + }, + { + 58, 58, 58, + }, + { + 68, 68, 68, + }, + { + 78, 78, 78, + }, + { + 88, 88, 88, + }, + { + 98, 98, 98, + }, + { + 108, 108, 108, + }, + { + 118, 118, 118, + }, + { + 128, 128, 128, + }, + { + 138, 138, 138, + }, + { + 148, 148, 148, + }, + { + 158, 158, 158, + }, + { + 168, 168, 168, + }, + { + 178, 178, 178, + }, + { + 188, 188, 188, + }, + { + 198, 198, 198, + }, + { + 208, 208, 208, + }, + { + 218, 218, 218, + }, + { + 228, 228, 228, + }, + { + 238, 238, 238, + }, +} From 336c5709aa9abda272f7ef554d5aa1eedd3f110e Mon Sep 17 00:00:00 2001 From: Igor Chubin Date: Sun, 12 Nov 2023 15:55:20 +0100 Subject: [PATCH 103/105] Add initial png rendering imlementation (#919) --- internal/fmt/png/png.go | 224 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 224 insertions(+) create mode 100644 internal/fmt/png/png.go diff --git a/internal/fmt/png/png.go b/internal/fmt/png/png.go new file mode 100644 index 0000000..c7fc38b --- /dev/null +++ b/internal/fmt/png/png.go @@ -0,0 +1,224 @@ +package main + +import ( + "fmt" + "log" + "os" + "strings" + + "github.com/fogleman/gg" + "github.com/hinshun/vt10x" +) + +func StringSliceToRuneSlice(s string) [][]rune { + strings := strings.Split(s, "\n") + result := make([][]rune, len(strings)) + + i := 0 + for _, str := range strings { + if len(str) == 0 { + continue + } + result[i] = []rune(str) + i++ + } + + return result +} + +func maxRowLength(rows [][]rune) int { + maxLen := 0 + for _, row := range rows { + if len(row) > maxLen { + maxLen = len(row) + } + } + return maxLen +} + +func GeneratePng() { + runes := StringSliceToRuneSlice(` +Weather report: Hochstadt an der Aisch, Germany + + \ / Partly cloudy + _ /"".-. +5(2) °C + \_( ). ↗ 9 km/h + /(___(__) 10 km + 0.0 mm + ┌─────────────┐ +┌───────────────────────┤ Sat 11 Nov ├───────────────────────┐ +│ Noon └──────┬──────┘ Night │ +├──────────────────────────────┼──────────────────────────────┤ +│ _'/"".-. Patchy rain po…│ _'/"".-. Patchy rain po…│ +│ ,\_( ). +6(3) °C │ ,\_( ). +5(2) °C │ +│ /(___(__) → 22-29 km/h │ /(___(__) ↗ 14-20 km/h │ +│ ‘ ‘ ‘ ‘ 10 km │ ‘ ‘ ‘ ‘ 10 km │ +│ ‘ ‘ ‘ ‘ 0.1 mm | 86% │ ‘ ‘ ‘ ‘ 0.0 mm | 89% │ +└──────────────────────────────┴──────────────────────────────┘ + ┌─────────────┐ +┌───────────────────────┤ Sun 12 Nov ├───────────────────────┐ +│ Noon └──────┬──────┘ Night │ +├──────────────────────────────┼──────────────────────────────┤ +│ \ / Partly cloudy │ .-. Light drizzle │ +│ _ /"".-. +8(7) °C │ ( ). +5(2) °C │ +│ \_( ). ↑ 7-8 km/h │ (___(__) ↑ 13-18 km/h │ +│ /(___(__) 10 km │ ‘ ‘ ‘ ‘ 2 km │ +│ 0.0 mm | 0% │ ‘ ‘ ‘ ‘ 0.3 mm | 76% │ +└──────────────────────────────┴──────────────────────────────┘ +`) + + // Dimensions of each rune in pixels + runeWidth := 8 + runeHeight := 14 + + // Compute the width and height of the final image + imageWidth := runeWidth * maxRowLength(runes) + imageHeight := runeHeight * len(runes) + + // Create a new context with the computed dimensions + dc := gg.NewContext(imageWidth, imageHeight) + + // fontPath := "/usr/share/fonts/truetype/dejavu/DejaVuSansMono.ttf" + // fontPath := "/usr/share/fonts/truetype/wqy/wqy-zenhei.ttc" + fontPath := "/usr/share/fonts/truetype/lexi/LexiGulim.ttf" + + err := dc.LoadFontFace(fontPath, 13) + if err != nil { + log.Fatal(err) + } + + // Loop through each rune in the array and draw it on the context + for i, row := range runes { + for j, char := range row { + // Compute the x and y coordinates for drawing the current rune + x := float64(j*runeWidth + runeWidth/2) + y := float64(i*runeHeight + runeHeight/2) + + // Set the appropriate color for the current rune + if char == '#' { + dc.SetRGB(0, 0, 0) // Black + } else if char == '@' { + dc.SetRGB(1, 0, 0) // Red + } else { + dc.SetRGB(1, 1, 1) // White + } + + character := string(char) + // if char == ' ' { + // character = fmt.Sprint(j % 10) + // } + dc.DrawRectangle(x, y, x+float64(runeWidth), y+float64(runeHeight)) + dc.Fill() + + // Draw a rectangle with the rune's dimensions and color + dc.DrawString(character, x, y) // Draw the character centered on the canvas + // dc.DrawStringAnchored(character, x, y, 0.5, 0.5) // Draw the character centered on the canvas + } + } + + // Save the image to a PNG file + err = dc.SavePNG("output.png") + if err != nil { + fmt.Println("Error saving PNG:", err) + return + } + + fmt.Println("PNG generated successfully") +} + +func GeneratePngFromANSI(input []byte, outputFile string) error { + // Dimensions of each rune in pixels + runeWidth := 8 + runeHeight := 14 + fontSize := 13.0 + // fontPath := "/usr/share/fonts/truetype/dejavu/DejaVuSansMono.ttf" + fontPath := "/usr/share/fonts/truetype/wqy/wqy-zenhei.ttc" + + imageCols := 80 + imageRows := 25 + + // Compute the width and height of the final image + imageWidth := runeWidth * imageCols + imageHeight := runeHeight * imageRows + + // Create terminal and feed it with input. + term := vt10x.New(vt10x.WithSize(imageCols, imageRows)) + _, err := term.Write([]byte("\033[20h")) + if err != nil { + return fmt.Errorf("virtual terminal write error: %w", err) + } + + _, err = term.Write(input) + if err != nil { + return fmt.Errorf("virtual terminal write error: %w", err) + } + + // Create a new context with the computed dimensions + dc := gg.NewContext(imageWidth, imageHeight) + + err = dc.LoadFontFace(fontPath, fontSize) // Set font size to 96 + if err != nil { + return fmt.Errorf("error loading font: %w", err) + } + + // Loop through each rune in the array and draw it on the context + for i := 0; i < imageRows; i++ { + for j := 0; j < imageCols; j++ { + // Compute the x and y coordinates for drawing the current rune + x := float64(j * runeWidth) + y := float64(i * runeHeight) + + cell := term.Cell(j, i) + character := string(cell.Char) + + dc.DrawRectangle(x, y, float64(runeWidth), float64(runeHeight)) + bg := colorANSItoRGB(cell.BG) + dc.SetRGB(bg[0], bg[1], bg[2]) + dc.Fill() + + fg := colorANSItoRGB(cell.FG) + dc.SetRGB(fg[0], fg[1], fg[2]) + + // Draw a rectangle with the rune's dimensions and color + dc.DrawString(character, x, y+float64(runeHeight)-3) // Draw the character centered on the canvas + // dc.DrawStringAnchored(character, x, y, 0.5, 0.5) // Draw the character centered on the canvas + } + } + + // Save the image to a PNG file + err = dc.SavePNG(outputFile) + if err != nil { + return fmt.Errorf("error saving png: %w", err) + } + + return nil +} + +func colorANSItoRGB(colorANSI vt10x.Color) [3]float64 { + defaultBG := vt10x.Color(0) + defaultFG := vt10x.Color(8) + + if colorANSI == vt10x.DefaultFG { + colorANSI = defaultFG + } + if colorANSI == vt10x.DefaultBG { + colorANSI = defaultBG + } + + if colorANSI > 255 { + return [3]float64{127, 127, 127} + } + return ansiColorsDB[colorANSI] +} + +func main() { + data, err := os.ReadFile("zh-text.txt") + if err != nil { + log.Fatalln(err) + } + + err = GeneratePngFromANSI(data, "output.png") + if err != nil { + log.Fatalln(err) + } +} From 72e468abf824f302e49acd5ccda61c4068315dea Mon Sep 17 00:00:00 2001 From: Igor Chubin Date: Sun, 12 Nov 2023 15:56:06 +0100 Subject: [PATCH 104/105] Add internal/fmt/png/go.{mod,sum} --- internal/fmt/png/go.mod | 10 ++++++++++ internal/fmt/png/go.sum | 8 ++++++++ 2 files changed, 18 insertions(+) create mode 100644 internal/fmt/png/go.mod create mode 100644 internal/fmt/png/go.sum diff --git a/internal/fmt/png/go.mod b/internal/fmt/png/go.mod new file mode 100644 index 0000000..43658e5 --- /dev/null +++ b/internal/fmt/png/go.mod @@ -0,0 +1,10 @@ +module example.com/m/v2 + +go 1.20 + +require ( + github.com/fogleman/gg v1.3.0 // indirect + github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0 // indirect + github.com/hinshun/vt10x v0.0.0-20220301184237-5011da428d02 // indirect + golang.org/x/image v0.14.0 // indirect +) diff --git a/internal/fmt/png/go.sum b/internal/fmt/png/go.sum new file mode 100644 index 0000000..4874ce5 --- /dev/null +++ b/internal/fmt/png/go.sum @@ -0,0 +1,8 @@ +github.com/fogleman/gg v1.3.0 h1:/7zJX8F6AaYQc57WQCyN9cAIz+4bCJGO9B+dyW29am8= +github.com/fogleman/gg v1.3.0/go.mod h1:R/bRT+9gY/C5z7JzPU0zXsXHKM4/ayA+zqcVNZzPa1k= +github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0 h1:DACJavvAHhabrF08vX0COfcOBJRhZ8lUbR+ZWIs0Y5g= +github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0/go.mod h1:E/TSTwGwJL78qG/PmXZO1EjYhfJinVAhrmmHX6Z8B9k= +github.com/hinshun/vt10x v0.0.0-20220301184237-5011da428d02 h1:AgcIVYPa6XJnU3phs104wLj8l5GEththEw6+F79YsIY= +github.com/hinshun/vt10x v0.0.0-20220301184237-5011da428d02/go.mod h1:Q48J4R4DvxnHolD5P8pOtXigYlRuPLGl6moFx3ulM68= +golang.org/x/image v0.14.0 h1:tNgSxAFe3jC4uYqvZdTr84SZoM1KfwdC9SKIFrLjFn4= +golang.org/x/image v0.14.0/go.mod h1:HUYqC05R2ZcZ3ejNQsIHQDQiwWM4JBqmm6MKANTp4LE= From 69f82dd315cd0a07e6991fefc58c35cad4b53a51 Mon Sep 17 00:00:00 2001 From: Igor Chubin Date: Sun, 12 Nov 2023 16:56:18 +0100 Subject: [PATCH 105/105] Use fork github.com/chubin/vt10x (#919) --- internal/fmt/png/go.mod | 2 +- internal/fmt/png/go.sum | 4 ++-- internal/fmt/png/png.go | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/internal/fmt/png/go.mod b/internal/fmt/png/go.mod index 43658e5..b41a130 100644 --- a/internal/fmt/png/go.mod +++ b/internal/fmt/png/go.mod @@ -3,8 +3,8 @@ module example.com/m/v2 go 1.20 require ( + github.com/chubin/vt10x v0.0.0-20231112153020-ef4f56837bf1 // indirect github.com/fogleman/gg v1.3.0 // indirect github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0 // indirect - github.com/hinshun/vt10x v0.0.0-20220301184237-5011da428d02 // indirect golang.org/x/image v0.14.0 // indirect ) diff --git a/internal/fmt/png/go.sum b/internal/fmt/png/go.sum index 4874ce5..cadbd96 100644 --- a/internal/fmt/png/go.sum +++ b/internal/fmt/png/go.sum @@ -1,8 +1,8 @@ +github.com/chubin/vt10x v0.0.0-20231112153020-ef4f56837bf1 h1:CHg5BTAJZmCjBaAAQrD92s248JHH3JTsLlaC6QBJo/Y= +github.com/chubin/vt10x v0.0.0-20231112153020-ef4f56837bf1/go.mod h1:mQssL2gI1LTqWgbffl6DESqe6QkAF67ujBdzSe4bWkU= github.com/fogleman/gg v1.3.0 h1:/7zJX8F6AaYQc57WQCyN9cAIz+4bCJGO9B+dyW29am8= github.com/fogleman/gg v1.3.0/go.mod h1:R/bRT+9gY/C5z7JzPU0zXsXHKM4/ayA+zqcVNZzPa1k= github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0 h1:DACJavvAHhabrF08vX0COfcOBJRhZ8lUbR+ZWIs0Y5g= github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0/go.mod h1:E/TSTwGwJL78qG/PmXZO1EjYhfJinVAhrmmHX6Z8B9k= -github.com/hinshun/vt10x v0.0.0-20220301184237-5011da428d02 h1:AgcIVYPa6XJnU3phs104wLj8l5GEththEw6+F79YsIY= -github.com/hinshun/vt10x v0.0.0-20220301184237-5011da428d02/go.mod h1:Q48J4R4DvxnHolD5P8pOtXigYlRuPLGl6moFx3ulM68= golang.org/x/image v0.14.0 h1:tNgSxAFe3jC4uYqvZdTr84SZoM1KfwdC9SKIFrLjFn4= golang.org/x/image v0.14.0/go.mod h1:HUYqC05R2ZcZ3ejNQsIHQDQiwWM4JBqmm6MKANTp4LE= diff --git a/internal/fmt/png/png.go b/internal/fmt/png/png.go index c7fc38b..1b9d7f6 100644 --- a/internal/fmt/png/png.go +++ b/internal/fmt/png/png.go @@ -6,8 +6,8 @@ import ( "os" "strings" + "github.com/chubin/vt10x" "github.com/fogleman/gg" - "github.com/hinshun/vt10x" ) func StringSliceToRuneSlice(s string) [][]rune {