Probe (client) check remote node

Please note that this commit is not complete. I commit to the repo
because I have something to do with my another project. Just don't
want to lost my work for the last couple hours.
This commit is contained in:
ditatompel 2024-05-04 22:36:57 +07:00
parent cee2b4341b
commit 8724b81431
No known key found for this signature in database
GPG key ID: 31D3D06D77950979
5 changed files with 268 additions and 5 deletions

View file

@ -1,14 +1,21 @@
# Prober config
# #############
SERVER_ENDPOINT="http://127.0.0.1:18901"
API_KEY=
ACCEPT_TOR=true
TOR_SOCKS="127.0.0.1:9050"
# Server Config
# #############
SECRET_KEY="" # must be 32 char length, use `openssl rand -base64 32` to generate random secret SECRET_KEY="" # must be 32 char length, use `openssl rand -base64 32` to generate random secret
LOG_LEVEL=INFO # can be DEBUG, INFO, WARNING, ERROR LOG_LEVEL=INFO # can be DEBUG, INFO, WARNING, ERROR
# Fiber Config # Fiber Config
APP_DEBUG=false # if this set to true , LOG_LEVEL will be set to DEBUG APP_DEBUG=false # if this set to true , LOG_LEVEL will be set to DEBUG
APP_PREFORK=true APP_PREFORK=true
APP_HOST="0.0.0.0" APP_HOST="127.0.0.1"
APP_PORT=18090 APP_PORT=18090
APP_PROXY_HEADER="X-Real-Ip" # CF-Connecting-IP APP_PROXY_HEADER="X-Real-Ip" # CF-Connecting-IP
APP_ALLOW_ORIGIN="http://localhost:5173,http://192.168.1.99:5173,https://ditatompel.com" APP_ALLOW_ORIGIN="http://localhost:5173,http://127.0.0.1:5173,https://ditatompel.com"
# DB settings: # DB settings:
DB_HOST=127.0.0.1 DB_HOST=127.0.0.1
DB_PORT=3306 DB_PORT=3306

241
cmd/probe.go Normal file
View file

@ -0,0 +1,241 @@
package cmd
import (
"bytes"
"context"
"encoding/json"
"fmt"
"io"
"net"
"net/http"
"os"
"time"
"github.com/ditatompel/xmr-nodes/internal/config"
"github.com/ditatompel/xmr-nodes/internal/repo"
"github.com/spf13/cobra"
"golang.org/x/net/proxy"
)
const RPCUserAgent = "ditatombot/0.0.1 (Monero RPC Monitoring; Contact: ditatombot@ditatompel.com)"
type proberClient struct {
config *config.App
}
func newProber(cfg *config.App) *proberClient {
return &proberClient{config: cfg}
}
var probeCmd = &cobra.Command{
Use: "probe",
Short: "Run Monero node prober",
Run: func(_ *cobra.Command, _ []string) {
runProbe()
},
}
func init() {
rootCmd.AddCommand(probeCmd)
}
func runProbe() {
cfg := config.AppCfg()
if cfg.ServerEndpoint == "" {
fmt.Println("Please set SERVER_ENDPOINT in .env")
os.Exit(1)
}
fmt.Printf("Accept Tor: %t\n", cfg.AcceptTor)
if cfg.AcceptTor && cfg.TorSocks == "" {
fmt.Println("Please set TOR_SOCKS in .env")
os.Exit(1)
}
probe := newProber(cfg)
node, err := probe.getJob()
if err != nil {
fmt.Println(err)
os.Exit(1)
}
fetchNode, err := probe.fetchNode(node)
if err != nil {
fmt.Println(err)
os.Exit(1)
}
fmt.Println(prettyPrint(fetchNode))
}
func (p *proberClient) getJob() (repo.MoneroNode, error) {
queryParams := ""
if p.config.ApiKey != "" {
queryParams = "?api_key=" + p.config.ApiKey
}
node := repo.MoneroNode{}
endpoint := fmt.Sprintf("%s/api/v1/job%s", p.config.ServerEndpoint, queryParams)
req, err := http.NewRequest(http.MethodGet, endpoint, nil)
if err != nil {
return node, err
}
req.Header.Add("X-Prober-Api-Key", p.config.ApiKey)
req.Header.Set("User-Agent", RPCUserAgent)
client := &http.Client{}
resp, err := client.Do(req)
if err != nil {
return node, err
}
defer resp.Body.Close()
if resp.StatusCode != 200 {
return node, fmt.Errorf("status code: %d", resp.StatusCode)
}
response := struct {
Data repo.MoneroNode `json:"data"`
}{}
err = json.NewDecoder(resp.Body).Decode(&response)
if err != nil {
return node, err
}
node = response.Data
return node, nil
}
func (p *proberClient) fetchNode(node repo.MoneroNode) (repo.MoneroNode, error) {
startTime := time.Now()
endpoint := fmt.Sprintf("%s://%s:%d/json_rpc", node.Protocol, node.Hostname, node.Port)
rpcParam := []byte(`{"jsonrpc": "2.0","id": "0","method": "get_info"}`)
req, err := http.NewRequest(http.MethodPost, endpoint, bytes.NewBuffer(rpcParam))
if err != nil {
return node, err
}
req.Header.Set("Content-Type", "application/json; charset=UTF-8")
req.Header.Set("User-Agent", RPCUserAgent)
req.Header.Set("Origin", "https://xmr.ditatompel.com")
var client http.Client
if p.config.AcceptTor && node.IsTor {
dialer, err := proxy.SOCKS5("tcp", p.config.TorSocks, nil, proxy.Direct)
if err != nil {
return node, err
}
dialContext := func(ctx context.Context, network, addr string) (net.Conn, error) {
return dialer.Dial(network, addr)
}
transport := &http.Transport{
DialContext: dialContext,
DisableKeepAlives: true,
}
client.Transport = transport
client.Timeout = 60 * time.Second
}
// reset the default node struct
node.IsAvailable = false
resp, err := client.Do(req)
if err != nil {
// TODO: Post report to server
return node, err
}
defer resp.Body.Close()
if resp.StatusCode != 200 {
// TODO: Post report to server
return node, fmt.Errorf("status code: %d", resp.StatusCode)
}
body, err := io.ReadAll(resp.Body)
if err != nil {
// TODO: Post report to server
return node, err
}
reportNode := struct {
repo.MoneroNode `json:"result"`
}{}
if err := json.Unmarshal(body, &reportNode); err != nil {
// TODO: Post report to server
return node, err
}
node.IsAvailable = true
node.NetType = reportNode.NetType
node.AdjustedTime = reportNode.AdjustedTime
node.DatabaseSize = reportNode.DatabaseSize
node.Difficulty = reportNode.Difficulty
node.NodeVersion = reportNode.NodeVersion
if resp.Header.Get("Access-Control-Allow-Origin") == "*" || resp.Header.Get("Access-Control-Allow-Origin") == "https://xmr.ditatompel.com" {
node.CorsCapable = true
}
if !node.IsTor {
hostIp, err := net.LookupIP(node.Hostname)
if err != nil {
fmt.Println("Warning: Could not resolve hostname: " + node.Hostname)
} else {
node.Ip = hostIp[0].String()
}
}
// Sleeping 1 second to avoid too many request on host behind CloudFlare
// time.Sleep(1 * time.Second)
// check fee
rpcCheckFeeParam := []byte(`{"jsonrpc": "2.0","id": "0","method": "get_fee_estimate"}`)
reqCheckFee, err := http.NewRequest(http.MethodPost, endpoint, bytes.NewBuffer(rpcCheckFeeParam))
if err != nil {
return node, err
}
reqCheckFee.Header.Set("Content-Type", "application/json; charset=UTF-8")
reqCheckFee.Header.Set("User-Agent", RPCUserAgent)
checkFee, err := client.Do(reqCheckFee)
if err != nil {
return node, err
}
defer checkFee.Body.Close()
if checkFee.StatusCode != 200 {
return node, fmt.Errorf("status code: %d", checkFee.StatusCode)
}
bodyCheckFee, err := io.ReadAll(checkFee.Body)
if err != nil {
return node, err
}
feeEstimate := struct {
Result struct {
Fee uint `json:"fee"`
} `json:"result"`
}{}
if err := json.Unmarshal(bodyCheckFee, &feeEstimate); err != nil {
return node, err
}
tookTime := time.Since(startTime).Seconds()
node.EstimateFee = feeEstimate.Result.Fee
fmt.Printf("Took %f seconds\n", tookTime)
return node, nil
}
// for debug purposes
func prettyPrint(i interface{}) string {
s, _ := json.MarshalIndent(i, "", "\t")
return string(s)
}

1
go.mod
View file

@ -10,6 +10,7 @@ require (
github.com/jmoiron/sqlx v1.4.0 github.com/jmoiron/sqlx v1.4.0
github.com/joho/godotenv v1.5.1 github.com/joho/godotenv v1.5.1
github.com/spf13/cobra v1.8.0 github.com/spf13/cobra v1.8.0
golang.org/x/net v0.21.0
golang.org/x/term v0.19.0 golang.org/x/term v0.19.0
) )

2
go.sum
View file

@ -56,6 +56,8 @@ golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg= golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg=
golang.org/x/net v0.21.0 h1:AQyQV4dYCvJ7vGmJyKki9+PBdyvhkSd8EIx/qb0AYv4=
golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=

View file

@ -6,6 +6,7 @@ import (
) )
type App struct { type App struct {
// configuration for server
Debug bool Debug bool
Prefork bool Prefork bool
Host string Host string
@ -14,6 +15,11 @@ type App struct {
AllowOrigin string AllowOrigin string
SecretKey string SecretKey string
LogLevel string LogLevel string
// configuration for prober (client)
ServerEndpoint string
ApiKey string
AcceptTor bool
TorSocks string
} }
var app = &App{} var app = &App{}
@ -22,8 +28,9 @@ func AppCfg() *App {
return app return app
} }
// LoadApp loads App configuration // loads App configuration
func LoadApp() { func LoadApp() {
// server configuration
app.Host = os.Getenv("APP_HOST") app.Host = os.Getenv("APP_HOST")
app.Port, _ = strconv.Atoi(os.Getenv("APP_PORT")) app.Port, _ = strconv.Atoi(os.Getenv("APP_PORT"))
app.Debug, _ = strconv.ParseBool(os.Getenv("APP_DEBUG")) app.Debug, _ = strconv.ParseBool(os.Getenv("APP_DEBUG"))
@ -38,4 +45,9 @@ func LoadApp() {
if app.Debug { if app.Debug {
app.LogLevel = "DEBUG" app.LogLevel = "DEBUG"
} }
// prober configuration
app.ServerEndpoint = os.Getenv("SERVER_ENDPOINT")
app.ApiKey = os.Getenv("API_KEY")
app.AcceptTor, _ = strconv.ParseBool(os.Getenv("ACCEPT_TOR"))
app.TorSocks = os.Getenv("TOR_SOCKS")
} }