Skip to content

Commit dbfadc5

Browse files
committed
Refactor
1 parent a83e26d commit dbfadc5

11 files changed

+127
-54
lines changed

bitwise.go

+1
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33

44
package main
55

6+
// validateBitZeros checks if the first n bits of a byte slice are all zeros.
67
func validateBitZeros(bs []byte, n uint) bool {
78
q := n / 8
89
r := n % 8

flags.go

+1
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ var (
1111
secondary bool
1212
)
1313

14+
// This init parses command line flags.
1415
func init() {
1516
flag.UintVar(&global.NeedBits, "difficulty", 20, "leading zero bits required for the challenge")
1617
flag.StringVar(&global.SourceURL, "source", "https://forge.lindenii.runxiyu.org/powxy/:/repos/powxy/", "url to the source code")

global.go

+5
Original file line numberDiff line numberDiff line change
@@ -3,11 +3,16 @@
33

44
package main
55

6+
// global is a struct that holds global information that the HTML template may
7+
// wish to access.
68
var global = struct {
9+
// Most of these fields should be filled in by flag parsing.
710
NeedBits uint
811
NeedBitsReverse uint
912
SourceURL string
1013
Version string
1114
}{
15+
// The version should be replaced by the init function in version.go,
16+
// if version.go is properly generated by the Makefile.
1217
Version: "(no version)",
1318
}

identifier.go

+19
Original file line numberDiff line numberDiff line change
@@ -6,11 +6,15 @@ package main
66
import (
77
"crypto/hmac"
88
"crypto/sha256"
9+
"crypto/subtle"
10+
"encoding/base64"
911
"encoding/binary"
1012
"net/http"
1113
"time"
1214
)
1315

16+
// makeIdentifierMAC generates an identifier that semi-uniquely identifies the client,
17+
// and generates a MAC for that identifier.
1418
func makeIdentifierMAC(request *http.Request) (identifier []byte, mac []byte) {
1519
identifier = make([]byte, 0, sha256.Size)
1620
mac = make([]byte, 0, sha256.Size)
@@ -37,3 +41,18 @@ func makeIdentifierMAC(request *http.Request) (identifier []byte, mac []byte) {
3741

3842
return
3943
}
44+
45+
// validateCookie checks if the cookie is valid by comparing the base64-decoded
46+
// value of the cookie with an expected MAC.
47+
func validateCookie(cookie *http.Cookie, expectedMAC []byte) bool {
48+
if cookie == nil {
49+
return false
50+
}
51+
52+
gotMAC, err := base64.StdEncoding.DecodeString(cookie.Value)
53+
if err != nil {
54+
return false
55+
}
56+
57+
return subtle.ConstantTimeCompare(gotMAC, expectedMAC) == 1
58+
}

ip.go

+26
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
// SPDX-License-Identifier: BSD-2-Clause
2+
// SPDX-FileCopyrightText: Copyright (c) 2025 Runxi Yu <https://runxiyu.org>
3+
4+
package main
5+
6+
import (
7+
"net/http"
8+
"strings"
9+
)
10+
11+
// getRemoteIP returns the remote IP address of the client. It respects
12+
// X-Forwarded-For if powxy is configured as a secondary proxy. Ports
13+
// are stripped.
14+
func getRemoteIP(request *http.Request) (remoteIP string) {
15+
if secondary {
16+
remoteIP, _, _ = strings.Cut(request.Header.Get("X-Forwarded-For"), ",")
17+
}
18+
if remoteIP == "" {
19+
remoteIP = request.RemoteAddr
20+
index := strings.LastIndex(remoteIP, ":")
21+
if index != -1 {
22+
remoteIP = remoteIP[:index]
23+
}
24+
}
25+
return
26+
}

main.go

+46-52
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,6 @@
44
package main
55

66
import (
7-
"crypto/sha256"
8-
"crypto/subtle"
97
"encoding/base64"
108
"errors"
119
"log"
@@ -21,29 +19,39 @@ type tparams struct {
2119

2220
func main() {
2321
log.Fatal(http.ListenAndServe(listenAddr, http.HandlerFunc(func(writer http.ResponseWriter, request *http.Request) {
22+
// Static resources for powxy itself.
2423
if strings.HasPrefix(request.URL.Path, "/.powxy/") {
2524
http.StripPrefix("/.powxy/", http.FileServer(http.FS(resourcesFS))).ServeHTTP(writer, request)
2625
return
2726
}
2827

28+
// We attempt to fetch the powxy cookie. Its non-existence
29+
// does not matter here; if the cookie does not exist, it
30+
// will be nil, so validation will simply fail and the user
31+
// will be prompted to solve the PoW challenge.
2932
cookie, err := request.Cookie("powxy")
30-
if err != nil {
31-
if !errors.Is(err, http.ErrNoCookie) {
32-
log.Println("COOKIE_ERR", getRemoteIP(request), request.RequestURI, request.Header.Get("User-Agent"))
33-
http.Error(writer, "error fetching cookie", http.StatusInternalServerError)
34-
return
35-
}
33+
if err != nil && !errors.Is(err, http.ErrNoCookie) {
34+
log.Println("COOKIE_ERR", getRemoteIP(request), request.RequestURI, request.Header.Get("User-Agent"))
35+
http.Error(writer, "error fetching cookie", http.StatusInternalServerError)
36+
return
3637
}
3738

39+
// We generate the identifier that identifies the client,
40+
// and the expected HMAC that the cookie should include.
3841
identifier, expectedMAC := makeIdentifierMAC(request)
3942

43+
// If the cookie exists and is valid, we simply proxy the
44+
// request.
4045
if validateCookie(cookie, expectedMAC) {
4146
log.Println("PROXY", getRemoteIP(request), request.RequestURI, request.Header.Get("User-Agent"))
4247
proxyRequest(writer, request)
4348
return
4449
}
4550

46-
authPage := func(message string) {
51+
// A convenience function to render the challenge page,
52+
// since all parameters but the message are constant at this
53+
// point.
54+
challengePage := func(message string) {
4755
err := tmpl.Execute(writer, tparams{
4856
Identifier: base64.StdEncoding.EncodeToString(identifier),
4957
Message: message,
@@ -54,46 +62,59 @@ func main() {
5462
}
5563
}
5664

65+
// This generally shouldn't happen, at least not for web
66+
// browesrs.
5767
if request.ParseForm() != nil {
5868
log.Println("MALFORMED", getRemoteIP(request), request.RequestURI, request.Header.Get("User-Agent"))
59-
authPage("You submitted a malformed form.")
69+
challengePage("You submitted a malformed form.")
6070
return
6171
}
6272

6373
formValues, ok := request.PostForm["powxy"]
6474
if !ok {
75+
// If there's simply no form value, the user is probably
76+
// just visiting the site for the first time or with an
77+
// expired cookie.
6578
log.Println("CHALLENGE", getRemoteIP(request), request.RequestURI, request.Header.Get("User-Agent"))
66-
authPage("")
79+
challengePage("")
6780
return
6881
} else if len(formValues) != 1 {
82+
// This should never happen, at least not for web
83+
// browsers.
6984
log.Println("FORM_VALUES", getRemoteIP(request), request.RequestURI, request.Header.Get("User-Agent"))
70-
authPage("You submitted an invalid number of form values.")
85+
challengePage("You submitted an invalid number of form values.")
7186
return
7287
}
7388

74-
nonce, err := base64.StdEncoding.DecodeString(formValues[0])
75-
if err != nil {
76-
log.Println("BASE64", getRemoteIP(request), request.RequestURI, request.Header.Get("User-Agent"))
77-
authPage("Your submission was improperly encoded.")
89+
// We validate that the length is reasonable before even
90+
// decoding it with base64.
91+
if len(formValues[0]) > 43 {
92+
log.Println("TOO_LONG", getRemoteIP(request), request.RequestURI, request.Header.Get("User-Agent"))
93+
challengePage("Your submission was too long.")
7894
return
7995
}
8096

81-
if len(nonce) > 32 {
82-
log.Println("TOO_LONG", getRemoteIP(request), request.RequestURI, request.Header.Get("User-Agent"))
83-
authPage("Your submission was too long.")
97+
// Actually decode the base64 value.
98+
nonce, err := base64.StdEncoding.DecodeString(formValues[0])
99+
if err != nil {
100+
log.Println("BASE64", getRemoteIP(request), request.RequestURI, request.Header.Get("User-Agent"))
101+
challengePage("Your submission was improperly encoded.")
84102
return
85103
}
86104

87-
h := sha256.New()
88-
h.Write(identifier)
89-
h.Write(nonce)
90-
ck := h.Sum(nil)
91-
if !validateBitZeros(ck, global.NeedBits) {
105+
// Validate the nonce.
106+
if !validateNonce(identifier, nonce) {
92107
log.Println("WRONG", getRemoteIP(request), request.RequestURI, request.Header.Get("User-Agent"))
93-
authPage("Your submission was incorrect, or your session has expired while submitting.")
108+
challengePage("Your submission was incorrect, or your session has expired while submitting.")
94109
return
95110
}
96111

112+
// Everything starting here: the nonce is valid, and we
113+
// can set the cookie and redirect them. The redirection is
114+
// needed as their "normal" request is most definitely
115+
// different from one to expect after solving the PoW
116+
// challenge.
117+
97118
http.SetCookie(writer, &http.Cookie{
98119
Name: "powxy",
99120
Value: base64.StdEncoding.EncodeToString(expectedMAC),
@@ -105,30 +126,3 @@ func main() {
105126
http.Redirect(writer, request, "", http.StatusSeeOther)
106127
})))
107128
}
108-
109-
func validateCookie(cookie *http.Cookie, expectedMAC []byte) bool {
110-
if cookie == nil {
111-
return false
112-
}
113-
114-
gotMAC, err := base64.StdEncoding.DecodeString(cookie.Value)
115-
if err != nil {
116-
return false
117-
}
118-
119-
return subtle.ConstantTimeCompare(gotMAC, expectedMAC) == 1
120-
}
121-
122-
func getRemoteIP(request *http.Request) (remoteIP string) {
123-
if secondary {
124-
remoteIP, _, _ = strings.Cut(request.Header.Get("X-Forwarded-For"), ",")
125-
}
126-
if remoteIP == "" {
127-
remoteIP = request.RemoteAddr
128-
index := strings.LastIndex(remoteIP, ":")
129-
if index != -1 {
130-
remoteIP = remoteIP[:index]
131-
}
132-
}
133-
return
134-
}

privkey.go

+6-1
Original file line numberDiff line numberDiff line change
@@ -10,10 +10,15 @@ import (
1010
)
1111

1212
var (
13-
privkey = make([]byte, 32)
13+
// The private key used to HMAC the challenge.
14+
privkey = make([]byte, 32)
15+
16+
// The hash of the private key. We use this as an element of the
17+
// identifier.
1418
privkeyHash = make([]byte, 0, sha256.Size)
1519
)
1620

21+
// This init generates a random private key and its hash.
1722
func init() {
1823
if _, err := rand.Read(privkey); err != nil {
1924
log.Fatal(err)

proxy.go

+3
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,8 @@ import (
1212

1313
var reverseProxy *httputil.ReverseProxy
1414

15+
// This init sets up the reverse proxy. Go's NewSingleHostReverseProxy is
16+
// sufficient for our use case.
1517
func init() {
1618
parsedURL, err := url.Parse(destHost)
1719
if err != nil {
@@ -20,6 +22,7 @@ func init() {
2022
reverseProxy = httputil.NewSingleHostReverseProxy(parsedURL)
2123
}
2224

25+
// proxyRequest proxies the incoming request to the destination host.
2326
func proxyRequest(writer http.ResponseWriter, request *http.Request) {
2427
reverseProxy.ServeHTTP(writer, request)
2528
}

tmpl.go

+1
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import (
1313
var tmplString string
1414
var tmpl *template.Template
1515

16+
// This init function parses the HTML template.
1617
func init() {
1718
var err error
1819
tmpl, err = template.New("powxy").Parse(tmplString)

unsafe.go

+1-1
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ package main
55

66
import "unsafe"
77

8-
// Converts a string to a byte slice without copying the string.
8+
// stringToBytes converts a string to a byte slice without copying the string.
99
// Memory is borrowed from the string.
1010
// The resulting byte slice must not be modified in any form.
1111
func stringToBytes(s string) (bytes []byte) {

validate.go

+18
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
// SPDX-License-Identifier: BSD-2-Clause
2+
// SPDX-FileCopyrightText: Copyright (c) 2025 Runxi Yu <https://runxiyu.org>
3+
4+
package main
5+
6+
import (
7+
"crypto/sha256"
8+
)
9+
10+
// validateNonce checks if the nonce for the proof of work challenge is valid
11+
// for the given identifier.
12+
func validateNonce(identifier, nonce []byte) bool {
13+
h := sha256.New()
14+
h.Write(identifier)
15+
h.Write(nonce)
16+
ck := h.Sum(nil)
17+
return validateBitZeros(ck, global.NeedBits)
18+
}

0 commit comments

Comments
 (0)