From 48a25bece0daf79993efc232a6ec04b5d4a9401b Mon Sep 17 00:00:00 2001 From: Christian Ditaputratama Date: Mon, 25 Nov 2024 04:51:51 +0700 Subject: [PATCH] feat: Store hashed user IP address when submitting new node This feature added to help trace spammers. The IP address stored with one-way hash + salt to maintain user privacy. --- .env.example | 5 ++++ internal/config/app.go | 4 +++- internal/database/schema.go | 17 ++++++++++++- internal/handler/response.go | 4 ++-- internal/handler/server.go | 5 ++-- internal/monero/monero.go | 46 ++++++++++++++++++++++++++++++++++-- 6 files changed, 73 insertions(+), 8 deletions(-) diff --git a/.env.example b/.env.example index 86a0bcd..32c76e7 100644 --- a/.env.example +++ b/.env.example @@ -16,6 +16,11 @@ IPV6_CAPABLE=false # ############# APP_URL="https://xmr.ditatompel.com" # URL where user can access the web UI, don't put trailing slash +# APP_SECRET is random 64-character hex string that give us 32 random bytes. +# For now, this used for ip address salt, but may be useful for another feature +# in the future. You can achieve this using `openssl rand -hex 32`. +APP_SECRET= + # Fiber Config APP_PREFORK=false APP_HOST="127.0.0.1" diff --git a/internal/config/app.go b/internal/config/app.go index 4ac6ceb..9fa10da 100644 --- a/internal/config/app.go +++ b/internal/config/app.go @@ -13,7 +13,8 @@ type App struct { LogLevel string // configuration for server - URL string // URL where user can access the web UI, don't put trailing slash + URL string // URL where user can access the web UI, don't put trailing slash + Secret string // random 64-character hex string that give us 32 random bytes // fiber specific config Prefork bool @@ -61,6 +62,7 @@ func LoadApp() { // server configuration app.URL = os.Getenv("APP_URL") + app.Secret = os.Getenv("APP_SECRET") // fiber specific config app.Host = os.Getenv("APP_HOST") diff --git a/internal/database/schema.go b/internal/database/schema.go index 8b05c38..614a5a8 100644 --- a/internal/database/schema.go +++ b/internal/database/schema.go @@ -7,7 +7,7 @@ import ( type migrateFn func(*DB) error -var dbMigrate = [...]migrateFn{v1, v2, v3, v4} +var dbMigrate = [...]migrateFn{v1, v2, v3, v4, v5} func MigrateDb(db *DB) error { version := getSchemaVersion(db) @@ -287,3 +287,18 @@ func v4(db *DB) error { return nil } + +func v5(db *DB) error { + // table: tbl_node + slog.Debug("[DB] Adding additional columns to tbl_node") + _, err := db.Exec(` + ALTER TABLE tbl_node + ADD COLUMN submitter_iphash CHAR(64) NOT NULL DEFAULT '' + COMMENT 'hashed IP address who submitted the node' + AFTER date_entered;`) + if err != nil { + return err + } + + return nil +} diff --git a/internal/handler/response.go b/internal/handler/response.go index 2cbaeab..ed2a8e2 100644 --- a/internal/handler/response.go +++ b/internal/handler/response.go @@ -66,7 +66,7 @@ func (s *fiberServer) addNodeHandler(c *fiber.Ctx) error { } moneroRepo := monero.New() - if err := moneroRepo.Add(f.Protocol, f.Hostname, uint(f.Port)); err != nil { + if err := moneroRepo.Add(c.IP(), s.secret, f.Protocol, f.Hostname, uint(f.Port)); err != nil { handler := adaptor.HTTPHandler(templ.Handler(views.Alert("error", err.Error()))) return handler(c) } @@ -354,7 +354,7 @@ func (s *fiberServer) addNodeAPI(c *fiber.Ctx) error { hostname := c.FormValue("hostname") moneroRepo := monero.New() - if err := moneroRepo.Add(protocol, hostname, uint(port)); err != nil { + if err := moneroRepo.Add(c.IP(), s.secret, protocol, hostname, uint(port)); err != nil { return c.JSON(fiber.Map{ "status": "error", "message": err.Error(), diff --git a/internal/handler/server.go b/internal/handler/server.go index 0a7b48e..220913d 100644 --- a/internal/handler/server.go +++ b/internal/handler/server.go @@ -8,8 +8,9 @@ import ( type fiberServer struct { *fiber.App - db *database.DB - url string + db *database.DB + url string + secret string } // NewServer returns a new fiber server diff --git a/internal/monero/monero.go b/internal/monero/monero.go index dc1f945..f6e2444 100644 --- a/internal/monero/monero.go +++ b/internal/monero/monero.go @@ -1,7 +1,9 @@ package monero import ( + "crypto/sha256" "database/sql" + "encoding/hex" "encoding/json" "errors" "fmt" @@ -54,6 +56,7 @@ type Node struct { Latitude float64 `json:"latitude" db:"lat"` Longitude float64 `json:"longitude" db:"lon"` DateEntered int64 `json:"date_entered,omitempty" db:"date_entered"` + SubmitterIPHash string `json:"submitter_iphash,omitempty" db:"submitter_iphash"` LastChecked int64 `json:"last_checked" db:"last_checked"` FailedCount uint `json:"failed_count,omitempty" db:"failed_count"` LastCheckStatus types.JSONText `json:"last_check_statuses" db:"last_check_status"` @@ -176,7 +179,35 @@ func (r *moneroRepo) Nodes(q QueryNodes) (Nodes, error) { query := fmt.Sprintf(` SELECT - * + id, + hostname, + ip_addr, + port, + protocol, + is_tor, + is_i2p, + is_available, + nettype, + height, + adjusted_time, + database_size, + difficulty, + version, + uptime, + estimate_fee, + asn, + asn_name, + country, + country_name, + city, + lat, + lon, + date_entered, + last_checked, + last_check_status, + cors_capable, + ipv6_only, + ip_addresses FROM tbl_node %s @@ -190,7 +221,7 @@ func (r *moneroRepo) Nodes(q QueryNodes) (Nodes, error) { return nodes, err } -func (r *moneroRepo) Add(protocol string, hostname string, port uint) error { +func (r *moneroRepo) Add(submitterIP, salt, protocol, hostname string, port uint) error { if protocol != "http" && protocol != "https" { return errors.New("Invalid protocol, must one of or HTTP/HTTPS") } @@ -270,6 +301,7 @@ func (r *moneroRepo) Add(protocol string, hostname string, port uint) error { lat, lon, date_entered, + submitter_iphash, last_checked, last_check_status, ip_addresses, @@ -288,6 +320,7 @@ func (r *moneroRepo) Add(protocol string, hostname string, port uint) error { ?, ?, ?, + ?, ? )`, protocol, @@ -300,6 +333,7 @@ func (r *moneroRepo) Add(protocol string, hostname string, port uint) error { 0, 0, time.Now().Unix(), + hashIPWithSalt(submitterIP, salt), 0, string(statusDb), ips, @@ -397,6 +431,14 @@ func (r *moneroRepo) Countries() ([]Countries, error) { return c, err } +// hashIPWithSalt hashes IP address with salt designed for checksumming, but +// still maintain user privacy, this is NOT cryptographic security. +func hashIPWithSalt(ip, salt string) string { + hasher := sha256.New() + hasher.Write([]byte(salt + ip)) // Combine salt and IP + return hex.EncodeToString(hasher.Sum(nil)) +} + // ParseNodeStatuses parses JSONText into [5]int // Used this to parse last_check_status for templ engine func ParseNodeStatuses(statuses types.JSONText) [5]int {