diff --git a/convex-cli/src/main/java/convex/cli/ACommand.java b/convex-cli/src/main/java/convex/cli/ACommand.java index 6fb410623..d0384bc62 100644 --- a/convex-cli/src/main/java/convex/cli/ACommand.java +++ b/convex-cli/src/main/java/convex/cli/ACommand.java @@ -22,7 +22,11 @@ public abstract class ACommand implements Runnable { public abstract Main cli(); public void showUsage() { - CommandLine.usage(this, System.out); + CommandLine cl=new CommandLine(this); + cl.setUsageHelpAutoWidth(true); + cl.setUsageHelpWidth(100); + cl.setUsageHelpLongOptionsMaxWidth(40); + cl.usage(System.out); } public CommandLine commandLine() { @@ -156,5 +160,10 @@ public void inform(String message) { if (isColoured()) message=Coloured.yellow(message); inform(1,message); } + + public void informWarning(String message) { + if (isColoured()) message=Coloured.orange(message); + inform(1,message); + } } diff --git a/convex-cli/src/main/java/convex/cli/Main.java b/convex-cli/src/main/java/convex/cli/Main.java index fc01f5bf3..fe2585897 100644 --- a/convex-cli/src/main/java/convex/cli/Main.java +++ b/convex-cli/src/main/java/convex/cli/Main.java @@ -104,9 +104,6 @@ public static void main(String[] args) { */ public int mainExecute(String[] args) { try { - // commandLine - // .setUsageHelpLongOptionsMaxWidth(80) - // .setUsageHelpWidth(40 * 4); // do a pre-parse to get the config filename. We need to load // in the defaults before running the full execute @@ -119,7 +116,7 @@ public int mainExecute(String[] args) { } if (commandLine.isUsageHelpRequested()) { - commandLine.usage(commandLine.getOut()); + showUsage(); return ExitCodes.SUCCESS; } else if (commandLine.isVersionHelpRequested()) { commandLine.printVersionHelp(commandLine.getOut()); diff --git a/convex-cli/src/main/java/convex/cli/client/Query.java b/convex-cli/src/main/java/convex/cli/client/Query.java index ad8cee257..13ffe5cb0 100644 --- a/convex-cli/src/main/java/convex/cli/client/Query.java +++ b/convex-cli/src/main/java/convex/cli/client/Query.java @@ -23,7 +23,9 @@ description="Execute user queries. ") public class Query extends AClientCommand { - @Parameters(paramLabel="queryCommand", description="Query command(s). Multiple commands will be executed in sequence unless one fails") + @Parameters( + paramLabel="queryCommand", + description="Query command(s). Multiple commands will be executed in sequence unless one fails") private String[] commands; @Override diff --git a/convex-cli/src/main/java/convex/cli/key/Key.java b/convex-cli/src/main/java/convex/cli/key/Key.java index 9bfe88cbf..ad3ad7b61 100644 --- a/convex-cli/src/main/java/convex/cli/key/Key.java +++ b/convex-cli/src/main/java/convex/cli/key/Key.java @@ -17,6 +17,7 @@ KeyGenerate.class, KeyList.class, KeyExport.class, + KeyDelete.class, CommandLine.HelpCommand.class }, mixinStandardHelpOptions=false, diff --git a/convex-cli/src/main/java/convex/cli/key/KeyDelete.java b/convex-cli/src/main/java/convex/cli/key/KeyDelete.java new file mode 100644 index 000000000..725fcbd3b --- /dev/null +++ b/convex-cli/src/main/java/convex/cli/key/KeyDelete.java @@ -0,0 +1,100 @@ +package convex.cli.key; + +import java.security.KeyStore; +import java.security.KeyStoreException; +import java.util.Enumeration; +import java.util.HashSet; + +import convex.cli.CLIError; +import convex.cli.ExitCodes; +import picocli.CommandLine.Command; +import picocli.CommandLine.Parameters; + +@Command(name="delete", + mixinStandardHelpOptions=false, + description="Delete key(s) from the keystore. Use with caution") +public class KeyDelete extends AKeyCommand { + + static final String WILD="+"; + + @Parameters( + paramLabel="keys", + description="Key(s) to delete. Should be a hex prefix of a specific key, or trailing '"+WILD+"' as a wildcard") + private String[] keys; + + protected void deleteEntry(String alias) throws KeyStoreException { + storeMixin.getKeystore().deleteEntry(alias); + inform(2,"Deleting Key: "+alias); + } + + @Override + public void run() { + + if ((keys==null)||(keys.length==0)) { + showUsage(); + return; + } + + KeyStore keyStore = storeMixin.loadKeyStore(); + if (keyStore==null) throw new CLIError("Keystore does not exist. Specify a valid --keystore or use `convex key gen` to create one."); + + HashSet toDelete=new HashSet<>(); + + for (String argKey : keys) { + String key=argKey.trim(); + if (key.startsWith("0x")) key=key.substring(2); + if (key.length()==0) throw new CLIError(ExitCodes.DATAERR,"Empty key specified?"); + + // If a trailing wildcard was used, strip it and set wildcard flag + boolean wild=key.endsWith(WILD); + if (wild) { + key=key.substring(0,key.length()-WILD.length()); + } + + inform(3,"Looking for keys to delete like: "+key); + + String found=null; + Enumeration aliases; + try { + aliases = keyStore.aliases(); + while (aliases.hasMoreElements()) { + String alias = aliases.nextElement(); + if (alias.startsWith(key)) { + if (wild) { + toDelete.add(alias); + } else { + if (found!=null) { + throw new CLIError("Duplicate keys found for prefix: "+argKey); + } + found=alias; + } + } + } + } catch (KeyStoreException e) { + throw new CLIError("Unexpected error reading keystore",e); + } + + // check if we can remove specific key + if (found!=null) { + toDelete.add(found); + } + } + + if (toDelete.isEmpty()) { + informWarning("No matching keys found"); + } else { + for (String s: toDelete) { + try { + deleteEntry(s); + } catch (KeyStoreException e) { + throw new CLIError("Unable to remove key: "+s,e); + } + } + } + + storeMixin.saveKeyStore(); + + } + + +} diff --git a/convex-cli/src/main/java/convex/cli/key/KeyExport.java b/convex-cli/src/main/java/convex/cli/key/KeyExport.java index 4eef82b46..79211866c 100644 --- a/convex-cli/src/main/java/convex/cli/key/KeyExport.java +++ b/convex-cli/src/main/java/convex/cli/key/KeyExport.java @@ -25,7 +25,7 @@ */ @Command(name="export", mixinStandardHelpOptions=false, - description="Export a private key from the keystore. Uee with caution") + description="Export a private key from the keystore. Use with caution.") public class KeyExport extends AKeyCommand { private static final Logger log = LoggerFactory.getLogger(KeyExport.class); diff --git a/convex-cli/src/main/java/convex/cli/local/Local.java b/convex-cli/src/main/java/convex/cli/local/Local.java index 42b94619f..a9deb7496 100644 --- a/convex-cli/src/main/java/convex/cli/local/Local.java +++ b/convex-cli/src/main/java/convex/cli/local/Local.java @@ -21,11 +21,11 @@ CommandLine.HelpCommand.class }, mixinStandardHelpOptions=true, - description="Operate a local Convex network and related utilities.") + description="Operate a local Convex network and related utilities. Primarily useful for development / testing.") public class Local extends ATopCommand { @Override public void run() { - + showUsage(); } } diff --git a/convex-cli/src/main/java/convex/cli/mixins/KeyMixin.java b/convex-cli/src/main/java/convex/cli/mixins/KeyMixin.java index 904cad4ba..5e039b5c8 100644 --- a/convex-cli/src/main/java/convex/cli/mixins/KeyMixin.java +++ b/convex-cli/src/main/java/convex/cli/mixins/KeyMixin.java @@ -11,13 +11,13 @@ public class KeyMixin extends AMixin { @Option(names = { "-k","--key" }, defaultValue = "${env:CONVEX_KEY}", scope = ScopeType.INHERIT, - description = "Key pair to use. Specifiy with a hex prefix of a public key / alias. Can specify with CONVEX_KEY environment variable.") + description = "Key pair to use from keystore. Supports a hex prefix. Can specify with CONVEX_KEY.") protected String publicKey; @Option(names = { "-p","--keypass" }, defaultValue = "${env:CONVEX_KEY_PASSWORD}", scope = ScopeType.INHERIT, - description = "Key pair password in key store. Can also specify with CONVEX_KEY_PASSWORD environment variable.") + description = "Key pair password in keystore. Can specify with CONVEX_KEY_PASSWORD.") protected String keyPassword; public String getPublicKey() { diff --git a/convex-cli/src/main/java/convex/cli/mixins/PeerKeyMixin.java b/convex-cli/src/main/java/convex/cli/mixins/PeerKeyMixin.java index 87baa6606..ef392a4c1 100644 --- a/convex-cli/src/main/java/convex/cli/mixins/PeerKeyMixin.java +++ b/convex-cli/src/main/java/convex/cli/mixins/PeerKeyMixin.java @@ -11,14 +11,14 @@ public class PeerKeyMixin extends AMixin { @Option(names = { "--peer-key" }, defaultValue = "${env:CONVEX_PEER_KEY}", scope = ScopeType.INHERIT, - description = "Peer Key pair. Specifiy with a hex prefix of a public key. "+ - "Can ALSO specify with CONVEX_PEER_KEY environment variable.") + description = "Peer key. Allows a hex prefix of a public key in keystore. "+ + "Can also specify with CONVEX_PEER_KEY.") protected String publicKey; @Option(names = { "--peer-keypass" }, defaultValue = "${env:CONVEX_PEER_KEY_PASSWORD}", scope = ScopeType.INHERIT, - description = "Peer Key pair password in key store. Can also specify with CONVEX_PEER_KEY_PASSWORD environment variable.") + description = "Peer key password in keystore. Can also specify with CONVEX_PEER_KEY_PASSWORD.") protected String keyPassword; /** diff --git a/convex-cli/src/main/java/convex/cli/mixins/RemotePeerMixin.java b/convex-cli/src/main/java/convex/cli/mixins/RemotePeerMixin.java index 21e51e0ca..2189c077b 100644 --- a/convex-cli/src/main/java/convex/cli/mixins/RemotePeerMixin.java +++ b/convex-cli/src/main/java/convex/cli/mixins/RemotePeerMixin.java @@ -13,13 +13,13 @@ public class RemotePeerMixin extends AMixin { @Option(names={"--port"}, - defaultValue="${env:CONVEX_PORT:-"+Constants.DEFAULT_PEER_PORT+"", - description="Port number to connect to a peer.") + defaultValue="${env:CONVEX_PORT:-"+Constants.DEFAULT_PEER_PORT+"}", + description="Port number to connect to a peer. Defaulting to: ${DEFAULT-VALUE}") private Integer port; @Option(names={"--host"}, - defaultValue=Constants.HOSTNAME_PEER, - description="Hostname for remote peer connection. Default: ${DEFAULT-VALUE}") + defaultValue="${env:CONVEX_HOST:-"+Constants.HOSTNAME_PEER+"}", + description="Hostname for remote peer connection. Defaulting to: ${DEFAULT-VALUE}") private String hostname; /** diff --git a/convex-cli/src/main/java/convex/cli/output/Coloured.java b/convex-cli/src/main/java/convex/cli/output/Coloured.java index 5c47d2e36..06c42e2ee 100644 --- a/convex-cli/src/main/java/convex/cli/output/Coloured.java +++ b/convex-cli/src/main/java/convex/cli/output/Coloured.java @@ -19,4 +19,8 @@ public static String blue(String text) { public static String yellow(String text) { return Ansi.ON.string("@|fg(226) "+text+"|@"); } + + public static String orange(String text) { + return Ansi.ON.string("@|fg(172) "+text+"|@"); + } } diff --git a/convex-cli/src/main/java/convex/cli/peer/APeerCommand.java b/convex-cli/src/main/java/convex/cli/peer/APeerCommand.java index 1f49c2a66..e3098bc4c 100644 --- a/convex-cli/src/main/java/convex/cli/peer/APeerCommand.java +++ b/convex-cli/src/main/java/convex/cli/peer/APeerCommand.java @@ -41,7 +41,7 @@ public EtchStore getEtchStore() { } /** - * Get the keypair for the peer + * Get the keypair for the peer. Always returns a valid key pair, may generate one. */ protected AKeyPair ensurePeerKey() { String peerPublicKey=peerKeyMixin.getPublicKey(); @@ -52,7 +52,7 @@ protected AKeyPair ensurePeerKey() { boolean shouldGenerate=question("No --peer-key specified. Generate one? (y/n)"); if (shouldGenerate) { AKeyPair kp=AKeyPair.generate(); - inform(2,"Generated peer key: "+kp.getAccountKey()); + inform(2,"Generated peer key: "+kp.getAccountKey().toChecksumHex()); char[] keyPass=peerKeyMixin.getKeyPassword(); storeMixin.addKeyPairToStore(kp, keyPass); storeMixin.saveKeyStore(); diff --git a/convex-cli/src/main/java/convex/cli/peer/Peer.java b/convex-cli/src/main/java/convex/cli/peer/Peer.java index 753c8a96e..924b864ac 100644 --- a/convex-cli/src/main/java/convex/cli/peer/Peer.java +++ b/convex-cli/src/main/java/convex/cli/peer/Peer.java @@ -40,8 +40,7 @@ public class Peer extends ATopCommand { @Override public void run() { - // sub command run with no command provided - CommandLine.usage(new Peer(), System.out); + showUsage(); } diff --git a/convex-cli/src/test/java/convex/cli/key/KeyTest.java b/convex-cli/src/test/java/convex/cli/key/KeyTest.java index 90a5f79d3..9a6f26cbb 100644 --- a/convex-cli/src/test/java/convex/cli/key/KeyTest.java +++ b/convex-cli/src/test/java/convex/cli/key/KeyTest.java @@ -4,6 +4,7 @@ import java.io.IOException; import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertNotEquals; import static org.junit.jupiter.api.Assertions.assertTrue; import org.junit.jupiter.api.Test; @@ -52,10 +53,20 @@ public void testKeyGenerateAndUse() throws IOException { // command key.list tester = CLTester.run("key", "list", "--storepass", KEYSTORE_PASSWORD, "--keystore", fileName); tester.assertExitCode(ExitCodes.SUCCESS); + assertTrue(tester.getOutput().contains(key)); // command key.list with non-existant keystore tester = CLTester.run("key", "list", "--storepass", KEYSTORE_PASSWORD, "--keystore","bad-keystore.pfx"); assertNotEquals(ExitCodes.SUCCESS,tester.getResult()); + // command key.list + tester = CLTester.run("key", "delete", "--storepass", KEYSTORE_PASSWORD, "--keystore", fileName, "+"); + tester.assertExitCode(ExitCodes.SUCCESS); + + // command key.list + tester = CLTester.run("key", "list", "-v0", "--storepass", KEYSTORE_PASSWORD, "--keystore", fileName); + tester.assertExitCode(ExitCodes.SUCCESS); + assertFalse(tester.getOutput().contains(key)); + } }