From 8724b81431fab9b798532802ebf605d77a315b15 Mon Sep 17 00:00:00 2001 From: ditatompel Date: Sat, 4 May 2024 22:36:57 +0700 Subject: [PATCH] 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. --- .env.example | 15 ++- cmd/probe.go | 241 +++++++++++++++++++++++++++++++++++++++++ go.mod | 1 + go.sum | 2 + internal/config/app.go | 14 ++- 5 files changed, 268 insertions(+), 5 deletions(-) create mode 100644 cmd/probe.go diff --git a/.env.example b/.env.example index 4b2808a..b521369 100644 --- a/.env.example +++ b/.env.example @@ -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 LOG_LEVEL=INFO # can be DEBUG, INFO, WARNING, ERROR - # Fiber Config APP_DEBUG=false # if this set to true , LOG_LEVEL will be set to DEBUG APP_PREFORK=true -APP_HOST="0.0.0.0" +APP_HOST="127.0.0.1" APP_PORT=18090 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_HOST=127.0.0.1 DB_PORT=3306 diff --git a/cmd/probe.go b/cmd/probe.go new file mode 100644 index 0000000..a1e596c --- /dev/null +++ b/cmd/probe.go @@ -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) +} diff --git a/go.mod b/go.mod index 840c6a7..4841713 100644 --- a/go.mod +++ b/go.mod @@ -10,6 +10,7 @@ require ( github.com/jmoiron/sqlx v1.4.0 github.com/joho/godotenv v1.5.1 github.com/spf13/cobra v1.8.0 + golang.org/x/net v0.21.0 golang.org/x/term v0.19.0 ) diff --git a/go.sum b/go.sum index e3c03fa..8c38525 100644 --- a/go.sum +++ b/go.sum @@ -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.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.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-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= diff --git a/internal/config/app.go b/internal/config/app.go index 2d1d9ec..8c535bb 100644 --- a/internal/config/app.go +++ b/internal/config/app.go @@ -6,6 +6,7 @@ import ( ) type App struct { + // configuration for server Debug bool Prefork bool Host string @@ -14,6 +15,11 @@ type App struct { AllowOrigin string SecretKey string LogLevel string + // configuration for prober (client) + ServerEndpoint string + ApiKey string + AcceptTor bool + TorSocks string } var app = &App{} @@ -22,8 +28,9 @@ func AppCfg() *App { return app } -// LoadApp loads App configuration +// loads App configuration func LoadApp() { + // server configuration app.Host = os.Getenv("APP_HOST") app.Port, _ = strconv.Atoi(os.Getenv("APP_PORT")) app.Debug, _ = strconv.ParseBool(os.Getenv("APP_DEBUG")) @@ -38,4 +45,9 @@ func LoadApp() { if app.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") }