aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--README.md17
-rw-r--r--flags.go16
-rw-r--r--go.mod2
-rw-r--r--go.sum2
-rw-r--r--handler.go20
-rw-r--r--irc.go139
-rw-r--r--irclog.go122
7 files changed, 308 insertions, 10 deletions
diff --git a/README.md b/README.md
index 4f6ab2e..6fc3780 100644
--- a/README.md
+++ b/README.md
@@ -1,3 +1,6 @@
+**Branch information:** This branch contains a version that uses IRC for
+logging. Connection details are hardcoded. You should probably not use this.
+
# Powxy – anti-scraper reverse proxy
Powxy is a reverse proxy that protects your upstream service by challenging
@@ -83,6 +86,20 @@ Usage of ./powxy:
leading zero bits required for the challenge (default 20)
-idle-timeout int
idle timeout in seconds, 0 for no timeout
+ -irc-addr string
+ irc server address (default "irc.runxiyu.org:6697")
+ -irc-channel string
+ irc channel (default "#logs")
+ -irc-net string
+ irc network transport (default "tcp")
+ -irc-nick string
+ irc nick (default "powxy")
+ -irc-realname string
+ irc realname (default "powxy")
+ -irc-tls
+ irc tls (default true)
+ -irc-username string
+ irc username (default "powxy")
-listen string
address to listen on (default ":8081")
-read-header-timeout int
diff --git a/flags.go b/flags.go
index 007332c..e7b9911 100644
--- a/flags.go
+++ b/flags.go
@@ -16,6 +16,14 @@ var (
writeTimeout int
idleTimeout int
readHeaderTimeout int
+ ircAddr string
+ ircNet string
+ ircTLS bool
+ ircChannel string
+ ircNick string
+ ircUsername string
+ ircRealname string
+ ircBuf uint
)
// This init parses command line flags.
@@ -29,6 +37,14 @@ func init() {
flag.IntVar(&writeTimeout, "write-timeout", 0, "write timeout in seconds, 0 for no timeout")
flag.IntVar(&idleTimeout, "idle-timeout", 0, "idle timeout in seconds, 0 for no timeout")
flag.IntVar(&readHeaderTimeout, "read-header-timeout", 30, "read header timeout in seconds, 0 for no timeout")
+ flag.StringVar(&ircAddr, "irc-addr", "irc.runxiyu.org:6697", "irc server address")
+ flag.StringVar(&ircNet, "irc-net", "tcp", "irc network transport")
+ flag.BoolVar(&ircTLS, "irc-tls", true, "irc tls")
+ flag.StringVar(&ircChannel, "irc-channel", "#logs", "irc channel")
+ flag.StringVar(&ircNick, "irc-nick", "powxy", "irc nick")
+ flag.StringVar(&ircUsername, "irc-username", "powxy", "irc username")
+ flag.StringVar(&ircRealname, "irc-realname", "powxy", "irc realname")
+ flag.UintVar(&ircBuf, "irc-buf", 3000, "irc buffer size")
flag.Parse()
global.NeedBitsReverse = sha256.Size - global.NeedBits
}
diff --git a/go.mod b/go.mod
index 62c2e32..3eecbfe 100644
--- a/go.mod
+++ b/go.mod
@@ -1,3 +1,5 @@
module go.lindenii.runxiyu.org/powxy
go 1.24.1
+
+require go.lindenii.runxiyu.org/lindenii-irc v0.0.0-20250322030600-1e47f911f1fa
diff --git a/go.sum b/go.sum
new file mode 100644
index 0000000..f5afd47
--- /dev/null
+++ b/go.sum
@@ -0,0 +1,2 @@
+go.lindenii.runxiyu.org/lindenii-irc v0.0.0-20250322030600-1e47f911f1fa h1:LU3ZN/9xVUOEHyUCa5d+lvrL2sqhy/PR2iM2DuAQDqs=
+go.lindenii.runxiyu.org/lindenii-irc v0.0.0-20250322030600-1e47f911f1fa/go.mod h1:fE6Ks8GK7PHZGAPkTWG593UmF7FmyugcRcqmey3Nvy0=
diff --git a/handler.go b/handler.go
index 2cbd225..9e5a679 100644
--- a/handler.go
+++ b/handler.go
@@ -29,7 +29,7 @@ func handler(writer http.ResponseWriter, request *http.Request) {
// will be prompted to solve the PoW challenge.
cookie, err := request.Cookie("powxy")
if err != nil && !errors.Is(err, http.ErrNoCookie) {
- slog.Error("error fetching cookie",
+ slog.Error("\x0304ERRCOOKIE",
"ip", remoteIP,
"uri", uri,
"user_agent", userAgent,
@@ -46,7 +46,7 @@ func handler(writer http.ResponseWriter, request *http.Request) {
// If the cookie exists and is valid, we simply proxy the
// request.
if validateCookie(cookie, expectedMAC) {
- slog.Info("proxying request",
+ slog.Info("\x0302PROXY",
"ip", remoteIP,
"uri", uri,
"user_agent", userAgent,
@@ -68,7 +68,7 @@ func handler(writer http.ResponseWriter, request *http.Request) {
Global: global,
})
if err != nil {
- slog.Error("template execution failed",
+ slog.Error("\x0304template execution failed",
"ip", remoteIP,
"uri", uri,
"user_agent", userAgent,
@@ -81,7 +81,7 @@ func handler(writer http.ResponseWriter, request *http.Request) {
// browesrs.
err = request.ParseForm()
if err != nil {
- slog.Warn("malformed form submission",
+ slog.Warn("\x0304MALFORMED",
"ip", remoteIP,
"uri", uri,
"user_agent", userAgent,
@@ -96,7 +96,7 @@ func handler(writer http.ResponseWriter, request *http.Request) {
// If there's simply no form value, the user is probably
// just visiting the site for the first time or with an
// expired cookie.
- slog.Info("serving challenge page",
+ slog.Info("\x0301POW CHL",
"ip", remoteIP,
"uri", uri,
"user_agent", userAgent,
@@ -106,7 +106,7 @@ func handler(writer http.ResponseWriter, request *http.Request) {
} else if len(formValues) != 1 {
// This should never happen, at least not for web
// browsers.
- slog.Warn("invalid number of form values",
+ slog.Warn("\x0304FORMNUM",
"ip", remoteIP,
"uri", uri,
"user_agent", userAgent,
@@ -119,7 +119,7 @@ func handler(writer http.ResponseWriter, request *http.Request) {
// We validate that the length is reasonable before even
// decoding it with base64.
if len(formValues[0]) > 44 {
- slog.Warn("submission too long",
+ slog.Warn("\x0304TOOLONG",
"ip", remoteIP,
"uri", uri,
"user_agent", userAgent,
@@ -132,7 +132,7 @@ func handler(writer http.ResponseWriter, request *http.Request) {
// Actually decode the base64 value.
nonce, err := base64.StdEncoding.DecodeString(formValues[0])
if err != nil {
- slog.Warn("base64 decoding failed",
+ slog.Warn("\x0304ERRBASE64",
"ip", remoteIP,
"uri", uri,
"user_agent", userAgent,
@@ -145,7 +145,7 @@ func handler(writer http.ResponseWriter, request *http.Request) {
// Validate the nonce.
if !validateNonce(identifier, nonce) {
- slog.Warn("wrong nonce",
+ slog.Warn("\x0304",
"ip", remoteIP,
"uri", uri,
"user_agent", userAgent,
@@ -169,7 +169,7 @@ func handler(writer http.ResponseWriter, request *http.Request) {
Path: "/",
})
- slog.Info("accepted proof of work",
+ slog.Info("\x0303POW ACK",
"ip", remoteIP,
"uri", uri,
"user_agent", userAgent,
diff --git a/irc.go b/irc.go
new file mode 100644
index 0000000..2577860
--- /dev/null
+++ b/irc.go
@@ -0,0 +1,139 @@
+package main
+
+import (
+ "crypto/tls"
+ "fmt"
+ "net"
+
+ irc "go.lindenii.runxiyu.org/lindenii-irc"
+)
+
+var (
+ ircSendBuffered chan string
+ ircSendDirectChan chan errorBack[string]
+)
+
+type errorBack[T any] struct {
+ content T
+ errorBack chan error
+}
+
+func ircBotSession() error {
+ var err error
+ var underlyingConn net.Conn
+ if ircTLS {
+ underlyingConn, err = tls.Dial(ircNet, ircAddr, nil)
+ } else {
+ underlyingConn, err = net.Dial(ircNet, ircAddr)
+ }
+ if err != nil {
+ return err
+ }
+ defer underlyingConn.Close()
+
+ conn := irc.NewConn(underlyingConn)
+
+ logAndWriteLn := func(s string) (n int, err error) {
+ return conn.WriteString(s + "\r\n")
+ }
+
+ _, err = logAndWriteLn("NICK :" + ircNick)
+ if err != nil {
+ return err
+ }
+ _, err = logAndWriteLn("USER " + ircUsername + " 0 * :" + ircRealname)
+ if err != nil {
+ return err
+ }
+
+ readLoopError := make(chan error)
+ writeLoopAbort := make(chan struct{})
+ go func() {
+ for {
+ select {
+ case <-writeLoopAbort:
+ return
+ default:
+ }
+
+ msg, line, err := conn.ReadMessage()
+ if err != nil {
+ readLoopError <- err
+ return
+ }
+
+ fmt.Println(line)
+
+ switch msg.Command {
+ case "001":
+ _, err = logAndWriteLn("JOIN " + ircChannel)
+ if err != nil {
+ readLoopError <- err
+ return
+ }
+ case "PING":
+ _, err = logAndWriteLn("PONG :" + msg.Args[0])
+ if err != nil {
+ readLoopError <- err
+ return
+ }
+ case "JOIN":
+ c, ok := msg.Source.(irc.Client)
+ if !ok {
+ }
+ if c.Nick != ircNick {
+ continue
+ }
+ default:
+ }
+ }
+ }()
+
+ for {
+ select {
+ case err = <-readLoopError:
+ return err
+ case line := <-ircSendBuffered:
+ _, err = logAndWriteLn(line)
+ if err != nil {
+ select {
+ case ircSendBuffered <- line:
+ default:
+ }
+ writeLoopAbort <- struct{}{}
+ return err
+ }
+ case lineErrorBack := <-ircSendDirectChan:
+ _, err = logAndWriteLn(lineErrorBack.content)
+ lineErrorBack.errorBack <- err
+ if err != nil {
+ writeLoopAbort <- struct{}{}
+ return err
+ }
+ }
+ }
+}
+
+func ircSendDirect(s string) error {
+ ech := make(chan error, 1)
+
+ ircSendDirectChan <- errorBack[string]{
+ content: s,
+ errorBack: ech,
+ }
+
+ return <-ech
+}
+
+func ircBotLoop() {
+ ircSendBuffered = make(chan string, ircBuf)
+ ircSendDirectChan = make(chan errorBack[string])
+
+ for {
+ _ = ircBotSession()
+ }
+}
+
+func init() {
+ go ircBotLoop()
+}
diff --git a/irclog.go b/irclog.go
new file mode 100644
index 0000000..f5c0f81
--- /dev/null
+++ b/irclog.go
@@ -0,0 +1,122 @@
+package main
+
+import (
+ "context"
+ "fmt"
+ "log/slog"
+ "os"
+ "strconv"
+ "strings"
+ "unicode"
+ "unicode/utf8"
+)
+
+type IRCLogHandler struct {
+ level slog.Level
+}
+
+func NewIRCLogHandler(level slog.Level) *IRCLogHandler {
+ return &IRCLogHandler{
+ level: level,
+ }
+}
+
+func (h *IRCLogHandler) Enabled(_ context.Context, level slog.Level) bool {
+ return level >= h.level
+}
+
+func (h *IRCLogHandler) Handle(_ context.Context, r slog.Record) error {
+ var sb strings.Builder
+
+ sb.WriteString("PRIVMSG " + ircChannel + " :")
+
+ sb.WriteString(r.Message)
+
+ r.Attrs(func(a slog.Attr) bool {
+ sb.WriteString(" ")
+
+ key := a.Key
+ if needsQuoting(key) {
+ key = strconv.Quote(key)
+ }
+ sb.WriteString(key)
+ sb.WriteString("=")
+
+ val := attrValueToString(a.Value)
+ if needsQuoting(val) {
+ val = strconv.Quote(val)
+ }
+ sb.WriteString(val)
+
+ return true
+ })
+
+ str := sb.String()
+
+ select {
+ case ircSendBuffered <- str:
+ default:
+ fmt.Fprintln(os.Stderr, "DROP")
+ }
+
+ fmt.Fprintln(os.Stderr, str)
+
+ return nil
+}
+
+func (h *IRCLogHandler) WithAttrs(attrs []slog.Attr) slog.Handler {
+ return h
+}
+
+func (h *IRCLogHandler) WithGroup(_ string) slog.Handler {
+ return h
+}
+
+func attrValueToString(v slog.Value) string {
+ return v.String()
+}
+
+func init() {
+ slog.SetDefault(slog.New(NewIRCLogHandler(slog.LevelInfo)))
+}
+
+// copied from slog
+func needsQuoting(s string) bool {
+ if len(s) == 0 {
+ return true
+ }
+ for i := 0; i < len(s); {
+ b := s[i]
+ if b < utf8.RuneSelf {
+ if b != '\\' && (b == ' ' || b == '=' || !safeSet[b]) {
+ return true
+ }
+ i++
+ continue
+ }
+ r, size := utf8.DecodeRuneInString(s[i:])
+ if r == utf8.RuneError || unicode.IsSpace(r) || !unicode.IsPrint(r) {
+ return true
+ }
+ i += size
+ }
+ return false
+}
+
+var safeSet = [256]bool{
+ '!': true, '#': true, '$': true, '%': true, '&': true, '\'': true,
+ '*': true, '+': true, ',': true, '-': true, '.': true, '/': true,
+ '0': true, '1': true, '2': true, '3': true, '4': true,
+ '5': true, '6': true, '7': true, '8': true, '9': true,
+ ':': true, ';': true, '<': true, '>': true, '?': true, '@': true,
+ 'A': true, 'B': true, 'C': true, 'D': true, 'E': true, 'F': true,
+ 'G': true, 'H': true, 'I': true, 'J': true, 'K': true, 'L': true,
+ 'M': true, 'N': true, 'O': true, 'P': true, 'Q': true, 'R': true,
+ 'S': true, 'T': true, 'U': true, 'V': true, 'W': true, 'X': true,
+ 'Y': true, 'Z': true, '[': true, ']': true, '^': true, '_': true,
+ 'a': true, 'b': true, 'c': true, 'd': true, 'e': true, 'f': true,
+ 'g': true, 'h': true, 'i': true, 'j': true, 'k': true, 'l': true,
+ 'm': true, 'n': true, 'o': true, 'p': true, 'q': true, 'r': true,
+ 's': true, 't': true, 'u': true, 'v': true, 'w': true, 'x': true,
+ 'y': true, 'z': true, '{': true, '|': true, '}': true, '~': true,
+}