feat: Store hashed user IP address when submitting new node
Some checks failed
Build / build (push) Has been cancelled
Test / test (push) Has been cancelled

This feature added to help trace spammers. The IP address stored with
one-way hash + salt to maintain user privacy.
This commit is contained in:
Christian Ditaputratama 2024-11-25 04:51:51 +07:00
parent d19e5844b0
commit 48a25bece0
No known key found for this signature in database
GPG key ID: 31D3D06D77950979
6 changed files with 73 additions and 8 deletions

View file

@ -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_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 # Fiber Config
APP_PREFORK=false APP_PREFORK=false
APP_HOST="127.0.0.1" APP_HOST="127.0.0.1"

View file

@ -13,7 +13,8 @@ type App struct {
LogLevel string LogLevel string
// configuration for server // 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 // fiber specific config
Prefork bool Prefork bool
@ -61,6 +62,7 @@ func LoadApp() {
// server configuration // server configuration
app.URL = os.Getenv("APP_URL") app.URL = os.Getenv("APP_URL")
app.Secret = os.Getenv("APP_SECRET")
// fiber specific config // fiber specific config
app.Host = os.Getenv("APP_HOST") app.Host = os.Getenv("APP_HOST")

View file

@ -7,7 +7,7 @@ import (
type migrateFn func(*DB) error 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 { func MigrateDb(db *DB) error {
version := getSchemaVersion(db) version := getSchemaVersion(db)
@ -287,3 +287,18 @@ func v4(db *DB) error {
return nil 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
}

View file

@ -66,7 +66,7 @@ func (s *fiberServer) addNodeHandler(c *fiber.Ctx) error {
} }
moneroRepo := monero.New() 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()))) handler := adaptor.HTTPHandler(templ.Handler(views.Alert("error", err.Error())))
return handler(c) return handler(c)
} }
@ -354,7 +354,7 @@ func (s *fiberServer) addNodeAPI(c *fiber.Ctx) error {
hostname := c.FormValue("hostname") hostname := c.FormValue("hostname")
moneroRepo := monero.New() 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{ return c.JSON(fiber.Map{
"status": "error", "status": "error",
"message": err.Error(), "message": err.Error(),

View file

@ -8,8 +8,9 @@ import (
type fiberServer struct { type fiberServer struct {
*fiber.App *fiber.App
db *database.DB db *database.DB
url string url string
secret string
} }
// NewServer returns a new fiber server // NewServer returns a new fiber server

View file

@ -1,7 +1,9 @@
package monero package monero
import ( import (
"crypto/sha256"
"database/sql" "database/sql"
"encoding/hex"
"encoding/json" "encoding/json"
"errors" "errors"
"fmt" "fmt"
@ -54,6 +56,7 @@ type Node struct {
Latitude float64 `json:"latitude" db:"lat"` Latitude float64 `json:"latitude" db:"lat"`
Longitude float64 `json:"longitude" db:"lon"` Longitude float64 `json:"longitude" db:"lon"`
DateEntered int64 `json:"date_entered,omitempty" db:"date_entered"` 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"` LastChecked int64 `json:"last_checked" db:"last_checked"`
FailedCount uint `json:"failed_count,omitempty" db:"failed_count"` FailedCount uint `json:"failed_count,omitempty" db:"failed_count"`
LastCheckStatus types.JSONText `json:"last_check_statuses" db:"last_check_status"` 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(` query := fmt.Sprintf(`
SELECT 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 FROM
tbl_node tbl_node
%s %s
@ -190,7 +221,7 @@ func (r *moneroRepo) Nodes(q QueryNodes) (Nodes, error) {
return nodes, err 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" { if protocol != "http" && protocol != "https" {
return errors.New("Invalid protocol, must one of or HTTP/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, lat,
lon, lon,
date_entered, date_entered,
submitter_iphash,
last_checked, last_checked,
last_check_status, last_check_status,
ip_addresses, ip_addresses,
@ -288,6 +320,7 @@ func (r *moneroRepo) Add(protocol string, hostname string, port uint) error {
?, ?,
?, ?,
?, ?,
?,
? ?
)`, )`,
protocol, protocol,
@ -300,6 +333,7 @@ func (r *moneroRepo) Add(protocol string, hostname string, port uint) error {
0, 0,
0, 0,
time.Now().Unix(), time.Now().Unix(),
hashIPWithSalt(submitterIP, salt),
0, 0,
string(statusDb), string(statusDb),
ips, ips,
@ -397,6 +431,14 @@ func (r *moneroRepo) Countries() ([]Countries, error) {
return c, err 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 // ParseNodeStatuses parses JSONText into [5]int
// Used this to parse last_check_status for templ engine // Used this to parse last_check_status for templ engine
func ParseNodeStatuses(statuses types.JSONText) [5]int { func ParseNodeStatuses(statuses types.JSONText) [5]int {