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}