001package org.consensusj.jsonrpc.cli;
002
003import com.fasterxml.jackson.databind.JsonNode;
004import com.fasterxml.jackson.databind.ObjectMapper;
005import com.fasterxml.jackson.databind.node.TextNode;
006import org.apache.commons.cli.CommandLine;
007import org.apache.commons.cli.CommandLineParser;
008import org.apache.commons.cli.DefaultParser;
009import org.apache.commons.cli.HelpFormatter;
010import org.apache.commons.cli.Options;
011import org.apache.commons.cli.ParseException;
012import org.consensusj.jsonrpc.DefaultRpcClient;
013import org.consensusj.jsonrpc.CompositeTrustManager;
014import org.consensusj.jsonrpc.JsonRpcClientJavaNet;
015import org.consensusj.jsonrpc.JsonRpcMessage;
016import org.consensusj.jsonrpc.JsonRpcRequest;
017import org.consensusj.jsonrpc.JsonRpcResponse;
018import org.slf4j.Logger;
019import org.slf4j.LoggerFactory;
020
021import javax.net.ssl.SSLContext;
022import java.io.FileNotFoundException;
023import java.io.IOException;
024import java.io.PrintWriter;
025import java.net.URI;
026import java.net.URISyntaxException;
027import java.nio.file.Path;
028import java.security.KeyManagementException;
029import java.security.KeyStoreException;
030import java.security.NoSuchAlgorithmException;
031import java.security.cert.CertificateException;
032import java.util.List;
033import java.util.concurrent.ExecutionException;
034import java.util.logging.Level;
035
036/**
037 * An abstract base class for JsonRpcClientTool that uses Apache Commons CLI
038 */
039public abstract class BaseJsonRpcTool implements JsonRpcClientTool {
040    private static final Logger log = LoggerFactory.getLogger(BaseJsonRpcTool.class);
041    private static final String name = "jsonrpc";
042    protected static final URI defaultUri = URI.create("http://localhost:8080/");
043    protected final String usage ="usage string";
044    protected final HelpFormatter formatter = new HelpFormatter();
045    protected JsonRpcMessage.Version jsonRpcVersion = JsonRpcMessage.Version.V2;
046    protected JsonRpcClientTool.OutputObject outputObject = OutputObject.RESULT;
047    //protected JsonRpcClientTool.OutputFormat outputFormat = OutputFormat.JSON;
048    protected final JsonRpcClientTool.OutputStyle outputStyle = OutputStyle.PRETTY;
049
050    public BaseJsonRpcTool() {
051        formatter.setLongOptPrefix("-");
052    }
053
054    @Override
055    public String name() {
056        return name;
057    }
058
059    public String usage() {
060        return usage;
061    }
062
063    abstract public Options options();
064
065    @Override
066    public CommonsCLICall createCall(PrintWriter out, PrintWriter err, String... args) {
067        return new CommonsCLICall(this, out, err, args);
068    }
069
070    @Override
071    public void run(Call call) {
072        run((CommonsCLICall) call);
073    }
074    
075    public void run(CommonsCLICall call) {
076        List<String> args = call.line.getArgList();
077        if (args.isEmpty()) {
078            printError(call, "jsonrpc method required");
079            printHelp(call, usage);
080            throw new ToolException(1, "jsonrpc method required");
081        }
082        if (call.line.hasOption("response")) {
083            // Print full JsonRpcResponse as output
084            outputObject = OutputObject.RESPONSE;
085        }
086        if (call.line.hasOption("V1")) {
087            jsonRpcVersion = JsonRpcMessage.Version.V1;
088        }
089        SSLContext sslContext = sslContext(call.line);
090        DefaultRpcClient client = call.rpcClient(sslContext);
091        CliParameterParser parser = new CliParameterParser(jsonRpcVersion, client.getMapper());
092        JsonRpcRequest request = parser.parse(args);
093        JsonRpcResponse<JsonNode> response;
094        try {
095            response = client.sendRequestForResponseAsync(request).get();
096        } catch (ExecutionException ee) {
097            log.error("send execution exception: ", ee);
098            Throwable t = ee.getCause() != null ? ee.getCause(): ee;
099            throw new ToolException(1, t.getMessage());
100        } catch (InterruptedException e) {
101            log.error("send interrupted exception: ", e);
102            throw new ToolException(1, e.getMessage());
103        }
104        String resultForPrinting = formatResponse(response, client.getMapper());
105        call.out.println(resultForPrinting);
106    }
107
108    SSLContext sslContext(CommandLine line) {
109        SSLContext sslContext;
110        if (line.hasOption("add-truststore")) {
111            // Create SSL sockets using additional truststore and CompositeTrustManager
112            String trustStorePathString = line.getOptionValue("add-truststore");
113            Path trustStorePath = Path.of(trustStorePathString);
114            try {
115                sslContext = CompositeTrustManager.getCompositeSSLContext(trustStorePath);
116            } catch (NoSuchAlgorithmException | KeyManagementException | FileNotFoundException e) {
117                throw new ToolException(1, e.getMessage());
118            }
119        } else if (line.hasOption("alt-truststore")) {
120            // Create SSL sockets using alternate truststore
121            String trustStorePathString = line.getOptionValue("alt-truststore");
122            Path trustStorePath = Path.of(trustStorePathString);
123            try {
124                sslContext = CompositeTrustManager.getAlternateSSLContext(trustStorePath);
125            } catch (NoSuchAlgorithmException | KeyManagementException | CertificateException | KeyStoreException | IOException e) {
126                throw new ToolException(1, e.getMessage());
127            }
128        } else {
129            // Otherwise, use the default SSLContext
130            try {
131                sslContext = SSLContext.getDefault();
132            } catch (NoSuchAlgorithmException e) {
133                throw new RuntimeException(e);
134            }
135        }
136        return sslContext;
137    }
138
139    private String formatResponse(JsonRpcResponse<?> response, ObjectMapper mapper) {
140        return (response.getResult() == null || response.getError() != null || outputObject == OutputObject.RESPONSE)
141            ? mapper.valueToTree(response).toPrettyString() // Pretty print the entire response as JSON
142            : switch (response.getResult()) {
143                case TextNode tn    -> tn.asText();         // Remove the surrounding quotes and don't print `\n` for newlines
144                case JsonNode jn    -> outputStyle == OutputStyle.PRETTY ? jn.toPrettyString() : jn.toString();
145                case Object obj     -> obj.toString();
146            };
147    }
148
149    public void printHelp(Call call, String usage) {
150        int leftPad = 4;
151        int descPad = 2;
152        int helpWidth = 120;
153        String header = "";
154        String footer = "";
155        formatter.printHelp(call.err, helpWidth, usage, header, options(), leftPad, descPad, footer, false);
156    }
157
158    public void printError(Call call, String str) {
159        call.err.println(str);
160    }
161
162    public static class CommonsCLICall extends JsonRpcClientTool.Call {
163        protected final BaseJsonRpcTool rpcTool;
164        public final CommandLine line;
165        public final boolean verbose;
166        private DefaultRpcClient client;
167
168        public CommonsCLICall(BaseJsonRpcTool parentTool, PrintWriter out, PrintWriter err, String[] args) {
169            super(out, err, args);
170            this.rpcTool = parentTool;
171            CommandLineParser parser = new DefaultParser();
172            try {
173                this.line = parser.parse(rpcTool.options(), args);
174            } catch (ParseException e) {
175                rpcTool.printError(this, e.getMessage());
176                rpcTool.printHelp(this, rpcTool.usage());
177                throw new JsonRpcClientTool.ToolException(1, "Parser error");
178            }
179            if (line.hasOption("?")) {
180                rpcTool.printHelp(this, rpcTool.usage());
181                throw new JsonRpcClientTool.ToolException(0, "Help Option was chosen");
182            }
183            verbose = line.hasOption("v");
184            if (verbose) {
185                JavaLoggingSupport.setVerbose();
186            }
187            boolean hasLogLevel = line.hasOption("log");
188            if (hasLogLevel ){
189                String intLevel = line.getOptionValue("log");
190                Level level = switch (intLevel) {
191                    case "0" -> Level.OFF;
192                    case "1" -> Level.SEVERE;
193                    case "2" -> Level.WARNING;
194                    case "3" -> Level.INFO;
195                    case "4" -> Level.FINE;
196                    case "5" -> Level.ALL;
197                    default -> throw new IllegalStateException("Unexpected value: " + intLevel);
198                };
199                JavaLoggingSupport.setLogLevel(level);
200            }
201            // TODO: Add rpcwait option for non-Bitcoin JsonRPC???
202        }
203
204        @Override
205        public DefaultRpcClient rpcClient(SSLContext sslContext) {
206            if (client == null) {
207                URI uri;
208                String urlString;
209                if ((urlString = line.getOptionValue("url")) != null ) {
210                    try {
211                        uri = new URI(urlString);
212                    } catch (URISyntaxException e) {
213                        throw new RuntimeException(e);
214                    }
215                } else {
216                    uri = defaultUri;
217                }
218                String rpcUser = null;
219                String rpcPassword = null;
220                String rawUserInfo = uri.getRawUserInfo();
221                if (rawUserInfo != null) {
222                    String[] split = rawUserInfo.split(":");
223                    rpcUser = split[0];
224                    rpcPassword = split[1];
225                }
226                client = new DefaultRpcClient(sslContext, rpcTool.jsonRpcVersion, uri, rpcUser, rpcPassword);
227            }
228            return client;
229        }
230
231        @Override
232        public DefaultRpcClient rpcClient() {
233            SSLContext sslContext;
234            try {
235                sslContext = SSLContext.getDefault();
236            } catch (NoSuchAlgorithmException e) {
237                throw new RuntimeException(e);
238            }
239            return rpcClient(sslContext);
240        }
241    }
242}