mirror of https://github.com/chubin/wttr.in
You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
267 lines
6.1 KiB
267 lines
6.1 KiB
package main
|
|
|
|
import (
|
|
"crypto/tls"
|
|
"fmt"
|
|
"io"
|
|
stdlog "log"
|
|
"net/http"
|
|
"strings"
|
|
"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"
|
|
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"
|
|
// v1 "github.com/chubin/wttr.in/internal/view/v1"
|
|
)
|
|
|
|
//nolint:gochecknoglobals
|
|
var cli struct {
|
|
ConfigFile string `name:"config-file" arg:"" optional:"" help:"Name of configuration file"`
|
|
|
|
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"`
|
|
LogLevel string `name:"log-level" short:"l" help:"Show log messages with level" default:"info"`
|
|
|
|
// V1 v1.Configuration
|
|
}
|
|
|
|
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 {
|
|
dst.Add(k, v)
|
|
}
|
|
}
|
|
}
|
|
|
|
func serveHTTP(mux *http.ServeMux, port int, logFile io.Writer, errs chan<- error) {
|
|
srv := &http.Server{
|
|
Addr: fmt.Sprintf(":%d", port),
|
|
ErrorLog: stdlog.New(logFile, logLineStart, stdlog.LstdFlags),
|
|
ReadTimeout: 5 * time.Second,
|
|
WriteTimeout: 10 * time.Second,
|
|
IdleTimeout: 1 * time.Second,
|
|
Handler: mux,
|
|
}
|
|
errs <- srv.ListenAndServe()
|
|
}
|
|
|
|
func serveHTTPS(mux *http.ServeMux, port int, certFile, keyFile string, 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,
|
|
// tls.TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256,
|
|
// },
|
|
// MinVersion: tls.VersionTLS13,
|
|
}
|
|
srv := &http.Server{
|
|
Addr: fmt.Sprintf(":%d", port),
|
|
ErrorLog: stdlog.New(logFile, logLineStart, stdlog.LstdFlags),
|
|
ReadTimeout: 5 * time.Second,
|
|
WriteTimeout: 20 * time.Second,
|
|
IdleTimeout: 1 * time.Second,
|
|
TLSConfig: tlsConfig,
|
|
Handler: mux,
|
|
}
|
|
errs <- srv.ListenAndServeTLS(certFile, keyFile)
|
|
}
|
|
|
|
func serve(conf *config.Config) error {
|
|
var (
|
|
// mux is main HTTP/HTTP requests multiplexer.
|
|
mux = http.NewServeMux()
|
|
|
|
// logger is optimized requests logger.
|
|
logger = logging.NewRequestLogger(
|
|
conf.Logging.AccessLog,
|
|
time.Duration(conf.Logging.Interval)*time.Second)
|
|
|
|
rp *processor.RequestProcessor
|
|
|
|
// errs is the servers errors channel.
|
|
errs = make(chan error, 1)
|
|
|
|
// numberOfServers started. If 0, exit.
|
|
numberOfServers int
|
|
|
|
errorsLog = logging.NewLogSuppressor(
|
|
conf.Logging.ErrorsLog,
|
|
suppressMessages(),
|
|
logLineStart,
|
|
)
|
|
|
|
err error
|
|
)
|
|
|
|
rp, err = processor.NewRequestProcessor(conf)
|
|
if err != nil {
|
|
return fmt.Errorf("log processor initialization: %w", err)
|
|
}
|
|
|
|
err = errorsLog.Open()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
err = rp.Start()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
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)
|
|
}
|
|
|
|
if checkURLForPNG(r) {
|
|
w.Write([]byte("PNG support temporary disabled"))
|
|
|
|
return
|
|
}
|
|
|
|
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)
|
|
_, err = w.Write(response.Body)
|
|
if err != nil {
|
|
log.Println(err)
|
|
}
|
|
}
|
|
}
|
|
|
|
func main() {
|
|
var (
|
|
conf *config.Config
|
|
err error
|
|
)
|
|
|
|
ctx := kong.Parse(&cli)
|
|
ctx.FatalIfErrorf(setLogLevel(cli.LogLevel))
|
|
|
|
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 {
|
|
//nolint:forbidigo
|
|
fmt.Print(string(conf.Dump()))
|
|
|
|
return
|
|
}
|
|
|
|
if cli.ConfigCheck {
|
|
return
|
|
}
|
|
|
|
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)
|
|
if loc != nil {
|
|
//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
|
|
}
|
|
|
|
return geoIPCache.ConvertCache()
|
|
}
|
|
|
|
func convertGeoLocationCache(conf *config.Config) error {
|
|
geoLocCache, err := geoloc.NewCache(conf)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
return geoLocCache.ConvertCache(false)
|
|
}
|
|
|
|
func setLogLevel(logLevel string) error {
|
|
parsedLevel, err := log.ParseLevel(logLevel)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
log.SetLevel(parsedLevel)
|
|
|
|
return nil
|
|
}
|
|
|
|
func checkURLForPNG(r *http.Request) bool {
|
|
url := r.URL.String()
|
|
return strings.Contains(url, ".png")
|
|
}
|