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_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"

View file

@ -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")

View file

@ -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
}

View file

@ -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(),

View file

@ -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

View file

@ -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 {