From 03a623b207b1fc123945e5e69cd12ffdbec154e7 Mon Sep 17 00:00:00 2001 From: eleksir Date: Mon, 10 Nov 2025 21:53:34 +0300 Subject: [PATCH] Implement sighandler, logfile can be not only stderr * Implement sighandler for int, quit, term signals * Implement log reopen on sighup * Catch http.Server's error log * Implement logfile setting in config --- .golangci.yml | 50 ++++++++++++++++++-- cmd/joyproxy/main.go | 59 +++++++++++++++++++++-- internal/joyproxy/globals.go | 17 +++++++ internal/joyproxy/joyproxyHandler.go | 22 ++++++--- internal/joyproxy/joyurlHandler.go | 12 ++--- internal/joyproxy/loadConf.go | 6 +++ internal/joyproxy/sigHandler.go | 44 +++++++++++++++++ internal/joyproxy/types.go | 1 + internal/log/log.go | 71 ++++++++++++++++++++++++---- joyproxy_sample.ini | 5 +- 10 files changed, 257 insertions(+), 30 deletions(-) create mode 100644 internal/joyproxy/sigHandler.go diff --git a/.golangci.yml b/.golangci.yml index 7d341cd..1224225 100644 --- a/.golangci.yml +++ b/.golangci.yml @@ -1,6 +1,6 @@ version: "2" run: - go: "1.25" + go: "1.24" modules-download-mode: readonly tests: false allow-parallel-runners: true @@ -81,7 +81,7 @@ linters: ignore-exported: false include-unexported: true disable: - - max-len + - max-len godot: scope: all exclude: @@ -196,7 +196,51 @@ linters: - fmt # https://staticcheck.dev/docs/configuration/options/#initialisms # Default: ["ACL", "API", "ASCII", "CPU", "CSS", "DNS", "EOF", "GUID", "HTML", "HTTP", "HTTPS", "ID", "IP", "JSON", "QPS", "RAM", "RPC", "SLA", "SMTP", "SQL", "SSH", "TCP", "TLS", "TTL", "UDP", "UI", "GID", "UID", "UUID", "URI", "URL", "UTF8", "VM", "XML", "XMPP", "XSRF", "XSS", "SIP", "RTP", "AMQP", "DB", "TS"] - initialisms: ["ACL", "API", "ASCII", "CPU", "CSS", "DNS", "EOF", "GUID", "HTML", "HTTP", "HTTPS", "ID", "IP", "JSON", "QPS", "RAM", "RPC", "SLA", "SMTP", "SQL", "SSH", "TCP", "TLS", "TTL", "UDP", "UI", "GID", "UID", "UUID", "URI", "URL", "UTF8", "VM", "XML", "XMPP", "XSRF", "XSS", "SIP", "RTP", "AMQP", "DB", "TS"] + initialisms: + [ + "ACL", + "API", + "ASCII", + "CPU", + "CSS", + "DNS", + "EOF", + "GUID", + "HTML", + "HTTP", + "HTTPS", + "ID", + "IP", + "JSON", + "QPS", + "RAM", + "RPC", + "SLA", + "SMTP", + "SQL", + "SSH", + "TCP", + "TLS", + "TTL", + "UDP", + "UI", + "GID", + "UID", + "UUID", + "URI", + "URL", + "UTF8", + "VM", + "XML", + "XMPP", + "XSRF", + "XSS", + "SIP", + "RTP", + "AMQP", + "DB", + "TS", + ] # https://staticcheck.dev/docs/configuration/options/#http_status_code_whitelist # Default: ["200", "400", "404", "500"] http-status-code-whitelist: ["200", "400", "404", "500"] diff --git a/cmd/joyproxy/main.go b/cmd/joyproxy/main.go index f80f5ce..7841d67 100644 --- a/cmd/joyproxy/main.go +++ b/cmd/joyproxy/main.go @@ -2,8 +2,11 @@ package main import ( + "errors" "net/http" "os" + "os/signal" + "syscall" "time" "joyproxy/internal/joyproxy" @@ -12,26 +15,74 @@ import ( // main application entry point. func main() { + var ( + err error + logfile *os.File + ) + joyproxy.LoadConf() - log.Init(joyproxy.Cfg.Loglevel, os.Stderr) + if joyproxy.Cfg.Logfile != "" { + logfile, err = os.OpenFile(joyproxy.Cfg.Logfile, os.O_WRONLY|os.O_APPEND|os.O_CREATE, 0644) + if err != nil { + log.Fatalf("Unable to open %s: %s", joyproxy.Cfg.Logfile, err) + } + } else { + logfile = os.Stderr + } + + log.Init(joyproxy.Cfg.Loglevel, logfile) + + log.Info("Install handler to /joyproxy/ location") http.HandleFunc("/joyproxy/", joyproxy.JoyproxyHandler) + + log.Info("Install handler to /joyurl location") http.HandleFunc("/joyurl", joyproxy.JoyurlHandler) - log.Infof("Starting application on port %v\n", joyproxy.Cfg.Port) + // It's time to set up signal trapper. + signal.Notify(joyproxy.SigChan, + syscall.SIGINT, + syscall.SIGTERM, + syscall.SIGQUIT, + syscall.SIGHUP, + ) + + go joyproxy.SigHandler() + + log.Infof("Listening on port %v\n", joyproxy.Cfg.Port) // To prevent slow loris ddos, we have to define custom handler because builtin http.ListenAndServe() have no // timeout and sits here forever. - server := &http.Server{ + joyproxy.Server = &http.Server{ Addr: joyproxy.Cfg.Port, ReadHeaderTimeout: time.Duration(joyproxy.Cfg.Timeout) * time.Second, ReadTimeout: time.Duration(joyproxy.Cfg.Timeout) * time.Second, WriteTimeout: time.Duration(joyproxy.Cfg.Timeout) * time.Second, IdleTimeout: time.Duration(joyproxy.Cfg.Timeout) * time.Second, + ErrorLog: &log.Logger, } - log.Warn(server.ListenAndServe().Error()) + for { + if joyproxy.Shutdown { + time.Sleep(100 * time.Microsecond) + + continue + } + + err = joyproxy.Server.ListenAndServe() + + if errors.Is(err, http.ErrServerClosed) { + log.Info("Shutdown joyproxy") + + break + } + + log.Errorf("Listener error: %s", err) + } + + <-joyproxy.QuitChan + os.Exit(0) } /* vim: set ft=go noet ai ts=4 sw=4 sts=4: */ diff --git a/internal/joyproxy/globals.go b/internal/joyproxy/globals.go index fa5df47..dfdb760 100644 --- a/internal/joyproxy/globals.go +++ b/internal/joyproxy/globals.go @@ -1,6 +1,11 @@ // Package joyproxy is main appliction lib. package joyproxy +import ( + "net/http" + "os" +) + var ( // Cfg represents struct with application config settings. Cfg Config @@ -42,6 +47,18 @@ var ( } postForm = "\n\n
\n
\n\n
%s
\n\n\n" + + // SigChan is channel for signal handler routine SigHandler(). + SigChan = make(chan os.Signal, 1) + + // Server represents http server object. + Server *http.Server + + // Shutdown set to true if service is shutting down via SigHandler(). + Shutdown bool + + // QuitChan is locking mechanism to wait until shutdown procedure completes. + QuitChan = make(chan bool, 1) ) /* vim: set ft=go noet ai ts=4 sw=4 sts=4: */ diff --git a/internal/joyproxy/joyproxyHandler.go b/internal/joyproxy/joyproxyHandler.go index d795cda..91c0665 100644 --- a/internal/joyproxy/joyproxyHandler.go +++ b/internal/joyproxy/joyproxyHandler.go @@ -14,12 +14,13 @@ import ( "github.com/davecgh/go-spew/spew" ) -// JoyproxyHandler request handler for /joyproxy location. +// JoyproxyHandler request handler for /joyproxy location. It reads data from reactor.cc and immediately streams it to +// client. func JoyproxyHandler(w http.ResponseWriter, r *http.Request) { //nolint: revive var err error if r.Method != http.MethodGet { - log.Infof("request method is not GET, but %s", r.Method) + log.Infof("Request method is not GET, but %s, sending 404 to client", r.Method) w.WriteHeader(http.StatusNotFound) if _, err = w.Write([]byte("404 Not Found")); err != nil { @@ -35,7 +36,7 @@ func JoyproxyHandler(w http.ResponseWriter, r *http.Request) { //nolint: revive joyHost := joyPath[2] if !regexp.MustCompile("^img[0-9]+[.]reactor[.]cc$").MatchString(joyHost) { - log.Infof("supplied proxy target host is not match with ^img[0-9]+[.]reactor[.]cc$ pattern: %s", joyHost) + log.Infof("Supplied proxy target host is not match with ^img[0-9]+[.]reactor[.]cc$ pattern: %s, sening 404 to client", joyHost) w.WriteHeader(http.StatusNotFound) if _, err = w.Write([]byte("404 Not Found")); err != nil { @@ -49,7 +50,7 @@ func JoyproxyHandler(w http.ResponseWriter, r *http.Request) { //nolint: revive dlReq.RequestURI = "/" + strings.Join(joyPath, "/") if !regexp.MustCompile("^/pics/post/mp4/.+[.]mp4$").MatchString(dlReq.RequestURI) { - log.Infof("supplied uri is not match with /pics/post/mp4/.+[.]mp4$ pattern: %s", dlReq.RequestURI) + log.Infof("Supplied uri is not match with /pics/post/mp4/.+[.]mp4$ pattern: %s, sending 404 to client", dlReq.RequestURI) w.WriteHeader(http.StatusNotFound) if _, err = w.Write([]byte("404 Not Found")); err != nil { @@ -63,7 +64,7 @@ func JoyproxyHandler(w http.ResponseWriter, r *http.Request) { //nolint: revive dlReq.URL, err = url.Parse(dlReq.RequestURI) if err != nil { - log.Infof("unable to parse %s via url.Parse(): %s", dlReq.RequestURI, err) + log.Infof("Unable to parse %s via url.Parse(): %s, sending 404 to client", dlReq.RequestURI, err) w.WriteHeader(http.StatusNotFound) if _, err = w.Write([]byte("404 Not Found")); err != nil { @@ -78,13 +79,16 @@ func JoyproxyHandler(w http.ResponseWriter, r *http.Request) { //nolint: revive dlReq.Header.Del("Referer") } + log.Debugf("Replacing/appending Referer header value with https://old.reactor.cc/all") dlReq.Header.Add("Referer", "https://old.reactor.cc/all") if dlReq.Header.Get("User-Agent") != "" { dlReq.Header.Del("User-Agent") } - dlReq.Header.Add("User-Agent", userAgentString[rand.Intn(len(userAgentString)-1)]) + ua := userAgentString[rand.Intn(len(userAgentString)-1)] + log.Debugf("Replacing User-Agent header value with %s", ua) + dlReq.Header.Add("User-Agent", ua) if dlReq.Header.Get("Accept") != "" { dlReq.Header.Del("Accept") @@ -108,6 +112,7 @@ func JoyproxyHandler(w http.ResponseWriter, r *http.Request) { //nolint: revive dlReq.Header.Del("Range") } + log.Debugf("Replacing/appending Range header value with bytes=0-") dlReq.Header.Add("Range", "bytes=0-") log.Debug(spew.Sdump(dlReq)) @@ -115,7 +120,7 @@ func JoyproxyHandler(w http.ResponseWriter, r *http.Request) { //nolint: revive target, err := url.Parse(u) if err != nil { - log.Infof("unable to parse %s via url.Parse(): %s", u, err) + log.Infof("Unable to parse %s via url.Parse(): %s, sending 404 to client", u, err) w.WriteHeader(http.StatusNotFound) if _, err = w.Write([]byte("404 Not Found")); err != nil { @@ -133,7 +138,10 @@ func JoyproxyHandler(w http.ResponseWriter, r *http.Request) { //nolint: revive KeepAlive: time.Duration(Cfg.Timeout) * time.Second, }).Dial, TLSHandshakeTimeout: time.Duration(Cfg.Timeout) * time.Second, + DisableKeepAlives: true, + DisableCompression: true, } + joyproxy.ServeHTTP(w, dlReq) } diff --git a/internal/joyproxy/joyurlHandler.go b/internal/joyproxy/joyurlHandler.go index 6c46dc3..754a0ac 100644 --- a/internal/joyproxy/joyurlHandler.go +++ b/internal/joyproxy/joyurlHandler.go @@ -18,19 +18,19 @@ func JoyurlHandler(w http.ResponseWriter, r *http.Request) { proxyURLString = " " ) - log.Debug(spew.Sdump(param)) + log.Debugf("Parse url into chunks: %s", spew.Sdump(param)) re := regexp.MustCompile("^https?://img[0-9]+[.]reactor[.]cc/.+[.][Ww][Ee][Bb][Mm]$") if len(param) > 0 && re.MatchString(param["joyurl"][0]) { - log.Debug("match with ^https?://img[0-9]+[.]reactor[.]cc/.+[.][Ww][Ee][Bb][Mm]$ pattern") + log.Debug("Url match with ^https?://img[0-9]+[.]reactor[.]cc/.+[.][Ww][Ee][Bb][Mm]$ pattern") // https://img1.reactor.cc/pics/post/webm/видосик.webm p := regexp.MustCompile("/").Split(param["joyurl"][0], -1) - log.Debug(spew.Sdump(p)) + log.Debugf("Split url string by / delimiter: %s", spew.Sdump(p)) if len(p) > 6 && p[5] == "webm" { - log.Debug("webm url part detected, let's make joyproxy url") + log.Debug("Substring webm is detected in url, let's make joyproxy url") file := p[6][:len(p[6])-5] @@ -46,10 +46,10 @@ func JoyurlHandler(w http.ResponseWriter, r *http.Request) { proxyURLString = html.EscapeString(proxyURLString) } else { - log.Debug("webm url part not detected, skip making joyproxy url") + log.Debug("Substring webm not detected in url, skip making joyproxy url") } } else { - log.Info("not match with ^https?://img[0-9]+[.]reactor[.]cc/.+[.][Ww][Ee][Bb][Mm]$ regex") + log.Info("Url is not match with ^https?://img[0-9]+[.]reactor[.]cc/.+[.][Ww][Ee][Bb][Mm]$ regex, skipping") } htmlText := fmt.Sprintf(postForm, proxyURLString) diff --git a/internal/joyproxy/loadConf.go b/internal/joyproxy/loadConf.go index 4e72f16..f25b3b5 100644 --- a/internal/joyproxy/loadConf.go +++ b/internal/joyproxy/loadConf.go @@ -52,6 +52,12 @@ func LoadConf() { if Cfg.Loglevel == "" { log.Fatalf("unable to get loglevel setting from settings section of %s", configINIPath) } + + Cfg.Logfile = configFH.Section("settings").Get("logfile") + + if Cfg.Logfile == "" { + log.Infof("Logfile parameter is not set in settings section of %s", configINIPath) + } } /* vim: set ft=go noet ai ts=4 sw=4 sts=4: */ diff --git a/internal/joyproxy/sigHandler.go b/internal/joyproxy/sigHandler.go new file mode 100644 index 0000000..dbf4179 --- /dev/null +++ b/internal/joyproxy/sigHandler.go @@ -0,0 +1,44 @@ +package joyproxy + +import ( + "joyproxy/internal/log" + "syscall" +) + +// SigHandler handles signals from OS. +func SigHandler() { + for { + var s = <-SigChan + switch s { + case syscall.SIGINT: + log.Info("Got SIGINT, quitting") + case syscall.SIGTERM: + log.Info("Got SIGTERM, quitting") + case syscall.SIGQUIT: + log.Info("Got SIGQUIT, quitting") + case syscall.SIGHUP: + log.Info("Got SIGHUP, reopening logs") + log.ReOpenLog() + + continue + + // Make new iteration since we 've got signal we not interested in. + default: + continue + } + + Shutdown = true + + if err := Server.Close(); err != nil { + log.Errorf("Unable to close server: %s", err.Error()) + } + + log.Close() + + QuitChan <- true + + break + } +} + +/* vim: set ft=go noet ai ts=4 sw=4 sts=4: */ diff --git a/internal/joyproxy/types.go b/internal/joyproxy/types.go index 24addd9..133778c 100644 --- a/internal/joyproxy/types.go +++ b/internal/joyproxy/types.go @@ -6,6 +6,7 @@ type ( Port string Proto string Loglevel string + Logfile string Timeout int } ) diff --git a/internal/log/log.go b/internal/log/log.go index 89dfe9b..f0ef92e 100644 --- a/internal/log/log.go +++ b/internal/log/log.go @@ -7,6 +7,7 @@ import ( "errors" "fmt" "io" + "log" "log/slog" "os" "time" @@ -24,30 +25,39 @@ var ( // reader is required for Writer to work properly. It is connected via io.Pipe() to Writer. reader io.Reader + + // Logger *log.Logger thing required for compatibility with old-good log package. + Logger log.Logger + + // LogFileHandler is currently opened log file descriptor. Stderr by default. + LogFileHandler = os.Stderr + + // LogLevel current loglevel. Info by default. + LogLevel = slog.LevelInfo ) // Init setup logger stuff. // level can be error, warn, info, debug if something other supplied info level selected. // fileDescriptor should be opened before supplying it to Init(). func Init(level string, fileDescriptor *os.File) { - var loglevel slog.Level + LogFileHandler = fileDescriptor // no panic, no trace. switch level { case "debug": - loglevel = slog.LevelDebug + LogLevel = slog.LevelDebug case "info": - loglevel = slog.LevelInfo + LogLevel = slog.LevelInfo case "warn": - loglevel = slog.LevelWarn + LogLevel = slog.LevelWarn case "error": - loglevel = slog.LevelError + LogLevel = slog.LevelError default: - loglevel = slog.LevelInfo + LogLevel = slog.LevelInfo } opts := &slog.HandlerOptions{ //nolint: exhaustruct @@ -68,15 +78,22 @@ func Init(level string, fileDescriptor *os.File) { return a }, - Level: loglevel, + Level: LogLevel, } - Handler = slog.NewTextHandler(fileDescriptor, opts) + // Documentation says that we can use *os.File in place of io.Writer, but in this case it did not work. + // So we have to cast fileDescriptor to io.Writer. + w := io.Writer(fileDescriptor) + + Handler = slog.NewTextHandler(w, opts) slog.SetDefault( slog.New(Handler), ) + // Catch log.Logger message at info loglevel of slog logging facility. + Logger = *slog.NewLogLogger(Handler, slog.LevelError) + reader, Writer = io.Pipe() go readIoPipe() @@ -104,7 +121,7 @@ func readIoPipe() { } } -// GetLevel returns current loglevel. +// GetLevel returns current loglevel as string. func GetLevel() string { switch { case Handler.Enabled(Ctx, slog.LevelDebug): @@ -176,4 +193,40 @@ func Debugf(format string, a ...any) { slog.Debug(fmt.Sprintf(format, a...)) } +// Close closes opened log file descriptors. +func Close() { + LogFileHandler.Close() +} + +// ReOpenLog closes and opens log file again. +func ReOpenLog() { + filename := LogFileHandler.Name() + + // If we writing to stderr, we should not re-open it. + if filename == os.Stderr.Name() { + return + } + + loglevel := GetLevel() + + LogFileHandler.Close() + + file, err := os.OpenFile(filename, os.O_CREATE|os.O_APPEND|os.O_WRONLY, 0644) + + if err != nil { + // Try to emulate our own log message format. + fmt.Fprintf( + os.Stderr, + "time=\"%s\" level=ERROR msg=\"Unable to open logfile %s: %s\"", + time.Now().Format(time.DateTime), + filename, + err, + ) + + return + } + + Init(loglevel, file) +} + /* vim: set ft=go noet ai ts=4 sw=4 sts=4: */ diff --git a/joyproxy_sample.ini b/joyproxy_sample.ini index be25d13..1233468 100644 --- a/joyproxy_sample.ini +++ b/joyproxy_sample.ini @@ -9,4 +9,7 @@ port = 9980 timeout = 5 ; loglevel - info or debug, info is what you want -loglevel = debug +loglevel = info + +; logfile - log to this file, default stderr +;logfile = /var/log/joyproxy.log