0

Change layout to more go-style-ish, replace some modules

* Deejay files, split them to achieve go-style project layout
* Replace logrus with log/slog module to reduce dependencies
* Replace gopkg.in/ini.v1 with github.com/wlevene/ini to make sure all modules BSD licensed
* Update linter config && fix tons of linter warnings
* Update .gitignore
* Update README.md
This commit is contained in:
eleksir
2025-11-09 22:16:04 +03:00
parent c5f4682cb9
commit 79d9a2f604
211 changed files with 12879 additions and 7732 deletions

View File

@@ -0,0 +1,47 @@
// Package joyproxy is main appliction lib.
package joyproxy
var (
// Cfg represents struct with application config settings.
Cfg Config
// userAgentString represents client names from real browsers to make joyreactor thing that we're real browser, not a
// proxy. We want to play good after all.
userAgentString = []string{
"Mozilla/5.0 (Macintosh; Intel Mac OS X 12_2) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/98.0.4758.81 Safari/537.36 OPR/83.0.4254.27",
"Mozilla/5.0 (Windows NT 10.0) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/98.0.4758.81 Safari/537.36",
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/87.0.4280.88 Safari/537.36 Vivaldi/3.5",
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/88.0.4324.182 Safari/537.36 Edg/88.0.705.81",
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/89.0.4389.114 Safari/537.36",
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/92.0.4515.159 Safari/537.36",
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/15.2 Safari/605.1.15",
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/95.0.4638.69 Safari/537.36",
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/95.0.4638.69 Safari/537.36 Edg/95.0.1020.44",
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/96.0.4664.110 Safari/537.36",
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/96.0.4664.110 Safari/537.36 Edg/96.0.1054.62",
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/96.0.4664.110 Safari/537.36 OPR/82.0.4227.50",
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/97.0.4692.71 Safari/537.36 Edg/97.0.1072.55",
"Mozilla/5.0 (Macintosh; Intel Mac OS X 12_2) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/98.0.4758.81 Safari/537.36",
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/98.0.4758.81 Safari/537.36",
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/98.0.4758.81 Safari/537.36 Edg/97.0.1072.69",
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/98.0.4758.81 Safari/537.36 Vivaldi/4.3",
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/98.0.4758.81 YaBrowser/22.1.0 Yowser/2.5 Safari/537.36",
"Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:91.0) Gecko/20100101 Firefox/91.0",
"Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:95.0) Gecko/20100101 Firefox/95.0",
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_6) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/15.1 Safari/605.1.15",
"Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:96.0) Gecko/20100101 Firefox/96.0",
"Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:97.0) Gecko/20100101 Firefox/97.0",
"Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/87.0.4280.88 Safari/537.36 Vivaldi/3.5",
"Mozilla/5.0 (Windows NT 10.0; WOW64; Trident/7.0; rv:11.0) like Gecko",
"Mozilla/5.0 (Windows NT 10.0; WOW64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/98.0.4758.81 Safari/537.36 OPR/83.0.4254.27",
"Mozilla/5.0 (X11; Fedora; Linux x86_64; rv:91.0) Gecko/20100101 Firefox/91.0",
"Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/98.0.4758.81 Safari/537.36 OPR/83.0.4254.27",
"Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/98.0.4758.81 Safari/537.36 Vivaldi/4.3",
"Mozilla/5.0 (X11; Ubuntu; Linux i686; rv:91.0) Gecko/20100101 Firefox/91.0",
"Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:97.0) Gecko/20100101 Firefox/97.0",
}
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"
)
/* vim: set ft=go noet ai ts=4 sw=4 sts=4: */

View File

@@ -0,0 +1,140 @@
package joyproxy
import (
"joyproxy/internal/log"
"math/rand"
"net"
"net/http"
"net/http/httputil"
"net/url"
"regexp"
"strings"
"time"
"github.com/davecgh/go-spew/spew"
)
// JoyproxyHandler request handler for /joyproxy location.
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)
w.WriteHeader(http.StatusNotFound)
if _, err = w.Write([]byte("404 Not Found")); err != nil {
log.Infof("Unable to write response to client: %s", err)
}
return
}
dlReq := r
joyPath := strings.Split(r.RequestURI, "/")
// joyPath[0] is empty, joyPath[1] is joyproxy.
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)
w.WriteHeader(http.StatusNotFound)
if _, err = w.Write([]byte("404 Not Found")); err != nil {
log.Infof("Unable to write response to client: %s", err)
}
return
}
joyPath = joyPath[3:]
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)
w.WriteHeader(http.StatusNotFound)
if _, err = w.Write([]byte("404 Not Found")); err != nil {
log.Infof("Unable to write response to client: %s", err)
}
return
}
dlReq.Host = joyHost
dlReq.URL, err = url.Parse(dlReq.RequestURI)
if err != nil {
log.Infof("unable to parse %s via url.Parse(): %s", dlReq.RequestURI, err)
w.WriteHeader(http.StatusNotFound)
if _, err = w.Write([]byte("404 Not Found")); err != nil {
log.Infof("Unable to write response to client: %s", err)
}
return
}
// Forge request as it comes from browser… kinda.
if dlReq.Header.Get("Referer") != "" {
dlReq.Header.Del("Referer")
}
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)])
if dlReq.Header.Get("Accept") != "" {
dlReq.Header.Del("Accept")
}
dlReq.Header.Add("Accept", "*/*")
if dlReq.Header.Get("Accept-Encoding") != "" {
dlReq.Header.Del("Accept-Encoding")
}
dlReq.Header.Add("Accept-Encoding", "identity;q=1, *;q=0")
if dlReq.Header.Get("Accept-Language") != "" {
dlReq.Header.Del("Accept-Language")
}
dlReq.Header.Add("Accept-Language", "ru-RU,ru;q=0.9,en-US;q=0.8,en;q=0.7")
if dlReq.Header.Get("Range") != "" {
dlReq.Header.Del("Range")
}
dlReq.Header.Add("Range", "bytes=0-")
log.Debug(spew.Sdump(dlReq))
u := "https://" + joyHost
target, err := url.Parse(u)
if err != nil {
log.Infof("unable to parse %s via url.Parse(): %s", u, err)
w.WriteHeader(http.StatusNotFound)
if _, err = w.Write([]byte("404 Not Found")); err != nil {
log.Infof("Unable to write response to client: %s", err)
}
return
}
joyproxy := httputil.NewSingleHostReverseProxy(target)
joyproxy.Transport = &http.Transport{
Dial: (&net.Dialer{
Timeout: time.Duration(Cfg.Timeout) * time.Second,
KeepAlive: time.Duration(Cfg.Timeout) * time.Second,
}).Dial,
TLSHandshakeTimeout: time.Duration(Cfg.Timeout) * time.Second,
}
joyproxy.ServeHTTP(w, dlReq)
}
/* vim: set ft=go noet ai ts=4 sw=4 sts=4: */

View File

@@ -0,0 +1,62 @@
package joyproxy
import (
"fmt"
"html"
"net/http"
"regexp"
"joyproxy/internal/log"
"github.com/davecgh/go-spew/spew"
)
// JoyurlHandler request handler function for /joyurl location.
func JoyurlHandler(w http.ResponseWriter, r *http.Request) {
var (
param = r.URL.Query()
proxyURLString = " "
)
log.Debug(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")
// https://img1.reactor.cc/pics/post/webm/видосик.webm
p := regexp.MustCompile("/").Split(param["joyurl"][0], -1)
log.Debug(spew.Sdump(p))
if len(p) > 6 && p[5] == "webm" {
log.Debug("webm url part detected, let's make joyproxy url")
file := p[6][:len(p[6])-5]
proxyURLString = fmt.Sprintf(
"%s://%s/joyproxy/%s/%s/%s/mp4/%s.mp4",
Cfg.Proto,
r.Host,
p[2],
p[3],
p[4],
file,
)
proxyURLString = html.EscapeString(proxyURLString)
} else {
log.Debug("webm url part not detected, skip making joyproxy url")
}
} else {
log.Info("not match with ^https?://img[0-9]+[.]reactor[.]cc/.+[.][Ww][Ee][Bb][Mm]$ regex")
}
htmlText := fmt.Sprintf(postForm, proxyURLString)
if _, err := fmt.Fprintln(w, htmlText); err != nil {
log.Infof("Unable to write response to client: %s", err)
}
}
/* vim: set ft=go noet ai ts=4 sw=4 sts=4: */

View File

@@ -0,0 +1,57 @@
package joyproxy
import (
"os"
"path/filepath"
"joyproxy/internal/log"
"github.com/wlevene/ini"
)
// LoadConf reads config data from config file and parses it to Cfg variable.
func LoadConf() {
executablePath, err := os.Executable()
if err != nil {
log.Fatalf("Unable to get current executable path: %s", err)
}
configINIPath := filepath.Dir(executablePath) + "/joyproxy.ini"
configFH := ini.New().LoadFile(configINIPath)
Cfg.Port = configFH.Section("settings").Get("port")
if Cfg.Port == "" {
log.Fatalf("unable to get port setting from settings section of %s", configINIPath)
}
Cfg.Port = ":" + Cfg.Port
Cfg.Proto = configFH.Section("settings").Get("proto")
if Cfg.Proto == "" {
log.Fatalf("unable to get proto setting from settings section of %s", configINIPath)
}
if Cfg.Proto != "http" && Cfg.Proto != "https" {
log.Fatalf("proto setting from settings section of %s must be either http or https, but we got %s",
configINIPath,
Cfg.Proto,
)
}
Cfg.Timeout = configFH.Section("settings").GetInt("timeout")
if Cfg.Timeout == 0 {
log.Fatalf("unable to get timeout value from settings section of %s", configINIPath)
}
Cfg.Loglevel = configFH.Section("settings").Get("loglevel")
if Cfg.Loglevel == "" {
log.Fatalf("unable to get loglevel setting from settings section of %s", configINIPath)
}
}
/* vim: set ft=go noet ai ts=4 sw=4 sts=4: */

View File

@@ -0,0 +1,13 @@
package joyproxy
// Config represents application config data structure.
type (
Config struct {
Port string
Proto string
Loglevel string
Timeout int
}
)
/* vim: set ft=go noet ai ts=4 sw=4 sts=4: */

179
internal/log/log.go Normal file
View File

@@ -0,0 +1,179 @@
// Package log wraps log/slog for more easier and convinient use.
package log
import (
"bufio"
"context"
"errors"
"fmt"
"io"
"log/slog"
"os"
"time"
)
var (
// Handler exposes slog handler struct.
Handler slog.Handler
// Ctx log context. For now it is just sits here and do nothing.
Ctx = context.Background()
// Writer go-writer eported for capturing other types of logs, even for std log go facility.
Writer io.Writer
// reader is required for Writer to work properly. It is connected via io.Pipe() to Writer.
reader io.Reader
)
// 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
// no panic, no trace.
switch level {
case "debug":
loglevel = slog.LevelDebug
case "info":
loglevel = slog.LevelInfo
case "warn":
loglevel = slog.LevelWarn
case "error":
loglevel = slog.LevelError
default:
loglevel = slog.LevelInfo
}
opts := &slog.HandlerOptions{ //nolint: exhaustruct
// Use the ReplaceAttr function on the handler options
// to be able to replace any single attribute in the log output.
ReplaceAttr: func(groups []string, a slog.Attr) slog.Attr { //nolint: revive
// Check that we are handling the time key.
if a.Key != slog.TimeKey {
return a
}
t := a.Value.Time()
// change the value from a time.Time to a String
// where the string has the correct time format.
a.Value = slog.StringValue(t.Format(time.DateTime))
return a
},
Level: loglevel,
}
Handler = slog.NewTextHandler(fileDescriptor, opts)
slog.SetDefault(
slog.New(Handler),
)
reader, Writer = io.Pipe()
go readIoPipe()
}
// readIoPipe reads pipe and writes it to slog.
func readIoPipe() {
scanner := bufio.NewScanner(reader)
for {
for scanner.Scan() {
line := scanner.Text() // Get the current line as a string.
slog.Debug(line)
}
if err := scanner.Err(); err != nil {
// io.EOF is expected when done reading, e.g. on exit.
if errors.Is(err, io.EOF) {
return
}
// something weird happen, we should it log to guess later what it was.
Errorf("Error during scanning log writer pipe: %v\n", err)
}
}
}
// GetLevel returns current loglevel.
func GetLevel() string {
switch {
case Handler.Enabled(Ctx, slog.LevelDebug):
return "debug"
case Handler.Enabled(Ctx, slog.LevelInfo):
return "info"
case Handler.Enabled(Ctx, slog.LevelWarn):
return "warn"
case Handler.Enabled(Ctx, slog.LevelError):
return "error"
default:
return "unknown"
}
}
// Fatal passes message directly to slog.Error() and exits with error code 1.
func Fatal(message string) {
slog.Error(message)
os.Exit(1)
}
// Fatalf logs message on error log level and exits with error code 1. It allows to supply arguments in printf() style .
func Fatalf(format string, a ...any) {
slog.Error(fmt.Sprintf(format, a...))
os.Exit(1)
}
// Error passes message directly to slog.Error().
func Error(message string) {
slog.Error(message)
}
// Errorf logs message on error log level and allows to supply arguments in printf() style.
func Errorf(format string, a ...any) {
slog.Error(fmt.Sprintf(format, a...))
}
// Warn passes message directly to slog.Warn().
func Warn(message string) {
slog.Warn(message)
}
// Warnf logs message on warn log level and allows to supply arguments in printf() style.
func Warnf(format string, a ...any) {
slog.Warn(fmt.Sprintf(format, a...))
}
// Info passes message directly to slog.Info().
func Info(message string) {
slog.Info(message)
}
// Infof logs message on info log level and allows to supply arguments in printf() style.
func Infof(format string, a ...any) {
slog.Info(fmt.Sprintf(format, a...))
}
// Debug passes message directly to slog.Debug().
func Debug(message string) {
slog.Debug(message)
}
// Debugf logs message on debug log level and allows to supply arguments in printf() style.
func Debugf(format string, a ...any) {
slog.Debug(fmt.Sprintf(format, a...))
}
/* vim: set ft=go noet ai ts=4 sw=4 sts=4: */