0

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
This commit is contained in:
eleksir
2025-11-10 21:53:34 +03:00
parent 79d9a2f604
commit 03a623b207
10 changed files with 257 additions and 30 deletions

View File

@@ -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 = "<html>\n<body>\n<form method='get' action='joyurl'>\n<input type='text' name='joyurl' size=100 autofocus><br>\n<input type='submit' value='Post it!' style='font-size:115%%;'>\n<br>%s<br>\n</body>\n</html>\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: */

View File

@@ -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)
}

View File

@@ -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)

View File

@@ -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: */

View File

@@ -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: */

View File

@@ -6,6 +6,7 @@ type (
Port string
Proto string
Loglevel string
Logfile string
Timeout int
}
)

View File

@@ -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: */