From a185d9834659e510bd1a8b6e8bf42e8958d5ab1a Mon Sep 17 00:00:00 2001
From: drlongle <drlongle@gmail.com>
Date: Fri, 23 Jun 2023 19:19:26 +0200
Subject: [PATCH] Add RPC/WS ports to server_info (#4427)

Enhance the /crawl endpoint by publishing WebSocket/RPC ports in the
server_info response. The function processing requests to the /crawl
endpoint actually calls server_info internally, so this change enables a
server to advertise its WebSocket/RPC port(s) to peers via the /crawl
endpoint. `grpc` and `peer` ports are included as well.

The new `ports` array contains objects, each containing a `port` for the
listening port (number string), and an array `protocol` listing the
supported protocol(s).

This allows crawlers to build a richer topology without needing to
port-scan nodes. For non-admin users (including peers), the info about
*admin* ports is excluded.

Also increase test coverage for RPC ServerInfo.

Fix #2837.
---
 src/ripple/app/main/Application.cpp |  7 ++++
 src/ripple/app/main/Application.h   |  3 ++
 src/ripple/app/misc/NetworkOPs.cpp  | 47 ++++++++++++++++++++++
 src/ripple/protocol/jss.h           |  5 ++-
 src/test/rpc/ServerInfo_test.cpp    | 61 ++++++++++++++++++++++++++++-
 5 files changed, 120 insertions(+), 3 deletions(-)

diff --git a/src/ripple/app/main/Application.cpp b/src/ripple/app/main/Application.cpp
index 16781ac09d4..8ed328df440 100644
--- a/src/ripple/app/main/Application.cpp
+++ b/src/ripple/app/main/Application.cpp
@@ -602,6 +602,13 @@ class ApplicationImp : public Application, public BasicApp
         return *m_networkOPs;
     }
 
+    virtual ServerHandlerImp&
+    getServerHandler() override
+    {
+        assert(serverHandler_);
+        return *serverHandler_;
+    }
+
     boost::asio::io_service&
     getIOService() override
     {
diff --git a/src/ripple/app/main/Application.h b/src/ripple/app/main/Application.h
index d8cb7d31815..d2ba8f7cc75 100644
--- a/src/ripple/app/main/Application.h
+++ b/src/ripple/app/main/Application.h
@@ -89,6 +89,7 @@ class Overlay;
 class PathRequests;
 class PendingSaves;
 class PublicKey;
+class ServerHandlerImp;
 class SecretKey;
 class STLedgerEntry;
 class TimeKeeper;
@@ -231,6 +232,8 @@ class Application : public beast::PropertyStream::Source
     getOPs() = 0;
     virtual OrderBookDB&
     getOrderBookDB() = 0;
+    virtual ServerHandlerImp&
+    getServerHandler() = 0;
     virtual TransactionMaster&
     getMasterTransaction() = 0;
     virtual perf::PerfLog&
diff --git a/src/ripple/app/misc/NetworkOPs.cpp b/src/ripple/app/misc/NetworkOPs.cpp
index 6be11c7dd6c..6f51f811055 100644
--- a/src/ripple/app/misc/NetworkOPs.cpp
+++ b/src/ripple/app/misc/NetworkOPs.cpp
@@ -65,9 +65,11 @@
 #include <ripple/rpc/BookChanges.h>
 #include <ripple/rpc/DeliveredAmount.h>
 #include <ripple/rpc/impl/RPCHelpers.h>
+#include <ripple/rpc/impl/ServerHandlerImp.h>
 #include <boost/asio/ip/host_name.hpp>
 #include <boost/asio/steady_timer.hpp>
 
+#include <algorithm>
 #include <mutex>
 #include <string>
 #include <tuple>
@@ -2661,6 +2663,51 @@ NetworkOPsImp::getServerInfo(bool human, bool admin, bool counters)
         info["reporting"] = app_.getReportingETL().getInfo();
     }
 
+    // This array must be sorted in increasing order.
+    static constexpr std::array<std::string_view, 7> protocols{
+        "http", "https", "peer", "ws", "ws2", "wss", "wss2"};
+    static_assert(std::is_sorted(std::begin(protocols), std::end(protocols)));
+    {
+        Json::Value ports{Json::arrayValue};
+        for (auto const& port : app_.getServerHandler().setup().ports)
+        {
+            // Don't publish admin ports for non-admin users
+            if (!admin &&
+                !(port.admin_nets_v4.empty() && port.admin_nets_v6.empty() &&
+                  port.admin_user.empty() && port.admin_password.empty()))
+                continue;
+            std::vector<std::string> proto;
+            std::set_intersection(
+                std::begin(port.protocol),
+                std::end(port.protocol),
+                std::begin(protocols),
+                std::end(protocols),
+                std::back_inserter(proto));
+            if (!proto.empty())
+            {
+                auto& jv = ports.append(Json::Value(Json::objectValue));
+                jv[jss::port] = std::to_string(port.port);
+                jv[jss::protocol] = Json::Value{Json::arrayValue};
+                for (auto const& p : proto)
+                    jv[jss::protocol].append(p);
+            }
+        }
+
+        if (app_.config().exists("port_grpc"))
+        {
+            auto const& grpcSection = app_.config().section("port_grpc");
+            auto const optPort = grpcSection.get("port");
+            if (optPort && grpcSection.get("ip"))
+            {
+                auto& jv = ports.append(Json::Value(Json::objectValue));
+                jv[jss::port] = *optPort;
+                jv[jss::protocol] = Json::Value{Json::arrayValue};
+                jv[jss::protocol].append("grpc");
+            }
+        }
+        info[jss::ports] = std::move(ports);
+    }
+
     return info;
 }
 
diff --git a/src/ripple/protocol/jss.h b/src/ripple/protocol/jss.h
index 92d9096da92..8fa1b6cc24c 100644
--- a/src/ripple/protocol/jss.h
+++ b/src/ripple/protocol/jss.h
@@ -465,13 +465,14 @@ JSS(peers);                       // out: InboundLedger, handlers/Peers, Overlay
 JSS(peer_disconnects);            // Severed peer connection counter.
 JSS(peer_disconnects_resources);  // Severed peer connections because of
                                   // excess resource consumption.
-JSS(port);                        // in: Connect
+JSS(port);                        // in: Connect, out: NetworkOPs
+JSS(ports);                       // out: NetworkOPs
 JSS(previous);                    // out: Reservations
 JSS(previous_ledger);             // out: LedgerPropose
 JSS(proof);                       // in: BookOffers
 JSS(propose_seq);                 // out: LedgerPropose
 JSS(proposers);                   // out: NetworkOPs, LedgerConsensus
-JSS(protocol);                    // out: PeerImp
+JSS(protocol);                    // out: NetworkOPs, PeerImp
 JSS(proxied);                     // out: RPC ping
 JSS(pubkey_node);                 // out: NetworkOPs
 JSS(pubkey_publisher);            // out: ValidatorList
diff --git a/src/test/rpc/ServerInfo_test.cpp b/src/test/rpc/ServerInfo_test.cpp
index 24cfd12299a..a69483cb130 100644
--- a/src/test/rpc/ServerInfo_test.cpp
+++ b/src/test/rpc/ServerInfo_test.cpp
@@ -17,6 +17,7 @@
 */
 //==============================================================================
 
+#include <ripple/app/misc/NetworkOPs.h>
 #include <ripple/beast/unit_test.h>
 #include <ripple/protocol/jss.h>
 #include <test/jtx.h>
@@ -55,6 +56,16 @@ class ServerInfo_test : public beast::unit_test::suite
 
 [validators]
 %2%
+
+[port_grpc]
+ip = 0.0.0.0
+port = 50051
+
+[port_admin]
+ip = 0.0.0.0
+port = 50052
+protocol = wss2
+admin = 127.0.0.1
 )rippleConfig");
 
         p->loadFromString(boost::str(
@@ -77,8 +88,30 @@ class ServerInfo_test : public beast::unit_test::suite
             BEAST_EXPECT(result[jss::result][jss::status] == "success");
             BEAST_EXPECT(result[jss::result].isMember(jss::info));
         }
+
         {
-            Env env(*this, makeValidatorConfig());
+            Env env(*this);
+
+            // Call NetworkOPs directly and set the admin flag to false.
+            // Expect that the admin ports are not included in the result.
+            auto const result =
+                env.app().getOPs().getServerInfo(true, false, 0);
+            auto const& ports = result[jss::ports];
+            BEAST_EXPECT(ports.isArray() && ports.size() == 0);
+        }
+
+        {
+            auto config = makeValidatorConfig();
+            auto const rpc_port =
+                (*config)["port_rpc"].get<unsigned int>("port");
+            auto const grpc_port =
+                (*config)["port_grpc"].get<unsigned int>("port");
+            auto const ws_port = (*config)["port_ws"].get<unsigned int>("port");
+            BEAST_EXPECT(grpc_port);
+            BEAST_EXPECT(rpc_port);
+            BEAST_EXPECT(ws_port);
+
+            Env env(*this, std::move(config));
             auto const result = env.rpc("server_info");
             BEAST_EXPECT(!result[jss::result].isMember(jss::error));
             BEAST_EXPECT(result[jss::result][jss::status] == "success");
@@ -86,6 +119,32 @@ class ServerInfo_test : public beast::unit_test::suite
             BEAST_EXPECT(
                 result[jss::result][jss::info][jss::pubkey_validator] ==
                 validator_data::public_key);
+
+            auto const& ports = result[jss::result][jss::info][jss::ports];
+            BEAST_EXPECT(ports.isArray() && ports.size() == 3);
+            for (auto const& port : ports)
+            {
+                auto const& proto = port[jss::protocol];
+                BEAST_EXPECT(proto.isArray());
+                auto const p = port[jss::port].asUInt();
+                BEAST_EXPECT(p == rpc_port || p == ws_port || p == grpc_port);
+                if (p == grpc_port)
+                {
+                    BEAST_EXPECT(proto.size() == 1);
+                    BEAST_EXPECT(proto[0u].asString() == "grpc");
+                }
+                if (p == rpc_port)
+                {
+                    BEAST_EXPECT(proto.size() == 2);
+                    BEAST_EXPECT(proto[0u].asString() == "http");
+                    BEAST_EXPECT(proto[1u].asString() == "ws2");
+                }
+                if (p == ws_port)
+                {
+                    BEAST_EXPECT(proto.size() == 1);
+                    BEAST_EXPECT(proto[0u].asString() == "ws");
+                }
+            }
         }
     }