eventTypes);
/**
- * @param listener the listener to remove
+ * @param listener The listener to remove.
*/
void removeEventListener(AtEventListener listener);
diff --git a/at_client/src/main/java/org/atsign/client/api/AtKeyNames.java b/at_client/src/main/java/org/atsign/client/api/AtKeyNames.java
index eee0d3ab..4a1997d7 100644
--- a/at_client/src/main/java/org/atsign/client/api/AtKeyNames.java
+++ b/at_client/src/main/java/org/atsign/client/api/AtKeyNames.java
@@ -1,9 +1,7 @@
package org.atsign.client.api;
-import org.atsign.common.AtSign;
-
/**
- * Constants and Utility methods for "well known" standard keys
+ * Constants and utility methods for "well known" keys.
*/
public class AtKeyNames {
@@ -54,4 +52,9 @@ public static String toSharedWithMeKeyName(AtSign sharedBy, AtSign sharedWith) {
return String.format("%s:%s%s", SHARED_KEY, sharedWith, sharedBy);
}
+
+ public static boolean isManagementKeyName(String s) {
+ return s.matches(".+\\.__manage@.+");
+ }
+
}
diff --git a/at_client/src/main/java/org/atsign/client/api/AtKeys.java b/at_client/src/main/java/org/atsign/client/api/AtKeys.java
index 3b5171d2..53d5aea2 100644
--- a/at_client/src/main/java/org/atsign/client/api/AtKeys.java
+++ b/at_client/src/main/java/org/atsign/client/api/AtKeys.java
@@ -7,17 +7,18 @@
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
-import org.atsign.client.util.EnrollmentId;
-
import lombok.Builder;
import lombok.Value;
+import org.atsign.client.impl.common.EnrollmentId;
/**
- * An immutable class used to hold an {@link org.atsign.client.api.AtClient}s keys.
+ * An immutable class used to hold an {@link org.atsign.client.api.AtClient}s keys. These
+ * are use for authentication and encryption.
*
* Examples:
*
*
+ *
* AtKeys keys = AtKeys.builder()
* .selfEncryptKey(generateAESKeyBase64())
* .apkamKeyPair(generateRSAKeyPair())
@@ -56,13 +57,13 @@ public class AtKeys {
* request uses this key to encrypt the {@link #selfEncryptKey} and {@link #encryptPrivateKey}
* in the response that it sends.
* The process which requested the enrollment can then decrypt and store those keys.
- * This ensures that all {@link AtKeys} got and {@link org.atsign.common.AtSign} share the same
+ * This ensures that all {@link AtKeys} got and {@link AtSign} share the same
* {@link #selfEncryptKey} and {@link #encryptPrivateKey}
*/
String apkamSymmetricKey;
/**
- * Encryption Key used to encrypt {@link org.atsign.common.Keys.SelfKey}s and the pkam and
+ * Encryption Key used to encrypt {@link Keys.SelfKey}s and the pkam and
* encryption key pairs
* when they are externalised as JSON
*/
@@ -70,16 +71,16 @@ public class AtKeys {
/**
* This is used to encrypt the symmetric keys that are used to encrypt
- * {@link org.atsign.common.Keys.SharedKey}s
- * where the shared with {@link org.atsign.common.AtSign} is this {@link org.atsign.common.AtSign}
+ * {@link Keys.SharedKey}s
+ * where the shared with {@link AtSign} is this {@link AtSign}
*/
String encryptPublicKey;
/**
* This is used to decrypt the symmetric keys that are used to encrypt
- * {@link org.atsign.common.Keys.SharedKey}s
- * where the shared with {@link org.atsign.common.AtSign} is this {@link org.atsign.common.AtSign}
+ * {@link Keys.SharedKey}s
+ * where the shared with {@link AtSign} is this {@link AtSign}
*/
String encryptPrivateKey;
@@ -129,8 +130,26 @@ public Map getCache() {
return Collections.unmodifiableMap(cache);
}
+
/**
- * Builder utility.
+ * A builder for instantiating {@link AtKeys}.
+ *
+ *
+ *
+ * AtKeys keys = AtKeys.builder()
+ * .selfEncryptKey(EncryptionUtil.generateAESKeyBase64())
+ * .apkamKeyPair(EncryptionUtil.generateRSAKeyPair())
+ * .apkamSymmetricKey(EncryptionUtil.generateAESKeyBase64())
+ * .build();
+ *
+ *
+ * NOTE {@link AtKeys} are immutable, so use the toBuilder() method
+ * to create a modified instance.
+ *
+ *
+ *
+ * AtKeys newKeys = keys.toBuilder().enrollmentId(enrollmentId).build();
+ *
*/
public static class AtKeysBuilder {
diff --git a/at_client/src/main/java/org/atsign/common/AtSign.java b/at_client/src/main/java/org/atsign/client/api/AtSign.java
similarity index 69%
rename from at_client/src/main/java/org/atsign/common/AtSign.java
rename to at_client/src/main/java/org/atsign/client/api/AtSign.java
index 2a3b1eea..101b54a5 100644
--- a/at_client/src/main/java/org/atsign/common/AtSign.java
+++ b/at_client/src/main/java/org/atsign/client/api/AtSign.java
@@ -1,7 +1,7 @@
-package org.atsign.common;
+package org.atsign.client.api;
-import org.atsign.client.util.Preconditions;
-import org.atsign.client.util.TypedString;
+import org.atsign.client.impl.common.Preconditions;
+import org.atsign.client.impl.common.TypedString;
/**
@@ -9,7 +9,7 @@
*/
public class AtSign extends TypedString {
- public AtSign(String s) {
+ private AtSign(String s) {
super(formatAtSign(s));
}
@@ -18,10 +18,10 @@ public String withoutPrefix() {
}
/**
- * Factory method
+ * A factory method for instantiating {@link AtSign} instances.
*
* @param s the string representation of the Atsign (can be with our without @ prefix)
- * @return null is s is null or blank, otherwise the corresponding {@link AtSign} for s
+ * @return null if param is null or blank, otherwise the corresponding {@link AtSign} for the param
*/
public static AtSign createAtSign(String s) {
return s != null && !s.isBlank() ? new AtSign(s) : null;
diff --git a/at_client/src/main/java/org/atsign/common/Keys.java b/at_client/src/main/java/org/atsign/client/api/Keys.java
similarity index 85%
rename from at_client/src/main/java/org/atsign/common/Keys.java
rename to at_client/src/main/java/org/atsign/client/api/Keys.java
index 1c88d0d7..635cb35b 100644
--- a/at_client/src/main/java/org/atsign/common/Keys.java
+++ b/at_client/src/main/java/org/atsign/client/api/Keys.java
@@ -1,9 +1,9 @@
-package org.atsign.common;
+package org.atsign.client.api;
-import static org.atsign.client.util.Preconditions.*;
-import static org.atsign.common.AtSign.createAtSign;
-import static org.atsign.common.Metadata.*;
+import static org.atsign.client.api.AtSign.createAtSign;
+import static org.atsign.client.api.Metadata.*;
+import static org.atsign.client.impl.common.Preconditions.*;
import java.util.List;
import java.util.concurrent.atomic.AtomicReference;
@@ -121,13 +121,15 @@ public String namespace() {
}
/**
- *
* @return the name without the namespace component.
*/
public String nameWithoutNamespace() {
return stripNamespace(name);
}
+ /**
+ * @return the rawKey.
+ */
@Override
public String toString() {
return rawKey;
@@ -146,7 +148,7 @@ protected String createRawKey() {
}
/**
- * Represents a "public key" in the Atsign Platform
+ * Models a "public key" in the Atsign Platform specification.
*/
public static class PublicKey extends AtKey {
protected PublicKey(AtSign sharedBy, String name, Metadata metadata) {
@@ -176,7 +178,29 @@ public static PublicKey publicKey(AtSign sharedBy, String name, String namespace
}
/**
- * Represents a "self key" in the Atsign Platform
+ * A builder for instantiating {@link Keys.PublicKey} instances.
+ *
+ *
+
+ * Keys.PublicKey key = Keys.publicKeyBuilder()
+ * .sharedBy(...) // the AtSign which is sharing the key
+ * .name(...) // the key name
+ * .namespace(...)
+ * .ttl(...)
+ * .ttb(...)
+ * .ttr(...)
+ * .ccd(...)
+ * .isCached(...)
+ * .isBinary(...)
+ * .metadata(...)
+ * .build();
+ *
+ */
+ public static class PublicKeyBuilder {
+ }
+
+ /**
+ * Models a "self key" in the Atsign Platform specification.
*/
public static class SelfKey extends AtKey {
protected SelfKey(AtSign sharedBy, AtSign sharedWith, String name, Metadata metadata) {
@@ -207,7 +231,30 @@ public static SelfKey selfKey(AtSign sharedBy, AtSign sharedWith, String name, S
}
/**
- * Represents a "shared key" in the Atsign Platform
+ * A builder for instantiating {@link Keys.SelfKey} instances.
+ *
+ *
+
+ * Keys.SelfKey key = Keys.selfKeyBuilder()
+ * .sharedBy(...) // the AtSign which is sharing the key
+ * .sharedWith(...)
+ * .name(...) // the key name
+ * .namespace(...)
+ * .ttl(...)
+ * .ttb(...)
+ * .ttr(...)
+ * .ccd(...)
+ * .isHidden(...)
+ * .isBinary(...)
+ * .metadata(...)
+ * .build();
+ *
+ */
+ public static class SelfKeyBuilder {
+ }
+
+ /**
+ * Models a "shared key" in the Atsign Platform specification.
*/
public static class SharedKey extends AtKey {
protected SharedKey(AtSign sharedBy, AtSign sharedWith, String name, Metadata metadata) {
@@ -246,6 +293,31 @@ public static SharedKey sharedKey(AtSign sharedBy, AtSign sharedWith, String nam
return new SharedKey(sharedBy, sharedWith, toName(name, namespace), metadata);
}
+ /**
+ * A builder for instantiating {@link Keys.SharedKey} instances.
+ *
+ *
+
+ * Keys.SharedKey key = Keys.sharedKeyBuilder()
+ * .sharedBy(...) // the AtSign which is sharing the key
+ * .sharedWith(...) // the AtSign which the key is being shared with
+ * .name(...) // the key name
+ * .namespace(...)
+ * .ttl(...)
+ * .ttb(...)
+ * .ttr(...)
+ * .ccd(...)
+ * .isCached(...)
+ * .isHidden(...)
+ * .isBinary(...)
+ * .metadata(...)
+ * .rawKey(...) // the raw key i.e. @sharedWith:name@sharedBy
+ * .build();
+ *
+ */
+ public static class SharedKeyBuilder {
+ }
+
/**
* Represents a "private hidden key" in the Atsign Platform
*/
@@ -274,6 +346,26 @@ public static AtKey key(String rawKey, Metadata metadata) {
throw new IllegalArgumentException(rawKey + " does NOT match any raw key parser");
}
+ /**
+ * A builder for instantiating typed {@link AtKey} instances from raw key names.
+ * e.g. @sharedWith:name@sharedBy or public:name@sharedBy. This builder will
+ * decode everything based on the raw key name.
+ *
+ *
+ *
+ * Keys.AtKey key = Keys.keyBuilder().rawKey(...).build();
+ *
+ *
+ * Metadata can also be provided.
+ *
+ *
+ *
+ * Keys.AtKey key = Keys.keyBuilder().rawKey(...).metadata(...).build();
+ *
+ */
+ public static class KeyBuilder {
+ }
+
private interface RawKeyParser extends Predicate {
T parse(String rawKey, Metadata metadata);
}
diff --git a/at_client/src/main/java/org/atsign/common/Metadata.java b/at_client/src/main/java/org/atsign/client/api/Metadata.java
similarity index 95%
rename from at_client/src/main/java/org/atsign/common/Metadata.java
rename to at_client/src/main/java/org/atsign/client/api/Metadata.java
index 6895fe6a..48f66d8f 100644
--- a/at_client/src/main/java/org/atsign/common/Metadata.java
+++ b/at_client/src/main/java/org/atsign/client/api/Metadata.java
@@ -1,13 +1,13 @@
-package org.atsign.common;
+package org.atsign.client.api;
import java.time.OffsetDateTime;
-import com.fasterxml.jackson.core.JsonProcessingException;
import lombok.Builder;
import lombok.Value;
import lombok.experimental.Accessors;
import lombok.extern.jackson.Jacksonized;
+import org.atsign.client.impl.util.JsonUtils;
/**
* Value class which models key metadata in the Atsign Platform
@@ -44,20 +44,23 @@ public class Metadata {
String encoding;
String ivNonce;
- // required for successful javadoc
-
/**
* A builder for instantiating {@link Metadata} instances. Note: Metadata is immutable so if you
- * want create a modified instance then use the toBuilder() method, override the fields and invoke
- * build().
+ * want to create a modified instance then use the toBuilder() method, override the fields and
+ * invoke build().
*/
public static class MetadataBuilder {
+ // required for javadoc
};
- public static Metadata fromJson(String json) throws JsonProcessingException {
- return Json.MAPPER.readValue(json, Metadata.class);
+ public static Metadata fromJson(String json) {
+ return JsonUtils.readValue(json, Metadata.class);
}
+ /**
+ *
+ * @return the encoded metadata fields as recognized by an At Server in an update command.
+ */
@Override
public String toString() {
return new StringBuilder()
diff --git a/at_client/src/main/java/org/atsign/client/api/Secondary.java b/at_client/src/main/java/org/atsign/client/api/Secondary.java
deleted file mode 100644
index 5fccbad0..00000000
--- a/at_client/src/main/java/org/atsign/client/api/Secondary.java
+++ /dev/null
@@ -1,191 +0,0 @@
-package org.atsign.client.api;
-
-import java.io.Closeable;
-import java.io.IOException;
-
-import org.atsign.common.AtException;
-import org.atsign.common.AtSign;
-import org.atsign.common.exceptions.*;
-
-/**
- * Clients ultimately talk to a Secondary server - usually this is a microservice which implements
- * the @ protocol server spec, running somewhere in the cloud.
- *
- * In the initial implementation we just have AtClientImpl talking to a RemoteSecondary which in
- * turn
- * talks, via TLS over a secure socket, to the cloud Secondary server.
- *
- * As we implement client-side offline storage, performance caching etc., we can expect e.g.
- *
- * AtClient {@code ->} FastCacheSecondary {@code ->} OfflineStorageSecondary {@code ->}
- * RemoteSecondary
- *
- * where FastCacheSecondary might be an in-memory LRU cache, and OfflineStorageSecondary is a
- * persistent cache of some or all of the information in the RemoteSecondary. To make this
- * possible, each Secondary will need to be able to fully handle the @ protocol, thus the
- * interface is effectively the same as when interacting with a cloud secondary via openssl
- * from command line.
- */
-public interface Secondary extends AtEvents.AtEventListener, Closeable {
- /**
- * @param command in @ protocol format
- * @param throwExceptionOnErrorResponse sometimes we want to inspect an error response,
- * sometimes we want to just throw an exception
- * @return response in @ protocol format
- * @throws AtException if there was an error response and throwExceptionOnErrorResponse is true
- * @throws IOException if one is encountered
- */
- Response executeCommand(String command, boolean throwExceptionOnErrorResponse) throws IOException, AtException;
-
- void startMonitor();
-
- void stopMonitor();
-
- boolean isMonitorRunning();
-
- /**
- * Used to hold the partially decoded response from a {@link Secondary}
- */
- class Response {
- private String rawDataResponse = null;
- private String rawErrorResponse;
- private String errorCode;
- private String errorText;
-
- public String getRawDataResponse() {
- return rawDataResponse;
- }
-
- public void setRawDataResponse(String s) {
- rawDataResponse = s;
- rawErrorResponse = null;
- errorCode = null;
- errorText = null;
- }
-
- public String getRawErrorResponse() {
- return rawErrorResponse;
- }
-
- public void setRawErrorResponse(String s) {
- // In format "AT1234-meaning of error code : "
- rawErrorResponse = s;
- rawDataResponse = null;
-
- int codeDelimiter = rawErrorResponse.indexOf(":");
- String errorCodeSegment = rawErrorResponse.substring(0, codeDelimiter).trim();
- String[] separatedByHyphen = errorCodeSegment.split("-");
- errorCode = separatedByHyphen[0].trim();
- errorText = rawErrorResponse.substring(codeDelimiter + 1).trim();
- }
-
- public boolean isError() {
- return rawErrorResponse != null;
- }
-
- public String getErrorCode() {
- return errorCode;
- }
-
- public String getErrorText() {
- return errorText;
- }
-
- @Override
- public String toString() {
- if (isError()) {
- return "error:" + rawErrorResponse;
- } else {
- return "data:" + rawDataResponse;
- }
- }
-
- public AtException getException() {
- if (!isError()) {
- return null;
- }
- if (AtServerRuntimeException.CODE.equals(errorCode)) {
- return new AtServerRuntimeException(errorText);
- } else if (AtInvalidSyntaxException.CODE.equals(errorCode)) {
- return new AtInvalidSyntaxException(errorText);
- } else if (AtBufferOverFlowException.CODE.equals(errorCode)) {
- return new AtBufferOverFlowException(errorText);
- } else if (AtOutboundConnectionLimitException.CODE.equals(errorCode)) {
- return new AtOutboundConnectionLimitException(errorText);
- } else if (AtSecondaryNotFoundException.CODE.equals(errorCode)) {
- return new AtSecondaryNotFoundException(errorText);
- } else if (AtHandShakeException.CODE.equals(errorCode)) {
- return new AtHandShakeException(errorText);
- } else if (AtUnauthorizedException.CODE.equals(errorCode)) {
- return new AtUnauthorizedException(errorText);
- } else if (AtInternalServerError.CODE.equals(errorCode)) {
- return new AtInternalServerError(errorText);
- } else if (AtInternalServerException.CODE.equals(errorCode)) {
- return new AtInternalServerException(errorText);
- } else if (AtInboundConnectionLimitException.CODE.equals(errorCode)) {
- return new AtInboundConnectionLimitException(errorText);
- } else if (AtBlockedConnectionException.CODE.equals(errorCode)) {
- return new AtBlockedConnectionException(errorText);
- } else if (AtKeyNotFoundException.CODE.equals(errorCode)) {
- return new AtKeyNotFoundException(errorText);
- } else if (AtInvalidAtKeyException.CODE.equals(errorCode)) {
- return new AtInvalidAtKeyException(errorText);
- } else if (AtSecondaryConnectException.CODE.equals(errorCode)) {
- return new AtSecondaryConnectException(errorText);
- } else if (AtIllegalArgumentException.CODE.equals(errorCode)) {
- return new AtIllegalArgumentException(errorText);
- } else if (AtTimeoutException.CODE.equals(errorCode)) {
- return new AtTimeoutException(errorText);
- } else if (AtServerIsPausedException.CODE.equals(errorCode)) {
- return new AtServerIsPausedException(errorText);
- } else if (AtUnauthenticatedException.CODE.equals(errorCode)) {
- return new AtUnauthenticatedException(errorText);
- }
-
- return new AtNewErrorCodeWhoDisException(errorCode, errorText);
- }
- }
-
- /**
- * Value class for hostname and port tuple
- */
- class Address {
- public final String host;
- public final int port;
-
- public Address(String host, int port) {
- this.host = host;
- this.port = port;
- }
-
- public static Address fromString(String hostAndPort) throws IllegalArgumentException {
- String[] split = hostAndPort.split(":");
- if (split.length != 2) {
- throw new IllegalArgumentException(
- "Cannot construct Secondary.Address from malformed host:port string '" + hostAndPort + "'");
- }
- String host = split[0];
- int port;
- try {
- port = Integer.parseInt(split[1]);
- } catch (NumberFormatException e) {
- throw new IllegalArgumentException(
- "Cannot construct Secondary.Address from malformed host:port string '" + hostAndPort + "'");
- }
- return new Address(host, port);
- }
-
- @Override
- public String toString() {
- return host + ":" + port;
- }
- }
-
- /**
- * Represents something that, given an {@link AtSign}, can resolve the {@link Address} of the
- * {@link Secondary} for this {@link AtSign}
- */
- interface AddressFinder {
- Address findSecondary(AtSign atSign) throws IOException, AtSecondaryNotFoundException;
- }
-}
diff --git a/at_client/src/main/java/org/atsign/client/api/impl/clients/AtClientImpl.java b/at_client/src/main/java/org/atsign/client/api/impl/clients/AtClientImpl.java
deleted file mode 100644
index 0a97523f..00000000
--- a/at_client/src/main/java/org/atsign/client/api/impl/clients/AtClientImpl.java
+++ /dev/null
@@ -1,796 +0,0 @@
-package org.atsign.client.api.impl.clients;
-
-import static org.atsign.client.api.AtEvents.AtEventType.decryptedUpdateNotification;
-import static org.atsign.client.api.AtKeyNames.toSharedByMeKeyName;
-import static org.atsign.client.util.Preconditions.checkNotNull;
-import static org.atsign.common.VerbBuilders.*;
-import static org.atsign.common.VerbBuilders.LookupOperation.all;
-import static org.atsign.common.VerbBuilders.LookupOperation.meta;
-
-import java.io.IOException;
-import java.util.*;
-import java.util.concurrent.CompletableFuture;
-import java.util.concurrent.CompletionException;
-import java.util.concurrent.TimeUnit;
-
-
-import lombok.extern.slf4j.Slf4j;
-import org.atsign.client.api.AtClient;
-import org.atsign.client.api.AtEvents.AtEventBus;
-import org.atsign.client.api.AtEvents.AtEventListener;
-import org.atsign.client.api.AtEvents.AtEventType;
-import org.atsign.client.api.AtKeyNames;
-import org.atsign.client.api.AtKeys;
-import org.atsign.client.api.Secondary;
-import org.atsign.client.util.EncryptionUtil;
-import org.atsign.common.*;
-import org.atsign.common.Keys.AtKey;
-import org.atsign.common.Keys.PublicKey;
-import org.atsign.common.Keys.SelfKey;
-import org.atsign.common.Keys.SharedKey;
-import org.atsign.common.exceptions.*;
-import org.atsign.common.options.GetRequestOptions;
-import org.atsign.common.response_models.LookupResponse;
-
-import com.fasterxml.jackson.core.JsonProcessingException;
-
-/**
- * Implementation of an {@link AtClient} which wraps a {@link Secondary}
- * in order to implement the "map like" features of the {@link AtClient} interface
- */
-@SuppressWarnings({"RedundantThrows", "unused"})
-@Slf4j
-public class AtClientImpl implements AtClient {
-
- // Factory method - creates an AtClientImpl with a RemoteSecondary
-
- private final AtSign atSign;
-
- @Override
- public AtSign getAtSign() {
- return atSign;
- }
-
- private final AtKeys keys;
-
- @Override
- public AtKeys getEncryptionKeys() {
- return keys;
- }
-
- private final Secondary secondary;
-
- @Override
- public Secondary getSecondary() {
- return secondary;
- }
-
- private final AtEventBus eventBus;
-
- public AtClientImpl(AtEventBus eventBus, AtSign atSign, AtKeys keys, Secondary secondary) {
- this.eventBus = eventBus;
- this.atSign = atSign;
- this.keys = keys;
- this.secondary = secondary;
- checkNotNull(keys.getEncryptPrivateKey(), "AtKeys have not been fully enrolled");
- eventBus.addEventListener(this, EnumSet.allOf(AtEventType.class));
- }
-
- @Override
- public void startMonitor() {
- secondary.startMonitor();
- }
-
- @Override
- public void stopMonitor() {
- secondary.stopMonitor();
- }
-
- @Override
- public boolean isMonitorRunning() {
- return secondary.isMonitorRunning();
- }
-
- @Override
- public synchronized void addEventListener(AtEventListener listener, Set eventTypes) {
- eventBus.addEventListener(listener, eventTypes);
- }
-
- @Override
- public synchronized void removeEventListener(AtEventListener listener) {
- eventBus.removeEventListener(listener);
- }
-
- @Override
- public int publishEvent(AtEventType eventType, Map eventData) {
- return eventBus.publishEvent(eventType, eventData);
- }
-
- @Override
- public synchronized void handleEvent(AtEventType eventType, Map eventData) {
- switch (eventType) {
- case sharedKeyNotification:
- // We've got notification that someone has shared an encryption key with us
- // If we also got a value, we can decrypt it and add it to our keys map
- // Note: a value isn't supplied when the ttr on the shared key was set to 0
- if (eventData.get("value") != null) {
- String sharedSharedKeyName = (String) eventData.get("key");
- String sharedSharedKeyEncryptedValue = (String) eventData.get("value");
- // decrypt it with our encryption private key
- try {
- String sharedKeyDecryptedValue =
- EncryptionUtil.rsaDecryptFromBase64(sharedSharedKeyEncryptedValue, keys.getEncryptPrivateKey());
- keys.put(sharedSharedKeyName, sharedKeyDecryptedValue);
- } catch (Exception e) {
- log.error("caught exception {} while decrypting received shared key {}", e, sharedSharedKeyName);
- }
- }
- break;
- case updateNotification:
- // Let's see if we can decrypt it on the fly
- if (eventData.get("value") != null) {
- String key = (String) eventData.get("key");
- String encryptedValue = (String) eventData.get("value");
- @SuppressWarnings("unchecked")
- Map metadata = (Map) eventData.get("metadata");
- String ivNonce = (String) metadata.get("ivNonce");
-
- try {
- // decrypt it with the symmetric key that the other atSign shared with me
- SharedKey sk = Keys.sharedKeyBuilder().rawKey(key).build();
- String encryptionKeySharedByOther = getEncryptionKeySharedByOther(sk);
-
- String decryptedValue =
- EncryptionUtil.aesDecryptFromBase64(encryptedValue, encryptionKeySharedByOther, ivNonce);
- HashMap newEventData = new HashMap<>(eventData);
- newEventData.put("decryptedValue", decryptedValue);
- eventBus.publishEvent(decryptedUpdateNotification, newEventData);
- } catch (Exception e) {
- log.error("caught exception while decrypting received data with key name [{}]", key, e);
- }
- }
- break;
- default:
- break;
- }
- }
-
- @Override
- public CompletableFuture get(SharedKey sharedKey) {
- return CompletableFuture.supplyAsync(() -> {
- try {
- return _get(sharedKey);
- } catch (Exception e) {
- throw new CompletionException(e);
- }
- });
- }
-
- @Override
- public CompletableFuture getBinary(SharedKey sharedKey) {
- return CompletableFuture.supplyAsync(() -> {
- try {
- return _getBinary(sharedKey);
- } catch (Exception e) {
- throw new CompletionException(e);
- }
- });
- }
-
- @Override
- public CompletableFuture put(SharedKey sharedKey, String value) {
- return CompletableFuture.supplyAsync(() -> {
- try {
- return _put(sharedKey, value);
- } catch (Exception e) {
- throw new CompletionException(e);
- }
- });
- }
-
- @Override
- public CompletableFuture delete(SharedKey sharedKey) {
- return CompletableFuture.supplyAsync(() -> {
- try {
- return _delete(sharedKey);
- } catch (Exception e) {
- throw new CompletionException(e);
- }
- });
- }
-
- @Override
- public CompletableFuture get(SelfKey selfKey) {
- return CompletableFuture.supplyAsync(() -> {
- try {
- return _get(selfKey);
- } catch (Exception e) {
- throw new CompletionException(e);
- }
- });
- }
-
- @Override
- public CompletableFuture getBinary(SelfKey selfKey) {
- return CompletableFuture.supplyAsync(() -> {
- try {
- return _getBinary(selfKey);
- } catch (Exception e) {
- throw new CompletionException(e);
- }
- });
- }
-
- @Override
- public CompletableFuture put(SelfKey selfKey, String value) {
- return CompletableFuture.supplyAsync(() -> {
- try {
- return _put(selfKey, value);
- } catch (Exception e) {
- throw new CompletionException(e);
- }
- });
- }
-
- @Override
- public CompletableFuture delete(SelfKey selfKey) {
- return CompletableFuture.supplyAsync(() -> {
- try {
- return _delete(selfKey);
- } catch (Exception e) {
- throw new CompletionException(e);
- }
- });
- }
-
- @Override
- public CompletableFuture get(PublicKey publicKey) {
- return CompletableFuture.supplyAsync(() -> {
- try {
- return _get(publicKey);
- } catch (Exception e) {
- throw new CompletionException(e);
- }
- });
- }
-
- @Override
- public CompletableFuture get(PublicKey publicKey, GetRequestOptions getRequestOptions) {
- return CompletableFuture.supplyAsync(() -> {
- try {
- return _get(publicKey, getRequestOptions);
- } catch (Exception e) {
- throw new CompletionException(e);
- }
- });
- }
-
- @Override
- public CompletableFuture getBinary(PublicKey publicKey) {
- return CompletableFuture.supplyAsync(() -> {
- try {
- return _getBinary(publicKey);
- } catch (Exception e) {
- throw new CompletionException(e);
- }
- });
- }
-
- @Override
- public CompletableFuture getBinary(PublicKey publicKey, GetRequestOptions getRequestOptions) {
- return CompletableFuture.supplyAsync(() -> {
- try {
- return _getBinary(publicKey, getRequestOptions);
- } catch (Exception e) {
- throw new CompletionException(e);
- }
- });
- }
-
- @Override
- public CompletableFuture put(PublicKey publicKey, String value) {
- return CompletableFuture.supplyAsync(() -> {
- try {
- return _put(publicKey, value);
- } catch (Exception e) {
- throw new CompletionException(e);
- }
- });
- }
-
- @Override
- public CompletableFuture delete(PublicKey publicKey) {
- return CompletableFuture.supplyAsync(() -> {
- try {
- return _delete(publicKey);
- } catch (Exception e) {
- throw new CompletionException(e);
- }
- });
- }
-
- @Override
- public CompletableFuture put(SharedKey sharedKey, byte[] value) {
- return CompletableFuture.supplyAsync(() -> {
- try {
- return _put(sharedKey, value);
- } catch (Exception e) {
- throw new CompletionException(e);
- }
- });
- }
-
- @Override
- public CompletableFuture put(SelfKey selfKey, byte[] value) {
- return CompletableFuture.supplyAsync(() -> {
- try {
- return _put(selfKey, value);
- } catch (Exception e) {
- throw new CompletionException(e);
- }
- });
- }
-
- @Override
- public CompletableFuture put(PublicKey publicKey, byte[] value) {
- return CompletableFuture.supplyAsync(() -> {
- try {
- return _put(publicKey, value);
- } catch (Exception e) {
- throw new CompletionException(e);
- }
- });
- }
-
- @Override
- public CompletableFuture> getAtKeys(String regex) {
- return getAtKeys(regex, true);
- }
-
- @Override
- public CompletableFuture> getAtKeys(String regex, boolean fetchMetadata) {
- return CompletableFuture.supplyAsync(() -> {
- try {
- return _getAtKeys(regex, fetchMetadata);
- } catch (Exception e) {
- throw new CompletionException(e);
- }
- });
- }
-
- /**
- * Synchronous, talks @-protocol directly to the client's Secondary server
- *
- * @param command in @ protocol format
- * @param throwExceptionOnErrorResponse sometimes we want to inspect an error response,
- * sometimes we want to just throw an exception
- * @return a Secondary Response
- * @throws AtException if the response from the Secondary starts with 'error:', or
- * if there is any other exception
- */
- @Override
- public Response executeCommand(String command, boolean throwExceptionOnErrorResponse)
- throws AtException, IOException {
- return secondary.executeCommand(command, throwExceptionOnErrorResponse);
- }
-
- @Override
- public void close() throws IOException {
- stopMonitor();
- secondary.close();
- }
-
- // ============================================================================================================================================
- // ============================================================================================================================================
- // ============================================================================================================================================
-
- //
- // Synchronous methods which do the actual work
- //
- private String _get(SharedKey sharedKey) throws AtException {
- if (sharedKey.sharedBy().equals(atSign)) {
- return _getSharedByMeWithOther(sharedKey);
- } else {
- return _getSharedByOtherWithMe(sharedKey);
- }
- }
-
- private String _getSharedByMeWithOther(SharedKey sharedKey) throws AtException {
- String shareEncryptionKey = getEncryptionKeySharedByMe(sharedKey);
-
- String command = llookupCommandBuilder().key(sharedKey).operation(all).build();
- LookupResponse response = getLookupResponse(command);
-
- try {
- return EncryptionUtil.aesDecryptFromBase64(response.data, shareEncryptionKey, response.metaData.ivNonce());
- } catch (Exception e) {
- throw new AtDecryptionException("Failed to decrypt value with shared encryption key", e);
- }
- }
-
- private String _getSharedByOtherWithMe(SharedKey sharedKey) throws AtException {
- String what;
- String shareEncryptionKey = getEncryptionKeySharedByOther(sharedKey);
-
- String command = lookupCommandBuilder().key(sharedKey).operation(all).build();
- LookupResponse response = getLookupResponse(command);
-
- what = "decrypt value with shared encryption key";
- try {
- return EncryptionUtil.aesDecryptFromBase64(response.data, shareEncryptionKey, response.metaData.ivNonce());
- } catch (Exception e) {
- throw new AtDecryptionException("Failed to " + what, e);
- }
- }
-
- private String _put(SharedKey sharedKey, String value) throws AtException {
- if (!this.atSign.equals(sharedKey.sharedBy())) {
- throw new AtIllegalArgumentException(
- "sharedBy is [" + sharedKey.sharedBy() + "] but should be this client's atSign [" + atSign + "]");
- }
- String what = "";
- String cipherText;
- try {
- what = "fetch/create shared encryption key";
- String shareToEncryptionKey = getEncryptionKeySharedByMe(sharedKey);
-
- what = "encrypt value with shared encryption key";
- String iv = EncryptionUtil.generateRandomIvBase64(16);
- sharedKey.updateMissingMetadata(Metadata.builder().ivNonce(iv).build());
- cipherText = EncryptionUtil.aesEncryptToBase64(value, shareToEncryptionKey, iv);
- } catch (Exception e) {
- throw new AtEncryptionException("Failed to " + what, e);
- }
-
- String command = updateCommandBuilder().key(sharedKey).value(cipherText).build();
-
- try {
- return secondary.executeCommand(command, true).toString();
- } catch (IOException e) {
- throw new AtSecondaryConnectException("Failed to execute " + command, e);
- }
- }
-
- private String _delete(SharedKey sharedKey) throws AtException {
- String command = deleteCommandBuilder().key(sharedKey).build();
- try {
- return secondary.executeCommand(command, true).toString();
- } catch (IOException e) {
- throw new AtSecondaryConnectException("Failed to execute " + command, e);
- }
- }
-
- private String _get(SelfKey key) throws AtException {
- // 1. build command
- String command = llookupCommandBuilder().key(key).operation(all).build();
-
- // 2. execute command
- LookupResponse fetched = getLookupResponse(command);
-
- // 3. decrypt the value
- String decryptedValue;
- String encryptedValue = fetched.data;
- String selfEncryptionKey = keys.getSelfEncryptKey();
- String iv = checkNotNull(fetched.metaData.ivNonce(), "ivNonce is null");
- decryptedValue = EncryptionUtil.aesDecryptFromBase64(encryptedValue, selfEncryptionKey, iv);
-
- // 4. update metadata. squash the fetchedMetadata with current key.metadata (fetchedMetadata has higher priority)
- key.overwriteMetadata(fetched.metaData);
-
- return decryptedValue;
- }
-
- private String _put(SelfKey selfKey, String value) throws AtException {
- // 1. generate dataSignature
- Metadata metadata = Metadata.builder()
- .dataSignature(generateSignature(value))
- .ivNonce(EncryptionUtil.generateRandomIvBase64(16))
- .build();
- selfKey.updateMissingMetadata(metadata);
- // 2. encrypt data with self encryption key
- String cipherText = EncryptionUtil.aesEncryptToBase64(value, keys.getSelfEncryptKey(), metadata.ivNonce());
-
- // 3. update secondary
- String command = updateCommandBuilder().key(selfKey).value(cipherText).build();
- try {
- return secondary.executeCommand(command, true).toString();
- } catch (IOException e) {
- throw new AtSecondaryConnectException("Failed to execute " + command, e);
- }
- }
-
- private String _delete(SelfKey key) throws AtException {
- // 1. build delete command
- String command = deleteCommandBuilder().key(key).build();
-
- // 2. run command
- try {
- return secondary.executeCommand(command, true).toString();
- } catch (IOException e) {
- throw new AtSecondaryConnectException("Failed to execute " + command, e);
- }
- }
-
- private String _get(PublicKey key) throws AtException {
- return _get(key, null);
- }
-
- private String _get(PublicKey key, GetRequestOptions getRequestOptions) throws AtException {
- // 1. build command
- String command;
- if (atSign.equals(key.sharedBy())) {
- command = llookupCommandBuilder().key(key).operation(all).build();
- } else {
- boolean bypassCache = getRequestOptions != null && getRequestOptions.getBypassCache();
- command = plookupCommandBuilder().key(key).bypassCache(bypassCache).operation(all).build();
- }
-
- // 2. run the command
- LookupResponse fetched = getLookupResponse(command);
-
- // 4. update key object metadata
- Metadata metadata = fetched.metaData.toBuilder().isCached(fetched.key.contains("cached:")).build();
- key.overwriteMetadata(metadata);
-
- // 5. return the AtValue
- return fetched.data;
- }
-
- private String _put(PublicKey publicKey, String value) throws AtException {
- // 1. generate dataSignature
- Metadata metadata = Metadata.builder().dataSignature(generateSignature(value)).build();
- publicKey.updateMissingMetadata(metadata);
-
- // 2. build command
- String command = updateCommandBuilder().key(publicKey).value(value).build();
-
- // 3. run command
- try {
- return secondary.executeCommand(command, true).toString();
- } catch (IOException e) {
- throw new AtSecondaryConnectException("Failed to execute " + command, e);
- }
- }
-
- private String _delete(PublicKey key) throws AtException {
- // 1. build command
- String command = deleteCommandBuilder().key(key).build();
-
- // 2. run command
- try {
- return secondary.executeCommand(command, true).toString();
- } catch (IOException e) {
- throw new AtSecondaryConnectException("Failed to execute " + command, e);
- }
- }
-
- private byte[] _getBinary(SharedKey sharedKey) throws AtException {
- throw new RuntimeException("Not Implemented");
- }
-
- private byte[] _getBinary(SelfKey selfKey) throws AtException {
- throw new RuntimeException("Not Implemented");
- }
-
- private byte[] _getBinary(PublicKey publicKey) throws AtException {
- throw new RuntimeException("Not Implemented");
- }
-
- private byte[] _getBinary(PublicKey publicKey, GetRequestOptions getRequestOptions) throws AtException {
- throw new RuntimeException("Not Implemented");
- }
-
- private String _put(SharedKey sharedKey, byte[] value) throws AtException {
- throw new RuntimeException("Not Implemented");
- }
-
- private String _put(SelfKey selfKey, byte[] value) throws AtException {
- throw new RuntimeException("Not Implemented");
- }
-
- private String _put(PublicKey publicKey, byte[] value) throws AtException {
- throw new RuntimeException("Not Implemented");
- }
-
- private List _getAtKeys(String regex, boolean fetchMetadata) throws AtException {
- String scanCommand = scanCommandBuilder().regex(regex).showHidden(true).build();
- Response scanRawResponse;
- try {
- scanRawResponse = executeCommand(scanCommand, true);
- } catch (IOException e) {
- throw new AtSecondaryConnectException("Failed to execute " + scanCommand, e);
- }
- ResponseTransformers.ScanResponseTransformer scanResponseTransformer =
- new ResponseTransformers.ScanResponseTransformer(AtClientImpl::isNotManagementKey);
- List rawArray = scanResponseTransformer.apply(scanRawResponse);
-
- List atKeys = new ArrayList<>();
- for (String atKeyRaw : rawArray) {
- AtKey atKey = Keys.keyBuilder().rawKey(atKeyRaw).build();
- if (fetchMetadata) {
- String llookupCommand = llookupCommandBuilder().operation(meta).rawKey(atKeyRaw).build();
- Response llookupMetaResponse;
- try {
- llookupMetaResponse = secondary.executeCommand(llookupCommand, true);
- } catch (IOException e) {
- throw new AtSecondaryConnectException("Failed to execute " + llookupCommand, e);
- }
- try {
- // atKey.metadata has priority over llookupMetaRaw.data
- Metadata responseMetadata = Metadata.fromJson(llookupMetaResponse.getRawDataResponse());
- atKey.updateMissingMetadata(responseMetadata);
- } catch (JsonProcessingException e) {
- throw new AtResponseHandlingException("Failed to parse JSON " + llookupMetaResponse.getRawDataResponse(), e);
- }
- }
- atKeys.add(atKey);
- }
- return atKeys;
- }
-
- // ============================================================================================================================================
- // ============================================================================================================================================
- // ============================================================================================================================================
-
- //
- // Internal utility methods. Will move these to another class later, so that other AtClient implementations can easily use them.
- //
-
- private LookupResponse getLookupResponse(String command) throws AtException {
- Response response;
- try {
- response = secondary.executeCommand(command, true);
- } catch (IOException e) {
- throw new AtSecondaryConnectException("Failed to execute " + command, e);
- }
-
- // 3. transform the data to a LlookupAllResponse object
- LookupResponse fetched;
- try {
- fetched = Json.MAPPER.readValue(response.getRawDataResponse(), LookupResponse.class);
- } catch (JsonProcessingException e) {
- throw new AtResponseHandlingException("Failed to parse JSON " + response.getRawDataResponse(), e);
- }
- return fetched;
- }
-
- private String getEncryptionKeySharedByMe(SharedKey key) throws AtException {
- Secondary.Response rawResponse;
- String command = llookupCommandBuilder().keyName(toSharedByMeKeyName(key.sharedWith())).sharedBy(atSign).build();
- try {
- rawResponse = secondary.executeCommand(command, false);
- } catch (IOException e) {
- throw new AtSecondaryConnectException("Failed to execute " + command, e);
- }
-
- if (rawResponse.isError()) {
- if (rawResponse.getException() instanceof AtKeyNotFoundException) {
- // No key found - so we should create one
- return createSharedEncryptionKey(key);
- } else {
- throw rawResponse.getException();
- }
- }
-
- // When we stored it, we encrypted it with our encryption public key; so we need to decrypt it now with our
- // encryption private key
- return EncryptionUtil.rsaDecryptFromBase64(rawResponse.getRawDataResponse(), keys.getEncryptPrivateKey());
- }
-
- private String getEncryptionKeySharedByOther(SharedKey sharedKey) throws AtException {
- // Let's see if it's in our in-memory cache
- String sharedSharedKeyName = AtKeyNames.toSharedWithMeKeyName(sharedKey.sharedBy(), sharedKey.sharedWith());
-
- String sharedKeyValue = keys.get(sharedSharedKeyName);
- if (sharedKeyValue != null) {
- return sharedKeyValue;
- }
-
- String what = "";
-
- // Not in memory so now let's try to fetch from remote - e.g. if I'm @bob, lookup:shared_key@alice
- String lookupCommand = lookupCommandBuilder().keyName(AtKeyNames.SHARED_KEY).sharedBy(sharedKey.sharedBy()).build();
- Response rawResponse;
- try {
- rawResponse = secondary.executeCommand(lookupCommand, true);
- } catch (IOException e) {
- throw new AtSecondaryConnectException("Failed to execute " + lookupCommand, e);
- }
-
- String sharedSharedKeyDecryptedValue;
- try {
- sharedSharedKeyDecryptedValue =
- EncryptionUtil.rsaDecryptFromBase64(rawResponse.getRawDataResponse(), keys.getEncryptPrivateKey());
- } catch (Exception e) {
- throw new AtDecryptionException("Failed to decrypt the shared_key with our encryption private key", e);
- }
- keys.put(sharedSharedKeyName, sharedSharedKeyDecryptedValue);
-
- return sharedSharedKeyDecryptedValue;
- }
-
- private String createSharedEncryptionKey(SharedKey sharedKey) throws AtException {
- // We need their public key
- String theirPublicEncryptionKey = getPublicEncryptionKey(sharedKey.sharedWith());
- if (theirPublicEncryptionKey == null) {
- throw new AtKeyNotFoundException(" public key " + sharedKey.sharedWith()
- + " not found but service is running - maybe that AtSign has not yet been onboarded");
- }
-
- // Cut an AES key
- String aesKey;
- try {
- aesKey = EncryptionUtil.generateAESKeyBase64();
- } catch (Exception e) {
- throw new AtEncryptionException("Failed to generate AES key for sharing with " + sharedKey.sharedWith(), e);
- }
-
- String what = "";
- try {
- // Encrypt key with the other at-sign's publickey and save it @bob:shared_key@alice
- what = "encrypt new shared key with their public key";
- String encryptedForOther = EncryptionUtil.rsaEncryptToBase64(aesKey, theirPublicEncryptionKey);
-
- what = "encrypt new shared key with our public key";
- // Encrypt key with our publickey and save it shared_key.bob@alice
- String encryptedForUs = EncryptionUtil.rsaEncryptToBase64(aesKey, keys.getEncryptPublicKey());
-
- what = "save encrypted shared key for us";
- String updateForUs = updateCommandBuilder()
- .keyName(toSharedByMeKeyName(sharedKey.sharedWith()))
- .sharedBy(sharedKey.sharedBy())
- .value(encryptedForUs)
- .build();
- secondary.executeCommand(updateForUs, true);
-
- what = "save encrypted shared key for them";
- String updateForOther = updateCommandBuilder()
- .keyName(AtKeyNames.SHARED_KEY)
- .sharedBy(sharedKey.sharedBy())
- .sharedWith(sharedKey.sharedWith())
- .ttr(TimeUnit.HOURS.toMillis(24))
- .value(encryptedForOther)
- .build();
- secondary.executeCommand(updateForOther, true);
- } catch (Exception e) {
- throw new AtEncryptionException("Failed to " + what, e);
- }
-
- return aesKey;
- }
-
- private String getPublicEncryptionKey(AtSign sharedWith) throws AtException {
- Secondary.Response rawResponse;
-
- String command = plookupCommandBuilder().keyName(AtKeyNames.PUBLIC_ENCRYPT).sharedBy(sharedWith).build();
- try {
- rawResponse = secondary.executeCommand(command, false);
- } catch (IOException e) {
- throw new AtSecondaryConnectException("Failed to execute " + command, e);
- }
-
- if (rawResponse.isError()) {
- if (rawResponse.getException() instanceof AtKeyNotFoundException) {
- return null;
- } else {
- throw rawResponse.getException();
- }
- } else {
- return rawResponse.getRawDataResponse();
- }
- }
-
- private String generateSignature(String value) throws AtException {
- String signature;
- try {
- signature = EncryptionUtil.signSHA256RSA(value, keys.getEncryptPrivateKey());
- } catch (Exception e) {
- throw new AtEncryptionException("Failed to sign value: " + value, e);
- }
- return signature;
- }
-
- private static boolean isNotManagementKey(String s) {
- return !s.matches(".+\\.__manage@.+");
- }
-}
diff --git a/at_client/src/main/java/org/atsign/client/api/impl/connections/AtConnectionBase.java b/at_client/src/main/java/org/atsign/client/api/impl/connections/AtConnectionBase.java
deleted file mode 100644
index bacd1fa9..00000000
--- a/at_client/src/main/java/org/atsign/client/api/impl/connections/AtConnectionBase.java
+++ /dev/null
@@ -1,193 +0,0 @@
-package org.atsign.client.api.impl.connections;
-
-import java.io.IOException;
-import java.io.PrintWriter;
-import java.net.Socket;
-import java.util.Scanner;
-
-import javax.net.SocketFactory;
-import javax.net.ssl.SSLSocketFactory;
-
-import lombok.extern.slf4j.Slf4j;
-import org.atsign.client.api.AtConnection;
-import org.atsign.client.api.AtEvents;
-import org.atsign.common.AtException;
-
-/**
- * Core implementation of an {@link AtConnection} which uses Java NIO. This
- * class contains the implementation of the socket connect / disconnect,
- * writing atprotocol commands and reading atprotocol responses.
- * If the socket becomes disconnected then it will be re-connected as part
- * of sending the next command.
- */
-@Slf4j
-public abstract class AtConnectionBase implements AtConnection {
-
- private final String url;
-
- @Override
- public String getUrl() {
- return url;
- }
-
- private final String host;
-
- @Override
- public String getHost() {
- return host;
- }
-
- private final int port;
-
- @Override
- public int getPort() {
- return port;
- }
-
- private Socket socket;
-
- @Override
- public Socket getSocket() {
- return socket;
- }
-
- private boolean connected = false;
-
- @Override
- public boolean isConnected() {
- return connected;
- }
-
- private final boolean autoReconnect;
-
- @Override
- public boolean isAutoReconnect() {
- return autoReconnect;
- }
-
- protected boolean verbose;
-
- @Override
- public boolean isVerbose() {
- return verbose;
- }
-
- @Override
- public void setVerbose(boolean verbose) {
- this.verbose = verbose;
- }
-
- protected final Authenticator authenticator;
-
- public Authenticator getAuthenticator() {
- return authenticator;
- }
-
- protected PrintWriter socketWriter;
- protected Scanner socketScanner;
-
- protected final AtEvents.AtEventBus eventBus;
-
- public AtConnectionBase(AtEvents.AtEventBus eventBus,
- String url,
- AtConnection.Authenticator authenticator,
- boolean autoReconnect,
- boolean verbose) {
- this.eventBus = eventBus;
- this.url = url;
- this.host = url.split(":")[0];
- this.port = Integer.parseInt(url.split(":")[1]);
- this.autoReconnect = autoReconnect;
- this.verbose = verbose;
- this.authenticator = authenticator;
- }
-
- @Override
- public synchronized void disconnect() {
- if (!isConnected()) {
- return;
- }
- connected = false;
- try {
- log.debug("disconnecting from {}:{}", host, port);
- socket.close();
- socketScanner.close();
- socketWriter.close();
- socket.shutdownInput();
- socket.shutdownOutput();
- } catch (Exception ignore) {
- }
- }
-
- @Override
- public synchronized void connect() throws IOException, AtException {
- if (isConnected()) {
- return;
- }
- SocketFactory sf = SSLSocketFactory.getDefault();
- log.debug("connecting to {}:{}...", host, port);
- this.socket = sf.createSocket(host, port);
- this.socketWriter = new PrintWriter(socket.getOutputStream());
- this.socketScanner = new Scanner(socket.getInputStream());
- log.debug("connected to {}:{}", host, port);
-
- if (authenticator != null) {
- authenticator.authenticate(this);
- }
- connected = true;
- }
-
- protected abstract String parseRawResponse(String rawResponse) throws IOException;
-
- @Override
- public final synchronized String executeCommand(String command) throws IOException {
- return executeCommand(command, autoReconnect, true);
- }
-
- protected synchronized String executeCommand(String command, boolean retryOnException, boolean readTheResponse)
- throws IOException {
- if (socket.isClosed()) {
- throw new IOException("executeCommand failed: socket is closed");
- }
- try {
- if (!command.endsWith("\n")) {
- command = command + "\n";
- }
- socketWriter.write(command);
- socketWriter.flush();
-
- if (verbose) {
- log.info("SENT: {}", command);
- }
-
- if (readTheResponse) {
- // Responses are always terminated by newline
- String rawResponse = socketScanner.nextLine();
- if (verbose) {
- log.info("RCVD: {}", rawResponse);
- }
-
- return parseRawResponse(rawResponse);
- } else {
- return "";
- }
- } catch (Exception first) {
- disconnect();
-
- if (retryOnException) {
- log.error("Caught exception {} : reconnecting", first.toString());
- try {
- connect();
- return executeCommand(command, false, true);
- } catch (Exception second) {
- log.error("failed on retry", second);
- throw new IOException("Failed to reconnect after original exception " + first + " : ", second);
- }
- } else {
- connected = false;
-
- throw new IOException(first);
- }
- }
- }
-}
diff --git a/at_client/src/main/java/org/atsign/client/api/impl/connections/AtMonitorConnection.java b/at_client/src/main/java/org/atsign/client/api/impl/connections/AtMonitorConnection.java
deleted file mode 100644
index dc80d9a5..00000000
--- a/at_client/src/main/java/org/atsign/client/api/impl/connections/AtMonitorConnection.java
+++ /dev/null
@@ -1,258 +0,0 @@
-package org.atsign.client.api.impl.connections;
-
-import static org.atsign.client.api.AtEvents.AtEventBus;
-import static org.atsign.client.api.AtEvents.AtEventType;
-import static org.atsign.client.api.AtEvents.AtEventType.deleteNotification;
-import static org.atsign.client.api.AtEvents.AtEventType.monitorException;
-import static org.atsign.client.api.AtEvents.AtEventType.monitorHeartbeatAck;
-import static org.atsign.client.api.AtEvents.AtEventType.sharedKeyNotification;
-import static org.atsign.client.api.AtEvents.AtEventType.statsNotification;
-import static org.atsign.client.api.AtEvents.AtEventType.updateNotification;
-
-import java.util.HashMap;
-
-import lombok.extern.slf4j.Slf4j;
-import org.atsign.common.AtSign;
-
-import com.fasterxml.jackson.databind.ObjectMapper;
-import org.atsign.common.Json;
-
-/**
- * A {@link AtMonitorConnection} represents a connection to an AtServer which,
- * when started will send the atprotocol monitor command and then processes
- * notifications and heartbeat messages until it is stopped. If the socket disconnects
- * then it will be automatically reconnected.
- */
-@Slf4j
-public class AtMonitorConnection extends AtSecondaryConnection implements Runnable {
-
- private static final ObjectMapper mapper = Json.MAPPER;
-
- private long _lastReceivedTime = 0;
-
- public long getLastReceivedTime() {
- return _lastReceivedTime;
- }
-
- public void setLastReceivedTime(long lastReceivedTime) {
- this._lastReceivedTime = lastReceivedTime;
- }
-
- private boolean running = false;
-
- public boolean isRunning() {
- return running;
- }
-
- private boolean _shouldBeRunning = false;
-
- private void setShouldBeRunning(boolean b) {
- _shouldBeRunning = b;
- }
-
- public boolean isShouldBeRunning() {
- return _shouldBeRunning;
- }
-
- public AtMonitorConnection(AtEventBus eventBus,
- AtSign atSign,
- String secondaryUrl,
- Authenticator authenticator,
- boolean verbose) {
- // Note that the Monitor doesn't make use of the auto-reconnect functionality, it does its own thing
- super(eventBus, atSign, secondaryUrl, authenticator, false, verbose);
- startHeartbeat();
- }
-
- private long lastHeartbeatSentTime = System.currentTimeMillis();
- private long lastHeartbeatAckTime = System.currentTimeMillis();
- private final int heartbeatIntervalMillis = 30000;
-
- private void startHeartbeat() {
- new Thread(() -> {
- while (true) {
- if (isShouldBeRunning()) {
- if (!isRunning() || lastHeartbeatSentTime - lastHeartbeatAckTime >= heartbeatIntervalMillis) {
- try {
- // heartbeats have stopped being acked
- log.error("Monitor heartbeats not being received");
- stopMonitor();
- long waitStartTime = System.currentTimeMillis();
- while (isRunning() && System.currentTimeMillis() - waitStartTime < 5000) {
- // wait for monitor to stop
- try {
- // noinspection BusyWait
- Thread.sleep(1000);
- } catch (Exception ignore) {
- }
- }
- if (isRunning()) {
- log.error("Monitor thread has not stopped, but going to start another one anyway");
- }
- startMonitor();
- } catch (Exception e) {
- log.error("Monitor restart failed", e);
- }
- } else {
- if (System.currentTimeMillis() - lastHeartbeatSentTime > heartbeatIntervalMillis) {
- try {
- executeCommand("noop:0", false, false);
- lastHeartbeatSentTime = System.currentTimeMillis();
- } catch (Exception ignore) {
- // Can't do anything, the heartbeat loop will take care of restarting the monitor connection
- }
- }
- }
- }
- try {
- // noinspection BusyWait
- Thread.sleep(heartbeatIntervalMillis / 5);
- } catch (Exception ignore) {
- }
- }
- }).start();
- }
-
- /**
- * @return true if the monitor start request has succeeded, or if the monitor is already running.
- */
- @SuppressWarnings("UnusedReturnValue")
- public synchronized boolean startMonitor() {
- lastHeartbeatSentTime = lastHeartbeatAckTime = System.currentTimeMillis();
-
- setShouldBeRunning(true);
- if (!running) {
- running = true;
- if (!isConnected()) {
- try {
- connect();
- } catch (Exception e) {
- log.error("startMonitor failed to connect to secondary : {}", e.getMessage());
- running = false;
- return false;
- }
- }
- new Thread(this).start();
- }
- return true;
- }
-
- public synchronized void stopMonitor() {
- setShouldBeRunning(false);
- lastHeartbeatSentTime = lastHeartbeatAckTime = System.currentTimeMillis();
- disconnect();
- }
-
- /**
- * Please don't call this directly. Call startMonitor() instead, which starts the monitor in its own
- * thread
- */
- @SuppressWarnings("unchecked")
- @Override
- public void run() {
- String what = "";
- // call executeCommand("monitor:")
- // while scanner.nextLine()
- // filter out the things that aren't interesting
- // and call the callback
- // and when there is an error
- // call connect() again
- // then call executeCommand("monitor:")
- // and go back into the while loop
- try {
- String monitorCommand = "monitor:" + getLastReceivedTime();
- what = "send monitor command " + monitorCommand;
- executeCommand(monitorCommand, true, false);
-
- while (isShouldBeRunning() && socketScanner.hasNextLine()) {
- what = "read from connection";
- String response = parseRawResponse(socketScanner.nextLine());
- if (verbose) {
- log.info("RCVD (MONITOR): {}", response);
- }
- AtEventType eventType;
- HashMap eventData = new HashMap<>();
- what = "parse monitor message";
- try {
- if (response.startsWith("data:ok")) {
- eventType = monitorHeartbeatAck;
- eventData.put("key", "__heartbeat__");
- eventData.put("value", response.substring("data:".length()));
- lastHeartbeatAckTime = System.currentTimeMillis();
-
- } else if (response.startsWith("data:")) {
- eventType = monitorException;
- eventData.put("key", "__monitorException__");
- eventData.put("value", response);
- eventData.put("exception", "Unexpected 'data:' message from server");
-
- } else if (response.startsWith("error:")) {
- eventType = monitorException;
- eventData.put("key", "__monitorException__");
- eventData.put("value", response);
- eventData.put("exception", "Unexpected 'error:' message from server");
-
- } else if (response.startsWith("notification:")) {
- // if id is -1 then it's a stats update
- // if id is > 0 then it's a data notification:
- // operation will be either 'update' or 'delete'
- // key will be the key that has changed
- // value will be the value, if available, or null, if not (e.g. when ttr == 0, value is not available)
- eventData = mapper.readValue(response.substring("notification:".length()), HashMap.class);
- String id = (String) eventData.get("id");
- String operation = (String) eventData.get("operation");
- String key = (String) eventData.get("key");
- setLastReceivedTime(eventData.containsKey("epochMillis")
- ? (long) eventData.get("epochMillis")
- : System.currentTimeMillis());
-
- if ("-1".equals(id)) {
- eventType = statsNotification;
-
- } else if ("update".equals(operation)) {
- if (key.startsWith(getAtSign() + ":shared_key@")) {
- eventType = sharedKeyNotification;
- } else {
- eventType = updateNotification;
- }
-
- } else if ("delete".equals(operation)) {
- eventType = deleteNotification;
-
- } else {
- eventType = monitorException;
- eventData.put("key", "__monitorException__");
- eventData.put("value", response);
- eventData.put("exception", "Unknown notification operation '" + operation);
- }
- } else {
- eventType = monitorException;
- eventData.put("key", "__monitorException__");
- eventData.put("value", response);
- eventData.put("exception", "Malformed response from server");
-
- }
- } catch (Exception e) {
- log.error("monitor exception : {}", e.toString());
- eventType = monitorException;
- eventData.put("key", "__monitorException__");
- eventData.put("value", response);
- eventData.put("exception", e.toString());
- }
- eventBus.publishEvent(eventType, eventData);
- }
- log.info("Monitor ending normally - shouldBeRunning is {}", isShouldBeRunning());
- } catch (Exception e) {
- if (!isShouldBeRunning()) {
- log.info("shouldBeRunning is false, and monitor has stopped OK. Exception was : {}", e.getMessage());
- } else {
- log.error("Monitor failed to {}", what, e);
- log.info("Monitor ending. Monitor heartbeat thread should restart the monitor shortly");
- disconnect();
- }
- } finally {
- running = false;
- disconnect();
- }
- }
-}
diff --git a/at_client/src/main/java/org/atsign/client/api/impl/connections/AtRootConnection.java b/at_client/src/main/java/org/atsign/client/api/impl/connections/AtRootConnection.java
deleted file mode 100644
index 538e2bc4..00000000
--- a/at_client/src/main/java/org/atsign/client/api/impl/connections/AtRootConnection.java
+++ /dev/null
@@ -1,80 +0,0 @@
-package org.atsign.client.api.impl.connections;
-
-import java.io.IOException;
-
-import org.atsign.client.api.AtEvents;
-import org.atsign.client.api.Secondary;
-import org.atsign.common.AtException;
-import org.atsign.common.AtSign;
-import org.atsign.common.exceptions.AtSecondaryNotFoundException;
-
-/**
- * A connection which understands how to talk with the root server.
- *
- * @see org.atsign.client.api.AtConnection
- */
-public class AtRootConnection extends AtConnectionBase implements Secondary.AddressFinder {
-
- public AtRootConnection(String rootUrl) {
- this(null, rootUrl);
- }
-
- public AtRootConnection(AtEvents.AtEventBus eventBus, String rootUrl) {
- this(eventBus, rootUrl, true, false);
- }
-
- public AtRootConnection(AtEvents.AtEventBus eventBus, String rootUrl, boolean autoReconnect, boolean verbose) {
- super(eventBus, rootUrl, null, autoReconnect, verbose);
- }
-
- @SuppressWarnings("RedundantThrows")
- @Override
- protected String parseRawResponse(String rawResponse) throws IOException {
- // responses from root are either 'null' or
- if (rawResponse.startsWith("@")) {
- rawResponse = rawResponse.substring(1);
- }
- return rawResponse;
- }
-
- /**
- * Looks up the address of the secondary for a given atsign
- *
- * @param atSign the AtSign being looked up
- * @return A {@link Secondary.Address}
- * @throws IOException if connection to root server is unavailable, encounters an error, or response
- * is malformed
- * @throws AtSecondaryNotFoundException if the root server returns the string 'null' as the lookup
- * response, which means that the atsign is not known to the root server
- */
- @Override
- public Secondary.Address findSecondary(AtSign atSign) throws IOException, AtSecondaryNotFoundException {
- if (!isConnected()) {
- try {
- connect();
- } catch (AtException e) {
- // connect will only throw an AtException if authentication fails. Root connections do not require authentication.
- throw new IOException(e);
- }
- }
- String response = executeCommand(atSign.withoutPrefix());
-
- if ("null".equals(response)) {
- throw new AtSecondaryNotFoundException("Root lookup returned null for " + atSign);
- } else {
- try {
- return Secondary.Address.fromString(response);
- } catch (IllegalArgumentException e) {
- throw new IOException(
- "Received malformed response " + response + " from lookup of " + atSign + " on root server");
- }
- }
- }
-
- /**
- * Wrapper for {@link #findSecondary(AtSign)}
- */
- public String lookupAtSign(AtSign atSign) throws IOException, AtSecondaryNotFoundException {
- return this.findSecondary(atSign).toString();
- }
-}
diff --git a/at_client/src/main/java/org/atsign/client/api/impl/connections/AtSecondaryConnection.java b/at_client/src/main/java/org/atsign/client/api/impl/connections/AtSecondaryConnection.java
deleted file mode 100644
index bfdd05d3..00000000
--- a/at_client/src/main/java/org/atsign/client/api/impl/connections/AtSecondaryConnection.java
+++ /dev/null
@@ -1,66 +0,0 @@
-package org.atsign.client.api.impl.connections;
-
-import java.io.Closeable;
-import java.io.IOException;
-
-import org.atsign.client.api.AtConnection;
-import org.atsign.client.api.AtEvents;
-import org.atsign.client.api.Secondary;
-import org.atsign.common.AtSign;
-
-/**
- * A connection which understands how to talk with the secondary server.
- *
- * @see org.atsign.client.api.AtConnection
- */
-public class AtSecondaryConnection extends AtConnectionBase implements Closeable {
- private final AtSign atSign;
-
- public AtSign getAtSign() {
- return atSign;
- }
-
- public AtSecondaryConnection(AtEvents.AtEventBus eventBus, AtSign atSign, Secondary.Address secondaryAddress,
- AtConnection.Authenticator authenticator, boolean autoReconnect, boolean verbose) {
- super(eventBus, secondaryAddress.toString(), authenticator, autoReconnect, verbose);
- this.atSign = atSign;
- }
-
- public AtSecondaryConnection(AtEvents.AtEventBus eventBus, AtSign atSign, String secondaryUrl,
- AtConnection.Authenticator authenticator, boolean autoReconnect, boolean verbose) {
- this(eventBus, atSign, Secondary.Address.fromString(secondaryUrl), authenticator, autoReconnect, verbose);
- }
-
- @Override
- protected String parseRawResponse(String rawResponse) throws IOException {
- // Response can look like this:
- // @ prompt - or @@ if connection has been authenticated
- // then either
- // data:
- // error:
- // data:ok (for no-op requests)
- // notification: for notifications
- //
- // So:
- // Find the first colon (exception if none)
- // In what's before the colon
- // Strip out the @alice@ or the single @
- int dataPos = rawResponse.indexOf("data:");
- int errorPos = rawResponse.indexOf("error:");
- int notificationPos = rawResponse.indexOf("notification:");
- if (dataPos >= 0) {
- return rawResponse.substring(dataPos);
- } else if (errorPos >= 0) {
- return rawResponse.substring(errorPos);
- } else if (notificationPos >= 0) {
- return rawResponse.substring(notificationPos);
- } else {
- throw new IOException("Invalid response from server: " + rawResponse);
- }
- }
-
- @Override
- public void close() throws IOException {
- disconnect();
- }
-}
diff --git a/at_client/src/main/java/org/atsign/client/api/impl/connections/DefaultAtConnectionFactory.java b/at_client/src/main/java/org/atsign/client/api/impl/connections/DefaultAtConnectionFactory.java
deleted file mode 100644
index 80736fad..00000000
--- a/at_client/src/main/java/org/atsign/client/api/impl/connections/DefaultAtConnectionFactory.java
+++ /dev/null
@@ -1,49 +0,0 @@
-package org.atsign.client.api.impl.connections;
-
-import org.atsign.client.api.AtConnection;
-import org.atsign.client.api.AtConnectionFactory;
-import org.atsign.client.api.AtEvents;
-import org.atsign.client.api.Secondary;
-import org.atsign.common.AtSign;
-
-/**
- * Standard implementation of {@link AtConnectionFactory} for creating {@link AtSecondaryConnection}
- * and {@link AtRootConnection} instances.
- */
-public class DefaultAtConnectionFactory implements AtConnectionFactory {
- @Override
- public AtSecondaryConnection getSecondaryConnection(AtEvents.AtEventBus eventBus,
- AtSign atSign,
- Secondary.Address secondaryAddress,
- AtConnection.Authenticator authenticator) {
- return new AtSecondaryConnection(eventBus, atSign, secondaryAddress, authenticator, true, false);
- }
-
- @Override
- public AtSecondaryConnection getSecondaryConnection(AtEvents.AtEventBus eventBus,
- AtSign atSign,
- Secondary.Address secondaryAddress,
- AtConnection.Authenticator authenticator,
- boolean verbose) {
- return new AtSecondaryConnection(eventBus, atSign, secondaryAddress, authenticator, true, verbose);
- }
-
- @Override
- public AtSecondaryConnection getSecondaryConnection(AtEvents.AtEventBus eventBus,
- AtSign atSign,
- String secondaryUrl,
- AtConnection.Authenticator authenticator,
- boolean verbose) {
- return new AtSecondaryConnection(eventBus, atSign, secondaryUrl, authenticator, true, verbose);
- }
-
- @Override
- public AtRootConnection getRootConnection(AtEvents.AtEventBus eventBus, String rootUrl) {
- return new AtRootConnection(eventBus, rootUrl, true, false);
- }
-
- @Override
- public AtRootConnection getRootConnection(AtEvents.AtEventBus eventBus, String rootUrl, boolean verbose) {
- return new AtRootConnection(eventBus, rootUrl, true, verbose);
- }
-}
diff --git a/at_client/src/main/java/org/atsign/client/api/impl/secondaries/RemoteSecondary.java b/at_client/src/main/java/org/atsign/client/api/impl/secondaries/RemoteSecondary.java
deleted file mode 100644
index 389d00a1..00000000
--- a/at_client/src/main/java/org/atsign/client/api/impl/secondaries/RemoteSecondary.java
+++ /dev/null
@@ -1,197 +0,0 @@
-package org.atsign.client.api.impl.secondaries;
-
-import static org.atsign.client.api.AtEvents.AtEventBus;
-import static org.atsign.client.api.AtEvents.AtEventType;
-
-import java.io.IOException;
-import java.util.Map;
-
-import lombok.extern.slf4j.Slf4j;
-import org.atsign.client.api.AtConnectionFactory;
-import org.atsign.client.api.AtKeys;
-import org.atsign.client.api.Secondary;
-import org.atsign.client.api.impl.connections.AtMonitorConnection;
-import org.atsign.client.api.impl.connections.AtSecondaryConnection;
-import org.atsign.client.util.AuthUtil;
-import org.atsign.common.AtException;
-import org.atsign.common.AtSign;
-import org.atsign.common.exceptions.AtIllegalArgumentException;
-import org.atsign.common.exceptions.AtInvalidAtKeyException;
-import org.atsign.common.exceptions.AtInvalidSyntaxException;
-import org.atsign.common.exceptions.AtUnknownResponseException;
-
-/**
- * Implementation of {@link Secondary} wraps an {@link AtSecondaryConnection} and
- * an {@link AtMonitorConnection}.
- */
-@Slf4j
-public class RemoteSecondary implements Secondary {
-
- private final AtConnectionFactory connectionFactory;
- private final AtEventBus eventBus;
-
- @SuppressWarnings("unused")
- public AtConnectionFactory getConnectionFactory() {
- return connectionFactory;
- }
-
- private final AtSecondaryConnection connection;
-
- @SuppressWarnings("unused")
- public AtSecondaryConnection getConnection() {
- return connection;
- }
-
- private volatile AtMonitorConnection monitorConnection;
-
- @SuppressWarnings("unused")
- public AtMonitorConnection getMonitorConnection() {
- return monitorConnection;
- }
-
- private final AtSign atSign;
-
- public AtSign getAtSign() {
- return atSign;
- }
-
- private final Secondary.Address secondaryAddress;
-
- @SuppressWarnings("unused")
- public Secondary.Address getSecondaryAddress() {
- return secondaryAddress;
- }
-
- @SuppressWarnings("unused")
- public String getSecondaryUrl() {
- return secondaryAddress.toString();
- }
-
- private boolean verbose;
-
- @SuppressWarnings("unused")
- public boolean isVerbose() {
- return verbose;
- }
-
- @SuppressWarnings("unused")
- public void setVerbose(boolean b) {
- verbose = b;
- this.connection.setVerbose(b);
- this.monitorConnection.setVerbose(b);
- }
-
- @SuppressWarnings("unused")
- public RemoteSecondary(AtEventBus eventBus, AtSign atSign, Secondary.Address secondaryAddress,
- AtKeys keys, AtConnectionFactory connectionFactory)
- throws IOException, AtException {
- this(eventBus, atSign, secondaryAddress, keys, connectionFactory, false);
- }
-
- public RemoteSecondary(AtEventBus eventBus, AtSign atSign, Secondary.Address secondaryAddress,
- AtKeys keys, AtConnectionFactory connectionFactory,
- boolean verbose)
- throws IOException, AtException {
- this.eventBus = eventBus;
- this.atSign = atSign;
- this.secondaryAddress = secondaryAddress;
- this.connectionFactory = connectionFactory;
- this.verbose = verbose;
-
- this.connection = connectionFactory.getSecondaryConnection(
- this.eventBus,
- this.atSign,
- this.secondaryAddress,
- connection -> new AuthUtil()
- .authenticateWithPkam(connection, atSign, keys),
- verbose);
- connection.connect();
- }
-
- @Override
- public Response executeCommand(String command, boolean throwExceptionOnErrorResponse)
- throws IOException, AtException {
- Response response = new Response();
- String rawResponse = connection.executeCommand(command);
-
- if (rawResponse.startsWith("data:")) {
- response.setRawDataResponse(rawResponse.substring("data:".length()));
- } else if (rawResponse.startsWith("error:")) {
- response.setRawErrorResponse(rawResponse.substring("error:".length()));
- AtException theServerException = response.getException();
-
- if (theServerException instanceof AtInvalidSyntaxException
- || theServerException instanceof AtIllegalArgumentException
- || theServerException instanceof AtInvalidAtKeyException) {
- // Secondaries used to close connections for these exceptions so let's disconnect and reconnect
- connection.disconnect();
- connection.connect();
- }
-
- if (throwExceptionOnErrorResponse) {
- throw theServerException;
- }
- } else {
- throw new AtUnknownResponseException("Unknown response " + rawResponse + " from command " + command);
- }
- return response;
- }
-
- @Override
- public void startMonitor() {
- ensureMonitorRunning();
- }
-
- @Override
- public void stopMonitor() {
- ensureMonitorNotRunning();
- }
-
- @Override
- public boolean isMonitorRunning() {
- return monitorConnection != null && monitorConnection.isRunning();
- }
-
- @Override
- public synchronized void handleEvent(AtEventType eventType, Map eventData) {
- // if (eventType == )
- }
-
- @Override
- public void close() throws IOException {
- ensureMonitorNotRunning();
- connection.close();
- }
-
- private void ensureMonitorRunning() {
- String what = "";
- try {
- if (monitorConnection == null) {
- what = "construct an AtMonitorConnection";
- monitorConnection = new AtMonitorConnection(eventBus, atSign, secondaryAddress.toString(),
- connection.getAuthenticator(), verbose);
- }
- if (!monitorConnection.isRunning()) {
- what = "call monitorConnection.startMonitor()";
- monitorConnection.startMonitor();
- }
- } catch (Exception e) {
- log.error("SEVERE: failed to {}", what, e);
- }
- }
-
- private void ensureMonitorNotRunning() {
- String what = "";
- try {
- if (monitorConnection == null) {
- return;
- }
- if (monitorConnection.isRunning()) {
- what = "call monitorConnection.stopMonitor()";
- monitorConnection.stopMonitor();
- }
- } catch (Exception e) {
- log.error("SEVERE: failed to {}", what, e);
- }
- }
-}
diff --git a/at_client/src/main/java/org/atsign/client/api/package-info.java b/at_client/src/main/java/org/atsign/client/api/package-info.java
new file mode 100644
index 00000000..6869ea32
--- /dev/null
+++ b/at_client/src/main/java/org/atsign/client/api/package-info.java
@@ -0,0 +1,4 @@
+/**
+ * The core interfaces and data types for the AtSign Java SDK.
+ */
+package org.atsign.client.api;
diff --git a/at_client/src/main/java/org/atsign/client/cli/AbstractCli.java b/at_client/src/main/java/org/atsign/client/cli/AbstractCli.java
deleted file mode 100644
index fa40866f..00000000
--- a/at_client/src/main/java/org/atsign/client/cli/AbstractCli.java
+++ /dev/null
@@ -1,272 +0,0 @@
-package org.atsign.client.cli;
-
-import static org.atsign.client.util.Preconditions.checkNotNull;
-import static org.atsign.common.VerbBuilders.*;
-
-import java.io.File;
-import java.io.IOException;
-import java.util.List;
-import java.util.Map;
-import java.util.function.Function;
-import java.util.regex.Matcher;
-import java.util.regex.Pattern;
-
-import org.atsign.client.api.AtKeys;
-import org.atsign.client.api.impl.connections.AtRootConnection;
-import org.atsign.client.api.impl.connections.AtSecondaryConnection;
-import org.atsign.client.api.impl.events.SimpleAtEventBus;
-import org.atsign.client.util.AuthUtil;
-import org.atsign.client.util.KeysUtil;
-import org.atsign.common.AtException;
-import org.atsign.common.AtSign;
-import org.atsign.common.Json;
-import org.atsign.common.exceptions.AtSecondaryNotFoundException;
-
-import com.fasterxml.jackson.core.type.TypeReference;
-
-import picocli.CommandLine.ITypeConverter;
-import picocli.CommandLine.Option;
-
-/**
- * Base class for Command Line Interface utilities. Holds common fields such as root server
- * the {@link AtSign} which is connecting and the file which contains the {@link AtKeys}
- *
- * @param used to provide fluent builder style API
- */
-public abstract class AbstractCli> {
-
- /**
- * models server response string which is non-empty JSON map
- */
- protected static final Pattern DATA_JSON_NON_EMPTY_MAP = Pattern.compile("data:(\\{.+})");
-
- /**
- * models server response string which is JSON map
- */
- protected static final Pattern DATA_JSON_MAP = Pattern.compile("data:(\\{.*})");
-
- /**
- * models server response string which is non-empty JSON list
- */
- protected static final Pattern DATA_JSON_NO_EMPTY_LIST = Pattern.compile("data:(\\[.+])");
-
- /**
- * models server response string which is integer
- */
- protected static final Pattern DATA_INT = Pattern.compile("data:\\d+");
-
- /**
- * models server response string containing no whitespace
- */
- public static final Pattern DATA_NON_WHITESPACE = Pattern.compile("data:(\\S+)");
-
- protected String rootUrl = "root.atsign.org";
- protected AtSign atSign;
- protected File keysFile;
- protected int connectionRetries = 1;
- private boolean verbose = false;
-
- protected abstract T self();
-
- public T setVerbose(boolean isVerbose) {
- this.verbose = isVerbose;
- return self();
- }
-
- public T setVerbose() {
- return setVerbose(true);
- }
-
- @Option(names = {"-r", "--root"}, paramLabel = "HOST:PORT",
- description = "atDirectory (aka root) server domain (e.g., root.atsign.org)")
- public T setRootUrl(String rootUrl) {
- this.rootUrl = rootUrl;
- return self();
- }
-
- @Option(names = {"-a", "--atsign"}, description = "the atsign e.g. @colin", paramLabel = "ATSIGN",
- converter = AtSignConverter.class)
- public T setAtSign(AtSign atSign) {
- this.atSign = atSign;
- return self();
- }
-
- @Option(names = {"-k", "--keys"}, paramLabel = "PATH", description = "path to atKeys file to use / create")
- public T setKeysFile(String path) {
- this.keysFile = new File(path);
- return self();
- }
-
- protected static File checkNotExists(File f) {
- if (f.exists()) {
- throw new IllegalArgumentException(f.getPath() + " would be overwritten");
- }
- return f;
- }
-
- protected static File checkExists(File f) {
- if (!f.exists()) {
- throw new IllegalArgumentException(f.getPath() + " not found");
- }
- return f;
- }
-
- protected static File getAtKeysFile(File keysFile, AtSign atSign) {
- return keysFile != null ? keysFile : KeysUtil.getKeysFile(atSign);
- }
-
- protected static void checkAtServerMatchesAtSign(AtSecondaryConnection connection, AtSign atSign) throws IOException {
- String command = scanCommandBuilder().build();
- if (!matchDataJsonList(connection.executeCommand(command)).contains("signing_publickey" + atSign)) {
- // TODO: understand precisely what this means (observed in Dart SDK)
- throw new IllegalStateException("TBC");
- }
- }
-
- protected static void deleteKey(AtSecondaryConnection connection, String rawKey) {
- try {
- String command = deleteCommandBuilder().rawKey(rawKey).build();
- match(connection.executeCommand(command), DATA_INT);
- } catch (IOException e) {
- throw new RuntimeException(e);
- }
- }
-
- protected static void authenticateWithApkam(AtSecondaryConnection connection, AtSign atSign, AtKeys keys)
- throws AtException, IOException {
- new AuthUtil().authenticateWithPkam(connection, atSign, keys);
- }
-
- protected AtSecondaryConnection createAtSecondaryConnection(AtSign atSign,
- String rootUrl,
- int retries)
- throws Exception {
- checkNotNull(atSign, "atsign not set");
- checkNotNull(rootUrl, "root server endpoint not set");
-
- String secondaryUrl = resolveSecondaryUrl(atSign, rootUrl, retries);
- AtSecondaryConnection conn =
- new AtSecondaryConnection(new SimpleAtEventBus(), atSign, secondaryUrl, null, false, verbose);
- int retriesRemaining = retries;
- Exception ex;
- do {
- try {
- conn.connect();
- return conn;
- } catch (Exception e) {
- ex = e;
- Thread.sleep(2000);
- }
- } while (retriesRemaining-- > 0);
- throw ex;
- }
-
- protected static String resolveSecondaryUrl(AtSign atSign, String rootUrl, int retries) throws Exception {
- int retriesRemaining = retries;
- Exception ex;
- do {
- try {
- return new AtRootConnection(rootUrl).lookupAtSign(atSign);
- } catch (AtSecondaryNotFoundException e) {
- ex = e;
- Thread.sleep(1000);
- }
- } while (retriesRemaining-- > 0);
- throw ex;
- }
-
- protected static Map decodeJsonMapOfStrings(String json) {
- try {
- return Json.MAPPER.readValue(json, new TypeReference>() {});
- } catch (Exception e) {
- throw new RuntimeException(e);
- }
- }
-
- protected static Map decodeJsonMapOfObjects(String json) {
- try {
- return Json.MAPPER.readValue(json, new TypeReference>() {});
- } catch (Exception e) {
- throw new RuntimeException(e);
- }
- }
-
- protected static List decodeJsonList(String json) {
- try {
- return Json.MAPPER.readValue(json, new TypeReference>() {});
- } catch (Exception e) {
- throw new RuntimeException(e);
- }
- }
-
- public static List decodeJsonListOfStrings(String json) {
- try {
- return Json.MAPPER.readValue(json, new TypeReference>() {});
- } catch (Exception e) {
- throw new RuntimeException(e);
- }
- }
-
- protected static String match(String input, Pattern pattern) {
- Matcher matcher = pattern.matcher(input);
- if (!matcher.matches()) {
- throw new RuntimeException("expected [" + pattern + "] but input was : " + input);
- }
- StringBuilder builder = new StringBuilder();
- if (matcher.groupCount() == 0) {
- builder.append(input);
- } else {
- for (int i = 1; i <= matcher.groupCount(); i++) {
- builder.append(matcher.group(i));
- }
- }
- return builder.toString();
- }
-
- protected static T match(String input, Pattern pattern, Function transformer) {
- return transformer.apply(match(input, pattern));
- }
-
- protected static String matchDataString(String input) {
- return match(input, DATA_NON_WHITESPACE, s -> s);
- }
-
- protected static int matchDataInt(String input) {
- return match(input, DATA_INT, Integer::parseInt);
- }
-
- protected static List matchDataJsonList(String input) {
- return match(input, DATA_JSON_NO_EMPTY_LIST, AbstractCli::decodeJsonList);
- }
-
- public static List matchDataJsonListOfStrings(String input) {
- return match(input, DATA_JSON_NO_EMPTY_LIST, AbstractCli::decodeJsonListOfStrings);
- }
-
- protected static Map matchDataJsonMapOfStrings(String input, boolean allowEmpty) {
- return match(input, allowEmpty ? DATA_JSON_MAP : DATA_JSON_NON_EMPTY_MAP, AbstractCli::decodeJsonMapOfStrings);
- }
-
- protected static Map matchDataJsonMapOfObjects(String input, boolean allowEmpty) {
- return match(input, allowEmpty ? DATA_JSON_MAP : DATA_JSON_NON_EMPTY_MAP, AbstractCli::decodeJsonMapOfObjects);
- }
-
- protected static Map matchDataJsonMapOfStrings(String input) {
- return matchDataJsonMapOfStrings(input, false);
- }
-
- protected static Map matchDataJsonMapOfObjects(String input) {
- return matchDataJsonMapOfObjects(input, false);
- }
-
- protected static String ensureNotNull(String value, String defaultValue) {
- return value != null ? value : defaultValue;
- }
-
- static class AtSignConverter implements ITypeConverter {
- @Override
- public AtSign convert(String s) {
- return new AtSign(s);
- }
- }
-}
diff --git a/at_client/src/main/java/org/atsign/client/cli/Activate.java b/at_client/src/main/java/org/atsign/client/cli/Activate.java
deleted file mode 100644
index 9284a1e1..00000000
--- a/at_client/src/main/java/org/atsign/client/cli/Activate.java
+++ /dev/null
@@ -1,548 +0,0 @@
-package org.atsign.client.cli;
-
-import static org.atsign.client.util.EncryptionUtil.*;
-import static org.atsign.client.util.EnrollmentId.createEnrollmentId;
-import static org.atsign.client.util.KeysUtil.saveKeys;
-import static org.atsign.client.util.Preconditions.checkNotNull;
-import static org.atsign.common.VerbBuilders.*;
-
-import java.io.File;
-import java.io.IOException;
-import java.security.NoSuchAlgorithmException;
-import java.util.LinkedHashMap;
-import java.util.List;
-import java.util.Map;
-import java.util.concurrent.Callable;
-import java.util.concurrent.TimeUnit;
-import java.util.stream.Collectors;
-
-import org.atsign.client.api.AtKeyNames;
-import org.atsign.client.api.AtKeys;
-import org.atsign.client.api.impl.connections.AtSecondaryConnection;
-import org.atsign.client.util.AuthUtil;
-import org.atsign.client.util.EnrollmentId;
-import org.atsign.client.util.KeysUtil;
-import org.atsign.common.AtException;
-import org.atsign.common.AtSign;
-import org.atsign.common.VerbBuilders;
-import org.atsign.common.exceptions.AtUnauthenticatedException;
-
-import picocli.CommandLine;
-import picocli.CommandLine.Command;
-import picocli.CommandLine.Option;
-import picocli.CommandLine.Parameters;
-
-/**
- * Utility (and CommandLineInterface) for onboarding and enrolling atSigns and AtSign application
- * devices
- */
-@Command(
- mixinStandardHelpOptions = true)
-public class Activate extends AbstractCli implements Callable {
-
- enum Action {
- onboard, enroll, otp, list, approve, deny, revoke, unrevoke
- };
-
- public static final String DEFAULT_FIRST_APP = "firstApp";
- public static final String DEFAULT_FIRST_DEVICE = "firstDevice";
-
- @Parameters(index = "0", description = "onboard action to perform")
- private Action action;
-
- private String appName;
- private String deviceName;
- private boolean overwriteKeysFile = false;
- private String cramSecret;
- private boolean deleteCramKey = true;
- private AtKeys keys;
- private EnrollmentId enrollmentId;
- private String requestStatus = "pending";
- private String otp;
- private Map namespaces = new LinkedHashMap<>();
- private int completionRetries = 5;
-
-
- public static void main(String[] args) {
- System.exit(execute(args));
- }
-
- public static int execute(String[] args) {
- return new CommandLine(new Activate())
- .setUsageHelpWidth(80)
- .setAllowOptionsAsOptionParameters(true)
- .execute(args);
- }
-
- public Activate() {
- // TODO replace with stack check after JDK upgrade
- deleteCramKey = !System.getProperty("test.mode", "false").equalsIgnoreCase("true");
- }
-
- @Override
- public Integer call() throws Exception {
- switch (action) {
- case onboard:
- System.out.println(onboard());
- break;
- case otp:
- System.out.println(otp());
- break;
- case enroll:
- enroll();
- complete(completionRetries, 1, TimeUnit.SECONDS);
- break;
- case list:
- list().forEach(System.out::println);
- break;
- case approve:
- approve();
- break;
- case deny:
- deny();
- break;
- case revoke:
- revoke();
- break;
- case unrevoke:
- unrevoke();
- break;
- default:
- throw new Exception("no action");
- }
- return 0;
- }
-
- @Override
- protected Activate self() {
- return this;
- }
-
- @Option(names = {"-p", "--app"}, description = "The name of the app being enrolled")
- public Activate setAppName(String appName) {
- this.appName = appName;
- return self();
- }
-
- @Option(names = {"-d", "--device"}, description = " A name for the device on which this app is running")
- public Activate setDeviceName(String deviceName) {
- this.deviceName = deviceName;
- return self();
- }
-
- public Activate allowOverwriteKeysFile() {
- this.overwriteKeysFile = true;
- return self();
- }
-
- @Option(names = {"-c", "--cramkey"}, description = "CRAM key")
- public Activate setCramSecret(String cramSecret) {
- this.cramSecret = cramSecret;
- return self();
- }
-
- public Activate setNoDeleteCramKey() {
- this.deleteCramKey = false;
- return self();
- }
-
- @Option(names = {"-i", "--enrollmentId"}, description = "the ID of the enrollment request")
- public void setEnrollmentId(String s) {
- this.enrollmentId = EnrollmentId.createEnrollmentId(s);
- }
-
- @Option(names = {"-es", " --enrollmentStatus"}, description = "A specific status to filter by",
- defaultValue = "pending")
- public void setRequestStatus(String s) {
- this.requestStatus = s;
- }
-
- @Option(names = {"-s", "--passcode"}, description = "passcode to present with this enrollment request (OTP)")
- public Activate setOtp(String otp) {
- this.otp = otp;
- return self();
- }
-
- @Option(names = {"-n", "--namespaces"}, description = "the namespace access list as comma-separated list " +
- "of name:value pairs e.g. \"ns:rw,contacts:rw,__manage:rw\"")
- public Activate setNamespaces(String namespaces) {
- for (String namespace : namespaces.split(",")) {
- String[] parts = namespace.split(":");
- addNamespace(parts[0], parts[1]);
- }
- return self();
- }
-
- @Option(names = {"--max-retries"}, defaultValue = "5",
- description = " number of times to check for approval before giving up")
- public Activate setCompletionRetries(int completionRetries) {
- this.completionRetries = completionRetries;
- return self();
- }
-
- public Activate addNamespace(String namespace, String accessControl) {
- this.namespaces.put(namespace, accessControl);
- return self();
- }
-
- public EnrollmentId onboard() throws Exception {
- try (AtSecondaryConnection connection = createAtSecondaryConnection(atSign, rootUrl, connectionRetries)) {
- return onboard(connection);
- }
- }
-
- public EnrollmentId onboard(AtSecondaryConnection connection) throws Exception {
- File file = getAtKeysFile(keysFile, atSign);
- if (!overwriteKeysFile) {
- checkNotExists(file);
- }
- checkAtServerMatchesAtSign(connection, atSign);
- authenticateWithCram(connection, atSign, cramSecret);
- AtKeys keys = generateAtKeys(true);
- EnrollmentId enrollmentId = enroll(connection,
- keys,
- ensureNotNull(appName, DEFAULT_FIRST_APP),
- ensureNotNull(deviceName, DEFAULT_FIRST_DEVICE));
- keys = keys.toBuilder().enrollmentId(enrollmentId).build();
- authenticateWithApkam(connection, atSign, keys);
- saveKeys(keys, file);
- storeEncryptPublicKey(connection, atSign, keys);
- if (deleteCramKey) {
- deleteCramSecret(connection);
- }
- return enrollmentId;
- }
-
- public Activate authenticate(AtSecondaryConnection connection) throws Exception {
- File file = checkExists(getAtKeysFile(keysFile, atSign));
- keys = KeysUtil.loadKeys(file);
- authenticateWithApkam(connection, atSign, keys);
- return this;
- }
-
- public List list() throws Exception {
- return list(requestStatus);
- }
-
- public List list(String status) throws Exception {
- try (AtSecondaryConnection connection = createAtSecondaryConnection(atSign, rootUrl, connectionRetries)) {
- return authenticate(connection).list(connection, status);
- }
- }
-
- public List list(AtSecondaryConnection connection, String status) throws Exception {
- String command = enrollCommandBuilder()
- .operation(VerbBuilders.EnrollOperation.list)
- .status(status)
- .build();
- return matchDataJsonMapOfObjects(connection.executeCommand(command), true).keySet().stream()
- .map(Activate::inferEnrollmentId)
- .collect(Collectors.toList());
- }
-
- private static EnrollmentId inferEnrollmentId(String key) {
- int endIndex = key.indexOf('.');
- if (key.contains("__manage") && endIndex > 0) {
- return EnrollmentId.createEnrollmentId(key.substring(0, endIndex));
- } else {
- throw new RuntimeException(key + " doesn't match expected enrollment key pattern");
- }
- }
-
- public void approve() throws Exception {
- approve(checkNotNull(enrollmentId, "enrollment id not set"));
- }
-
- public void approve(EnrollmentId enrollmentId) throws Exception {
- try (AtSecondaryConnection connection = createAtSecondaryConnection(atSign, rootUrl, connectionRetries)) {
- authenticate(connection).approve(connection, enrollmentId);
- }
- }
-
- public void approve(AtSecondaryConnection connection, EnrollmentId enrollmentId) throws Exception {
- String key = fetchApkamSymmetricKey(connection, enrollmentId);
- String privateKeyIv = generateRandomIvBase64(16);
- String encryptPrivateKey = aesEncryptToBase64(keys.getEncryptPrivateKey(), key, privateKeyIv);
- String selfEncryptKeyIv = generateRandomIvBase64(16);
- String selfEncryptKey = aesEncryptToBase64(keys.getSelfEncryptKey(), key, selfEncryptKeyIv);
-
- String command = enrollCommandBuilder()
- .operation(VerbBuilders.EnrollOperation.approve)
- .enrollmentId(enrollmentId)
- .encryptPrivateKey(encryptPrivateKey)
- .encryptPrivateKeyIv(privateKeyIv)
- .selfEncryptKey(selfEncryptKey)
- .selfEncryptKeyIv(selfEncryptKeyIv)
- .build();
-
- Map response = matchDataJsonMapOfStrings(connection.executeCommand(command));
- if (!"approved".equals(response.get("status"))) {
- throw new RuntimeException("status is not approved : " + response.get("status"));
- }
- }
-
- private String fetchApkamSymmetricKey(AtSecondaryConnection connection, EnrollmentId enrollmentId) throws Exception {
- String command = enrollCommandBuilder()
- .operation(VerbBuilders.EnrollOperation.fetch)
- .enrollmentId(enrollmentId)
- .build();
- Map request = matchDataJsonMapOfObjects(connection.executeCommand(command));
- if (!"pending".equals(request.get("status"))) {
- throw new RuntimeException("status is not pending : " + request.get("status"));
- }
- String encryptedApkamSymmetricKey =
- (String) request.get(VerbBuilders.EnrollParameters.ENCRYPTED_APKAM_SYMMETRIC_KEY);
- return rsaDecryptFromBase64(encryptedApkamSymmetricKey, keys.getEncryptPrivateKey());
- }
-
- public void deny() throws Exception {
- deny(checkNotNull(enrollmentId, "enrollment id not set"));
- }
-
- public void deny(EnrollmentId enrollmentId) throws Exception {
- try (AtSecondaryConnection connection = createAtSecondaryConnection(atSign, rootUrl, connectionRetries)) {
- authenticate(connection).deny(connection, enrollmentId);
- }
- }
-
- public void revoke() throws Exception {
- revoke(checkNotNull(enrollmentId, "enrollment id not set"));
- }
-
- public void revoke(EnrollmentId enrollmentId) throws Exception {
- try (AtSecondaryConnection connection = createAtSecondaryConnection(atSign, rootUrl, connectionRetries)) {
- authenticate(connection).revoke(connection, enrollmentId);
- }
- }
-
- public void unrevoke() throws Exception {
- unrevoke(checkNotNull(enrollmentId, "enrollment id not set"));
- }
-
- public void unrevoke(EnrollmentId enrollmentId) throws Exception {
- try (AtSecondaryConnection connection = createAtSecondaryConnection(atSign, rootUrl, connectionRetries)) {
- authenticate(connection).unrevoke(connection, enrollmentId);
- }
- }
-
- public void delete(EnrollmentId enrollmentId) throws Exception {
- try (AtSecondaryConnection connection = createAtSecondaryConnection(atSign, rootUrl, connectionRetries)) {
- authenticate(connection).delete(connection, enrollmentId);
- }
- }
-
- public void deny(AtSecondaryConnection connection, EnrollmentId enrollmentId) throws Exception {
- singleArgEnrollAction(connection, "deny", enrollmentId, "denied");
- }
-
- public void revoke(AtSecondaryConnection connection, EnrollmentId enrollmentId) throws Exception {
- singleArgEnrollAction(connection, "revoke", enrollmentId, "revoked");
- }
-
- public void unrevoke(AtSecondaryConnection connection, EnrollmentId enrollmentId) throws Exception {
- singleArgEnrollAction(connection, "unrevoke", enrollmentId, "approved");
- }
-
- public void delete(AtSecondaryConnection connection, EnrollmentId enrollmentId) throws Exception {
- singleArgEnrollAction(connection, "delete", enrollmentId, "deleted");
- }
-
- public String otp() throws Exception {
- try (AtSecondaryConnection connection = createAtSecondaryConnection(atSign, rootUrl, connectionRetries)) {
- return otp(connection);
- }
- }
-
- public String otp(AtSecondaryConnection connection) throws IOException, AtException {
- File file = checkExists(getAtKeysFile(keysFile, atSign));
- AtKeys keys = KeysUtil.loadKeys(file);
- authenticateWithApkam(connection, atSign, keys);
- String command = otpCommandBuilder().build();
- return match(connection.executeCommand(command), DATA_NON_WHITESPACE);
- }
-
- public List scan() throws Exception {
- try (AtSecondaryConnection connection = createAtSecondaryConnection(atSign, rootUrl, connectionRetries)) {
- return scan(connection);
- }
- }
-
- public List scan(AtSecondaryConnection connection) throws Exception {
- String command = scanCommandBuilder()
- .showHidden(true)
- .regex(".*")
- .build();
- return matchDataJsonListOfStrings(connection.executeCommand(command));
- }
-
- private void singleArgEnrollAction(AtSecondaryConnection connection,
- String action,
- EnrollmentId enrollmentId,
- String expectedStatus)
- throws Exception {
- String command = enrollCommandBuilder()
- .operation(VerbBuilders.EnrollOperation.valueOf(action))
- .enrollmentId(enrollmentId)
- .build();
- Map map = matchDataJsonMapOfStrings(connection.executeCommand(command));
- if (!expectedStatus.equals(map.get("status"))) {
- throw new RuntimeException("status is not " + expectedStatus + " : " + map.get("status"));
- }
- }
-
- protected static void authenticateWithCram(AtSecondaryConnection connection, AtSign atSign, String cramSecret)
- throws AtException, IOException {
- checkNotNull(cramSecret, "CRAM secret not set");
- new AuthUtil().authenticateWithCram(connection, atSign, cramSecret);
- }
-
- private static EnrollmentId enroll(AtSecondaryConnection connection,
- AtKeys keys,
- String appName,
- String deviceName)
- throws Exception {
- String command = enrollCommandBuilder()
- .operation(VerbBuilders.EnrollOperation.request)
- .appName(appName)
- .deviceName(deviceName)
- .apkamPublicKey(keys.getApkamPublicKey())
- .build();
- Map response = matchDataJsonMapOfStrings(connection.executeCommand(command));
- if (!response.get("status").equals("approved")) {
- throw new RuntimeException("enroll request failed, expected status approved : " + response);
- }
- return createEnrollmentId(response.get("enrollmentId"));
- }
-
- protected static void storeEncryptPublicKey(AtSecondaryConnection connection, AtSign atSign, AtKeys keys)
- throws IOException {
- String command = updateCommandBuilder()
- .sharedBy(atSign)
- .keyName(AtKeyNames.PUBLIC_ENCRYPT)
- .isPublic(true)
- .value(keys.getEncryptPublicKey())
- .build();
- match(connection.executeCommand(command), DATA_INT);
- }
-
- protected static void deleteCramSecret(AtSecondaryConnection connection) {
- deleteKey(connection, AtKeyNames.PRIVATE_AT_SECRET);
- }
-
- protected static AtKeys generateAtKeys(boolean generateEncryptionKeyPair) throws NoSuchAlgorithmException {
- AtKeys.AtKeysBuilder builder = AtKeys.builder()
- .selfEncryptKey(generateAESKeyBase64())
- .apkamKeyPair(generateRSAKeyPair())
- .apkamSymmetricKey(generateAESKeyBase64());
- if (generateEncryptionKeyPair) {
- builder.encryptKeyPair(generateRSAKeyPair());
- }
- return builder.build();
- }
-
- public EnrollmentId enroll() throws Exception {
- try (AtSecondaryConnection connection = createAtSecondaryConnection(atSign, rootUrl, connectionRetries)) {
- return enroll(connection);
- }
- }
-
- public EnrollmentId enroll(AtSecondaryConnection connection) throws Exception {
- String command = lookupCommandBuilder()
- .keyName(AtKeyNames.PUBLIC_ENCRYPT)
- .sharedBy(atSign)
- .build();
- String publicKey = matchDataString(connection.executeCommand(command));
- File file = keysFile;
- if (!overwriteKeysFile) {
- checkNotExists(file);
- }
- AtKeys keys = generateAtKeys(false).toBuilder()
- .encryptPublicKey(publicKey)
- .build();
- enrollmentId = enroll(connection, keys);
- keys = keys.toBuilder().enrollmentId(enrollmentId).build();
- KeysUtil.saveKeys(keys, keysFile);
- return keys.getEnrollmentId();
- }
-
- private EnrollmentId enroll(AtSecondaryConnection connection, AtKeys keys) throws Exception {
- String command = enrollCommandBuilder()
- .operation(VerbBuilders.EnrollOperation.request)
- .appName(appName)
- .deviceName(deviceName)
- .apkamPublicKey(keys.getApkamPublicKey())
- .apkamSymmetricKey(rsaEncryptToBase64(keys.getApkamSymmetricKey(), keys.getEncryptPublicKey()))
- .otp(otp)
- .namespaces(namespaces)
- .build();
- Map response = matchDataJsonMapOfStrings(connection.executeCommand(command));
- if ("pending".equals(response.get("status"))) {
- return EnrollmentId.createEnrollmentId(response.get("enrollmentId"));
- } else {
- throw new RuntimeException("expected status pending : " + response);
- }
- }
-
- public void complete() throws Exception {
- try (AtSecondaryConnection connection = createAtSecondaryConnection(atSign, rootUrl, connectionRetries)) {
- complete(connection);
- }
- }
-
- public void complete(AtSecondaryConnection connection) throws Exception {
- AtKeys keys = KeysUtil.loadKeys(keysFile);
- authenticate(connection);
- String selfEncryptKey = keysGetDecrypted(connection, atSign, keys, AtKeyNames.SELF_ENCRYPTION_KEY);
- String encryptPrivateKey = keysGetDecrypted(connection, atSign, keys, AtKeyNames.ENCRYPT_PRIVATE_KEY);
- keys = keys.toBuilder()
- .selfEncryptKey(selfEncryptKey)
- .encryptPrivateKey(encryptPrivateKey)
- .build();
- KeysUtil.saveKeys(keys, keysFile);
- }
-
- public void complete(int retries, long sleepDuration, TimeUnit sleepUnit) throws Exception {
- try (AtSecondaryConnection connection = createAtSecondaryConnection(atSign, rootUrl, connectionRetries)) {
- complete(connection, retries, sleepDuration, sleepUnit);
- }
- }
-
- public void complete(AtSecondaryConnection connection, int retries, long sleepDuration, TimeUnit sleepUnit)
- throws Exception {
- Exception exception;
- int remainingRetries = retries;
- do {
- Thread.sleep(sleepUnit.toMillis(sleepDuration));
- try {
- complete(connection);
- return;
- } catch (AtUnauthenticatedException e) {
- exception = e.getMessage().contains("is pending") ? null : e;
- } catch (Exception e) {
- exception = e;
- }
- } while (exception == null && remainingRetries-- > 0);
-
- throw exception != null ? exception : new IllegalArgumentException();
- }
-
- private static String keysGetDecrypted(AtSecondaryConnection connection,
- AtSign atSign,
- AtKeys keys,
- String keyConstant)
- throws Exception {
- String rawKeyName = keys.getEnrollmentId() + "." + keyConstant + ".__manage" + atSign;
- String command = keysCommandBuilder()
- .operation(VerbBuilders.KeysOperation.get)
- .keyName(rawKeyName)
- .build();
- return decryptEncryptedKey(connection.executeCommand(command), keys.getApkamSymmetricKey());
- }
-
- protected static String decryptEncryptedKey(String json, String keyBase64) throws Exception {
- Map map = matchDataJsonMapOfStrings(json);
- String encryptedKey = map.get("value");
- String iv = map.get("iv");
- return aesDecryptFromBase64(encryptedKey, keyBase64, iv);
- }
-}
diff --git a/at_client/src/main/java/org/atsign/client/cli/DumpKeys.java b/at_client/src/main/java/org/atsign/client/cli/DumpKeys.java
deleted file mode 100644
index 735cd200..00000000
--- a/at_client/src/main/java/org/atsign/client/cli/DumpKeys.java
+++ /dev/null
@@ -1,19 +0,0 @@
-package org.atsign.client.cli;
-
-import lombok.extern.slf4j.Slf4j;
-import org.atsign.client.api.AtKeys;
-import org.atsign.client.util.KeysUtil;
-import org.atsign.common.AtSign;
-
-/**
- * Utility which, given an {@link AtSign} will load {@link AtKeys} from the
- * default location and dump the contents to stdout
- */
-@Slf4j
-public class DumpKeys {
- public static void main(String[] args) throws Exception {
- AtSign atSign = new AtSign(args[0]);
- AtKeys keys = KeysUtil.loadKeys(atSign);
- System.out.println(KeysUtil.dump(keys));
- }
-}
diff --git a/at_client/src/main/java/org/atsign/client/cli/Register.java b/at_client/src/main/java/org/atsign/client/cli/Register.java
deleted file mode 100644
index f2f7974c..00000000
--- a/at_client/src/main/java/org/atsign/client/cli/Register.java
+++ /dev/null
@@ -1,288 +0,0 @@
-package org.atsign.client.cli;
-
-import java.io.IOException;
-import java.util.*;
-import java.util.concurrent.Callable;
-
-import org.atsign.client.util.RegisterUtil;
-import org.atsign.common.*;
-import org.atsign.common.exceptions.AtRegistrarException;
-import org.atsign.config.ConfigReader;
-
-import picocli.CommandLine;
-import picocli.CommandLine.Command;
-import picocli.CommandLine.Option;
-
-/**
- * Command line interface to claim a free atsign. Requires one-time-password
- * received on the provided email to validate.
- * Registers the free atsign to provided email
- */
-@Command(name = "register", description = "Get an atsign and register")
-public class Register implements Callable {
- @Option(names = {"-e", "--email"}, description = "email to register a free atsign using otp-auth")
- static String email = "";
-
- @Option(names = {"-k", "--api-key"}, description = "register an atsign using super-API key")
- static String apiKey = "";
-
- Map params = new HashMap<>();
- boolean superApiKeyMode = false;
-
- public static void main(String[] args) throws AtException {
- int status = new CommandLine(new Register()).execute(args);
- System.exit(status);
- }
-
- /**
- * contains actual register logic.
- * main() calls this method with args passed through CLI
- */
- @Override
- public String call() throws Exception {
-
- readParameters();
- if (superApiKeyMode) {
- new RegistrationFlow(params).add(new GetFreeAtsignWithSuperApiKey()).add(new ActivateAtsignWithSuperApiKey())
- .start();
-
- } else {
- // parameter confirmation needs to be manually inserted into the params map
- params.put("confirmation", "false");
- new RegistrationFlow(params).add(new GetFreeAtsign()).add(new RegisterAtsign()).add(new ValidateOtp()).start();
- }
-
- String[] onboardArgs = new String[] {
- "onboard",
- "-r", params.get("rootDomain") + ":" + params.get("rootPort"),
- "-a", params.get("atSign"),
- "-c", params.get("cram")};
- Activate.main(onboardArgs);
-
- return "Done.";
- }
-
- void readParameters() throws IOException {
-
- // checks to ensure only either of email or super-API key are provided as args.
- // if super-API key is provided sets superApiKeyMode to true
- if ("".equals(email) && !"".equals(apiKey)) {
- superApiKeyMode = true;
- } else if ("".equals(apiKey) && !"".equals(email)) {
- superApiKeyMode = false;
- } else {
- System.err.println(
- "Usage: Register -e (or)\nRegister -k "
- + "\nNOTE: Use email if you prefer activating using verification code."
- + " Use API key option if you have a SuperAPI key. You can NOT use both.");
- System.exit(1);
- }
-
- params.put("rootDomain", ConfigReader.getProperty("rootServer", "domain"));
- if (params.get("rootDomain") == null) {
- // reading config from older configuration syntax for backwards compatibility
- params.put("rootDomain", ConfigReader.getProperty("ROOT_DOMAIN"));
- }
-
- params.put("rootPort", ConfigReader.getProperty("rootServer", "port"));
- if (params.get("rootPort") == null) {
- // reading config from older configuration syntax for backwards compatibility
- params.put("rootPort", ConfigReader.getProperty("ROOT_PORT"));
- }
- System.out.println("RootServer is " + params.get("rootDomain") + ":" + params.get("rootPort"));
-
- params.put("registrarUrl", ConfigReader.getProperty("registrarV3", "url"));
- if (params.get("registrarUrl") == null) {
- // reading config from older configuration syntax for backwards compatibility
- params.put("registrarUrl", ConfigReader.getProperty("REGISTRAR_URL"));
- }
-
- if (!superApiKeyMode && "".equals(apiKey)) {
- params.put("apiKey", ConfigReader.getProperty("registrar", "apiKey"));
- if (params.get("apiKey") == null) {
- // reading config from older configuration syntax for backwards compatibility
- params.put("apiKey", ConfigReader.getProperty("API_KEY"));
- }
- }
-
- // adding email/apiKey to params whichever is passed through command line args
- if (!superApiKeyMode) {
- params.put("email", email);
- } else {
- params.put("apiKey", apiKey);
- }
-
- // ensure all required params have been set
- if (!params.containsKey("rootDomain") || !params.containsKey("rootPort") || !params.containsKey("registrarUrl")
- || !params.containsKey("apiKey")) {
- System.err.println(
- "Please make sure to set all relevant configuration in src/main/resources/config.yaml");
- System.exit(1);
- }
- }
-}
-
-
-class RegistrationFlow {
- List>>> processFlow = new ArrayList<>();
- RegisterApiResult> result;
- Map params;
- RegisterUtil registerUtil = new RegisterUtil();
-
- RegistrationFlow(Map params) {
- this.params = params;
- }
-
- RegistrationFlow add(RegisterApiTask>> task) {
- processFlow.add(task);
- return this;
- }
-
- void start() throws Exception {
- for (RegisterApiTask>> task : processFlow) {
- // initialize each task by passing params to init()
- task.init(params, registerUtil);
- result = task.run();
- if (result.apiCallStatus.equals(ApiCallStatus.retry)) {
- while (task.shouldRetry()
- && result.apiCallStatus.equals(ApiCallStatus.retry)) {
- result = task.run();
- task.retryCount++;
- }
- }
- if (result.apiCallStatus.equals(ApiCallStatus.success)) {
- params.putAll(result.data);
- } else {
- throw result.atException;
- }
- }
- }
-}
-
-
-class GetFreeAtsign extends RegisterApiTask>> {
-
- @Override
- public RegisterApiResult> run() {
- System.out.println("Fetching free atsign ...");
- try {
- result.data.put("atSign",
- registerUtil.getFreeAtsign(params.get("registrarUrl"), params.get("apiKey")));
- result.apiCallStatus = ApiCallStatus.success;
- System.out.println("\tFetched new atsign: " + "@" + result.data.get("atSign"));
- } catch (AtRegistrarException e) {
- result.atException = e;
- } catch (Exception e) {
- result.atException = new AtRegistrarException("error while getting free atsign", e);
- result.apiCallStatus = retryCount < maxRetries ? ApiCallStatus.retry : ApiCallStatus.failure;
- }
- return result;
- }
-}
-
-
-class RegisterAtsign extends RegisterApiTask>> {
-
- @Override
- public RegisterApiResult> run() {
- System.out.println("Sending verification code to: " + params.get("email"));
- try {
- result.data.put("otpSent",
- registerUtil.registerAtsign(params.get("email"), new AtSign(params.get("atSign")),
- params.get("registrarUrl"), params.get("apiKey"))
- .toString());
- result.apiCallStatus = ApiCallStatus.success;
- } catch (Exception e) {
- result.atException = new AtRegistrarException(e.getMessage(), e.getCause());
- result.apiCallStatus = retryCount < maxRetries ? ApiCallStatus.retry : ApiCallStatus.failure;
- }
- return result;
- }
-}
-
-
-class ValidateOtp extends RegisterApiTask>> {
- Scanner scanner = new Scanner(System.in);
-
- @Override
- public RegisterApiResult> run() {
- try {
- // only ask for user input the first time. use the otp entry in params map in
- // subsequent api requests
- if (!params.containsKey("otp")) {
- System.out.println("Enter verification code received on " + params.get("email")
- + " [verification code is case sensitive]");
- params.put("otp", scanner.nextLine());
- System.out.println("Validating verification code ...");
- }
- String apiResponse = registerUtil.validateOtp(params.get("email"), new AtSign(params.get("atSign")),
- params.get("otp"), params.get("registrarUrl"), params.get("apiKey"),
- Boolean.parseBoolean(params.get("confirmation")));
- if ("retry".equals(apiResponse)) {
- System.err.println("Incorrect OTP!!! Please re-enter your OTP");
- params.put("otp", scanner.nextLine());
- result.apiCallStatus = ApiCallStatus.retry;
- result.atException = new AtRegistrarException("Only 3 retries allowed to re-enter OTP - Incorrect OTP entered");
- } else if ("follow-up".equals(apiResponse)) {
- params.put("confirmation", "true");
- result.apiCallStatus = ApiCallStatus.retry;
- } else if (apiResponse.startsWith("@")) {
- result.data.put("cram", apiResponse.split(":")[1]);
- System.out.println("\tRCVD cram: " + result.data.get("cram"));
- System.out.println("Done.");
- result.apiCallStatus = ApiCallStatus.success;
- scanner.close();
- }
- } catch (AtRegistrarException e) {
- result.atException = e;
- result.apiCallStatus = retryCount < maxRetries ? ApiCallStatus.retry : ApiCallStatus.failure;
- } catch (Exception e) {
- result.atException = new AtRegistrarException("Failed while validating OTP", e);
- result.apiCallStatus = retryCount < maxRetries ? ApiCallStatus.retry : ApiCallStatus.failure;
- }
- return result;
- }
-}
-
-
-class GetFreeAtsignWithSuperApiKey extends RegisterApiTask>> {
- @Override
- public RegisterApiResult> run() {
- System.out.println("Getting atSign ...");
- try {
- result.data.putAll(registerUtil.getAtsignWithSuperApiKey(params.get("registrarUrl"), params.get("apiKey")));
- System.out.println("Got atsign: " + result.data.get("atSign"));
- result.apiCallStatus = ApiCallStatus.success;
- } catch (AtRegistrarException e) {
- result.atException = e;
- result.apiCallStatus = retryCount < maxRetries ? ApiCallStatus.retry : ApiCallStatus.failure;
- } catch (Exception e) {
- result.atException = new AtRegistrarException("Failed while getting atSign", e);
- result.apiCallStatus = retryCount < maxRetries ? ApiCallStatus.retry : ApiCallStatus.failure;
- }
- return result;
- }
-}
-
-
-class ActivateAtsignWithSuperApiKey extends RegisterApiTask>> {
- @Override
- public RegisterApiResult> run() {
- try {
- result.data.put(
- "cram", registerUtil
- .activateAtsignWithSuperApiKey(params.get("registrarUrl"), params.get("apiKey"),
- new AtSign(params.get("atSign")), params.get("ActivationKey"))
- .split(":")[1]);
- result.apiCallStatus = ApiCallStatus.success;
- System.out.println("Your cram secret: " + result.data.get("cram"));
- } catch (AtRegistrarException e) {
- result.atException = e;
- result.apiCallStatus = retryCount < maxRetries ? ApiCallStatus.retry : ApiCallStatus.failure;
- } catch (Exception e) {
- result.atException = new AtRegistrarException("Failed while activating atSign", e);
- result.apiCallStatus = retryCount < maxRetries ? ApiCallStatus.retry : ApiCallStatus.failure;
- }
- return result;
- }
-}
diff --git a/at_client/src/main/java/org/atsign/client/impl/AtClientImpl.java b/at_client/src/main/java/org/atsign/client/impl/AtClientImpl.java
new file mode 100644
index 00000000..4bb1696c
--- /dev/null
+++ b/at_client/src/main/java/org/atsign/client/impl/AtClientImpl.java
@@ -0,0 +1,322 @@
+package org.atsign.client.impl;
+
+import static org.atsign.client.api.AtEvents.AtEventType.decryptedUpdateNotification;
+import static org.atsign.client.impl.common.Preconditions.checkNotNull;
+import static org.atsign.client.impl.util.EncryptionUtils.aesDecryptFromBase64;
+import static org.atsign.client.impl.util.EncryptionUtils.rsaDecryptFromBase64;
+
+import java.util.*;
+import java.util.concurrent.CompletableFuture;
+import java.util.concurrent.CompletionException;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.atomic.AtomicBoolean;
+
+import org.atsign.client.api.AtClient;
+import org.atsign.client.api.AtCommandExecutor;
+import org.atsign.client.api.AtEvents.AtEventBus;
+import org.atsign.client.api.AtEvents.AtEventListener;
+import org.atsign.client.api.AtEvents.AtEventType;
+import org.atsign.client.api.AtKeys;
+import org.atsign.client.api.AtSign;
+import org.atsign.client.api.Keys.AtKey;
+import org.atsign.client.api.Keys.PublicKey;
+import org.atsign.client.api.Keys.SelfKey;
+import org.atsign.client.api.Keys.SharedKey;
+import org.atsign.client.impl.commands.*;
+import org.atsign.client.impl.exceptions.AtDecryptionException;
+import org.atsign.client.impl.exceptions.AtException;
+
+import lombok.Builder;
+import lombok.extern.slf4j.Slf4j;
+
+/**
+ * Implementation of an {@link AtClient} which uses a {@link AtCommandExecutor} and
+ * the "composite" AtPlatform Protocol commands in {@link org.atsign.client.impl.commands}
+ * with an embedded {@link AtEventBus}.
+ * Example usage:
+ *
+ *
+ * AtClientImplBuilder builder = AtClientImpl.builder()
+ * .atSign(...)
+ * .keys(...)
+ * .executor(...)
+ * .eventBus(...);
+ *
+ * try (AtClient client = builder.build()) {
+ * client.startMonitor();
+ * client.put(...);
+ * client.get(...);
+ * }
+ *
+ */
+@Slf4j
+public class AtClientImpl implements AtClient {
+
+ private final AtSign atSign;
+ private final AtKeys keys;
+ private final AtCommandExecutor executor;
+ private final AtEventBus eventBus;
+ private final AtomicBoolean isMonitoring = new AtomicBoolean();
+ private final Notifications.EventBusBridge eventBusBridge;
+
+ @Override
+ public AtSign getAtSign() {
+ return atSign;
+ }
+
+ @Override
+ public AtCommandExecutor getCommandExecutor() {
+ return executor;
+ }
+
+ @Builder
+ public AtClientImpl(AtSign atSign, AtKeys keys, AtCommandExecutor executor, AtEventBus eventBus) {
+ this.atSign = checkNotNull(atSign, "atSign not set");
+ this.keys = checkNotNull(keys, "keys not set");
+ this.executor = checkNotNull(executor, "executor not set");
+ this.eventBus = checkNotNull(eventBus, "eventBus not set");
+ this.eventBus.addEventListener(this::handleEvent, EnumSet.allOf(AtEventType.class));
+ this.eventBusBridge = new Notifications.EventBusBridge(eventBus, atSign);
+ checkNotNull(keys.getEncryptPrivateKey(), "keys have not been fully enrolled");
+ }
+
+ /**
+ * A builder for instantiating {@link AtCommandExecutor} implementations that are included in
+ * this library. Example usage:
+ *
+ *
+ *
+ * AtClientImpl.builder()
+ * .atSign(...) // the AtSign that this client will authenticate as
+ * .keys(...) // the AtKeys that this client will use
+ * .executor() // the AtCommandExecutor this client will use
+ * .eventBus() // the AtEventBus this client will publish to
+ * .build();
+ * }
+ *
+ */
+ public static class AtClientImplBuilder {
+ // required for javadoc
+ }
+
+
+ @Override
+ public void close() throws Exception {
+ executor.close();
+ }
+
+ @Override
+ public void startMonitor() {
+ isMonitoring.compareAndSet(false, true);
+ executor.onReady(Notifications.monitor(atSign, keys, eventBusBridge::accept));
+ }
+
+ @Override
+ public void stopMonitor() {
+ isMonitoring.compareAndSet(true, false);
+ executor.onReady(AuthenticationCommands.pkamAuthenticator(atSign, keys));
+ }
+
+ @Override
+ public boolean isMonitorRunning() {
+ return isMonitoring.get();
+ }
+
+ @Override
+ public synchronized void addEventListener(AtEventListener listener, Set eventTypes) {
+ eventBus.addEventListener(listener, eventTypes);
+ }
+
+ @Override
+ public synchronized void removeEventListener(AtEventListener listener) {
+ eventBus.removeEventListener(listener);
+ }
+
+ @Override
+ public int publishEvent(AtEventType eventType, Map eventData) {
+ return eventBus.publishEvent(eventType, eventData);
+ }
+
+ @Override
+ public CompletableFuture get(SharedKey sharedKey) {
+ return wrapAsync(() -> SharedKeyCommands.get(executor, atSign, keys, sharedKey));
+ }
+
+ @Override
+ public CompletableFuture getBinary(SharedKey sharedKey) {
+ throw new UnsupportedOperationException("to be implemented");
+ }
+
+ @Override
+ public CompletableFuture put(SharedKey sharedKey, String value) {
+ return wrapAsync(() -> SharedKeyCommands.put(executor, atSign, keys, sharedKey, value));
+ }
+
+ @Override
+ public CompletableFuture delete(SharedKey sharedKey) {
+ return wrapAsync(() -> KeyCommands.deleteKey(executor, sharedKey));
+ }
+
+ @Override
+ public CompletableFuture get(SelfKey selfKey) {
+ return wrapAsync(() -> SelfKeyCommands.get(executor, atSign, keys, selfKey));
+ }
+
+ @Override
+ public CompletableFuture getBinary(SelfKey selfKey) {
+ throw new UnsupportedOperationException("to be implemented");
+ }
+
+ @Override
+ public CompletableFuture put(SelfKey selfKey, String value) {
+ return wrapAsync(() -> SelfKeyCommands.put(executor, atSign, keys, selfKey, value));
+ }
+
+ @Override
+ public CompletableFuture delete(SelfKey selfKey) {
+ return wrapAsync(() -> KeyCommands.deleteKey(executor, selfKey));
+ }
+
+ @Override
+ public CompletableFuture get(PublicKey publicKey) {
+ return wrapAsync(() -> PublicKeyCommands.get(executor, atSign, publicKey, null));
+ }
+
+ @Override
+ public CompletableFuture get(PublicKey publicKey, GetRequestOptions options) {
+ return wrapAsync(() -> PublicKeyCommands.get(executor, atSign, publicKey, options));
+ }
+
+ @Override
+ public CompletableFuture getBinary(PublicKey publicKey) {
+ throw new UnsupportedOperationException("to be implemented");
+ }
+
+ @Override
+ public CompletableFuture getBinary(PublicKey publicKey, GetRequestOptions options) {
+ throw new UnsupportedOperationException("to be implemented");
+ }
+
+ @Override
+ public CompletableFuture put(PublicKey publicKey, String value) {
+ return wrapAsync(() -> PublicKeyCommands.put(executor, atSign, keys, publicKey, value));
+ }
+
+ @Override
+ public CompletableFuture delete(PublicKey publicKey) {
+ return wrapAsync(() -> KeyCommands.deleteKey(executor, publicKey));
+ }
+
+ @Override
+ public CompletableFuture put(SharedKey sharedKey, byte[] value) {
+ throw new UnsupportedOperationException("to be implemented");
+ }
+
+ @Override
+ public CompletableFuture put(SelfKey selfKey, byte[] value) {
+ throw new UnsupportedOperationException("to be implemented");
+ }
+
+ @Override
+ public CompletableFuture put(PublicKey publicKey, byte[] value) {
+ throw new UnsupportedOperationException("to be implemented");
+ }
+
+ @Override
+ public CompletableFuture> getAtKeys(String regex) {
+ return getAtKeys(regex, true);
+ }
+
+ @Override
+ public CompletableFuture> getAtKeys(String regex, boolean fetchMetadata) {
+ return CompletableFuture.supplyAsync(() -> {
+ try {
+ return KeyCommands.getKeys(executor, regex, fetchMetadata);
+ } catch (Exception e) {
+ throw new CompletionException(e);
+ }
+ });
+ }
+
+ private void handleEvent(AtEventType eventType, Map eventData) {
+ try {
+ switch (eventType) {
+ case sharedKeyNotification:
+ onSharedKeyNotification(eventData);
+ break;
+ case updateNotification:
+ onUpdateNotification(eventData);
+ break;
+ default:
+ break;
+ }
+ } catch (Exception e) {
+ log.error("unexpected exception handling {} : {}", eventType, eventData, e);
+ }
+ }
+
+ private void onSharedKeyNotification(Map eventData) throws AtDecryptionException {
+ // We've got notification that someone has shared an encryption key with us
+ // If we also got a value, we can decrypt it and add it to our keys map
+ // Note: a value isn't supplied when the ttr on the shared key was set to 0
+ if (eventData.get("value") != null) {
+ String keyName = (String) eventData.get("key");
+ String value = (String) eventData.get("value");
+ String decrypted = rsaDecryptFromBase64(value, keys.getEncryptPrivateKey());
+ keys.put(keyName, decrypted);
+ }
+ }
+
+ private void onUpdateNotification(Map eventData) throws AtException {
+ // Let's see if we can decrypt it on the fly
+ if (eventData.get("value") != null) {
+ String key = (String) eventData.get("key");
+ String encryptedValue = (String) eventData.get("value");
+ Map metadata = (Map) eventData.get("metadata");
+ String ivNonce = (String) metadata.get("ivNonce");
+ SharedKey sk = org.atsign.client.api.Keys.sharedKeyBuilder().rawKey(key).build();
+ String encryptKeySharedByOther = SharedKeyCommands.getEncryptKeySharedByOther(executor, keys, sk);
+ String decryptedValue = aesDecryptFromBase64(encryptedValue, encryptKeySharedByOther, ivNonce);
+ HashMap newEventData = new HashMap<>(eventData);
+ newEventData.put("decryptedValue", decryptedValue);
+ eventBus.publishEvent(decryptedUpdateNotification, newEventData);
+ }
+ }
+
+ /**
+ * A runnable command which returns a value but can throw {@link AtException}s or execution
+ * exceptions
+ */
+ public interface AtCommandThatReturnsString {
+ String run() throws AtException, ExecutionException, InterruptedException;
+ }
+
+ private static CompletableFuture wrapAsync(AtCommandThatReturnsString command) {
+ return CompletableFuture.supplyAsync(() -> {
+ try {
+ return command.run();
+ } catch (Exception e) {
+ throw new CompletionException(e);
+ }
+ });
+ }
+
+ /**
+ * A runnable command which does NOT return a value but can throw {@link AtException}s or execution
+ * exceptions
+ */
+ public interface AtCommandThatReturnsVoid {
+ void run() throws AtException, ExecutionException, InterruptedException;
+ }
+
+ private static CompletableFuture wrapAsync(AtCommandThatReturnsVoid command) {
+ return CompletableFuture.supplyAsync(() -> {
+ try {
+ command.run();
+ return null;
+ } catch (Exception e) {
+ throw new CompletionException(e);
+ }
+ });
+ }
+}
diff --git a/at_client/src/main/java/org/atsign/client/impl/AtClients.java b/at_client/src/main/java/org/atsign/client/impl/AtClients.java
new file mode 100644
index 00000000..a7df018d
--- /dev/null
+++ b/at_client/src/main/java/org/atsign/client/impl/AtClients.java
@@ -0,0 +1,99 @@
+package org.atsign.client.impl;
+
+import static org.atsign.client.impl.common.Preconditions.checkNotNull;
+
+import org.atsign.client.api.AtClient;
+import org.atsign.client.api.AtCommandExecutor;
+import org.atsign.client.api.AtKeys;
+import org.atsign.client.api.AtSign;
+import org.atsign.client.impl.common.ReconnectStrategy;
+import org.atsign.client.impl.common.SimpleAtEventBus;
+import org.atsign.client.impl.common.SimpleReconnectStrategy;
+import org.atsign.client.impl.exceptions.AtException;
+import org.atsign.client.impl.util.KeysUtils;
+
+import lombok.Builder;
+
+/**
+ * Utility methods for instantiating {@link AtClient} implementations that are included in this
+ * library. Example usage:
+ *
+ *
+ *
+ * try (AtClient client = AtClients.builder().atSign(createAtSign("colin")).build()) {
+ * client.startMonitor();
+ * client.put(...);
+ * client.get(...);
+ * }
+ *
+ *
+ */
+public class AtClients {
+
+ @Builder(builderClassName = "AtClientBuilder")
+ public static AtClient createAtClient(String url,
+ AtSign atSign,
+ AtKeys keys,
+ Long timeoutMillis,
+ Long awaitReadyMillis,
+ ReconnectStrategy reconnect,
+ Boolean isVerbose)
+ throws AtException {
+
+ checkNotNull(atSign, "atSign not set");
+ keys = keys != null ? keys : KeysUtils.loadKeys(atSign);
+
+ AtCommandExecutor executor = AtCommandExecutors.builder()
+ .url(url)
+ .atSign(atSign)
+ .keys(keys)
+ .timeoutMillis(timeoutMillis)
+ .awaitReadyMillis(awaitReadyMillis)
+ .reconnect(reconnect)
+ .isVerbose(isVerbose)
+ .build();
+
+ SimpleAtEventBus eventBus = new SimpleAtEventBus();
+
+ return AtClientImpl.builder()
+ .atSign(atSign)
+ .keys(keys)
+ .executor(executor)
+ .eventBus(eventBus)
+ .build();
+ }
+
+ /**
+ * A builder for instantiating {@link AtClient} implementations that are included in
+ * this library.
+ *
+ *
+ *
+ * AtClients.builder()
+ * .atSign(...) // the AtSign that this client will authenticate as
+ * .url(...) // the url for the root server or proxy (optional)
+ * .keys(...) // the AtKeys that this client will use (optional)
+ * .timeoutMillis() // timeout after which commands will complete exceptionally (optional)
+ * .awaitReadyMillis() // how long to wait for executor to become ready during build() (optional)
+ * .reconnect() // a ReconnectStrategy (optional)
+ * .isVerbose(...) // true or false (optional)
+ * .build();
+ * }
+ *
+ *
+ * If url is not set then the builder will default to
+ * {@link AtEndpointSuppliers#DEFAULT_ROOT_URL}.
+ * If keys is not set then the builder will default to attempting to load the keys
+ * which correspond to the atSign field in ~/.atsign/keys (or the environment variable
+ * / system property {@link KeysUtils#ATSIGN_KEYS_DIR} if set).
+ * If timeoutMillis is not set then builder will default to
+ * {@link AtCommandExecutors#DEFAULT_TIMEOUT_MILLIS}.
+ * If awaitReadyMillis is not set then the builder will default to
+ * {@link AtCommandExecutors#DEFAULT_TIMEOUT_MILLIS}.
+ * If reconnect is not set then the builder will default to a {@link SimpleReconnectStrategy}
+ * with no limit to the retry attempts.
+ */
+ public static class AtClientBuilder {
+ // required for javadoc
+ }
+}
diff --git a/at_client/src/main/java/org/atsign/client/impl/AtCommandExecutors.java b/at_client/src/main/java/org/atsign/client/impl/AtCommandExecutors.java
new file mode 100644
index 00000000..d560f253
--- /dev/null
+++ b/at_client/src/main/java/org/atsign/client/impl/AtCommandExecutors.java
@@ -0,0 +1,121 @@
+package org.atsign.client.impl;
+
+
+import static org.atsign.client.impl.common.Preconditions.checkNotNull;
+
+import java.util.concurrent.TimeUnit;
+import java.util.function.Consumer;
+
+import org.atsign.client.api.AtCommandExecutor;
+import org.atsign.client.api.AtKeys;
+import org.atsign.client.api.AtSign;
+import org.atsign.client.impl.commands.AuthenticationCommands;
+import org.atsign.client.impl.common.ReconnectStrategy;
+import org.atsign.client.impl.common.SimpleReconnectStrategy;
+import org.atsign.client.impl.exceptions.AtException;
+import org.atsign.client.impl.netty.NettyAtCommandExecutor;
+
+import lombok.Builder;
+
+/**
+ * Utility methods / builders for instantiating {@link AtCommandExecutor} implementations
+ * that are included in this library.
+ * Example usage:
+ *
+ *
+ * AtCommandExecutorBuilder builder = AtCommandExecutors.builder()
+ * .url("vip.ve.atsign.zone:64")
+ * .atSign(createAtSign("colin"))
+ * .keys(...);
+ *
+ * try (AtCommandExecutor executor = builder.build()) {
+ * client.sendSync(...);
+ * }
+ *
+ *
+ * NOTE: If the url is prefixed with proxy (e.g. proxy:host:port) then the builder
+ * will automatically attempt to connect to an At Server at host:port.
+ * NOTE: If atSign and keys are provided then the builder
+ * will automatically configure the {@link AtCommandExecutor} to authenticate with PKAM.
+ * NOTE: If reconnect is not set then the builder will default to a
+ * {@link SimpleReconnectStrategy}
+ */
+public class AtCommandExecutors {
+
+ public static final long DEFAULT_TIMEOUT_MILLIS = TimeUnit.SECONDS.toMillis(5);
+
+ @Builder(builderClassName = "AtCommandExecutorBuilder")
+ public static AtCommandExecutor createCommandExecutor(String url,
+ AtSign atSign,
+ AtKeys keys,
+ Long timeoutMillis,
+ Long awaitReadyMillis,
+ ReconnectStrategy reconnect,
+ Boolean isVerbose)
+ throws AtException {
+
+ if (AtEndpointSuppliers.isProxyUrl(url)) {
+ checkNotNull(atSign, "atSign not set");
+ }
+
+ return NettyAtCommandExecutor.builder()
+ .endpoint(AtEndpointSuppliers.builder().url(url).atSign(atSign).build())
+ .isVerbose(isVerbose)
+ .timeoutMillis(defaultIfNotSet(timeoutMillis, DEFAULT_TIMEOUT_MILLIS))
+ .awaitReadyMillis(defaultIfNotSet(awaitReadyMillis, DEFAULT_TIMEOUT_MILLIS))
+ .reconnect(defaultIfNotSet(reconnect))
+ .onReady(createOnReady(atSign, keys))
+ .build();
+ }
+
+ /**
+ * A builder for instantiating {@link AtCommandExecutor} implementations that are included in
+ * this library. Example usage:
+ *
+ *
+ *
+ * AtCommandExecutors.builder()
+ * .url(...) // the url for the root server or proxy (optional)
+ * .atSign(...) // the AtSign that this client will authenticate as (optional)
+ * .keys(...) // the AtKeys that this client will use (optional)
+ * .timeoutMillis() // timeout after which commands will complete exceptionally (optional)
+ * .awaitReadyMillis() // how long to wait for executor to become ready during build() (optional)
+ * .reconnect() // a ReconnectStrategy (optional)
+ * .isVerbose(...) // default false
+ * .build();
+ * }
+ *
+ *
+ * If url is not set then the builder will default to
+ * {@link AtEndpointSuppliers#DEFAULT_ROOT_URL}.
+ * If timeoutMillis is not set then builder will default to
+ * {@link AtCommandExecutors#DEFAULT_TIMEOUT_MILLIS}.
+ * If awaitReadyMillis is not set then the builder will default to
+ * {@link AtCommandExecutors#DEFAULT_TIMEOUT_MILLIS}.
+ * If reconnect is not set then the builder will default to a {@link SimpleReconnectStrategy}
+ * with no limit to the retry attempts.
+ */
+ public static class AtCommandExecutorBuilder {
+ // required for javadoc
+ }
+
+ private static Consumer createOnReady(AtSign atSign, AtKeys keys) {
+ Consumer onReady;
+ if (atSign != null && keys != null) {
+ onReady = AuthenticationCommands.pkamAuthenticator(atSign, keys);
+ } else {
+ onReady = c -> {
+ };
+ }
+ return onReady;
+ }
+
+ private static ReconnectStrategy defaultIfNotSet(ReconnectStrategy reconnect) {
+ return reconnect != null ? reconnect : SimpleReconnectStrategy.builder().build();
+ }
+
+ private static long defaultIfNotSet(Long l, long defaultValue) {
+ return l != null ? l : defaultValue;
+ }
+
+}
diff --git a/at_client/src/main/java/org/atsign/client/impl/AtEndpointSupplier.java b/at_client/src/main/java/org/atsign/client/impl/AtEndpointSupplier.java
new file mode 100644
index 00000000..d141db40
--- /dev/null
+++ b/at_client/src/main/java/org/atsign/client/impl/AtEndpointSupplier.java
@@ -0,0 +1,17 @@
+package org.atsign.client.impl;
+
+import org.atsign.client.impl.exceptions.AtSecondaryNotFoundException;
+
+/**
+ * Something that is capable of supplying an endpoint string (e.g. tcp://host:port).
+ */
+public interface AtEndpointSupplier {
+
+ /**
+ *
+ * @return host and port separated by a colon symbol
+ * @throws AtSecondaryNotFoundException if it is not possible to resolve the endpoint
+ */
+ String get() throws AtSecondaryNotFoundException;
+
+}
diff --git a/at_client/src/main/java/org/atsign/client/impl/AtEndpointSuppliers.java b/at_client/src/main/java/org/atsign/client/impl/AtEndpointSuppliers.java
new file mode 100644
index 00000000..f6ce24d8
--- /dev/null
+++ b/at_client/src/main/java/org/atsign/client/impl/AtEndpointSuppliers.java
@@ -0,0 +1,98 @@
+package org.atsign.client.impl;
+
+import static org.atsign.client.impl.common.Preconditions.checkNotNull;
+
+import java.util.concurrent.TimeUnit;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+import org.atsign.client.api.AtSign;
+import org.atsign.client.impl.common.SimpleReconnectStrategy;
+import org.atsign.client.impl.netty.NettyAtEndpointSupplier;
+
+import lombok.Builder;
+
+/**
+ * Utility methods / builders for instantiating {@link AtEndpointSupplier} implementations
+ * that are included in this library.
+ * Example usage:
+ *
+ *
+ * AtEndpointSupplierBuilder builder = AtEndpointSupplier.builder()
+ * .url("vip.ve.atsign.zone:64")
+ * .atSign(createAtSign("colin"));
+ *
+ * try (AtEndpointSupplier endpoint = builder.build()) {
+ * return endpoint.get();
+ * }
+ *
+ *
+ * NOTE: If the url is proxy:host:port then the {@link AtEndpointSupplier} will always be
+ * host:port.
+ * NOTE: If the url is host:port then the {@link AtEndpointSupplier} will assume that is the
+ * endpoint
+ * for the root server, and will resolve the {@link AtSign} endpoint via the root server.
+ * NOTE: If port is not specified then it will default to {@link #ROOT_SERVER_PORT}
+ * {@link SimpleReconnectStrategy}
+ */
+public class AtEndpointSuppliers {
+
+ private static final Pattern PATTERN_PROXY_URL = Pattern.compile("proxy:(.+)");
+
+ private static final Pattern PATTERN_ROOT_URL = Pattern.compile("([^:]+)(?::(\\d+))?");
+
+ public static final String DEFAULT_ROOT_URL = "root.atsign.org:64";
+
+ public static final int ROOT_SERVER_PORT = 64;
+
+ @Builder(builderClassName = "AtEndpointSuppliersBuilder")
+ public static AtEndpointSupplier createEndpointSupplier(String url, AtSign atSign) {
+
+ url = url != null ? url : DEFAULT_ROOT_URL;
+
+ Matcher proxyMatcher = PATTERN_PROXY_URL.matcher(url);
+ if (proxyMatcher.matches()) {
+ return () -> proxyMatcher.group(1);
+ }
+
+ checkNotNull(atSign, "atSign not set");
+ Matcher rootMatcher = PATTERN_ROOT_URL.matcher(url);
+ if (rootMatcher.matches()) {
+ checkNotNull(atSign, "atSign must be set to resolve at server endpoint from " + url);
+ String hostname = rootMatcher.group(1);
+ String port = rootMatcher.group(2);
+ return NettyAtEndpointSupplier.builder()
+ .rootUrl(hostname + ":" + (port != null ? port : ROOT_SERVER_PORT))
+ .atsign(atSign)
+ .timeoutMillis(TimeUnit.SECONDS.toMillis(5))
+ .awaitReadyMillis(TimeUnit.SECONDS.toMillis(5))
+ .reconnect(SimpleReconnectStrategy.builder().maxReconnectRetries(3).build())
+ .build();
+ }
+
+ throw new IllegalArgumentException("url is invalid");
+ }
+
+ /**
+ * A builder for instantiating {@link AtEndpointSupplier} implementations that are included in
+ * this library.
+ *
+ *
+ *
+ * AtEndpointSuppliers.builder()
+ * .url(...) // the url for the root server or a proxy (optional)
+ * .atSign(...) // the AtSign that this supplier will resolve if using root server
+ * .build();
+ * }
+ *
+ *
+ * If url is not set then the builder will default to {@link #DEFAULT_ROOT_URL}.
+ */
+ public static class AtEndpointSuppliersBuilder {
+ // required for javadoc
+ }
+
+ public static boolean isProxyUrl(String s) {
+ return s != null && PATTERN_PROXY_URL.matcher(s).matches();
+ }
+}
diff --git a/at_client/src/main/java/org/atsign/client/impl/cli/AbstractCli.java b/at_client/src/main/java/org/atsign/client/impl/cli/AbstractCli.java
new file mode 100644
index 00000000..153eab1f
--- /dev/null
+++ b/at_client/src/main/java/org/atsign/client/impl/cli/AbstractCli.java
@@ -0,0 +1,131 @@
+package org.atsign.client.impl.cli;
+
+import java.io.File;
+import java.util.concurrent.TimeUnit;
+
+import org.atsign.client.api.AtCommandExecutor;
+import org.atsign.client.api.AtKeys;
+import org.atsign.client.api.AtSign;
+import org.atsign.client.impl.AtEndpointSupplier;
+import org.atsign.client.impl.AtEndpointSuppliers;
+import org.atsign.client.impl.commands.AuthenticationCommands;
+import org.atsign.client.impl.common.SimpleReconnectStrategy;
+import org.atsign.client.impl.exceptions.AtClientConfigException;
+import org.atsign.client.impl.exceptions.AtException;
+import org.atsign.client.impl.netty.NettyAtCommandExecutor;
+import org.atsign.client.impl.netty.NettyAtCommandExecutor.NettyAtCommandExecutorBuilder;
+import org.atsign.client.impl.util.KeysUtils;
+
+import picocli.CommandLine.ITypeConverter;
+import picocli.CommandLine.Option;
+
+import static org.atsign.client.api.AtSign.createAtSign;
+
+/**
+ * Base class for Command Line Interface utilities. Holds common fields such as root server
+ * the {@link AtSign} which is connecting and the file which contains the {@link AtKeys}
+ *
+ * @param used to provide fluent builder style API
+ */
+public abstract class AbstractCli> {
+
+ protected String rootUrl = "root.atsign.org";
+ protected AtSign atSign;
+ protected File keysFile;
+ protected int connectionRetries = 2;
+ private boolean verbose = false;
+
+ protected abstract T self();
+
+ public T setVerbose(boolean isVerbose) {
+ this.verbose = isVerbose;
+ return self();
+ }
+
+ public T setVerbose() {
+ return setVerbose(true);
+ }
+
+ @Option(names = {"-r", "--root"}, paramLabel = "HOST:PORT",
+ description = "atDirectory (aka root) server domain (e.g., root.atsign.org)")
+ public T setRootUrl(String rootUrl) {
+ this.rootUrl = rootUrl;
+ return self();
+ }
+
+ @Option(names = {"-a", "--atsign"}, description = "the atsign e.g. @colin", paramLabel = "ATSIGN",
+ converter = AtSignConverter.class)
+ public T setAtSign(AtSign atSign) {
+ this.atSign = atSign;
+ return self();
+ }
+
+ @Option(names = {"-k", "--keys"}, paramLabel = "PATH", description = "path to atKeys file to use / create")
+ public T setKeysFile(String path) {
+ this.keysFile = new File(path);
+ return self();
+ }
+
+ protected static File checkNotExists(File f) {
+ if (f.exists()) {
+ throw new IllegalArgumentException(f.getPath() + " would be overwritten");
+ }
+ return f;
+ }
+
+ protected static File checkExists(File f) {
+ if (!f.exists()) {
+ throw new IllegalArgumentException(f.getPath() + " not found");
+ }
+ return f;
+ }
+
+ protected static File getAtKeysFile(File keysFile, AtSign atSign) {
+ return keysFile != null ? keysFile : KeysUtils.getKeysFile(atSign);
+ }
+
+
+ protected static String ensureNotNull(String value, String defaultValue) {
+ return value != null ? value : defaultValue;
+ }
+
+ protected AtCommandExecutor createConnection(String rootUrl, AtSign atSign, int retries) throws AtException {
+ return createCommandExecutorBuilder(rootUrl, atSign, retries, verbose).build();
+ }
+
+ protected AtCommandExecutor createAuthenticatedConnection(String rootUrl, AtSign atSign, int retries)
+ throws AtException {
+ return createCommandExecutorBuilder(rootUrl, atSign, retries, verbose)
+ .onReady(AuthenticationCommands.pkamAuthenticator(atSign, getKeys()))
+ .build();
+ }
+
+ private static NettyAtCommandExecutorBuilder createCommandExecutorBuilder(String rootUrl, AtSign atSign, int retries,
+ boolean verbose) {
+ AtEndpointSupplier endpoint = AtEndpointSuppliers.builder().url(rootUrl).atSign(atSign).build();
+ SimpleReconnectStrategy reconnect = SimpleReconnectStrategy.builder()
+ .maxReconnectRetries(retries)
+ .reconnectPauseMillis(TimeUnit.SECONDS.toMillis(2))
+ .build();
+ return NettyAtCommandExecutor.builder()
+ .endpoint(endpoint)
+ .reconnect(reconnect)
+ .isVerbose(verbose);
+ }
+
+ protected AtKeys getKeys() {
+ try {
+ File file = checkExists(getAtKeysFile(keysFile, atSign));
+ return KeysUtils.loadKeys(file);
+ } catch (AtClientConfigException e) {
+ throw new RuntimeException(e);
+ }
+ }
+
+ static class AtSignConverter implements ITypeConverter {
+ @Override
+ public AtSign convert(String s) {
+ return createAtSign(s);
+ }
+ }
+}
diff --git a/at_client/src/main/java/org/atsign/client/impl/cli/Activate.java b/at_client/src/main/java/org/atsign/client/impl/cli/Activate.java
new file mode 100644
index 00000000..5bf2cc40
--- /dev/null
+++ b/at_client/src/main/java/org/atsign/client/impl/cli/Activate.java
@@ -0,0 +1,342 @@
+package org.atsign.client.impl.cli;
+
+import static org.atsign.client.impl.util.EncryptionUtils.generateAESKeyBase64;
+import static org.atsign.client.impl.util.EncryptionUtils.generateRSAKeyPair;
+import static org.atsign.client.impl.util.KeysUtils.saveKeys;
+import static org.atsign.client.impl.common.Preconditions.checkNotNull;
+
+import java.io.File;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.concurrent.Callable;
+import java.util.concurrent.TimeUnit;
+
+import org.atsign.client.api.AtKeys;
+import org.atsign.client.api.AtCommandExecutor;
+import org.atsign.client.impl.commands.EnrollCommands;
+import org.atsign.client.impl.commands.ScanCommands;
+import org.atsign.client.impl.common.EnrollmentId;
+import org.atsign.client.impl.util.KeysUtils;
+import org.atsign.client.impl.exceptions.AtEncryptionException;
+import org.atsign.client.impl.exceptions.AtUnauthenticatedException;
+
+import picocli.CommandLine;
+import picocli.CommandLine.Command;
+import picocli.CommandLine.Option;
+import picocli.CommandLine.Parameters;
+
+/**
+ * Utility (and CommandLineInterface) for onboarding and enrolling atSigns and AtSign application
+ * devices
+ */
+@Command(
+ mixinStandardHelpOptions = true)
+public class Activate extends AbstractCli implements Callable {
+
+ enum Action {
+ onboard, enroll, otp, list, approve, deny, revoke, unrevoke
+ };
+
+ public static final String DEFAULT_FIRST_APP = "firstApp";
+ public static final String DEFAULT_FIRST_DEVICE = "firstDevice";
+
+ @Parameters(index = "0", description = "onboard action to perform")
+ private Action action;
+
+ private String appName;
+ private String deviceName;
+ private boolean overwriteKeysFile = false;
+ private String cramSecret;
+ private boolean deleteCramKey = true;
+ private AtKeys keys;
+ private EnrollmentId enrollmentId;
+ private String requestStatus = "pending";
+ private String otp;
+ private Map namespaces = new LinkedHashMap<>();
+ private int completionRetries = 5;
+
+
+ public static void main(String[] args) {
+ System.exit(execute(args));
+ }
+
+ public static int execute(String[] args) {
+ return new CommandLine(new Activate())
+ .setUsageHelpWidth(80)
+ .setAllowOptionsAsOptionParameters(true)
+ .execute(args);
+ }
+
+ public Activate() {
+ // TODO replace with stack check after JDK upgrade
+ deleteCramKey = !System.getProperty("test.mode", "false").equalsIgnoreCase("true");
+ }
+
+ @Override
+ public Integer call() throws Exception {
+ switch (action) {
+ case onboard:
+ System.out.println(onboard());
+ break;
+ case otp:
+ System.out.println(otp());
+ break;
+ case enroll:
+ enroll();
+ complete(completionRetries, 1, TimeUnit.SECONDS);
+ break;
+ case list:
+ list().forEach(System.out::println);
+ break;
+ case approve:
+ approve();
+ break;
+ case deny:
+ deny();
+ break;
+ case revoke:
+ revoke();
+ break;
+ case unrevoke:
+ unrevoke();
+ break;
+ default:
+ throw new Exception("no action");
+ }
+ return 0;
+ }
+
+ @Override
+ protected Activate self() {
+ return this;
+ }
+
+ @Option(names = {"-p", "--app"}, description = "The name of the app being enrolled")
+ public Activate setAppName(String appName) {
+ this.appName = appName;
+ return self();
+ }
+
+ @Option(names = {"-d", "--device"}, description = " A name for the device on which this app is running")
+ public Activate setDeviceName(String deviceName) {
+ this.deviceName = deviceName;
+ return self();
+ }
+
+ public Activate allowOverwriteKeysFile() {
+ this.overwriteKeysFile = true;
+ return self();
+ }
+
+ @Option(names = {"-c", "--cramkey"}, description = "CRAM key")
+ public Activate setCramSecret(String cramSecret) {
+ this.cramSecret = cramSecret;
+ return self();
+ }
+
+ public Activate setNoDeleteCramKey() {
+ this.deleteCramKey = false;
+ return self();
+ }
+
+ @Option(names = {"-i", "--enrollmentId"}, description = "the ID of the enrollment request")
+ public void setEnrollmentId(String s) {
+ this.enrollmentId = EnrollmentId.createEnrollmentId(s);
+ }
+
+ @Option(names = {"-es", " --enrollmentStatus"}, description = "A specific status to filter by",
+ defaultValue = "pending")
+ public void setRequestStatus(String s) {
+ this.requestStatus = s;
+ }
+
+ @Option(names = {"-s", "--passcode"}, description = "passcode to present with this enrollment request (OTP)")
+ public Activate setOtp(String otp) {
+ this.otp = otp;
+ return self();
+ }
+
+ @Option(names = {"-n", "--namespaces"}, description = "the namespace access list as comma-separated list " +
+ "of name:value pairs e.g. \"ns:rw,contacts:rw,__manage:rw\"")
+ public Activate setNamespaces(String namespaces) {
+ for (String namespace : namespaces.split(",")) {
+ String[] parts = namespace.split(":");
+ addNamespace(parts[0], parts[1]);
+ }
+ return self();
+ }
+
+ @Option(names = {"--max-retries"}, defaultValue = "5",
+ description = " number of times to check for approval before giving up")
+ public Activate setCompletionRetries(int completionRetries) {
+ this.completionRetries = completionRetries;
+ return self();
+ }
+
+ public Activate addNamespace(String namespace, String accessControl) {
+ this.namespaces.put(namespace, accessControl);
+ return self();
+ }
+
+ public EnrollmentId onboard() throws Exception {
+ try (AtCommandExecutor executor = createConnection(rootUrl, atSign, connectionRetries)) {
+ return onboard(executor);
+ }
+ }
+
+ public EnrollmentId onboard(AtCommandExecutor executor) throws Exception {
+ File file = getAtKeysFile(keysFile, atSign);
+ if (!overwriteKeysFile) {
+ checkNotExists(file);
+ }
+ AtKeys keys = generateAtKeys(true);
+ keys = EnrollCommands.onboard(executor,
+ atSign,
+ keys,
+ cramSecret,
+ ensureNotNull(appName, DEFAULT_FIRST_APP),
+ ensureNotNull(deviceName, DEFAULT_FIRST_DEVICE),
+ deleteCramKey);
+ saveKeys(keys, file);
+ return enrollmentId;
+ }
+
+ public List list() throws Exception {
+ return list(requestStatus);
+ }
+
+ public List list(String status) throws Exception {
+ try (AtCommandExecutor executor = createAuthenticatedConnection(rootUrl, atSign, connectionRetries)) {
+ return EnrollCommands.list(executor, status);
+ }
+ }
+
+ public void approve() throws Exception {
+ approve(checkNotNull(enrollmentId, "enrollment id not set"));
+ }
+
+ public void approve(EnrollmentId enrollmentId) throws Exception {
+ try (AtCommandExecutor executor = createAuthenticatedConnection(rootUrl, atSign, connectionRetries)) {
+ EnrollCommands.approve(executor, getKeys(), enrollmentId);
+ }
+ }
+
+ public void deny() throws Exception {
+ deny(checkNotNull(enrollmentId, "enrollment id not set"));
+ }
+
+ public void deny(EnrollmentId enrollmentId) throws Exception {
+ try (AtCommandExecutor executor = createAuthenticatedConnection(rootUrl, atSign, connectionRetries)) {
+ EnrollCommands.deny(executor, enrollmentId);
+ }
+ }
+
+ public void revoke() throws Exception {
+ revoke(checkNotNull(enrollmentId, "enrollment id not set"));
+ }
+
+ public void revoke(EnrollmentId enrollmentId) throws Exception {
+ try (AtCommandExecutor executor = createAuthenticatedConnection(rootUrl, atSign, connectionRetries)) {
+ EnrollCommands.revoke(executor, enrollmentId);
+ }
+ }
+
+ public void unrevoke() throws Exception {
+ unrevoke(checkNotNull(enrollmentId, "enrollment id not set"));
+ }
+
+ public void unrevoke(EnrollmentId enrollmentId) throws Exception {
+ try (AtCommandExecutor executor = createAuthenticatedConnection(rootUrl, atSign, connectionRetries)) {
+ EnrollCommands.unrevoke(executor, enrollmentId);
+ }
+ }
+
+ public void delete(EnrollmentId enrollmentId) throws Exception {
+ try (AtCommandExecutor executor = createAuthenticatedConnection(rootUrl, atSign, connectionRetries)) {
+ delete(executor, enrollmentId);
+ }
+ }
+
+ public static void delete(AtCommandExecutor executor, EnrollmentId enrollmentId) throws Exception {
+ EnrollCommands.delete(executor, enrollmentId);
+ }
+
+ public String otp() throws Exception {
+ try (AtCommandExecutor executor = createAuthenticatedConnection(rootUrl, atSign, connectionRetries)) {
+ return EnrollCommands.otp(executor);
+ }
+ }
+
+ public List scan() throws Exception {
+ try (AtCommandExecutor executor = createConnection(rootUrl, atSign, connectionRetries)) {
+ return ScanCommands.scan(executor, true, ".*");
+ }
+ }
+
+ public EnrollmentId enroll() throws Exception {
+ try (AtCommandExecutor executor = createConnection(rootUrl, atSign, connectionRetries)) {
+ return enroll(executor);
+ }
+ }
+
+ public EnrollmentId enroll(AtCommandExecutor executor) throws Exception {
+ File file = keysFile;
+ if (!overwriteKeysFile) {
+ checkNotExists(file);
+ }
+ AtKeys keys = generateAtKeys(false);
+ keys = EnrollCommands.enroll(executor, atSign, keys, otp, appName, deviceName, namespaces);
+ KeysUtils.saveKeys(keys, keysFile);
+ return keys.getEnrollmentId();
+ }
+
+ public void complete() throws Exception {
+ try (AtCommandExecutor executor = createConnection(rootUrl, atSign, connectionRetries)) {
+ complete(executor);
+ }
+ }
+
+ public void complete(AtCommandExecutor executor) throws Exception {
+ AtKeys keys = KeysUtils.loadKeys(keysFile);
+ keys = EnrollCommands.complete(executor, atSign, keys);
+ KeysUtils.saveKeys(keys, keysFile);
+ }
+
+ public void complete(int retries, long sleepDuration, TimeUnit sleepUnit) throws Exception {
+ try (AtCommandExecutor executor = createConnection(rootUrl, atSign, connectionRetries)) {
+ complete(executor, retries, sleepDuration, sleepUnit);
+ }
+ }
+
+ public void complete(AtCommandExecutor executor, int retries, long sleepDuration, TimeUnit sleepUnit)
+ throws Exception {
+ Exception exception;
+ int remainingRetries = retries;
+ do {
+ Thread.sleep(sleepUnit.toMillis(sleepDuration));
+ try {
+ complete(executor);
+ return;
+ } catch (AtUnauthenticatedException e) {
+ exception = e.getMessage().contains("is pending") ? null : e;
+ } catch (Exception e) {
+ exception = e;
+ }
+ } while (exception == null && remainingRetries-- > 0);
+
+ throw exception != null ? exception : new IllegalArgumentException();
+ }
+
+ protected static AtKeys generateAtKeys(boolean generateEncryptionKeyPair) throws AtEncryptionException {
+ AtKeys.AtKeysBuilder builder = AtKeys.builder()
+ .selfEncryptKey(generateAESKeyBase64())
+ .apkamKeyPair(generateRSAKeyPair())
+ .apkamSymmetricKey(generateAESKeyBase64());
+ if (generateEncryptionKeyPair) {
+ builder.encryptKeyPair(generateRSAKeyPair());
+ }
+ return builder.build();
+ }
+
+}
diff --git a/at_client/src/main/java/org/atsign/client/cli/Delete.java b/at_client/src/main/java/org/atsign/client/impl/cli/Delete.java
similarity index 61%
rename from at_client/src/main/java/org/atsign/client/cli/Delete.java
rename to at_client/src/main/java/org/atsign/client/impl/cli/Delete.java
index c52372ab..88545ee1 100644
--- a/at_client/src/main/java/org/atsign/client/cli/Delete.java
+++ b/at_client/src/main/java/org/atsign/client/impl/cli/Delete.java
@@ -1,13 +1,16 @@
-package org.atsign.client.cli;
+package org.atsign.client.impl.cli;
import org.atsign.client.api.AtClient;
import org.atsign.client.api.AtKeys;
-import org.atsign.client.util.KeysUtil;
-import org.atsign.common.AtSign;
-import org.atsign.common.Keys;
+import org.atsign.client.impl.AtClients;
+import org.atsign.client.impl.util.KeysUtils;
+import org.atsign.client.api.AtSign;
+import org.atsign.client.api.Keys;
import lombok.extern.slf4j.Slf4j;
+import static org.atsign.client.api.AtSign.createAtSign;
+
/**
* A command-line interface half-example half-utility to delete something that was previously shared
*/
@@ -23,14 +26,14 @@ public static void main(String[] args) throws Exception {
}
String rootUrl = args[0];
- AtSign atSign = new AtSign(args[1]);
- AtSign otherAtSign = new AtSign(args[2]);
+ AtSign atSign = createAtSign(args[1]);
+ AtSign otherAtSign = createAtSign(args[2]);
String keyName = args[3];
// all AtClients require AtKeys, this loads them based on the AtSign from the default location
- AtKeys keys = KeysUtil.loadKeys(atSign);
+ AtKeys keys = KeysUtils.loadKeys(atSign);
- try (AtClient atClient = AtClient.withRemoteSecondary(rootUrl, atSign, keys, true)) {
+ try (AtClient atClient = AtClients.builder().url(rootUrl).atSign(atSign).keys(keys).build()) {
Keys.SharedKey key = Keys.sharedKeyBuilder()
.sharedBy(atSign)
@@ -38,9 +41,7 @@ public static void main(String[] args) throws Exception {
.name(keyName)
.build();
- String response = atClient.delete(key).get();
-
- log.info("delete response : {}", response);
+ atClient.delete(key).get();
}
}
}
diff --git a/at_client/src/main/java/org/atsign/client/impl/cli/DumpKeys.java b/at_client/src/main/java/org/atsign/client/impl/cli/DumpKeys.java
new file mode 100644
index 00000000..c3c1060b
--- /dev/null
+++ b/at_client/src/main/java/org/atsign/client/impl/cli/DumpKeys.java
@@ -0,0 +1,21 @@
+package org.atsign.client.impl.cli;
+
+import lombok.extern.slf4j.Slf4j;
+import org.atsign.client.api.AtKeys;
+import org.atsign.client.impl.util.KeysUtils;
+import org.atsign.client.api.AtSign;
+
+import static org.atsign.client.api.AtSign.createAtSign;
+
+/**
+ * Utility which, given an {@link AtSign} will load {@link AtKeys} from the
+ * default location and dump the contents to stdout
+ */
+@Slf4j
+public class DumpKeys {
+ public static void main(String[] args) throws Exception {
+ AtSign atSign = createAtSign(args[0]);
+ AtKeys keys = KeysUtils.loadKeys(atSign);
+ System.out.println(KeysUtils.dump(keys));
+ }
+}
diff --git a/at_client/src/main/java/org/atsign/client/cli/Get.java b/at_client/src/main/java/org/atsign/client/impl/cli/Get.java
similarity index 61%
rename from at_client/src/main/java/org/atsign/client/cli/Get.java
rename to at_client/src/main/java/org/atsign/client/impl/cli/Get.java
index afc23334..533210b5 100644
--- a/at_client/src/main/java/org/atsign/client/cli/Get.java
+++ b/at_client/src/main/java/org/atsign/client/impl/cli/Get.java
@@ -1,13 +1,16 @@
-package org.atsign.client.cli;
+package org.atsign.client.impl.cli;
import org.atsign.client.api.AtClient;
import org.atsign.client.api.AtKeys;
-import org.atsign.client.util.KeysUtil;
-import org.atsign.common.AtSign;
-import org.atsign.common.Keys;
+import org.atsign.client.impl.AtClients;
+import org.atsign.client.impl.util.KeysUtils;
+import org.atsign.client.api.AtSign;
+import org.atsign.client.api.Keys;
import lombok.extern.slf4j.Slf4j;
+import static org.atsign.client.api.AtSign.createAtSign;
+
/**
* A command-line interface half-example half-utility to get something that was shared by another
* atSign
@@ -23,14 +26,14 @@ public static void main(String[] args) throws Exception {
}
String rootUrl = args[0];
- AtSign atSign = new AtSign(args[1]);
- AtSign otherAtSign = new AtSign(args[2]);
+ AtSign atSign = createAtSign(args[1]);
+ AtSign otherAtSign = createAtSign(args[2]);
String keyName = args[3];
// all AtClients require AtKeys, this loads them based on the AtSign from the default location
- AtKeys keys = KeysUtil.loadKeys(atSign);
+ AtKeys keys = KeysUtils.loadKeys(atSign);
- try (AtClient atClient = AtClient.withRemoteSecondary(rootUrl, atSign, keys, true)) {
+ try (AtClient atClient = AtClients.builder().url(rootUrl).atSign(atSign).keys(keys).build()) {
Keys.SharedKey key = Keys.sharedKeyBuilder()
.sharedBy(otherAtSign)
@@ -40,7 +43,7 @@ public static void main(String[] args) throws Exception {
String response = atClient.get(key).get();
- log.info("get response : {}", response);
+ System.out.printf("get response : %s", response);
}
}
}
diff --git a/at_client/src/main/java/org/atsign/client/cli/Scan.java b/at_client/src/main/java/org/atsign/client/impl/cli/Scan.java
similarity index 86%
rename from at_client/src/main/java/org/atsign/client/cli/Scan.java
rename to at_client/src/main/java/org/atsign/client/impl/cli/Scan.java
index 74ec7013..90eef338 100644
--- a/at_client/src/main/java/org/atsign/client/cli/Scan.java
+++ b/at_client/src/main/java/org/atsign/client/impl/cli/Scan.java
@@ -1,4 +1,4 @@
-package org.atsign.client.cli;
+package org.atsign.client.impl.cli;
import java.io.PrintStream;
import java.util.List;
@@ -6,14 +6,17 @@
import org.atsign.client.api.AtClient;
import org.atsign.client.api.AtKeys;
-import org.atsign.client.util.KeysUtil;
-import org.atsign.client.util.StringUtil;
-import org.atsign.common.AtSign;
-import org.atsign.common.Keys.AtKey;
-import org.atsign.common.Metadata;
+import org.atsign.client.impl.AtClients;
+import org.atsign.client.impl.util.KeysUtils;
+import org.atsign.client.impl.util.StringUtils;
+import org.atsign.client.api.AtSign;
+import org.atsign.client.api.Keys.AtKey;
+import org.atsign.client.api.Metadata;
import lombok.extern.slf4j.Slf4j;
+import static org.atsign.client.api.AtSign.createAtSign;
+
/**
* A command-line interface for scanning keys in your secondary (must have keys to atSign in keys/)
*/
@@ -27,13 +30,13 @@ public static void main(String[] args) throws Exception {
}
String rootUrl = args[0];
- AtSign atSign = new AtSign(args[1]);
+ AtSign atSign = createAtSign(args[1]);
String regex = args[2];
// all AtClients require AtKeys, this loads them based on the AtSign from the default location
- AtKeys keys = KeysUtil.loadKeys(atSign);
+ AtKeys keys = KeysUtils.loadKeys(atSign);
- try (AtClient atClient = AtClient.withRemoteSecondary(rootUrl, atSign, keys, true)) {
+ try (AtClient atClient = AtClients.builder().url(rootUrl).atSign(atSign).keys(keys).build()) {
List response = atClient.getAtKeys(regex).get();
@@ -43,7 +46,7 @@ public static void main(String[] args) throws Exception {
System.out.println();
System.out.println("Enter index you want to llookup (l to list, q to quit):");
input = scanner.nextLine();
- if (StringUtil.isNumeric(input)) {
+ if (StringUtils.isNumeric(input)) {
int index = Integer.parseInt(input);
if (index < response.size()) {
printKeyInfo(response.get(index), System.out);
diff --git a/at_client/src/main/java/org/atsign/client/cli/Share.java b/at_client/src/main/java/org/atsign/client/impl/cli/Share.java
similarity index 65%
rename from at_client/src/main/java/org/atsign/client/cli/Share.java
rename to at_client/src/main/java/org/atsign/client/impl/cli/Share.java
index f0866d23..7cba0362 100644
--- a/at_client/src/main/java/org/atsign/client/cli/Share.java
+++ b/at_client/src/main/java/org/atsign/client/impl/cli/Share.java
@@ -1,10 +1,13 @@
-package org.atsign.client.cli;
+package org.atsign.client.impl.cli;
+
+import static org.atsign.client.api.AtSign.createAtSign;
import org.atsign.client.api.AtClient;
import org.atsign.client.api.AtKeys;
-import org.atsign.client.util.KeysUtil;
-import org.atsign.common.AtSign;
-import org.atsign.common.Keys;
+import org.atsign.client.impl.AtClients;
+import org.atsign.client.impl.util.KeysUtils;
+import org.atsign.client.api.AtSign;
+import org.atsign.client.api.Keys;
import lombok.extern.slf4j.Slf4j;
@@ -22,16 +25,16 @@ public static void main(String[] args) throws Exception {
}
String rootUrl = args[0];
- AtSign atSign = new AtSign(args[1]);
- AtSign otherAtSign = new AtSign(args[2]);
+ AtSign atSign = createAtSign(args[1]);
+ AtSign otherAtSign = createAtSign(args[2]);
String keyName = args[3];
String toShare = args[4];
int ttr = args.length == 6 ? Integer.parseInt(args[5]) : 0;
// all AtClients require AtKeys, this loads them based on the AtSign from the default location
- AtKeys keys = KeysUtil.loadKeys(atSign);
+ AtKeys keys = KeysUtils.loadKeys(atSign);
- try (AtClient atClient = AtClient.withRemoteSecondary(rootUrl, atSign, keys, true)) {
+ try (AtClient atClient = AtClients.builder().url(rootUrl).atSign(atSign).keys(keys).build()) {
Keys.SharedKey key = Keys.sharedKeyBuilder()
.sharedBy(atSign)
.sharedWith(otherAtSign)
@@ -40,9 +43,7 @@ public static void main(String[] args) throws Exception {
.ttr((long) ttr)
.build();
- String response = atClient.put(key, toShare).get();
-
- log.info("put response : {}", response);
+ atClient.put(key, toShare).get();
}
}
diff --git a/at_client/src/main/java/org/atsign/client/impl/cli/register/ActivateAtsignWithSuperApiKey.java b/at_client/src/main/java/org/atsign/client/impl/cli/register/ActivateAtsignWithSuperApiKey.java
new file mode 100644
index 00000000..9348188b
--- /dev/null
+++ b/at_client/src/main/java/org/atsign/client/impl/cli/register/ActivateAtsignWithSuperApiKey.java
@@ -0,0 +1,30 @@
+package org.atsign.client.impl.cli.register;
+
+import org.atsign.client.impl.exceptions.AtRegistrarException;
+
+import java.util.Map;
+
+import static org.atsign.client.api.AtSign.createAtSign;
+
+class ActivateAtsignWithSuperApiKey extends RegisterApiTask>> {
+ @Override
+ public RegisterApiResult> run() {
+ try {
+ result.data.put(
+ "cram", registerUtil
+ .activateAtsignWithSuperApiKey(params.get("registrarUrl"), params.get("apiKey"),
+ createAtSign(params.get("atSign")),
+ params.get("ActivationKey"))
+ .split(":")[1]);
+ result.apiCallStatus = ApiCallStatus.success;
+ System.out.println("Your cram secret: " + result.data.get("cram"));
+ } catch (AtRegistrarException e) {
+ result.atException = e;
+ result.apiCallStatus = retryCount < maxRetries ? ApiCallStatus.retry : ApiCallStatus.failure;
+ } catch (Exception e) {
+ result.atException = new AtRegistrarException("Failed while activating atSign", e);
+ result.apiCallStatus = retryCount < maxRetries ? ApiCallStatus.retry : ApiCallStatus.failure;
+ }
+ return result;
+ }
+}
diff --git a/at_client/src/main/java/org/atsign/common/ApiCallStatus.java b/at_client/src/main/java/org/atsign/client/impl/cli/register/ApiCallStatus.java
similarity index 68%
rename from at_client/src/main/java/org/atsign/common/ApiCallStatus.java
rename to at_client/src/main/java/org/atsign/client/impl/cli/register/ApiCallStatus.java
index af18a7f6..ccada8f3 100644
--- a/at_client/src/main/java/org/atsign/common/ApiCallStatus.java
+++ b/at_client/src/main/java/org/atsign/client/impl/cli/register/ApiCallStatus.java
@@ -1,4 +1,4 @@
-package org.atsign.common;
+package org.atsign.client.impl.cli.register;
/**
* Registrar API call status
diff --git a/at_client/src/main/java/org/atsign/config/ConfigReader.java b/at_client/src/main/java/org/atsign/client/impl/cli/register/ConfigReader.java
similarity index 96%
rename from at_client/src/main/java/org/atsign/config/ConfigReader.java
rename to at_client/src/main/java/org/atsign/client/impl/cli/register/ConfigReader.java
index c36f63fa..6bb792bc 100644
--- a/at_client/src/main/java/org/atsign/config/ConfigReader.java
+++ b/at_client/src/main/java/org/atsign/client/impl/cli/register/ConfigReader.java
@@ -1,4 +1,4 @@
-package org.atsign.config;
+package org.atsign.client.impl.cli.register;
import java.io.IOException;
import java.io.InputStream;
diff --git a/at_client/src/main/java/org/atsign/client/util/Constants.java b/at_client/src/main/java/org/atsign/client/impl/cli/register/Constants.java
similarity index 90%
rename from at_client/src/main/java/org/atsign/client/util/Constants.java
rename to at_client/src/main/java/org/atsign/client/impl/cli/register/Constants.java
index 7f993ede..fd1c19f4 100644
--- a/at_client/src/main/java/org/atsign/client/util/Constants.java
+++ b/at_client/src/main/java/org/atsign/client/impl/cli/register/Constants.java
@@ -1,4 +1,4 @@
-package org.atsign.client.util;
+package org.atsign.client.impl.cli.register;
/**
* Atsign Platform constants
diff --git a/at_client/src/main/java/org/atsign/client/impl/cli/register/GetFreeAtsign.java b/at_client/src/main/java/org/atsign/client/impl/cli/register/GetFreeAtsign.java
new file mode 100644
index 00000000..e245d531
--- /dev/null
+++ b/at_client/src/main/java/org/atsign/client/impl/cli/register/GetFreeAtsign.java
@@ -0,0 +1,25 @@
+package org.atsign.client.impl.cli.register;
+
+import org.atsign.client.impl.exceptions.AtRegistrarException;
+
+import java.util.Map;
+
+class GetFreeAtsign extends RegisterApiTask>> {
+
+ @Override
+ public RegisterApiResult> run() {
+ System.out.println("Fetching free atsign ...");
+ try {
+ result.data.put("atSign",
+ registerUtil.getFreeAtsign(params.get("registrarUrl"), params.get("apiKey")));
+ result.apiCallStatus = ApiCallStatus.success;
+ System.out.println("\tFetched createAtSign: " + "@" + result.data.get("atSign"));
+ } catch (AtRegistrarException e) {
+ result.atException = e;
+ } catch (Exception e) {
+ result.atException = new AtRegistrarException("error while getting free atsign", e);
+ result.apiCallStatus = retryCount < maxRetries ? ApiCallStatus.retry : ApiCallStatus.failure;
+ }
+ return result;
+ }
+}
diff --git a/at_client/src/main/java/org/atsign/client/impl/cli/register/GetFreeAtsignWithSuperApiKey.java b/at_client/src/main/java/org/atsign/client/impl/cli/register/GetFreeAtsignWithSuperApiKey.java
new file mode 100644
index 00000000..1f3c011c
--- /dev/null
+++ b/at_client/src/main/java/org/atsign/client/impl/cli/register/GetFreeAtsignWithSuperApiKey.java
@@ -0,0 +1,24 @@
+package org.atsign.client.impl.cli.register;
+
+import org.atsign.client.impl.exceptions.AtRegistrarException;
+
+import java.util.Map;
+
+class GetFreeAtsignWithSuperApiKey extends RegisterApiTask>> {
+ @Override
+ public RegisterApiResult> run() {
+ System.out.println("Getting atSign ...");
+ try {
+ result.data.putAll(registerUtil.getAtsignWithSuperApiKey(params.get("registrarUrl"), params.get("apiKey")));
+ System.out.println("Got atsign: " + result.data.get("atSign"));
+ result.apiCallStatus = ApiCallStatus.success;
+ } catch (AtRegistrarException e) {
+ result.atException = e;
+ result.apiCallStatus = retryCount < maxRetries ? ApiCallStatus.retry : ApiCallStatus.failure;
+ } catch (Exception e) {
+ result.atException = new AtRegistrarException("Failed while getting atSign", e);
+ result.apiCallStatus = retryCount < maxRetries ? ApiCallStatus.retry : ApiCallStatus.failure;
+ }
+ return result;
+ }
+}
diff --git a/at_client/src/main/java/org/atsign/common/NotificationStatus.java b/at_client/src/main/java/org/atsign/client/impl/cli/register/NotificationStatus.java
similarity index 70%
rename from at_client/src/main/java/org/atsign/common/NotificationStatus.java
rename to at_client/src/main/java/org/atsign/client/impl/cli/register/NotificationStatus.java
index 94ae4931..f966e692 100644
--- a/at_client/src/main/java/org/atsign/common/NotificationStatus.java
+++ b/at_client/src/main/java/org/atsign/client/impl/cli/register/NotificationStatus.java
@@ -1,4 +1,4 @@
-package org.atsign.common;
+package org.atsign.client.impl.cli.register;
/**
* Notification status
diff --git a/at_client/src/main/java/org/atsign/client/impl/cli/register/Register.java b/at_client/src/main/java/org/atsign/client/impl/cli/register/Register.java
new file mode 100644
index 00000000..0a54be68
--- /dev/null
+++ b/at_client/src/main/java/org/atsign/client/impl/cli/register/Register.java
@@ -0,0 +1,121 @@
+package org.atsign.client.impl.cli.register;
+
+import java.io.IOException;
+import java.util.*;
+import java.util.concurrent.Callable;
+
+import org.atsign.client.impl.exceptions.AtException;
+import org.atsign.client.impl.cli.Activate;
+
+import picocli.CommandLine;
+import picocli.CommandLine.Command;
+import picocli.CommandLine.Option;
+
+/**
+ * Command line interface to claim a free atsign. Requires one-time-password
+ * received on the provided email to validate.
+ * Registers the free atsign to provided email
+ */
+@Command(name = "register", description = "Get an atsign and register")
+public class Register implements Callable {
+ @Option(names = {"-e", "--email"}, description = "email to register a free atsign using otp-auth")
+ static String email = "";
+
+ @Option(names = {"-k", "--api-key"}, description = "register an atsign using super-API key")
+ static String apiKey = "";
+
+ Map params = new HashMap<>();
+ boolean superApiKeyMode = false;
+
+ public static void main(String[] args) throws AtException {
+ int status = new CommandLine(new Register()).execute(args);
+ System.exit(status);
+ }
+
+ /**
+ * contains actual register logic.
+ * main() calls this method with args passed through CLI
+ */
+ @Override
+ public String call() throws Exception {
+
+ readParameters();
+ if (superApiKeyMode) {
+ new RegistrationFlow(params).add(new GetFreeAtsignWithSuperApiKey()).add(new ActivateAtsignWithSuperApiKey())
+ .start();
+
+ } else {
+ // parameter confirmation needs to be manually inserted into the params map
+ params.put("confirmation", "false");
+ new RegistrationFlow(params).add(new GetFreeAtsign()).add(new RegisterAtsign()).add(new ValidateOtp()).start();
+ }
+
+ String[] onboardArgs = new String[] {
+ "onboard",
+ "-r", params.get("rootDomain") + ":" + params.get("rootPort"),
+ "-a", params.get("atSign"),
+ "-c", params.get("cram")};
+ Activate.main(onboardArgs);
+
+ return "Done.";
+ }
+
+ void readParameters() throws IOException {
+
+ // checks to ensure only either of email or super-API key are provided as args.
+ // if super-API key is provided sets superApiKeyMode to true
+ if ("".equals(email) && !"".equals(apiKey)) {
+ superApiKeyMode = true;
+ } else if ("".equals(apiKey) && !"".equals(email)) {
+ superApiKeyMode = false;
+ } else {
+ System.err.println(
+ "Usage: Register -e (or)\nRegister -k "
+ + "\nNOTE: Use email if you prefer activating using verification code."
+ + " Use API key option if you have a SuperAPI key. You can NOT use both.");
+ System.exit(1);
+ }
+
+ params.put("rootDomain", ConfigReader.getProperty("rootServer", "domain"));
+ if (params.get("rootDomain") == null) {
+ // reading config from older configuration syntax for backwards compatibility
+ params.put("rootDomain", ConfigReader.getProperty("ROOT_DOMAIN"));
+ }
+
+ params.put("rootPort", ConfigReader.getProperty("rootServer", "port"));
+ if (params.get("rootPort") == null) {
+ // reading config from older configuration syntax for backwards compatibility
+ params.put("rootPort", ConfigReader.getProperty("ROOT_PORT"));
+ }
+ System.out.println("RootServer is " + params.get("rootDomain") + ":" + params.get("rootPort"));
+
+ params.put("registrarUrl", ConfigReader.getProperty("registrarV3", "url"));
+ if (params.get("registrarUrl") == null) {
+ // reading config from older configuration syntax for backwards compatibility
+ params.put("registrarUrl", ConfigReader.getProperty("REGISTRAR_URL"));
+ }
+
+ if (!superApiKeyMode && "".equals(apiKey)) {
+ params.put("apiKey", ConfigReader.getProperty("registrar", "apiKey"));
+ if (params.get("apiKey") == null) {
+ // reading config from older configuration syntax for backwards compatibility
+ params.put("apiKey", ConfigReader.getProperty("API_KEY"));
+ }
+ }
+
+ // adding email/apiKey to params whichever is passed through command line args
+ if (!superApiKeyMode) {
+ params.put("email", email);
+ } else {
+ params.put("apiKey", apiKey);
+ }
+
+ // ensure all required params have been set
+ if (!params.containsKey("rootDomain") || !params.containsKey("rootPort") || !params.containsKey("registrarUrl")
+ || !params.containsKey("apiKey")) {
+ System.err.println(
+ "Please make sure to set all relevant configuration in src/main/resources/config.yaml");
+ System.exit(1);
+ }
+ }
+}
diff --git a/at_client/src/main/java/org/atsign/common/RegisterApiResult.java b/at_client/src/main/java/org/atsign/client/impl/cli/register/RegisterApiResult.java
similarity index 69%
rename from at_client/src/main/java/org/atsign/common/RegisterApiResult.java
rename to at_client/src/main/java/org/atsign/client/impl/cli/register/RegisterApiResult.java
index 65c0775c..3b6bce68 100644
--- a/at_client/src/main/java/org/atsign/common/RegisterApiResult.java
+++ b/at_client/src/main/java/org/atsign/client/impl/cli/register/RegisterApiResult.java
@@ -1,4 +1,6 @@
-package org.atsign.common;
+package org.atsign.client.impl.cli.register;
+
+import org.atsign.client.impl.exceptions.AtException;
/**
* Registrar API response record
diff --git a/at_client/src/main/java/org/atsign/common/RegisterApiTask.java b/at_client/src/main/java/org/atsign/client/impl/cli/register/RegisterApiTask.java
similarity index 95%
rename from at_client/src/main/java/org/atsign/common/RegisterApiTask.java
rename to at_client/src/main/java/org/atsign/client/impl/cli/register/RegisterApiTask.java
index 62f58dae..d7f6c563 100644
--- a/at_client/src/main/java/org/atsign/common/RegisterApiTask.java
+++ b/at_client/src/main/java/org/atsign/client/impl/cli/register/RegisterApiTask.java
@@ -1,10 +1,8 @@
-package org.atsign.common;
+package org.atsign.client.impl.cli.register;
import java.util.HashMap;
import java.util.Map;
-import org.atsign.client.util.RegisterUtil;
-
/**
* Represents a task in an AtSign registration cycle
*
diff --git a/at_client/src/main/java/org/atsign/client/impl/cli/register/RegisterAtsign.java b/at_client/src/main/java/org/atsign/client/impl/cli/register/RegisterAtsign.java
new file mode 100644
index 00000000..56c301d1
--- /dev/null
+++ b/at_client/src/main/java/org/atsign/client/impl/cli/register/RegisterAtsign.java
@@ -0,0 +1,26 @@
+package org.atsign.client.impl.cli.register;
+
+import org.atsign.client.impl.exceptions.AtRegistrarException;
+
+import java.util.Map;
+
+import static org.atsign.client.api.AtSign.createAtSign;
+
+class RegisterAtsign extends RegisterApiTask>> {
+
+ @Override
+ public RegisterApiResult> run() {
+ System.out.println("Sending verification code to: " + params.get("email"));
+ try {
+ result.data.put("otpSent",
+ registerUtil.registerAtsign(params.get("email"), createAtSign(params.get("atSign")),
+ params.get("registrarUrl"), params.get("apiKey"))
+ .toString());
+ result.apiCallStatus = ApiCallStatus.success;
+ } catch (Exception e) {
+ result.atException = new AtRegistrarException(e.getMessage(), e.getCause());
+ result.apiCallStatus = retryCount < maxRetries ? ApiCallStatus.retry : ApiCallStatus.failure;
+ }
+ return result;
+ }
+}
diff --git a/at_client/src/main/java/org/atsign/client/util/RegisterUtil.java b/at_client/src/main/java/org/atsign/client/impl/cli/register/RegisterUtil.java
similarity index 90%
rename from at_client/src/main/java/org/atsign/client/util/RegisterUtil.java
rename to at_client/src/main/java/org/atsign/client/impl/cli/register/RegisterUtil.java
index 8b932cfd..9bfb9359 100644
--- a/at_client/src/main/java/org/atsign/client/util/RegisterUtil.java
+++ b/at_client/src/main/java/org/atsign/client/impl/cli/register/RegisterUtil.java
@@ -1,7 +1,8 @@
-package org.atsign.client.util;
+package org.atsign.client.impl.cli.register;
import static java.util.AbstractMap.SimpleEntry;
import static java.util.stream.Collectors.toMap;
+import static org.atsign.client.api.AtSign.createAtSign;
import java.io.BufferedReader;
import java.io.IOException;
@@ -16,20 +17,16 @@
import javax.net.ssl.HttpsURLConnection;
-import org.atsign.common.AtException;
-import org.atsign.common.AtSign;
-import org.atsign.common.Json;
-import org.atsign.common.exceptions.AtRegistrarException;
-
-import com.fasterxml.jackson.databind.ObjectMapper;
+import org.atsign.client.api.AtSign;
+import org.atsign.client.impl.exceptions.AtException;
+import org.atsign.client.impl.exceptions.AtRegistrarException;
+import org.atsign.client.impl.util.JsonUtils;
/**
* Utility class for obtaining a new {@link AtSign}
*/
public class RegisterUtil {
- ObjectMapper objectMapper = Json.MAPPER;
-
/**
* Calls API to get atsigns which are ready to be claimed. Returns a free atsign.
*
@@ -49,7 +46,7 @@ public String getFreeAtsign(String registrarUrl, String apiKey) throws AtExcepti
BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(connection.getInputStream()));
String response = bufferedReader.readLine();
@SuppressWarnings("unchecked")
- Map> responseData = objectMapper.readValue(response, Map.class);
+ Map> responseData = JsonUtils.readValue(response, Map.class);
Map data = responseData.get("data");
return data.get("atsign");
} else {
@@ -89,22 +86,22 @@ public Map getAtsignWithSuperApiKey(String registrarUrl, String
throws AtException, IOException {
Map paramsMap = new HashMap<>();
if (!atsign.isEmpty()) {
- paramsMap.put("atSign", new AtSign(atsign).withoutPrefix());
+ paramsMap.put("atSign", createAtSign(atsign).withoutPrefix());
}
if (!activationKey.isEmpty()) {
paramsMap.put("ActivationKey", activationKey);
}
- String paramsJson = objectMapper.writeValueAsString(paramsMap);
+ String paramsJson = JsonUtils.writeValueAsString(paramsMap);
HttpsURLConnection httpsConnection =
postRequestToAPI(new URL(registrarUrl + Constants.GET_ATSIGN_V3), apiKey, paramsJson);
if (httpsConnection.getResponseCode() == HttpsURLConnection.HTTP_OK) {
BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(httpsConnection.getInputStream()));
String responseRaw = bufferedReader.readLine();
@SuppressWarnings("unchecked")
- Map responseData = objectMapper.readValue(responseRaw, Map.class);
+ Map responseData = JsonUtils.readValue(responseRaw, Map.class);
if (responseData.get("status").equals("success")) {
@SuppressWarnings("unchecked")
- Map> responseDataMap = objectMapper.readValue(responseRaw, Map.class);
+ Map> responseDataMap = JsonUtils.readValue(responseRaw, Map.class);
return responseDataMap.get("value");
} else {
throw new AtRegistrarException("Failed getting atsign. Response from API: " + responseData.get("status"));
@@ -132,7 +129,7 @@ public Boolean registerAtsign(String email, AtSign atsign, String registrarUrl,
new SimpleEntry<>("atsign", atsign.withoutPrefix()),
new SimpleEntry<>("email", email))
.collect(toMap(SimpleEntry::getKey, SimpleEntry::getValue));
- String paramsJson = objectMapper.writeValueAsString(paramsMap);
+ String paramsJson = JsonUtils.writeValueAsString(paramsMap);
HttpsURLConnection httpsConnection =
postRequestToAPI(new URL(registrarUrl + Constants.REGISTER_ATSIGN), apiKey, paramsJson);
@@ -140,7 +137,7 @@ public Boolean registerAtsign(String email, AtSign atsign, String registrarUrl,
BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(httpsConnection.getInputStream()));
String response = bufferedReader.readLine();
@SuppressWarnings("unchecked")
- Map responseData = objectMapper.readValue(response, Map.class);
+ Map responseData = JsonUtils.readValue(response, Map.class);
String data = responseData.get("message");
System.out.println("\tVerification code: " + data);
return response.contains("Sent Successfully");
@@ -164,10 +161,11 @@ public Boolean registerAtsign(String email, AtSign atsign, String registrarUrl,
* will return list of already existing atsigns. If the user already has existing atsigns
* user will have to select a listed atsign old/new and place a second call to the same API
* endpoint with confirmation set to true with previously received OTP. The second follow-up
- * call is automated by this client using new atsign for user simplicity
+ * call is automated by this client using createAtSign for user simplicity
* @return Case 1("verified") - the API has registered the atsign to provided email and CRAM key
* present in HTTP_RESPONSE Body. Case 2("follow-up"): User already has existing atsigns and
- * new atsign registered successfully. To receive the CRAM key, follow-up by calling the API
+ * createAtSign registered successfully. To receive the CRAM key, follow-up by calling the
+ * API
* with one of the existing listed atsigns, with confirmation set to true. Case 3("retry"):
* Incorrect OTP send request again with correct OTP.
* @throws IOException thrown if anything goes wrong while using the HttpsURLConnection.
@@ -183,7 +181,7 @@ public String validateOtp(String email, AtSign atsign, String otp, String regist
new SimpleEntry<>("otp", otp),
new SimpleEntry<>("confirmation", confirmation.toString()))
.collect(toMap(SimpleEntry::getKey, SimpleEntry::getValue));
- String paramsJson = objectMapper.writeValueAsString(paramsMap);
+ String paramsJson = JsonUtils.writeValueAsString(paramsMap);
HttpsURLConnection httpsConnection =
postRequestToAPI(new URL(registrarUrl + Constants.VALIDATE_OTP), apiKey, paramsJson);
@@ -194,20 +192,19 @@ public String validateOtp(String email, AtSign atsign, String otp, String regist
// appending HTTP_RESPONSE to the string buffer line-after-line
String response = bufferedReader.readLine();
@SuppressWarnings("unchecked")
- Map responseDataStringObject = objectMapper.readValue(response, Map.class);
+ Map responseDataStringObject = JsonUtils.readValue(response, Map.class);
// API in some cases returns response with a data field of Type
// Map> the following if condition casts this
// response to Map
if (response.startsWith("{\"data")) {
@SuppressWarnings("unchecked")
- Map> responseDataMapObject = objectMapper.readValue(response, Map.class);
+ Map> responseDataMapObject = JsonUtils.readValue(response, Map.class);
responseDataStringObject = responseDataMapObject.get("data");
// The following if condition logs the existing atsigns if the API response
// contains a List of atsigns.
if (responseDataStringObject.containsKey("atsigns") || responseDataStringObject.containsKey("newAtsign")) {
@SuppressWarnings("unchecked")
- Map>> responseDataArrayListObject =
- objectMapper.readValue(response, Map.class);
+ Map>> responseDataArrayListObject = JsonUtils.readValue(response, Map.class);
System.out
.println("Your existing atsigns: " + responseDataArrayListObject.get("data").get("atsigns").toString());
}
@@ -249,14 +246,14 @@ public String activateAtsignWithSuperApiKey(String registrarUrl, String apiKey,
.of(new SimpleEntry<>("atSign", atsign.withoutPrefix()), new SimpleEntry<>("ActivationKey", activationKey))
.collect(toMap(SimpleEntry::getKey, SimpleEntry::getValue));
- String paramsJson = objectMapper.writeValueAsString(paramsMap);
+ String paramsJson = JsonUtils.writeValueAsString(paramsMap);
HttpsURLConnection httpsConnection =
postRequestToAPI(new URL(registrarUrl + Constants.ACTIVATE_ATSIGN), apiKey, paramsJson);
if (httpsConnection.getResponseCode() == HttpsURLConnection.HTTP_OK) {
BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(httpsConnection.getInputStream()));
String response = bufferedReader.readLine();
@SuppressWarnings("unchecked")
- Map responseData = objectMapper.readValue(response, Map.class);
+ Map responseData = JsonUtils.readValue(response, Map.class);
if (responseData.get("status").equals("success")) {
return responseData.get("cramkey");
} else {
diff --git a/at_client/src/main/java/org/atsign/client/impl/cli/register/RegistrationFlow.java b/at_client/src/main/java/org/atsign/client/impl/cli/register/RegistrationFlow.java
new file mode 100644
index 00000000..67df5dfa
--- /dev/null
+++ b/at_client/src/main/java/org/atsign/client/impl/cli/register/RegistrationFlow.java
@@ -0,0 +1,41 @@
+package org.atsign.client.impl.cli.register;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Map;
+
+class RegistrationFlow {
+ List>>> processFlow = new ArrayList<>();
+ RegisterApiResult> result;
+ Map params;
+ RegisterUtil registerUtil = new RegisterUtil();
+
+ RegistrationFlow(Map params) {
+ this.params = params;
+ }
+
+ RegistrationFlow add(RegisterApiTask>> task) {
+ processFlow.add(task);
+ return this;
+ }
+
+ void start() throws Exception {
+ for (RegisterApiTask>> task : processFlow) {
+ // initialize each task by passing params to init()
+ task.init(params, registerUtil);
+ result = task.run();
+ if (result.apiCallStatus.equals(ApiCallStatus.retry)) {
+ while (task.shouldRetry()
+ && result.apiCallStatus.equals(ApiCallStatus.retry)) {
+ result = task.run();
+ task.retryCount++;
+ }
+ }
+ if (result.apiCallStatus.equals(ApiCallStatus.success)) {
+ params.putAll(result.data);
+ } else {
+ throw result.atException;
+ }
+ }
+ }
+}
diff --git a/at_client/src/main/java/org/atsign/client/impl/cli/register/ValidateOtp.java b/at_client/src/main/java/org/atsign/client/impl/cli/register/ValidateOtp.java
new file mode 100644
index 00000000..82f71b70
--- /dev/null
+++ b/at_client/src/main/java/org/atsign/client/impl/cli/register/ValidateOtp.java
@@ -0,0 +1,51 @@
+package org.atsign.client.impl.cli.register;
+
+import org.atsign.client.impl.exceptions.AtRegistrarException;
+
+import java.util.Map;
+import java.util.Scanner;
+
+import static org.atsign.client.api.AtSign.createAtSign;
+
+class ValidateOtp extends RegisterApiTask>> {
+ Scanner scanner = new Scanner(System.in);
+
+ @Override
+ public RegisterApiResult> run() {
+ try {
+ // only ask for user input the first time. use the otp entry in params map in
+ // subsequent api requests
+ if (!params.containsKey("otp")) {
+ System.out.println("Enter verification code received on " + params.get("email")
+ + " [verification code is case sensitive]");
+ params.put("otp", scanner.nextLine());
+ System.out.println("Validating verification code ...");
+ }
+ String apiResponse = registerUtil.validateOtp(params.get("email"), createAtSign(params.get("atSign")),
+ params.get("otp"), params.get("registrarUrl"), params.get("apiKey"),
+ Boolean.parseBoolean(params.get("confirmation")));
+ if ("retry".equals(apiResponse)) {
+ System.err.println("Incorrect OTP!!! Please re-enter your OTP");
+ params.put("otp", scanner.nextLine());
+ result.apiCallStatus = ApiCallStatus.retry;
+ result.atException = new AtRegistrarException("Only 3 retries allowed to re-enter OTP - Incorrect OTP entered");
+ } else if ("follow-up".equals(apiResponse)) {
+ params.put("confirmation", "true");
+ result.apiCallStatus = ApiCallStatus.retry;
+ } else if (apiResponse.startsWith("@")) {
+ result.data.put("cram", apiResponse.split(":")[1]);
+ System.out.println("\tRCVD cram: " + result.data.get("cram"));
+ System.out.println("Done.");
+ result.apiCallStatus = ApiCallStatus.success;
+ scanner.close();
+ }
+ } catch (AtRegistrarException e) {
+ result.atException = e;
+ result.apiCallStatus = retryCount < maxRetries ? ApiCallStatus.retry : ApiCallStatus.failure;
+ } catch (Exception e) {
+ result.atException = new AtRegistrarException("Failed while validating OTP", e);
+ result.apiCallStatus = retryCount < maxRetries ? ApiCallStatus.retry : ApiCallStatus.failure;
+ }
+ return result;
+ }
+}
diff --git a/at_client/src/main/java/org/atsign/client/impl/commands/AtExceptions.java b/at_client/src/main/java/org/atsign/client/impl/commands/AtExceptions.java
new file mode 100644
index 00000000..cbb81f88
--- /dev/null
+++ b/at_client/src/main/java/org/atsign/client/impl/commands/AtExceptions.java
@@ -0,0 +1,87 @@
+package org.atsign.client.impl.commands;
+
+import org.atsign.client.api.AtCommandExecutor;
+import org.atsign.client.impl.exceptions.*;
+
+import java.util.concurrent.ExecutionException;
+import java.util.function.Consumer;
+
+/**
+ * Utilities for throwing and handling typed {@link AtException} instances
+ */
+public class AtExceptions {
+
+ public static AtException toTypedException(String code, String text) {
+ if (AtServerRuntimeException.CODE.equals(code)) {
+ return new AtServerRuntimeException(text);
+ } else if (AtInvalidSyntaxException.CODE.equals(code)) {
+ return new AtInvalidSyntaxException(text);
+ } else if (AtBufferOverFlowException.CODE.equals(code)) {
+ return new AtBufferOverFlowException(text);
+ } else if (AtOutboundConnectionLimitException.CODE.equals(code)) {
+ return new AtOutboundConnectionLimitException(text);
+ } else if (AtSecondaryNotFoundException.CODE.equals(code)) {
+ return new AtSecondaryNotFoundException(text);
+ } else if (AtHandShakeException.CODE.equals(code)) {
+ return new AtHandShakeException(text);
+ } else if (AtUnauthorizedException.CODE.equals(code)) {
+ return new AtUnauthorizedException(text);
+ } else if (AtInternalServerError.CODE.equals(code)) {
+ return new AtInternalServerError(text);
+ } else if (AtInternalServerException.CODE.equals(code)) {
+ return new AtInternalServerException(text);
+ } else if (AtInboundConnectionLimitException.CODE.equals(code)) {
+ return new AtInboundConnectionLimitException(text);
+ } else if (AtBlockedConnectionException.CODE.equals(code)) {
+ return new AtBlockedConnectionException(text);
+ } else if (AtKeyNotFoundException.CODE.equals(code)) {
+ return new AtKeyNotFoundException(text);
+ } else if (AtInvalidAtKeyException.CODE.equals(code)) {
+ return new AtInvalidAtKeyException(text);
+ } else if (AtSecondaryConnectException.CODE.equals(code)) {
+ return new AtSecondaryConnectException(text);
+ } else if (AtIllegalArgumentException.CODE.equals(code)) {
+ return new AtIllegalArgumentException(text);
+ } else if (AtTimeoutException.CODE.equals(code)) {
+ return new AtTimeoutException(text);
+ } else if (AtServerIsPausedException.CODE.equals(code)) {
+ return new AtServerIsPausedException(text);
+ } else if (AtUnauthenticatedException.CODE.equals(code)) {
+ return new AtUnauthenticatedException(text);
+ }
+
+ return new AtNewErrorCodeWhoDisException(code, text);
+ }
+
+ /**
+ * A Command / Runnable that may throw exceptions.
+ */
+ public interface AtClientConnectionCommand {
+ void run(AtCommandExecutor executor) throws AtException, ExecutionException, InterruptedException;
+ }
+
+ /**
+ * Wraps an {@link AtClientConnectionCommand}, typically an
+ * {@link AtCommandExecutor#onReady(Consumer)}
+ * command, such that AtExceptions are caught and rethrown as {@link AtOnReadyException}.
+ * NOTE: {@link AtOnReadyException}s are regarded as fatal, i.e. the {@link AtCommandExecutor} is
+ * unusable.
+ *
+ * @param command A command/runnable thatmay throw exceptions.
+ * @return a consumer that is designed to be passed as an argument to
+ * {@link AtCommandExecutor#onReady(Consumer)}
+ */
+ public static Consumer throwOnReadyException(AtClientConnectionCommand command) {
+ return executor -> {
+ try {
+ command.run(executor);
+ } catch (AtException e) {
+ // AtOnReadyExceptions are fatal for the executor
+ throw new AtOnReadyException(e.getMessage(), e);
+ } catch (ExecutionException | InterruptedException e) {
+ throw new RuntimeException(e);
+ }
+ };
+ }
+
+}
diff --git a/at_client/src/main/java/org/atsign/client/impl/commands/AuthenticationCommands.java b/at_client/src/main/java/org/atsign/client/impl/commands/AuthenticationCommands.java
new file mode 100644
index 00000000..b97b695b
--- /dev/null
+++ b/at_client/src/main/java/org/atsign/client/impl/commands/AuthenticationCommands.java
@@ -0,0 +1,119 @@
+package org.atsign.client.impl.commands;
+
+import static org.atsign.client.impl.commands.AtExceptions.throwOnReadyException;
+import static org.atsign.client.impl.commands.DataResponses.matchDataStringNoWhitespace;
+import static org.atsign.client.impl.commands.DataResponses.matchDataSuccess;
+import static org.atsign.client.impl.commands.ErrorResponses.throwExceptionIfError;
+
+import java.nio.charset.StandardCharsets;
+import java.security.MessageDigest;
+import java.security.NoSuchAlgorithmException;
+import java.util.concurrent.ExecutionException;
+import java.util.function.Consumer;
+
+import org.atsign.client.api.AtKeys;
+import org.atsign.client.api.AtCommandExecutor;
+import org.atsign.client.impl.util.EncryptionUtils;
+import org.atsign.client.impl.exceptions.AtException;
+import org.atsign.client.api.AtSign;
+import org.atsign.client.impl.exceptions.AtEncryptionException;
+import org.atsign.client.impl.exceptions.AtUnauthenticatedException;
+
+/**
+ * Utility methods for handling authentication within the At Protocol
+ */
+public class AuthenticationCommands {
+
+ public static Consumer pkamAuthenticator(AtSign atSign, AtKeys keys) {
+ return throwOnReadyException(executor -> authenticateWithPkam(executor, atSign, keys));
+ }
+
+ /**
+ * Implements the protocol workflow / sequence for PKAM authentication.
+ *
+ * @param executor The executor with which to send the commands.
+ * @param atSign The asign to authenticate.
+ * @param keys The keys to use to authenticate.
+ * @throws AtException If authentication fails.
+ */
+ public static void authenticateWithPkam(AtCommandExecutor executor, AtSign atSign, AtKeys keys)
+ throws AtException {
+ try {
+
+ // send a from command and expect to receive a challenge
+ String fromCommand = CommandBuilders.fromCommandBuilder().atSign(atSign).build();
+ String fromResponse = executor.sendSync(fromCommand);
+ String challenge = matchDataStringNoWhitespace(throwExceptionIfError(fromResponse));
+
+ // send a pkam command with the signed challenge
+ String signature = EncryptionUtils.signSHA256RSA(challenge, keys.getApkamPrivateKey());
+ CommandBuilders.PkamCommandBuilder pkamCommandBuilder = CommandBuilders.pkamCommandBuilder();
+ if (keys.hasEnrollmentId()) {
+ pkamCommandBuilder.signingAlgo(EncryptionUtils.SIGNING_ALGO_RSA);
+ pkamCommandBuilder.hashingAlgo(EncryptionUtils.HASHING_ALGO_SHA256);
+ pkamCommandBuilder.enrollmentId(keys.getEnrollmentId());
+ }
+ pkamCommandBuilder.digest(signature);
+ String pkamCommand = pkamCommandBuilder.build();
+ String pkamResponse = executor.sendSync(pkamCommand);
+
+ // verify that pkam has succeeded
+ matchDataSuccess(throwExceptionIfError(pkamResponse));
+ } catch (RuntimeException | ExecutionException | InterruptedException e) {
+ throw new AtUnauthenticatedException("PKAM command failed : " + e.getMessage());
+ }
+ }
+
+ /**
+ * Implements the protocol workflow / sequence for CRAM authentication.
+ *
+ * @param executor The executor with which to send the commands.
+ * @param atSign The asign to authenticate.
+ * @param cramSecret The cramSecret that was assigned during At Server provisioning.
+ * @throws AtException If authentication fails.
+ */
+ public static void authenticateWithCram(AtCommandExecutor executor, AtSign atSign, String cramSecret)
+ throws AtException {
+ try {
+
+ // send a from command and expect to receive a challenge
+ String fromCommand = CommandBuilders.fromCommandBuilder().atSign(atSign).build();
+ String fromResponse = executor.sendSync(fromCommand);
+ String challenge = matchDataStringNoWhitespace(throwExceptionIfError(fromResponse));
+
+ // send a cram command
+ String cramDigest = createDigest(cramSecret, challenge);
+ String cramCommand = CommandBuilders.cramCommandBuilder().digest(cramDigest).build();
+ String cramResponse = executor.sendSync(cramCommand);
+
+ // verify that cram succeeded
+ matchDataSuccess(throwExceptionIfError(cramResponse));
+
+ } catch (RuntimeException | ExecutionException | InterruptedException e) {
+ throw new AtUnauthenticatedException("CRAM command failed : " + e.getMessage());
+ }
+ }
+
+ private static String createDigest(String cramSecret, String challenge) throws AtEncryptionException {
+ try {
+ String digestInput = cramSecret + challenge;
+ byte[] digestInputBytes = digestInput.getBytes(StandardCharsets.UTF_8);
+ byte[] digest = MessageDigest.getInstance("SHA-512").digest(digestInputBytes);
+ return bytesToHex(digest);
+ } catch (RuntimeException | NoSuchAlgorithmException e) {
+ throw new AtEncryptionException("failed to generate cramDigest", e);
+ }
+ }
+
+ private static final char[] HEX_ARRAY = "0123456789abcdef".toCharArray();
+
+ public static String bytesToHex(byte[] bytes) {
+ char[] hexChars = new char[bytes.length * 2];
+ for (int j = 0; j < bytes.length; j++) {
+ int v = bytes[j] & 0xFF;
+ hexChars[j * 2] = HEX_ARRAY[v >>> 4];
+ hexChars[j * 2 + 1] = HEX_ARRAY[v & 0x0F];
+ }
+ return new String(hexChars);
+ }
+}
diff --git a/at_client/src/main/java/org/atsign/common/VerbBuilders.java b/at_client/src/main/java/org/atsign/client/impl/commands/CommandBuilders.java
similarity index 93%
rename from at_client/src/main/java/org/atsign/common/VerbBuilders.java
rename to at_client/src/main/java/org/atsign/client/impl/commands/CommandBuilders.java
index 9da7ef66..64a860ce 100644
--- a/at_client/src/main/java/org/atsign/common/VerbBuilders.java
+++ b/at_client/src/main/java/org/atsign/client/impl/commands/CommandBuilders.java
@@ -1,34 +1,37 @@
-package org.atsign.common;
+package org.atsign.client.impl.commands;
import static java.util.Collections.singletonList;
import static java.util.Collections.singletonMap;
-import static org.atsign.client.util.Preconditions.*;
-import static org.atsign.client.util.StringUtil.isBlank;
-import static org.atsign.common.Metadata.*;
+import static org.atsign.client.impl.common.Preconditions.*;
+import static org.atsign.client.impl.util.StringUtils.isBlank;
+import static org.atsign.client.api.Metadata.*;
import java.util.LinkedHashMap;
import java.util.Map;
-import org.atsign.client.util.EnrollmentId;
-import org.atsign.client.util.TypedString;
-import org.atsign.common.Keys.AtKey;
-import org.atsign.common.Metadata.MetadataBuilder;
+import org.atsign.client.api.AtSign;
+import org.atsign.client.api.Keys;
+import org.atsign.client.api.Metadata;
+import org.atsign.client.impl.common.EnrollmentId;
+import org.atsign.client.impl.util.JsonUtils;
+import org.atsign.client.impl.common.TypedString;
+import org.atsign.client.api.Keys.AtKey;
+import org.atsign.client.api.Metadata.MetadataBuilder;
-import com.fasterxml.jackson.core.JsonProcessingException;
import lombok.Builder;
/**
*
- * Contains builders for composing Atsign protocol command strings
+ * Builders for composing At Protocol command strings.
*
*/
-public class VerbBuilders {
+public class CommandBuilders {
private static final Metadata EMPTY_METADATA = Metadata.builder().build();
/**
- * A builder to compose an Atsign protocol command with the from verb. The from verb
+ * A builder to compose an At Protocol command with the from verb. The from verb
* is used to tell the Atsign server whom you claim to be and initiates the authentication workflow.
*
* @param atSign The {@link AtSign} you claim to be
@@ -42,7 +45,7 @@ public static String from(AtSign atSign) {
}
/**
- * A builder to compose an Atsign protocol command with the cram verb. The cram verb
+ * A builder to compose an At Protocol command with the cram verb. The cram verb
* is used to boostrap authenticate one's own self as an owner of the Atsign server. It is intended
* to be used once until a set of PKAM keys are cut on the owner's mobile device and from then on we
* use the pkam verb.
@@ -58,7 +61,7 @@ public static String cram(String digest) {
}
/**
- * A builder to compose an Atsign protocol command with the pol verb. The pol verb
+ * A builder to compose an At Protocol command with the pol verb. The pol verb
* is part of the pkam process to authenticate oneself while connecting to someone else's atServer.
* The term 'pol' means 'proof of life' as it provides a near realtime assurance that the requestor
* is who it claims to be.
@@ -71,7 +74,7 @@ public static String pol() {
}
/**
- * A builder to compose an Atsign protocol command with the pkam verb. The pkam verb
+ * A builder to compose an At Protocol command with the pkam verb. The pkam verb
* follows the from verb. As an owner of the atServer, you should be able to take the
* challenge thrown by the from verb and encrypt using the private key of the RSA key pair
* with what the server has been bound with. Upon receiving the cram verb along with the digest, the
@@ -105,7 +108,7 @@ public static String pkam(String digest, String signingAlgo, String hashingAlgo,
}
/**
- * A builder to compose an Atsign protocol command with the update verb. The update
+ * A builder to compose an At Protocol command with the update verb. The update
* is used to insert key/value pairs into a Key Store. An update command can only be sent by the
* {@link AtSign} that "owns" the key value and can only be sent to their own Atsign server.
*
@@ -143,7 +146,7 @@ public static String pkam(String digest, String signingAlgo, String hashingAlgo,
* @param value the value of the key / value. This overrides the metadata param if this is also set.
* @param key a {@link AtKey} instance from which keyName, sharedBy, sharedWith and metadata will be
* taken from.
- * @param rawKey the Atsign protocol key with cached and public qualifications
+ * @param rawKey the At Protocol key with cached and public qualifications
* @return A correctly formed update verb command.
* @throws IllegalArgumentException If mandatory fields are not set or if field values conflict.
*/
@@ -203,7 +206,7 @@ public enum LookupOperation {
};
/**
- * A builder to compose an Atsign protocol command with the llookup verb. The llookup
+ * A builder to compose an At Protocol command with the llookup verb. The llookup
* verb is used to look up key values "owned" / shared by the {@link AtSign} that is sending the
* command.
*
@@ -253,7 +256,7 @@ public static String llookup(String keyName, AtSign sharedBy, AtSign sharedWith,
}
/**
- * A builder to compose an Atsign protocol command with the lookup verb. The lookup
+ * A builder to compose an At Protocol command with the lookup verb. The lookup
* verb is used to look up key values shared by other {@link AtSign}s with the {@link AtSign} that
* is sending the command.
*
@@ -290,7 +293,7 @@ public static String lookup(String keyName, AtSign sharedBy, LookupOperation ope
}
/**
- * A builder to compose an Atsign protocol command with the plookup verb. The plookup
+ * A builder to compose an At Protocol command with the plookup verb. The plookup
* verb is used to look up a public key value shared by an {@link AtSign} other than the one that
* is sending the command.
*
@@ -332,7 +335,7 @@ public static String plookup(String keyName, AtSign sharedBy, Boolean bypassCach
}
/**
- * A builder to compose an Atsign protocol command with the delete verb. The delete
+ * A builder to compose an At Protocol command with the delete verb. The delete
* verb is used to remove key/value pairs into a Key Store. An delete command can only be
* sent by the {@link AtSign} that "owns" the key value and can only be sent to their own Atsign
* server.
@@ -380,7 +383,7 @@ public static String delete(String keyName, AtSign sharedBy, AtSign sharedWith,
}
/**
- * A builder to compose an Atsign protocol command with the scan verb. The scan verb
+ * A builder to compose an At Protocol command with the scan verb. The scan verb
* is used to list the keys in an {@link AtSign}'s Atsign server.
*
* @param regex If set only show keys that match this regular expression pattern.
@@ -405,7 +408,7 @@ public static String scan(String regex, AtSign fromAtSign, Boolean showHidden) {
}
/**
- * A builder to compose an Atsign protocol command with the notify:messageType:text verb.
+ * A builder to compose an At Protocol command with the notify:messageType:text verb.
* The notify:messageType:text verb is used to send an arbitrary message to another
* {@link AtSign}.
*
@@ -429,7 +432,7 @@ public enum NotifyOperation {
}
/**
- * A builder to compose an Atsign protocol command with the
+ * A builder to compose an At Protocol command with the
* notify:(update|delete):messageType:key verb.
* The notify:(update|delete):messageType:key verb is used to send key change notifications
* to other
@@ -469,7 +472,7 @@ public static String notifyKeyChange(NotifyOperation operation, AtSign recipient
}
/**
- * A builder to compose an Atsign protocol command with the notify:status verb.
+ * A builder to compose an At Protocol command with the notify:status verb.
* The notify:status verb is query the status of a previously sent notification.
*
* @param notificationId The unique id of a notification. This will have been the response for to a
@@ -512,7 +515,7 @@ public static class EnrollParameters {
}
/**
- * A builder to compose an Atsign protocol command with the enroll verb.
+ * A builder to compose an At Protocol command with the enroll verb.
* The enroll verb is used to submit an APKAM enrollment.
*
* @param operation The specific enroll operation to perform see {@link EnrollOperation}.
@@ -604,7 +607,7 @@ public static String enroll(EnrollOperation operation, String status, Enrollment
return new StringBuilder("enroll:")
.append(operation)
- .append(params != null ? encodeAsJson(params) : "")
+ .append(params != null ? JsonUtils.writeValueAsString(params) : "")
.toString();
}
@@ -616,7 +619,7 @@ public enum KeysOperation {
};
/**
- * A builder to compose an Atsign protocol command with the keys verb.
+ * A builder to compose an At Protocol command with the keys verb.
* The keys verb is specifically used to update security keys in the Atsign server.
*
* @param operation put, get or delete.
@@ -637,7 +640,7 @@ public static String keys(KeysOperation operation, String keyName) {
}
/**
- * A builder to compose an Atsign protocol command with the otp verb.
+ * A builder to compose an At Protocol command with the otp verb.
* The otp verb is used to request a one time password for the enrollment workflow.
*
* @return A correctly formed otp verb command.
@@ -664,14 +667,6 @@ protected static Map toObjectMap(Object... nameValuePairs) {
return map;
}
- protected static String encodeAsJson(Object o) {
- try {
- return Json.MAPPER.writeValueAsString(o);
- } catch (JsonProcessingException e) {
- throw new IllegalArgumentException("json encoding exception", e);
- }
- }
-
private static boolean isTrue(Boolean bool) {
return bool != null && bool;
}
diff --git a/at_client/src/main/java/org/atsign/client/impl/commands/DataResponses.java b/at_client/src/main/java/org/atsign/client/impl/commands/DataResponses.java
new file mode 100644
index 00000000..367a47c7
--- /dev/null
+++ b/at_client/src/main/java/org/atsign/client/impl/commands/DataResponses.java
@@ -0,0 +1,183 @@
+package org.atsign.client.impl.commands;
+
+import java.util.List;
+import java.util.Map;
+import java.util.regex.Pattern;
+
+import org.atsign.client.api.Metadata;
+import org.atsign.client.impl.util.JsonUtils;
+
+/**
+ * Utilities for handling data responses in the At Protocol.
+ */
+public class DataResponses {
+
+ /**
+ * models server response string which is non-empty JSON map
+ */
+ private static final Pattern DATA_JSON_NON_EMPTY_MAP = Pattern.compile("data:(\\{.+})");
+
+ /**
+ * models server response string which is JSON map
+ */
+ private static final Pattern DATA_JSON_MAP = Pattern.compile("data:(\\{.*})");
+
+ /**
+ * models server response string which is non-empty JSON list
+ */
+ private static final Pattern DATA_JSON_LIST = Pattern.compile("data:(\\[.*])");
+
+ /**
+ * models server response string which is integer
+ */
+ private static final Pattern DATA_INT = Pattern.compile("data:(-?\\d+)");
+
+ /**
+ * models server response string containing no whitespace
+ */
+ private static final Pattern DATA_NON_WHITESPACE = Pattern.compile("data:(\\S+)");
+
+ /**
+ * models server data response
+ */
+ private static final Pattern DATA = Pattern.compile("data:(.+)");
+
+ /**
+ * models server response string containing no whitespace
+ */
+ private static final Pattern DATA_SUCCESS = Pattern.compile("data:(success)");
+
+ /**
+ * Use this to verify a "data:success" response.
+ *
+ * @param input The At Server response to verify.
+ * @return "success" if this matches.
+ */
+ public static String matchDataSuccess(String input) {
+ return Responses.match(input, DATA_SUCCESS);
+ }
+
+ /**
+ * Use this to verify a "data:xxxxx" response (no whitespace in the value).
+ *
+ * @param input The At Server response to verify.
+ * @return The value after the "data:" prefix.
+ */
+ public static String matchDataStringNoWhitespace(String input) {
+ return Responses.match(input, DATA_NON_WHITESPACE);
+ }
+
+ /**
+ * Use this to verify a "data:x xx xx" response (whitespace permitted).
+ *
+ * @param input The At Server response to verify.
+ * @return The value after the "data:" prefix.
+ */
+ public static String matchData(String input) {
+ return Responses.match(input, DATA);
+ }
+
+ /**
+ * Use this to verify a "data:123" response (negative numbers permitted).
+ *
+ * @param input The At Server response to verify.
+ * @return The value after the "data:" prefix.
+ */
+ public static int matchDataInt(String input) {
+ return Responses.match(input, DATA_INT, Integer::parseInt);
+ }
+
+ /**
+ * Use this to verify a "data:[...]" response where the value is a JSON encoded list.
+ *
+ * @param input The At Server response to verify.
+ * @return The {@link List} after the "data:" prefix.
+ */
+ public static List matchDataJsonList(String input) {
+ return Responses.match(input, DATA_JSON_LIST, Responses::decodeJsonList);
+ }
+
+ /**
+ * Use this to verify a "data:[...]" response where the value is a JSON encoded list of Strings.
+ *
+ * @param input The At Server response to verify.
+ * @return The {@link List} after the "data:" prefix.
+ */
+ public static List matchDataJsonListOfStrings(String input) {
+ return Responses.match(input, DATA_JSON_LIST, Responses::decodeJsonListOfStrings);
+ }
+
+ /**
+ * Use this to verify a "data:{...}" response where the value is a JSON encoded map of Strings.
+ *
+ * @param input The At Server response to verify.
+ * @param allowEmpty If true then empty map is permitted.
+ * @return The {@link Map} after the "data:" prefix.
+ */
+ public static Map matchDataJsonMapOfStrings(String input, boolean allowEmpty) {
+ return Responses.match(input, allowEmpty ? DATA_JSON_MAP : DATA_JSON_NON_EMPTY_MAP,
+ Responses::decodeJsonMapOfStrings);
+ }
+
+ /**
+ * Use this to verify a "data:{...}" response where the value is a JSON encoded map of objects.
+ *
+ * @param input The At Server response to verify.
+ * @param allowEmpty If true then empty map is permitted.
+ * @return The {@link Map} after the "data:" prefix.
+ */
+ public static Map matchDataJsonMapOfObjects(String input, boolean allowEmpty) {
+ return Responses.match(input, allowEmpty ? DATA_JSON_MAP : DATA_JSON_NON_EMPTY_MAP,
+ Responses::decodeJsonMapOfObjects);
+ }
+
+ /**
+ * Use this to verify a "data:{...}" response where the value is a JSON encoded map of Strings.
+ *
+ * @param input The At Server response to verify.
+ * @return The {@link Map} after the "data:" prefix.
+ */
+ public static Map matchDataJsonMapOfStrings(String input) {
+ return matchDataJsonMapOfStrings(input, false);
+ }
+
+ /**
+ * Use this to verify a "data:{...}" response where the value is a JSON encoded map of objects.
+ *
+ * @param input The At Server response to verify.
+ * @return The {@link Map} after the "data:" prefix.
+ */
+ public static Map matchDataJsonMapOfObjects(String input) {
+ return matchDataJsonMapOfObjects(input, false);
+ }
+
+ /**
+ * Use this to verify a "data:{...}" response where the value is a JSON encoded
+ * {@link LookupResponse}
+ *
+ * @param input The At Server response to verify.
+ * @return The {@link LookupResponse} after the "data:" prefix.
+ */
+ public static LookupResponse matchLookupResponse(String input) {
+ return Responses.match(input, DATA_JSON_NON_EMPTY_MAP, DataResponses::toLookupResponse);
+ }
+
+ private static LookupResponse toLookupResponse(String input) {
+ return JsonUtils.readValue(input, LookupResponse.class);
+ }
+
+ /**
+ * Use this to verify a "data:{...}" response where the value is a JSON encoded
+ * {@link Metadata}
+ *
+ * @param input The At Server response to verify.
+ * @return The {@link Metadata} after the "data:" prefix.
+ */
+ public static Metadata matchMetadata(String input) {
+ return Responses.match(input, DATA_JSON_NON_EMPTY_MAP, DataResponses::toMetaData);
+ }
+
+ private static Metadata toMetaData(String input) {
+ return JsonUtils.readValue(input, Metadata.class);
+ }
+}
diff --git a/at_client/src/main/java/org/atsign/client/impl/commands/EnrollCommands.java b/at_client/src/main/java/org/atsign/client/impl/commands/EnrollCommands.java
new file mode 100644
index 00000000..9b1694cf
--- /dev/null
+++ b/at_client/src/main/java/org/atsign/client/impl/commands/EnrollCommands.java
@@ -0,0 +1,417 @@
+package org.atsign.client.impl.commands;
+
+import static org.atsign.client.impl.commands.CommandBuilders.EnrollParameters.ENCRYPTED_APKAM_SYMMETRIC_KEY;
+import static org.atsign.client.impl.commands.DataResponses.*;
+import static org.atsign.client.impl.commands.ErrorResponses.throwExceptionIfError;
+import static org.atsign.client.impl.common.EnrollmentId.createEnrollmentId;
+import static org.atsign.client.impl.common.Preconditions.checkNotNull;
+import static org.atsign.client.impl.util.EncryptionUtils.*;
+
+import java.util.List;
+import java.util.Map;
+import java.util.Objects;
+import java.util.concurrent.ExecutionException;
+import java.util.stream.Collectors;
+
+import org.atsign.client.api.AtCommandExecutor;
+import org.atsign.client.api.AtKeyNames;
+import org.atsign.client.api.AtKeys;
+import org.atsign.client.api.AtSign;
+import org.atsign.client.impl.common.EnrollmentId;
+import org.atsign.client.impl.exceptions.AtException;
+
+/**
+ * At Protocol composite commands for activating an AtServer, enrolling the first set of keys
+ * and enrolling subsequent sets of keys.
+ */
+public class EnrollCommands {
+
+ /**
+ * Performs the onboarding commands which activate an At Server and
+ * enroll the first set of keys. This initial enrollment automatically completes and creates the
+ * app/device keys that can approve subsequent enrollments.
+ *
+ * @param executor The {@link AtCommandExecutor} to use.
+ * @param atSign The AtSign that corresponds to the executor.
+ * @param keys The {@link AtKeys} for the {@link AtSign}, these should already be populated.
+ * @param cramSecret The CRAM secret.
+ * @param appName The app name for this first enrollment.
+ * @param deviceName The device name for this first enrollment.
+ * @param deleteCramKey Whether the CRAM key should be removed from the AtServer.
+ * @return A new copy of the {@link AtKeys} that has the enrollment id set. These need to be
+ * persisted by the caller.
+ * @throws AtException If any of the commands fail.
+ */
+ public static AtKeys onboard(AtCommandExecutor executor,
+ AtSign atSign,
+ AtKeys keys,
+ String cramSecret,
+ String appName,
+ String deviceName,
+ boolean deleteCramKey)
+ throws AtException {
+ try {
+
+ // verify that the executor is connected to the atsigns atserver
+ String scanCommand = CommandBuilders.scanCommandBuilder().build();
+ String scanResponse = throwExceptionIfError(executor.sendSync(scanCommand));
+ List rawKeys = matchDataJsonListOfStrings(scanResponse);
+ if (!rawKeys.contains("signing_publickey" + atSign)) {
+ throw new IllegalStateException("not connected to the atsign's at server");
+ }
+
+ // authenticate with CRAM
+ AuthenticationCommands.authenticateWithCram(executor, atSign, cramSecret);
+
+ // send an enroll request which should automatically be approved after CRAM authentication
+ String requestCommand = CommandBuilders.enrollCommandBuilder()
+ .operation(CommandBuilders.EnrollOperation.request)
+ .appName(appName)
+ .deviceName(deviceName)
+ .apkamPublicKey(keys.getApkamPublicKey())
+ .build();
+ String requestResponse = executor.sendSync(requestCommand);
+ Map response = matchDataJsonMapOfStrings(throwExceptionIfError(requestResponse));
+ checkStatus(response, "approved");
+
+ // update the AtKeys with the enrollment id
+ keys = keys.toBuilder()
+ .enrollmentId(createEnrollmentId(response.get("enrollmentId")))
+ .build();
+
+ // authenticate with PKAM
+ AuthenticationCommands.authenticateWithPkam(executor, atSign, keys);
+
+ // explicitly store the public encryption key in the atserver
+ String updateCommand = CommandBuilders.updateCommandBuilder()
+ .sharedBy(atSign)
+ .keyName(AtKeyNames.PUBLIC_ENCRYPT)
+ .isPublic(true)
+ .value(keys.getEncryptPublicKey())
+ .build();
+ String updateResponse = executor.sendSync(updateCommand);
+ matchDataInt(throwExceptionIfError(updateResponse));
+
+ if (deleteCramKey) {
+ deleteCramSecret(executor);
+ }
+
+ } catch (ExecutionException | InterruptedException e) {
+ throw new RuntimeException(e);
+ }
+ return keys;
+ }
+
+ /**
+ * Deletes the key which holds the CRAM secret prior to onboarding an At Server.
+ *
+ * @param executor The {@link AtCommandExecutor} to use.
+ * @throws AtException If the command fails.
+ */
+ public static void deleteCramSecret(AtCommandExecutor executor) throws AtException {
+ KeyCommands.deleteKey(executor, AtKeyNames.PRIVATE_AT_SECRET);
+ }
+
+ /**
+ * Can be used to obtain a one time password for enrollment.
+ *
+ * @param executor The {@link AtCommandExecutor} to use.
+ * @return a unique one time password.
+ * @throws AtException If the command fails.
+ */
+ public static String otp(AtCommandExecutor executor) throws AtException {
+ try {
+
+ // send otp command
+ String otpCommand = CommandBuilders.otpCommandBuilder().build();
+ String otpResponse = executor.sendSync(otpCommand);
+
+ // verify that response is an OTP
+ return matchDataStringNoWhitespace(throwExceptionIfError(otpResponse));
+
+ } catch (ExecutionException | InterruptedException e) {
+ throw new RuntimeException(e);
+ }
+ }
+
+ /**
+ * Performs the enrollment request commands for a new application / device set of keys.
+ * NOTE The result of this command will be a pending request that must be approved
+ * ({@link #approve(AtCommandExecutor, AtKeys, EnrollmentId)}) using keys that have access
+ * to the manage namespace (the original keys from onboard) and then completed
+ * ({@link #complete(AtCommandExecutor, AtSign, AtKeys)}).
+ *
+ * @param executor An executor to an atserver command interface.
+ * @param atSign The AtSign that corresponds to the executor.
+ * @param keys The {@link AtKeys} for the {@link AtSign}, this should have the APKAM keys populated.
+ * The rest of the fields will be set once enrollment is complated.
+ * @param otp A one time password.
+ * @param appName The app name for this enrollment.
+ * @param deviceName The device name for this enrollment.
+ * @param namespaces A map of namespace names and access control e.g. r,rw.
+ * @return A new copy of the {@link AtKeys} that has the public encrypt key and enrollment id set.
+ * These need to be persisted by the caller.
+ * @throws AtException If any of the commands fail.
+ */
+ public static AtKeys enroll(AtCommandExecutor executor,
+ AtSign atSign,
+ AtKeys keys,
+ String otp,
+ String appName,
+ String deviceName,
+ Map namespaces)
+ throws Exception {
+
+ checkNotNull(keys.getApkamPublicKey(), "apkam public key not set");
+ checkNotNull(keys.getApkamSymmetricKey(), "apkam symmetric key not set");
+
+ // lookup the public encryption key for the atserver's atsign, this will have been set during onboarding
+ String lookupCommand = CommandBuilders.lookupCommandBuilder()
+ .keyName(AtKeyNames.PUBLIC_ENCRYPT)
+ .sharedBy(atSign)
+ .build();
+ String lookupResponse = executor.sendSync(lookupCommand);
+ String publicKey = matchDataStringNoWhitespace(throwExceptionIfError(lookupResponse));
+
+ // send an enroll request and verify that the status is pending
+ String requestCommand = CommandBuilders.enrollCommandBuilder()
+ .operation(CommandBuilders.EnrollOperation.request)
+ .appName(appName)
+ .deviceName(deviceName)
+ .apkamPublicKey(keys.getApkamPublicKey())
+ .apkamSymmetricKey(rsaEncryptToBase64(keys.getApkamSymmetricKey(), publicKey))
+ .otp(otp)
+ .namespaces(namespaces)
+ .build();
+ String requestResponse = executor.sendSync(requestCommand);
+ Map map = matchDataJsonMapOfStrings(throwExceptionIfError(requestResponse));
+ checkStatus(map, "pending");
+
+ // return a copy of the provided AtKeys with the public encryption key and enrollment id set
+ return keys.toBuilder()
+ .encryptPublicKey(publicKey)
+ .enrollmentId(EnrollmentId.createEnrollmentId(map.get("enrollmentId")))
+ .build();
+ }
+
+ /**
+ * Performs the enrollment completion commands for a new application / device set of keys.
+ * NOTE This command would only be expected to succeed if the enrollment request
+ * has been approved.
+ *
+ * @param executor An executor to an atserver command interface.
+ * @param atSign The AtSign that corresponds to the executor.
+ * @param keys The {@link AtKeys} for the {@link AtSign}, this should have the APKAM keys populated.
+ * The rest of the fields will be set once enrollment is completed.
+ * @return A new copy of the {@link AtKeys} that has the private encrypt key and self encrypt key
+ * set.
+ * These need to be persisted by the caller.
+ * @throws AtException If any of the commands fail.
+ */
+ public static AtKeys complete(AtCommandExecutor executor, AtSign atSign, AtKeys keys) throws AtException {
+
+ // attempt to authenticate with PKAM, this will succeed once the enroll request is approved
+ AuthenticationCommands.authenticateWithPkam(executor, atSign, keys);
+
+ // Use the keys:get command to obtain the private encryption key and self encryption key
+ String selfEncryptKey = keysGetSelfEncryptKey(executor, atSign, keys);
+ String encryptPrivateKey = keysGetEncryptPrivateKey(executor, atSign, keys);
+
+ // return a copy of the provided AtKeys with the private encryption key and self encryption key set
+ return keys.toBuilder()
+ .selfEncryptKey(selfEncryptKey)
+ .encryptPrivateKey(encryptPrivateKey)
+ .build();
+
+ }
+
+ /**
+ * Performs the commands to response processing to provide a list of {@link EnrollmentId}.
+ *
+ * @param executor An executor to an atserver command interface.
+ * @param status A filter for the request statuses.
+ * @throws AtException If any of the commands fail.
+ */
+ public static List list(AtCommandExecutor executor, String status) throws AtException {
+ try {
+
+ // get the list of enrollment requests that have the status
+ String listCommand = CommandBuilders.enrollCommandBuilder()
+ .operation(CommandBuilders.EnrollOperation.list)
+ .status(status)
+ .build();
+ String listResponse = executor.sendSync(listCommand);
+ Map map = matchDataJsonMapOfObjects(throwExceptionIfError(listResponse), true);
+
+ // return the list enrollment ids, extracted from the keys
+ return map.keySet().stream()
+ .map(EnrollCommands::inferEnrollmentId)
+ .collect(Collectors.toList());
+
+ } catch (ExecutionException | InterruptedException e) {
+ throw new RuntimeException(e);
+ }
+ }
+
+ /**
+ * Performs the enrollment approve commands for a new application / device set of keys.
+ *
+ * @param executor An executor to an atserver command interface.
+ * @param keys The {@link AtKeys} for the {@link AtSign} that have the authority to manage
+ * enrollment requests. Typically the first set of keys from the onboard.
+ * @param enrollmentId The {@link EnrollmentId} to approve.
+ * @throws AtException If any of the commands fail.
+ */
+ public static void approve(AtCommandExecutor executor, AtKeys keys, EnrollmentId enrollmentId) throws AtException {
+ try {
+
+ // fetch the request and decrypt the apkam symmetric key that will be used encrypt the shared keys
+ String fetchCommand = CommandBuilders.enrollCommandBuilder()
+ .operation(CommandBuilders.EnrollOperation.fetch)
+ .enrollmentId(enrollmentId)
+ .build();
+ String fetchResponse = executor.sendSync(fetchCommand);
+ Map request = matchDataJsonMapOfObjects(throwExceptionIfError(fetchResponse));
+ checkStatus(request, "pending");
+ String encryptedApkamSymmetricKey = (String) request.get(ENCRYPTED_APKAM_SYMMETRIC_KEY);
+ String key = rsaDecryptFromBase64(encryptedApkamSymmetricKey, keys.getEncryptPrivateKey());
+
+ // encrypt the private encryption key and the self encryption key with the apkam symmetric key
+ String privateKeyIv = generateRandomIvBase64(16);
+ String encryptPrivateKey = aesEncryptToBase64(keys.getEncryptPrivateKey(), key, privateKeyIv);
+ String selfEncryptKeyIv = generateRandomIvBase64(16);
+ String selfEncryptKey = aesEncryptToBase64(keys.getSelfEncryptKey(), key, selfEncryptKeyIv);
+
+ // approve the request
+ String approveCommand = CommandBuilders.enrollCommandBuilder()
+ .operation(CommandBuilders.EnrollOperation.approve)
+ .enrollmentId(enrollmentId)
+ .encryptPrivateKey(encryptPrivateKey)
+ .encryptPrivateKeyIv(privateKeyIv)
+ .selfEncryptKey(selfEncryptKey)
+ .selfEncryptKeyIv(selfEncryptKeyIv)
+ .build();
+ String approveResponse = executor.sendSync(approveCommand);
+
+ // check the response
+ Map response = matchDataJsonMapOfStrings(throwExceptionIfError(approveResponse));
+ checkStatus(response, "approved");
+
+ } catch (ExecutionException | InterruptedException e) {
+ throw new RuntimeException(e);
+ }
+
+ }
+
+ /**
+ * Performs the enrollment revoke commands for a pending application / device set of keys.
+ *
+ * @param executor An executor to an atserver command interface.
+ * @param enrollmentId The {@link EnrollmentId} to deny.
+ * @throws AtException If any of the commands fail.
+ */
+ public static void deny(AtCommandExecutor executor, EnrollmentId enrollmentId) throws Exception {
+ singleArgEnrollAction(executor, "deny", enrollmentId, "denied");
+ }
+
+ /**
+ * Performs the enrollment revoke commands for a previously approved application / device set of
+ * keys.
+ *
+ * @param executor An executor to an atserver command interface.
+ * @param enrollmentId The {@link EnrollmentId} to revoke.
+ * @throws AtException If any of the commands fail.
+ */
+ public static void revoke(AtCommandExecutor executor, EnrollmentId enrollmentId) throws Exception {
+ singleArgEnrollAction(executor, "revoke", enrollmentId, "revoked");
+ }
+
+ /**
+ * Performs the enrollment revoke commands for a previously revoked application / device set of
+ * keys.
+ *
+ * @param executor An executor to an atserver command interface.
+ * @param enrollmentId The {@link EnrollmentId} to unrevoke.
+ * @throws AtException If any of the commands fail.
+ */
+ public static void unrevoke(AtCommandExecutor executor, EnrollmentId enrollmentId) throws Exception {
+ singleArgEnrollAction(executor, "unrevoke", enrollmentId, "approved");
+ }
+
+ /**
+ * Performs the enrollment revoke commands for an application / device set of keys.
+ *
+ * @param executor An executor to an atserver command interface.
+ * @param enrollmentId The {@link EnrollmentId} to delete.
+ * @throws AtException If any of the commands fail.
+ */
+ public static void delete(AtCommandExecutor executor, EnrollmentId enrollmentId) throws Exception {
+ singleArgEnrollAction(executor, "delete", enrollmentId, "deleted");
+ }
+
+ private static void singleArgEnrollAction(AtCommandExecutor executor,
+ String action,
+ EnrollmentId enrollmentId,
+ String expectedStatus)
+ throws Exception {
+ String actionCommand = CommandBuilders.enrollCommandBuilder()
+ .operation(CommandBuilders.EnrollOperation.valueOf(action))
+ .enrollmentId(enrollmentId)
+ .build();
+ String actionResponse = executor.sendSync(actionCommand);
+ Map map = matchDataJsonMapOfStrings(actionResponse);
+ checkStatus(map, expectedStatus);
+ }
+
+ private static EnrollmentId inferEnrollmentId(String key) {
+ int endIndex = key.indexOf('.');
+ if (key.contains("__manage") && endIndex > 0) {
+ return EnrollmentId.createEnrollmentId(key.substring(0, endIndex));
+ } else {
+ throw new RuntimeException(key + " doesn't match expected enrollment key pattern");
+ }
+ }
+
+ private static void checkStatus(Map map, String expectedStatus) {
+ String actualStatus = (String) map.get("status");
+ if (!Objects.equals(actualStatus, expectedStatus)) {
+ throw new RuntimeException("status is " + actualStatus);
+ }
+ }
+
+ private static String keysGetSelfEncryptKey(AtCommandExecutor executor, AtSign atSign, AtKeys keys)
+ throws AtException {
+ String keyName = keys.getEnrollmentId() + "." + "default_self_enc_key" + ".__manage" + atSign;
+ return keysGet(executor, keyName, keys.getApkamSymmetricKey());
+ }
+
+ private static String keysGetEncryptPrivateKey(AtCommandExecutor executor, AtSign atSign, AtKeys keys)
+ throws AtException {
+ String keyName = keys.getEnrollmentId() + "." + "default_enc_private_key" + ".__manage" + atSign;
+ return keysGet(executor, keyName, keys.getApkamSymmetricKey());
+ }
+
+ private static String keysGet(AtCommandExecutor executor, String keyName, String keyBase64) throws AtException {
+ try {
+
+ // send a key:get command
+ String keysGetCommand = CommandBuilders.keysCommandBuilder()
+ .operation(CommandBuilders.KeysOperation.get)
+ .keyName(keyName)
+ .build();
+ String keyGetResponse = executor.sendSync(keysGetCommand);
+
+ // unmarshall the encrypted value and the encryption iv that was used
+ Map map = matchDataJsonMapOfStrings(throwExceptionIfError(keyGetResponse));
+ String encryptedKey = map.get("value");
+ String iv = map.get("iv");
+
+ // return the decrypted value
+ return aesDecryptFromBase64(encryptedKey, keyBase64, iv);
+
+ } catch (ExecutionException | InterruptedException e) {
+ throw new RuntimeException(e);
+ }
+ }
+
+}
diff --git a/at_client/src/main/java/org/atsign/client/impl/commands/ErrorResponses.java b/at_client/src/main/java/org/atsign/client/impl/commands/ErrorResponses.java
new file mode 100644
index 00000000..60b22e10
--- /dev/null
+++ b/at_client/src/main/java/org/atsign/client/impl/commands/ErrorResponses.java
@@ -0,0 +1,62 @@
+package org.atsign.client.impl.commands;
+
+import static org.atsign.client.impl.commands.AtExceptions.toTypedException;
+import static org.atsign.client.impl.common.Preconditions.checkNotNull;
+
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+import org.atsign.client.impl.exceptions.AtException;
+
+/**
+ * Utility methods for handling error responses in the At Protocol.
+ */
+public class ErrorResponses {
+
+ /**
+ * Models server data response
+ */
+ private static final Pattern ERROR = Pattern.compile("error:(.+)");
+
+ private static final Pattern ERROR_WITH_CODE = Pattern.compile("error:(AT\\d+)([^:]*):\\s*(.+)");
+
+ /**
+ * Use this to verify a "error:xxxx" response.
+ *
+ * @param response A response string from an AtSign command interface
+ * @return the response with the "error:" prefix removed
+ */
+ public static String matchError(String response) {
+ return Responses.match(response, ERROR);
+ }
+
+ /**
+ * Utility method that can be used to wrap a response and throw a typed
+ * {@link AtException} is the response represents an error.
+ *
+ * @param response A response string from an AtSign command interface
+ * @return the response parameter if the response is NOT an error
+ * @throws AtException a typed exception if the response is an error
+ */
+ public static String throwExceptionIfError(String response) throws AtException {
+ AtException ex = getAtExceptionIfError(response);
+ if (ex != null) {
+ throw ex;
+ }
+ return response;
+ }
+
+ private static AtException getAtExceptionIfError(String response) throws AtException {
+ checkNotNull(response);
+ Matcher matcher = ERROR_WITH_CODE.matcher(response);
+ if (matcher.matches()) {
+ return toTypedException(matcher.group(1), matcher.group(3));
+ }
+ matcher = ERROR.matcher(response);
+ if (matcher.matches()) {
+ return new AtException(matcher.group(1));
+ }
+ return null;
+ }
+
+}
diff --git a/at_client/src/main/java/org/atsign/client/impl/commands/KeyCommands.java b/at_client/src/main/java/org/atsign/client/impl/commands/KeyCommands.java
new file mode 100644
index 00000000..965d294f
--- /dev/null
+++ b/at_client/src/main/java/org/atsign/client/impl/commands/KeyCommands.java
@@ -0,0 +1,107 @@
+package org.atsign.client.impl.commands;
+
+import static org.atsign.client.impl.commands.CommandBuilders.LookupOperation.meta;
+import static org.atsign.client.impl.commands.DataResponses.matchDataJsonListOfStrings;
+import static org.atsign.client.impl.commands.DataResponses.matchMetadata;
+import static org.atsign.client.impl.commands.ErrorResponses.throwExceptionIfError;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.concurrent.ExecutionException;
+
+import org.atsign.client.api.AtCommandExecutor;
+import org.atsign.client.api.AtKeyNames;
+import org.atsign.client.api.Keys.AtKey;
+import org.atsign.client.api.Metadata;
+import org.atsign.client.impl.exceptions.AtException;
+
+import lombok.extern.slf4j.Slf4j;
+
+/**
+ * At Protocol utility code that relates to keys (the records) that can be stored in an
+ * At Server.
+ *
+ */
+@Slf4j
+public class KeyCommands {
+
+ /**
+ * Sends the delete command and verifies the response.
+ *
+ * @param executor The {@link AtCommandExecutor} to use.
+ * @param rawKey The key to delete.
+ * @throws AtException If any of the commands fail.
+ */
+ public static void deleteKey(AtCommandExecutor executor, String rawKey) throws AtException {
+ try {
+
+ // send a delete command
+ String deleteCommand = CommandBuilders.deleteCommandBuilder().rawKey(rawKey).build();
+ String deleteResponse = executor.sendSync(deleteCommand);
+
+ // verify command succeeded
+ DataResponses.matchDataInt(throwExceptionIfError(deleteResponse));
+
+ } catch (ExecutionException | InterruptedException e) {
+ throw new RuntimeException(e);
+ }
+ }
+
+ /**
+ * Sends the delete command and verifies the response for an {@link AtKey}
+ *
+ * @param executor The {@link AtCommandExecutor} to use.
+ * @param key The key to delete.
+ * @throws AtException If any of the commands fail.
+ */
+ public static void deleteKey(AtCommandExecutor executor, AtKey key) throws AtException {
+ deleteKey(executor, key.rawKey());
+ }
+
+ /**
+ * Sends the scan command to list the matching keys. And optionally then does a lookup
+ * to obtain the metadata for each key.
+ *
+ * @param executor The {@link AtCommandExecutor} to use.
+ * @param regex A regular expression to match the key names.
+ * @param fetchMetadata If true then utility will lookup the metadata for each key.
+ * @return A list of {@link AtKey} subclasses that correspond to the scan results.
+ * @throws AtException If any of the commands fail.
+ */
+ public static List getKeys(AtCommandExecutor executor, String regex, boolean fetchMetadata)
+ throws AtException {
+
+ try {
+
+ // send scan command and decode response
+ String scanCommand = CommandBuilders.scanCommandBuilder().regex(regex).showHidden(true).build();
+ String scanResponse = executor.sendSync(scanCommand);
+ List keyNames = matchDataJsonListOfStrings(throwExceptionIfError(scanResponse));
+
+ // build typed key instances from each key name (optionally looking up metadata)
+ List atKeys = new ArrayList<>();
+ for (String keyName : keyNames) {
+ if (!AtKeyNames.isManagementKeyName(keyName)) {
+ Metadata metadata = fetchMetadata ? fetchMetadata(executor, keyName) : null;
+ AtKey atKey = org.atsign.client.api.Keys.keyBuilder()
+ .rawKey(keyName)
+ .metadata(metadata)
+ .build();
+ atKeys.add(atKey);
+ }
+ }
+
+ return atKeys;
+
+ } catch (ExecutionException | InterruptedException e) {
+ throw new RuntimeException(e);
+ }
+ }
+
+ private static Metadata fetchMetadata(AtCommandExecutor executor, String keyName)
+ throws ExecutionException, InterruptedException, AtException {
+ String llookupCommand = CommandBuilders.llookupCommandBuilder().operation(meta).rawKey(keyName).build();
+ String llookupResponse = executor.sendSync(llookupCommand);
+ return matchMetadata(throwExceptionIfError(llookupResponse));
+ }
+}
diff --git a/at_client/src/main/java/org/atsign/common/response_models/LookupResponse.java b/at_client/src/main/java/org/atsign/client/impl/commands/LookupResponse.java
similarity index 58%
rename from at_client/src/main/java/org/atsign/common/response_models/LookupResponse.java
rename to at_client/src/main/java/org/atsign/client/impl/commands/LookupResponse.java
index 7a354baf..3faecc0a 100644
--- a/at_client/src/main/java/org/atsign/common/response_models/LookupResponse.java
+++ b/at_client/src/main/java/org/atsign/client/impl/commands/LookupResponse.java
@@ -1,11 +1,11 @@
-package org.atsign.common.response_models;
+package org.atsign.client.impl.commands;
-import org.atsign.common.Metadata;
+import org.atsign.client.api.Metadata;
import com.fasterxml.jackson.annotation.JsonProperty;
/**
- * Data class used to hold a response to lookup, llookup or plookup commands
+ * Models the response to lookup, llookup or plookup commands
*/
public class LookupResponse {
diff --git a/at_client/src/main/java/org/atsign/client/impl/commands/Notifications.java b/at_client/src/main/java/org/atsign/client/impl/commands/Notifications.java
new file mode 100644
index 00000000..1ba03664
--- /dev/null
+++ b/at_client/src/main/java/org/atsign/client/impl/commands/Notifications.java
@@ -0,0 +1,130 @@
+package org.atsign.client.impl.commands;
+
+import static org.atsign.client.api.AtEvents.AtEventType.*;
+import static org.atsign.client.impl.commands.AtExceptions.throwOnReadyException;
+import static org.atsign.client.impl.commands.AuthenticationCommands.authenticateWithPkam;
+
+import java.util.Map;
+import java.util.concurrent.CompletableFuture;
+import java.util.concurrent.ExecutionException;
+import java.util.function.Consumer;
+import java.util.regex.Pattern;
+
+import org.atsign.client.api.AtCommandExecutor;
+import org.atsign.client.api.AtEvents;
+import org.atsign.client.api.AtKeys;
+import org.atsign.client.api.AtSign;
+import org.atsign.client.impl.exceptions.AtException;
+
+import lombok.extern.slf4j.Slf4j;
+
+/**
+ * Utility methods for managing notifications within the At Protocol.
+ */
+public class Notifications {
+
+ /**
+ * Models server response string which is non-empty JSON map
+ */
+ private static final Pattern NOTIFICATION_JSON_NON_EMPTY_MAP = Pattern.compile("notification:\\s*(\\{.+})");
+
+ /**
+ * Creates a {@link Consumer} that can be passed to {@link AtCommandExecutor#onReady(Consumer)} to
+ * request notifications.