From 9ec2a91a6677a5da643be71bf8fee842e78d7d58 Mon Sep 17 00:00:00 2001 From: Adam Bender Date: Sun, 9 Feb 2025 22:22:39 -0800 Subject: [PATCH 1/4] Initial commit --- ipieces/README.md | 32 ++++ ipieces/ipieces.go | 267 +++++++++++++++++++++++++++++++ ipieces/static/error.html | 18 +++ ipieces/static/index.tmpl | 28 ++++ ipieces/static/lights.gif | Bin 0 -> 4529 bytes ipieces/static/rate-limited.html | 17 ++ ipieces/static/style.css | 55 +++++++ ipieces/static/vpn.html | 17 ++ ipieces/template.go | 36 +++++ 9 files changed, 470 insertions(+) create mode 100644 ipieces/README.md create mode 100644 ipieces/ipieces.go create mode 100644 ipieces/static/error.html create mode 100644 ipieces/static/index.tmpl create mode 100644 ipieces/static/lights.gif create mode 100644 ipieces/static/rate-limited.html create mode 100644 ipieces/static/style.css create mode 100644 ipieces/static/vpn.html create mode 100644 ipieces/template.go diff --git a/ipieces/README.md b/ipieces/README.md new file mode 100644 index 0000000..928c438 --- /dev/null +++ b/ipieces/README.md @@ -0,0 +1,32 @@ +# ipieces + +ipieces is a Go package used to create Geocaching puzzles such as [GCB1ZXB](https://coord.info/GCB1ZXB). + +## Documentation + +Available at https://pkg.go.dev/github/bitlux/caches/ipieces. + +## Deployment instructions + +I deploy on [Google Cloud Run](https://cloud.google.com/run). To do that, you must first sign +into the [Google Cloud console](https://console.cloud.google.com/), +[create a project](https://cloud.google.com/resource-manager/docs/creating-managing-projects), and have +[`gcloud`](https://cloud.google.com/sdk) installed. + +To deploy, `cd` to the directory with your `main.go` file and run: +``` +gcloud run deploy --source . [--allow-unauthenticated] +``` + +## Testing + +To set the IP address of the request to `foo`, use: +``` +curl -H ": foo" localhost:8080/text +``` +where `` is the `Puzzle.Backdoor` string you set. + +## Contact / Support + +I welcome issues and pull requests on GitHub and messages and email on +[geocaching.com](https://www.geocaching.com/profile/?u=bitlux). \ No newline at end of file diff --git a/ipieces/ipieces.go b/ipieces/ipieces.go new file mode 100644 index 0000000..a4ecc2a --- /dev/null +++ b/ipieces/ipieces.go @@ -0,0 +1,267 @@ +// Package ipieces allows users to create IP address-based Geocaching puzzles. +// +// To create a puzzle, you need to populate a [Puzzle] struct and call [Run] on it. +// For example: +// +// package main +// +// import ( +// "github.com/bitlux/caches/ipieces" +// "github.com/bitlux/vpnapi" +// ) +// +// func main() { +// p := ipieces.Puzzle{ +// Final: []ipieces.Digit{ +// ipieces.Digit{Value: "3", Status: ipieces.VISIBLE} +// ipieces.Digit{Value: "7", Status: ipieces.VISIBLE} +// ipieces.Digit{Value: "2", Status: ipieces.VISIBLE} +// ipieces.Digit{Value: "4", Status: ipieces.HIDDEN} +// ipieces.Digit{Value: "0", Status: ipieces.HIDDEN} +// ipieces.Digit{Value: "0", Status: ipieces.HIDDEN} +// ipieces.Digit{Value: "0", Status: ipieces.HIDDEN} +// ipieces.Digit{Value: "1", Status: ipieces.VISIBLE} +// ipieces.Digit{Value: "2", Status: ipieces.VISIBLE} +// ipieces.Digit{Value: "2", Status: ipieces.VISIBLE} +// ipieces.Digit{Value: "0", Status: ipieces.VISIBLE} +// ipieces.Digit{Value: "4", Status: ipieces.HIDDEN} +// ipieces.Digit{Value: "0", Status: ipieces.HIDDEN} +// ipieces.Digit{Value: "0", Status: ipieces.HIDDEN} +// ipieces.Digit{Value: "0", Status: ipieces.HIDDEN} +// }, +// IndexFunc: func(b [sha256.Size]byte) int { +// return int(b[sha256.Size-1]) % 8 +// }, +// // Setting Client is optional. +// Client: vpnapi.New("YOUR-API-KEY-HERE"), +// Backdoor: "topsecret", +// GCCode: "GCB2PKC", +// } +// p.Run() +// } +// +// [Run] creates two handlers: +// - a text endpoint at `/text` which responds with a short plaintext page with the client's IP, +// the computed index into the final coordinates, and the revealed coordinate, and +// - a default endpoint, which serves any path other than `/text`, and responds with an HTML page. +package ipieces + +import ( + "crypto/sha256" + "encoding/hex" + "fmt" + "html/template" + "io" + "log" + "net/http" + "net/netip" + "os" + "slices" + + "github.com/bitlux/vpnapi" +) + +// TODO: All logging is printed to stdout. This is fine for Google Cloud Run, +// but consider using log/slog. + +func init() { + hostname, _ := os.Hostname() + fmt.Printf("STARTING on %s, pid %d\n", hostname, os.Getpid()) +} + +// VPNChecker determines whether a client's IP address belongs to a VPN or proxy. Its concrete +// implementation is *github.com/bitlux/vpnapi.Client. In order to use vpnapi.Client, you must +// first obtain an API key from http://vpnapi.io. +type VPNChecker interface { + Query(string) (*vpnapi.Response, error) +} + +// Display controls how a Digit is displayed. +type Display int + +const ( + // Visible by default + VISIBLE Display = iota + // Hidden by default + HIDDEN + // Hidden by default, revealed in this rendering + REVEALED +) + +// A Digit is one of the digits that make up the coordinates of the final. +type Digit struct { + // Value is a single digit, in string form. + Value string + // Status is how this digit should be displayed: visible, hidden, or revealed. + Status Display +} + +func (d Digit) format() template.HTML { + switch d.Status { + case VISIBLE: + return template.HTML(d.Value) + case HIDDEN: + return template.HTML(` `) + case REVEALED: + return template.HTML(fmt.Sprintf(`%s`, d.Value)) + default: + return template.HTML("ERROR") + } +} + +type Puzzle struct { + // Final is the full final coordinates of the puzzle. + Final []Digit + + // IndexFunc determines which digit of Final is revealed. The return value must be less than the number + // of hidden Digits, because it is used as an index into the hidden Digits in Final. + IndexFunc func([sha256.Size]byte) int + + // Client determines how to handle requests from IP addresses that belong to VPNs or proxies. + // If Client is nil, no VPN checking will be done. In order to do VPN checking, first obtain an + // API key from http://vpnapi.io. Pass that key to github.com/bitlux/vpnapi.Client.New to create + // a *vpnapi.Client, and set Client to that value. + Client VPNChecker + + // Backdoor allows you to test how the server handles a specific IP address. Backdoor will be accepted + // as an HTTP header name. The server will read the header value as the client's IP address. For + // example, if Backdoor is "geocache" and the server is running on localhost:8080, the following + // command will tell the server that the request is coming from IP 1.2.3.4: + // curl -H "geocache: 1.2.3.4" localhost:8080 + // Any string is a valid . + Backdoor string + + // GCCode is used to link back to the puzzle on geocaching.com. + GCCode string + + hiddenCount int +} + +type data struct { + IP string + Hash string + Digits []Digit + Index int + Revealed string + GCCode string +} + +func ipFromHeaders(h http.Header, backdoor string) string { + if vals := h.Values(("X-Forwarded-For")); len(vals) > 1 { + fmt.Println("X-Forwarded-For:", vals) + } + if ip := h.Get(backdoor); ip != "" { + fmt.Println("Setting IP to", ip, "via header") + return ip + } + return h.Get("X-Forwarded-For") +} + +func (p Puzzle) dataFromReq(req *http.Request) (*data, error) { + ip := ipFromHeaders(req.Header, p.Backdoor) + if ip == "" { + ap, err := netip.ParseAddrPort(req.RemoteAddr) + if err != nil { + return nil, fmt.Errorf("IP error: ParseAddrPort(%s) returned %v", req.RemoteAddr, err) + } + ip = ap.Addr().String() + } + + sha := sha256.Sum256([]byte(ip)) + d := &data{ + IP: ip, + Hash: hex.EncodeToString(sha[:]), + Digits: slices.Clone(p.Final), + Index: p.IndexFunc(sha), + GCCode: p.GCCode, + } + d.flip() + return d, nil +} + +func (d *data) flip() { + count := 0 + for i := range d.Digits { + if d.Digits[i].Status == HIDDEN { + if count == d.Index { + d.Digits[i].Status = REVEALED + d.Revealed = d.Digits[i].Value + return + } + count++ + } + } +} + +func (p Puzzle) handle(w http.ResponseWriter, req *http.Request, tmpl *template.Template) { + d, err := p.dataFromReq(req) + fmt.Printf("IP %s index %d\n", d.IP, d.Index) + if err != nil { + fmt.Println("dataFromReq failed:", err) + w.WriteHeader(http.StatusInternalServerError) + io.WriteString(w, errorPage) + return + } + + if p.Client != nil { + resp, err := p.Client.Query(d.IP) + if err != nil { + if err == vpnapi.ErrRateLimited { + fmt.Println("rate limited:", err) + w.WriteHeader(http.StatusInternalServerError) + io.WriteString(w, rateLimitPage) + return + } + // TODO: This fails closed. This may be too strict, especially if vpnapi.io is unreliable. + fmt.Println("Query failed:", err) + w.WriteHeader(http.StatusInternalServerError) + io.WriteString(w, errorPage) + return + } + + if resp.Security.VPN || resp.Security.Proxy || resp.Security.Tor || resp.Security.Relay { + fmt.Printf("%t %t %t %t\n", resp.Security.VPN, resp.Security.Proxy, resp.Security.Tor, resp.Security.Relay) + w.WriteHeader(http.StatusForbidden) + io.WriteString(w, vpnPage) + return + } + } + + if err := tmpl.Execute(w, d); err != nil { + fmt.Println("tmpl.Execute failed:", err) + w.WriteHeader(http.StatusInternalServerError) + io.WriteString(w, errorPage) + return + } +} + +// Runs starts an HTTP server and blocks forever (or until a fatal error occurs). +func (p Puzzle) Run() { + http.HandleFunc("/", func(w http.ResponseWriter, req *http.Request) { + p.handle(w, req, indexTmpl) + }) + http.HandleFunc("/text", func(w http.ResponseWriter, req *http.Request) { + p.handle(w, req, textTmpl) + }) + http.HandleFunc("/style.css", func(w http.ResponseWriter, _ *http.Request) { + w.Header().Set("Content-Type", "text/css; charset=utf-8") + io.WriteString(w, stylesheet) + }) + http.HandleFunc("/lights.gif", func(w http.ResponseWriter, _ *http.Request) { + io.WriteString(w, lights) + }) + + for _, d := range p.Final { + if d.Status == HIDDEN { + p.hiddenCount++ + } + } + + // Google Cloud Run passes the port in the PORT environment variable. + port := os.Getenv("PORT") + if port == "" { + port = "8080" + } + fmt.Println("Listening on port", port) + log.Fatal(http.ListenAndServe(":8080", nil)) +} diff --git a/ipieces/static/error.html b/ipieces/static/error.html new file mode 100644 index 0000000..1c67c50 --- /dev/null +++ b/ipieces/static/error.html @@ -0,0 +1,18 @@ + + + + + + IPieces of the puzzle + + + + + +
+ Sorry, an error occurred. Please contact + bitlux if this persists. +
+ + + \ No newline at end of file diff --git a/ipieces/static/index.tmpl b/ipieces/static/index.tmpl new file mode 100644 index 0000000..8909cc6 --- /dev/null +++ b/ipieces/static/index.tmpl @@ -0,0 +1,28 @@ + + + + + IPieces of the puzzle + + + + +
+ Your IP address is
+ {{.IP}} +{{ comment .IP .Hash .Index }} +
+
+ Your piece of the puzzle is
+ + N {{index .Digits 0 | format}}{{index .Digits 1 | format}}° {{index .Digits 2 | format}}{{index .Digits 3 | format}}.{{index .Digits 4 | format}}{{index .Digits 5 | format}}{{index .Digits 6 | format}} + W {{index .Digits 7 | format}}{{index .Digits 8 | format}}{{index .Digits 9 | format}}° {{index .Digits 10 | format}}{{index .Digits 11 | format}}.{{index .Digits 12 | format}}{{index .Digits 13 | format}}{{index .Digits 14 | format}} + +
+
+ + {{.GCCode}} + +
+ + diff --git a/ipieces/static/lights.gif b/ipieces/static/lights.gif new file mode 100644 index 0000000000000000000000000000000000000000..89c1ceb4cd7c638e447d4e1b0fb65d0cc613a5bb GIT binary patch literal 4529 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 + + + + + IPieces of the puzzle + + + + + +
+ You have been rate-limited. Please slow down. +
+ + + \ No newline at end of file diff --git a/ipieces/static/style.css b/ipieces/static/style.css new file mode 100644 index 0000000..0e9001e --- /dev/null +++ b/ipieces/static/style.css @@ -0,0 +1,55 @@ +body { + font-size: 40px; + text-align: center; + color: #101010; + background: #d0e6ff; + font-family: Verdana, Tahoma, sans-serif; + line-height: 1.5; +} + +/*** other body styles ***/ +body.yellow { + background: #f3d104; +} + +body.red { + background: #d61111; + font-size: 60px; +} + +div { + padding-top: 1em; +} + +/*** spans ***/ + +.mono { + font-size: 45px; + font-family: Monaco, monospace; + letter-spacing: 2px; +} + +/* used for spacing */ +.small { + font-size: 15px; + letter-spacing: 0; +} + +.underline { + margin: 0 3px; + text-decoration: 4px #101010 underline; +} + +span.red { + margin: 0 3px; + text-decoration: 4px #ff0000 underline; +} + +.linkback { + padding-top: 30px; + font-size: 16px; +} + +img { + padding: 0 20px; +} \ No newline at end of file diff --git a/ipieces/static/vpn.html b/ipieces/static/vpn.html new file mode 100644 index 0000000..7c9378e --- /dev/null +++ b/ipieces/static/vpn.html @@ -0,0 +1,17 @@ + + + + + + IPieces of the puzzle + + + + + +
+ VPN DETECTED! +
+ + + \ No newline at end of file diff --git a/ipieces/template.go b/ipieces/template.go new file mode 100644 index 0000000..860d69b --- /dev/null +++ b/ipieces/template.go @@ -0,0 +1,36 @@ +package ipieces + +import ( + "fmt" + "html/template" + + _ "embed" +) + +var ( + //go:embed static/style.css + stylesheet string + //go:embed static/index.tmpl + index string + //go:embed static/error.html + errorPage string + //go:embed static/rate-limited.html + rateLimitPage string + //go:embed static/vpn.html + vpnPage string + //go:embed static/lights.gif + lights string + + // All Templates are meant to be executed with a `data` struct. + // + // HTTP 200 template + indexTmpl = template.Must(template.New("").Funcs(template.FuncMap{ + "comment": func(ip, hash string, index int) template.HTML { + return template.HTML(fmt.Sprintf("", ip, hash, index)) + }, + "format": func(d Digit) template.HTML { return d.format() }, + }).Parse(index)) + + // /text template + textTmpl = template.Must(template.New("").Parse("IP: {{.IP}} Digits[{{.Index}}]: {{.Revealed}}\n")) +) From d1ab3ce01c64d410a82813c40473289345445a42 Mon Sep 17 00:00:00 2001 From: Adam Bender Date: Sun, 9 Feb 2025 22:22:39 -0800 Subject: [PATCH 2/4] Initial commit --- go.mod | 7 +++++-- go.sum | 2 ++ 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/go.mod b/go.mod index cd32182..5f0859e 100644 --- a/go.mod +++ b/go.mod @@ -1,7 +1,10 @@ module github.com/bitlux/caches -go 1.23 +go 1.23.4 require github.com/keep94/sqroot/v3 v3.7.2 -require github.com/keep94/consume2 v0.7.0 // indirect +require ( + github.com/bitlux/vpnapi v0.0.0-20250207215125-f066bb2314a4 // indirect + github.com/keep94/consume2 v0.7.0 // indirect +) diff --git a/go.sum b/go.sum index b705e62..4675f94 100644 --- a/go.sum +++ b/go.sum @@ -1,3 +1,5 @@ +github.com/bitlux/vpnapi v0.0.0-20250207215125-f066bb2314a4 h1:zYVWlKFsyl3tMu3Ak9es+EMW2DUe9nqPQaOQNBtu2XQ= +github.com/bitlux/vpnapi v0.0.0-20250207215125-f066bb2314a4/go.mod h1:ou6ccQPRIv8uzPNeLaRLxnwd2felkT30fwGnJtbVYCg= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/keep94/consume2 v0.7.0 h1:JbS/XxmPbHgEG+1pvGEGc192nCOEx+S/DmJBZz9fkvQ= From 52fcc83ac9ab65e9e26d2a6c6d38f79af9a3ca4e Mon Sep 17 00:00:00 2001 From: Adam Bender Date: Sun, 9 Feb 2025 22:35:21 -0800 Subject: [PATCH 3/4] Make common function for writing static responses --- ipieces/ipieces.go | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/ipieces/ipieces.go b/ipieces/ipieces.go index a4ecc2a..acb91b7 100644 --- a/ipieces/ipieces.go +++ b/ipieces/ipieces.go @@ -193,13 +193,19 @@ func (d *data) flip() { } } +func writeResponse(w http.ResponseWriter, code int, body string, format string, args ...any) { + fmt.Printf(format, args...) + w.WriteHeader(code) + if _, err := io.WriteString(w, body); err != nil { + fmt.Println("WriteString failed:", err) + } +} + func (p Puzzle) handle(w http.ResponseWriter, req *http.Request, tmpl *template.Template) { d, err := p.dataFromReq(req) fmt.Printf("IP %s index %d\n", d.IP, d.Index) if err != nil { - fmt.Println("dataFromReq failed:", err) - w.WriteHeader(http.StatusInternalServerError) - io.WriteString(w, errorPage) + writeResponse(w, http.StatusInternalServerError, errorPage, "dataFromReq failed: %v", err) return } @@ -207,22 +213,16 @@ func (p Puzzle) handle(w http.ResponseWriter, req *http.Request, tmpl *template. resp, err := p.Client.Query(d.IP) if err != nil { if err == vpnapi.ErrRateLimited { - fmt.Println("rate limited:", err) - w.WriteHeader(http.StatusInternalServerError) - io.WriteString(w, rateLimitPage) + writeResponse(w, http.StatusTooManyRequests, rateLimitPage, "rate limited: %v", err) return } // TODO: This fails closed. This may be too strict, especially if vpnapi.io is unreliable. - fmt.Println("Query failed:", err) - w.WriteHeader(http.StatusInternalServerError) - io.WriteString(w, errorPage) + writeResponse(w, http.StatusInternalServerError, errorPage, "Query failed: %v", err) return } if resp.Security.VPN || resp.Security.Proxy || resp.Security.Tor || resp.Security.Relay { - fmt.Printf("%t %t %t %t\n", resp.Security.VPN, resp.Security.Proxy, resp.Security.Tor, resp.Security.Relay) - w.WriteHeader(http.StatusForbidden) - io.WriteString(w, vpnPage) + writeResponse(w, http.StatusForbidden, vpnPage, "%t %t %t %t\n", resp.Security.VPN, resp.Security.Proxy, resp.Security.Tor, resp.Security.Relay) return } } From 724cf3a9d173f11b35b1dc56ffa9cab6592ded42 Mon Sep 17 00:00:00 2001 From: Adam Bender Date: Sun, 9 Feb 2025 22:40:47 -0800 Subject: [PATCH 4/4] Check io.WriteString return value every time --- ipieces/ipieces.go | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/ipieces/ipieces.go b/ipieces/ipieces.go index acb91b7..8ba6d9d 100644 --- a/ipieces/ipieces.go +++ b/ipieces/ipieces.go @@ -205,7 +205,7 @@ func (p Puzzle) handle(w http.ResponseWriter, req *http.Request, tmpl *template. d, err := p.dataFromReq(req) fmt.Printf("IP %s index %d\n", d.IP, d.Index) if err != nil { - writeResponse(w, http.StatusInternalServerError, errorPage, "dataFromReq failed: %v", err) + writeResponse(w, http.StatusInternalServerError, errorPage, "dataFromReq failed: %v\n", err) return } @@ -213,11 +213,11 @@ func (p Puzzle) handle(w http.ResponseWriter, req *http.Request, tmpl *template. resp, err := p.Client.Query(d.IP) if err != nil { if err == vpnapi.ErrRateLimited { - writeResponse(w, http.StatusTooManyRequests, rateLimitPage, "rate limited: %v", err) + writeResponse(w, http.StatusTooManyRequests, rateLimitPage, "rate limited: %v\n", err) return } // TODO: This fails closed. This may be too strict, especially if vpnapi.io is unreliable. - writeResponse(w, http.StatusInternalServerError, errorPage, "Query failed: %v", err) + writeResponse(w, http.StatusInternalServerError, errorPage, "Query failed: %v\n", err) return } @@ -228,9 +228,7 @@ func (p Puzzle) handle(w http.ResponseWriter, req *http.Request, tmpl *template. } if err := tmpl.Execute(w, d); err != nil { - fmt.Println("tmpl.Execute failed:", err) - w.WriteHeader(http.StatusInternalServerError) - io.WriteString(w, errorPage) + writeResponse(w, http.StatusInternalServerError, errorPage, "tmpl.Execute failed: %v\n", err) return } } @@ -245,10 +243,14 @@ func (p Puzzle) Run() { }) http.HandleFunc("/style.css", func(w http.ResponseWriter, _ *http.Request) { w.Header().Set("Content-Type", "text/css; charset=utf-8") - io.WriteString(w, stylesheet) + if _, err := io.WriteString(w, stylesheet); err != nil { + fmt.Println("WriteString failed:", err) + } }) http.HandleFunc("/lights.gif", func(w http.ResponseWriter, _ *http.Request) { - io.WriteString(w, lights) + if _, err := io.WriteString(w, lights); err != nil { + fmt.Println("WriteString failed:", err) + } }) for _, d := range p.Final {