Author: matthieu
Date: Fri Dec 11 10:08:12 2015
New Revision: 1719321
URL: http://svn.apache.org/viewvc?rev=1719321&view=rev
Log:
JAMES-1644 rework exceptions handling to return different status when a bad
continuation token is given
Modified:
james/project/trunk/server/protocols/jmap/src/main/java/org/apache/james/jmap/AuthenticationServlet.java
james/project/trunk/server/protocols/jmap/src/main/java/org/apache/james/jmap/api/ContinuationTokenManager.java
james/project/trunk/server/protocols/jmap/src/main/java/org/apache/james/jmap/crypto/JamesSignatureHandler.java
james/project/trunk/server/protocols/jmap/src/main/java/org/apache/james/jmap/crypto/SignatureHandler.java
james/project/trunk/server/protocols/jmap/src/main/java/org/apache/james/jmap/crypto/SignedContinuationTokenManager.java
james/project/trunk/server/protocols/jmap/src/main/java/org/apache/james/jmap/model/ContinuationToken.java
james/project/trunk/server/protocols/jmap/src/test/java/org/apache/james/jmap/JMAPAuthenticationTest.java
james/project/trunk/server/protocols/jmap/src/test/java/org/apache/james/jmap/crypto/JamesSignatureHandlerTest.java
james/project/trunk/server/protocols/jmap/src/test/java/org/apache/james/jmap/crypto/SignedContinuationTokenManagerTest.java
Modified:
james/project/trunk/server/protocols/jmap/src/main/java/org/apache/james/jmap/AuthenticationServlet.java
URL:
http://svn.apache.org/viewvc/james/project/trunk/server/protocols/jmap/src/main/java/org/apache/james/jmap/AuthenticationServlet.java?rev=1719321&r1=1719320&r2=1719321&view=diff
==============================================================================
---
james/project/trunk/server/protocols/jmap/src/main/java/org/apache/james/jmap/AuthenticationServlet.java
(original)
+++
james/project/trunk/server/protocols/jmap/src/main/java/org/apache/james/jmap/AuthenticationServlet.java
Fri Dec 11 10:08:12 2015
@@ -139,15 +139,17 @@ public class AuthenticationServlet exten
}
private void handleAccessTokenRequest(AccessTokenRequest request,
HttpServletResponse resp) throws IOException {
- try {
- if (!continuationTokenManager.isValid(request.getToken())) {
- LOG.warn("Use of an invalid ContinuationToken : " +
request.getToken().serialize());
- returnUnauthorizedResponse(resp);
- } else {
- manageAuthenticationResponse(request, resp);
- }
- } catch(Exception e) {
- throw new InternalErrorException("Internal error while managing
access token request", e);
+ switch (continuationTokenManager.getValidity(request.getToken())) {
+ case EXPIRED:
+ returnRestartAuthentication(resp);
+ break;
+ case INVALID:
+ LOG.warn("Use of an invalid ContinuationToken : " +
request.getToken().serialize());
+ returnUnauthorizedResponse(resp);
+ break;
+ case OK:
+ manageAuthenticationResponse(request, resp);
+ break;
}
}
@@ -197,4 +199,7 @@ public class AuthenticationServlet exten
resp.sendError(HttpServletResponse.SC_UNAUTHORIZED);
}
+ private void returnRestartAuthentication(HttpServletResponse resp) throws
IOException {
+ resp.sendError(HttpServletResponse.SC_FORBIDDEN);
+ }
}
Modified:
james/project/trunk/server/protocols/jmap/src/main/java/org/apache/james/jmap/api/ContinuationTokenManager.java
URL:
http://svn.apache.org/viewvc/james/project/trunk/server/protocols/jmap/src/main/java/org/apache/james/jmap/api/ContinuationTokenManager.java?rev=1719321&r1=1719320&r2=1719321&view=diff
==============================================================================
---
james/project/trunk/server/protocols/jmap/src/main/java/org/apache/james/jmap/api/ContinuationTokenManager.java
(original)
+++
james/project/trunk/server/protocols/jmap/src/main/java/org/apache/james/jmap/api/ContinuationTokenManager.java
Fri Dec 11 10:08:12 2015
@@ -22,9 +22,16 @@ package org.apache.james.jmap.api;
import org.apache.james.jmap.model.ContinuationToken;
public interface ContinuationTokenManager {
+ public static enum ContinuationTokenStatus {
+ OK,
+ INVALID,
+ EXPIRED
+ }
- ContinuationToken generateToken(String username) throws Exception;
+ ContinuationToken generateToken(String username);
+
+ ContinuationTokenStatus getValidity(ContinuationToken token);
- boolean isValid(ContinuationToken token) throws Exception;
+ boolean isValid(ContinuationToken token);
}
Modified:
james/project/trunk/server/protocols/jmap/src/main/java/org/apache/james/jmap/crypto/JamesSignatureHandler.java
URL:
http://svn.apache.org/viewvc/james/project/trunk/server/protocols/jmap/src/main/java/org/apache/james/jmap/crypto/JamesSignatureHandler.java?rev=1719321&r1=1719320&r2=1719321&view=diff
==============================================================================
---
james/project/trunk/server/protocols/jmap/src/main/java/org/apache/james/jmap/crypto/JamesSignatureHandler.java
(original)
+++
james/project/trunk/server/protocols/jmap/src/main/java/org/apache/james/jmap/crypto/JamesSignatureHandler.java
Fri Dec 11 10:08:12 2015
@@ -20,22 +20,30 @@
package org.apache.james.jmap.crypto;
import java.io.InputStream;
+import java.security.InvalidKeyException;
import java.security.Key;
import java.security.KeyStore;
+import java.security.NoSuchAlgorithmException;
import java.security.PrivateKey;
import java.security.PublicKey;
import java.security.Signature;
+import java.security.SignatureException;
import org.apache.commons.codec.binary.Base64;
import org.apache.commons.configuration.ConfigurationException;
import org.apache.commons.configuration.HierarchicalConfiguration;
import org.apache.james.filesystem.api.FileSystem;
import org.apache.james.lifecycle.api.Configurable;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
import com.google.common.base.Preconditions;
+import com.google.common.base.Throwables;
public class JamesSignatureHandler implements SignatureHandler, Configurable {
+ private static final Logger LOGGER =
LoggerFactory.getLogger(JamesSignatureHandler.class);
+
public static final String ALIAS = "james";
public static final String ALGORITHM = "SHA1withRSA";
public static final String JKS = "JKS";
@@ -68,21 +76,32 @@ public class JamesSignatureHandler imple
}
@Override
- public String sign(String source) throws Exception {
+ public String sign(String source) {
Preconditions.checkNotNull(source);
- Signature javaSignature = Signature.getInstance(ALGORITHM);
- javaSignature.initSign(privateKey);
- javaSignature.update(source.getBytes());
- return new Base64().encodeAsString(javaSignature.sign());
+ try {
+ Signature javaSignature = Signature.getInstance(ALGORITHM);
+ javaSignature.initSign(privateKey);
+ javaSignature.update(source.getBytes());
+ return new Base64().encodeAsString(javaSignature.sign());
+ } catch (NoSuchAlgorithmException | InvalidKeyException |
SignatureException e) {
+ throw Throwables.propagate(e);
+ }
}
@Override
- public boolean verify(String source, String signature) throws Exception {
+ public boolean verify(String source, String signature) {
Preconditions.checkNotNull(source);
Preconditions.checkNotNull(signature);
- Signature javaSignature = Signature.getInstance(ALGORITHM);
- javaSignature.initVerify(publicKey);
- javaSignature.update(source.getBytes());
- return javaSignature.verify(new Base64().decode(signature));
+ try {
+ Signature javaSignature = Signature.getInstance(ALGORITHM);
+ javaSignature.initVerify(publicKey);
+ javaSignature.update(source.getBytes());
+ return javaSignature.verify(new Base64().decode(signature));
+ } catch (NoSuchAlgorithmException | InvalidKeyException e) {
+ throw Throwables.propagate(e);
+ } catch (SignatureException e) {
+ LOGGER.warn("Attempt to use a malformed signature '"+ signature +
"' for source '" + source + "'", e);
+ return false;
+ }
}
}
Modified:
james/project/trunk/server/protocols/jmap/src/main/java/org/apache/james/jmap/crypto/SignatureHandler.java
URL:
http://svn.apache.org/viewvc/james/project/trunk/server/protocols/jmap/src/main/java/org/apache/james/jmap/crypto/SignatureHandler.java?rev=1719321&r1=1719320&r2=1719321&view=diff
==============================================================================
---
james/project/trunk/server/protocols/jmap/src/main/java/org/apache/james/jmap/crypto/SignatureHandler.java
(original)
+++
james/project/trunk/server/protocols/jmap/src/main/java/org/apache/james/jmap/crypto/SignatureHandler.java
Fri Dec 11 10:08:12 2015
@@ -21,8 +21,8 @@ package org.apache.james.jmap.crypto;
public interface SignatureHandler {
- String sign(String source) throws Exception;
+ String sign(String source);
- boolean verify(String source, String signature) throws Exception;
+ boolean verify(String source, String signature);
}
Modified:
james/project/trunk/server/protocols/jmap/src/main/java/org/apache/james/jmap/crypto/SignedContinuationTokenManager.java
URL:
http://svn.apache.org/viewvc/james/project/trunk/server/protocols/jmap/src/main/java/org/apache/james/jmap/crypto/SignedContinuationTokenManager.java?rev=1719321&r1=1719320&r2=1719321&view=diff
==============================================================================
---
james/project/trunk/server/protocols/jmap/src/main/java/org/apache/james/jmap/crypto/SignedContinuationTokenManager.java
(original)
+++
james/project/trunk/server/protocols/jmap/src/main/java/org/apache/james/jmap/crypto/SignedContinuationTokenManager.java
Fri Dec 11 10:08:12 2015
@@ -19,21 +19,17 @@
package org.apache.james.jmap.crypto;
-import com.google.common.base.Preconditions;
+import java.time.ZonedDateTime;
+import java.time.format.DateTimeFormatter;
+
import org.apache.james.jmap.api.ContinuationTokenManager;
import org.apache.james.jmap.model.ContinuationToken;
import org.apache.james.jmap.utils.ZonedDateTimeProvider;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
-import java.security.SignatureException;
-import java.time.ZonedDateTime;
-import java.time.format.DateTimeFormatter;
+import com.google.common.base.Preconditions;
public class SignedContinuationTokenManager implements
ContinuationTokenManager {
- private static final Logger LOGGER =
LoggerFactory.getLogger(SignedContinuationTokenManager.class);
-
private final SignatureHandler signatureHandler;
private final ZonedDateTimeProvider zonedDateTimeProvider;
@@ -43,7 +39,7 @@ public class SignedContinuationTokenMana
}
@Override
- public ContinuationToken generateToken(String username) throws Exception {
+ public ContinuationToken generateToken(String username) {
Preconditions.checkNotNull(username);
ZonedDateTime expirationTime =
zonedDateTimeProvider.provide().plusMinutes(15);
return new ContinuationToken(username,
@@ -52,25 +48,28 @@ public class SignedContinuationTokenMana
}
@Override
- public boolean isValid(ContinuationToken token) throws Exception {
+ public ContinuationTokenStatus getValidity(ContinuationToken token) {
Preconditions.checkNotNull(token);
- try {
- return ! isTokenOutdated(token)
- && isCorrectlySigned(token);
- } catch (SignatureException e) {
- LOGGER.warn("Attempt to use a malformed signature for user " +
token.getUsername(), e);
- return false;
+ if (! isCorrectlySigned(token)) {
+ return ContinuationTokenStatus.INVALID;
+ }
+ if (isExpired(token)) {
+ return ContinuationTokenStatus.EXPIRED;
}
+ return ContinuationTokenStatus.OK;
+ }
+
+ @Override
+ public boolean isValid(ContinuationToken token) {
+ Preconditions.checkNotNull(token);
+ return ContinuationTokenStatus.OK.equals(getValidity(token));
}
- private boolean isCorrectlySigned(ContinuationToken token) throws
Exception {
- return signatureHandler.verify(token.getUsername()
- + ContinuationToken.SEPARATOR
- +
DateTimeFormatter.ISO_OFFSET_DATE_TIME.format(token.getExpirationDate()),
- token.getSignature());
+ private boolean isCorrectlySigned(ContinuationToken token) {
+ return signatureHandler.verify(token.getContent(),
token.getSignature());
}
- private boolean isTokenOutdated(ContinuationToken token) {
+ private boolean isExpired(ContinuationToken token) {
return
token.getExpirationDate().isBefore(zonedDateTimeProvider.provide());
}
}
Modified:
james/project/trunk/server/protocols/jmap/src/main/java/org/apache/james/jmap/model/ContinuationToken.java
URL:
http://svn.apache.org/viewvc/james/project/trunk/server/protocols/jmap/src/main/java/org/apache/james/jmap/model/ContinuationToken.java?rev=1719321&r1=1719320&r2=1719321&view=diff
==============================================================================
---
james/project/trunk/server/protocols/jmap/src/main/java/org/apache/james/jmap/model/ContinuationToken.java
(original)
+++
james/project/trunk/server/protocols/jmap/src/main/java/org/apache/james/jmap/model/ContinuationToken.java
Fri Dec 11 10:08:12 2015
@@ -112,12 +112,16 @@ public class ContinuationToken {
}
public String serialize() {
- return username
- + SEPARATOR
- + DateTimeFormatter.ISO_OFFSET_DATE_TIME.format(expirationDate)
+ return getContent()
+ SEPARATOR
+ signature;
}
+
+ public String getContent() {
+ return username
+ + SEPARATOR
+ + DateTimeFormatter.ISO_OFFSET_DATE_TIME.format(expirationDate);
+ }
@Override
public boolean equals(Object other) {
Modified:
james/project/trunk/server/protocols/jmap/src/test/java/org/apache/james/jmap/JMAPAuthenticationTest.java
URL:
http://svn.apache.org/viewvc/james/project/trunk/server/protocols/jmap/src/test/java/org/apache/james/jmap/JMAPAuthenticationTest.java?rev=1719321&r1=1719320&r2=1719321&view=diff
==============================================================================
---
james/project/trunk/server/protocols/jmap/src/test/java/org/apache/james/jmap/JMAPAuthenticationTest.java
(original)
+++
james/project/trunk/server/protocols/jmap/src/test/java/org/apache/james/jmap/JMAPAuthenticationTest.java
Fri Dec 11 10:08:12 2015
@@ -44,6 +44,7 @@ import org.apache.james.jmap.crypto.Acce
import org.apache.james.jmap.crypto.JamesSignatureHandlerProvider;
import org.apache.james.jmap.crypto.SignedContinuationTokenManager;
import org.apache.james.jmap.memory.access.MemoryAccessTokenRepository;
+import org.apache.james.jmap.model.ContinuationToken;
import org.apache.james.jmap.utils.ZonedDateTimeProvider;
import org.apache.james.user.api.UsersRepository;
import org.apache.james.user.api.UsersRepositoryException;
@@ -244,6 +245,25 @@ public class JMAPAuthenticationTest {
@Test
public void
mustReturnAuthenticationFailedWhenContinuationTokenIsRejectedByTheContinuationTokenManager()
throws Exception {
+ ContinuationToken badContinuationToken = new
ContinuationToken("[email protected]", newDate, "badSignature");
+
+ when(mockedUsersRepository.test("[email protected]", "password"))
+ .thenReturn(true);
+ when(mockedZonedDateTimeProvider.provide())
+ .thenReturn(oldDate);
+
+ given()
+ .contentType(ContentType.JSON)
+ .accept(ContentType.JSON)
+ .body("{\"token\": \"" + badContinuationToken.serialize() + "\",
\"method\": \"password\", \"password\": \"password\"}")
+ .when()
+ .post("/authentication")
+ .then()
+ .statusCode(401);
+ }
+
+ @Test
+ public void
mustReturnRestartAuthenticationWhenContinuationTokenIsExpired() throws
Exception {
when(mockedZonedDateTimeProvider.provide())
.thenReturn(oldDate);
@@ -261,7 +281,7 @@ public class JMAPAuthenticationTest {
.when()
.post("/authentication")
.then()
- .statusCode(401);
+ .statusCode(403);
}
@Test
Modified:
james/project/trunk/server/protocols/jmap/src/test/java/org/apache/james/jmap/crypto/JamesSignatureHandlerTest.java
URL:
http://svn.apache.org/viewvc/james/project/trunk/server/protocols/jmap/src/test/java/org/apache/james/jmap/crypto/JamesSignatureHandlerTest.java?rev=1719321&r1=1719320&r2=1719321&view=diff
==============================================================================
---
james/project/trunk/server/protocols/jmap/src/test/java/org/apache/james/jmap/crypto/JamesSignatureHandlerTest.java
(original)
+++
james/project/trunk/server/protocols/jmap/src/test/java/org/apache/james/jmap/crypto/JamesSignatureHandlerTest.java
Fri Dec 11 10:08:12 2015
@@ -24,8 +24,6 @@ import static org.assertj.core.api.Asser
import org.junit.Before;
import org.junit.Test;
-import java.security.SignatureException;
-
public class JamesSignatureHandlerTest {
public static final String SIGNATURE =
"NeIFNei4p6vn085wCEw0pbEwJ+Oak5yEIRLZsDcRVzT9rWWOcLvDFUA3S6awi/bxPiFxqJFreVz6xqzehnUI4tUBupk3sIsqeXShhFWBpaV+m58mC41lT/A0RJa3GgCvg6kmweCRf3tOo0+gvwOQJdwCL2B21GjDCKqBHaiK+OHcsSjrQW0xuew5z84EAz3ErdH4MMNjITksxK5FG/cGQ9V6LQgwcPk0RrprVC4eY7FFHw/sQNlJpZKsSFLnn5igPQkQtjiQ4ay1/xoB7FU7aJLakxRhYOnTKgper/Ur7UWOZJaE+4EjcLwCFLF9GaCILwp9W+mf/f7j92PVEU50Vg==";
@@ -49,9 +47,9 @@ public class JamesSignatureHandlerTest {
assertThat(signatureHandler.verify(SOURCE,
signatureHandler.sign(FAKE_SIGNATURE))).isFalse();
}
- @Test(expected = SignatureException.class)
- public void incorrectLengthSignatureShouldThrow() throws Exception {
- signatureHandler.verify(SOURCE, "signature");
+ @Test
+ public void incorrectLengthSignatureShouldReturnFalse() throws Exception {
+ assertThat(signatureHandler.verify(SOURCE, "signature")).isFalse();
}
@Test(expected = NullPointerException.class)
Modified:
james/project/trunk/server/protocols/jmap/src/test/java/org/apache/james/jmap/crypto/SignedContinuationTokenManagerTest.java
URL:
http://svn.apache.org/viewvc/james/project/trunk/server/protocols/jmap/src/test/java/org/apache/james/jmap/crypto/SignedContinuationTokenManagerTest.java?rev=1719321&r1=1719320&r2=1719321&view=diff
==============================================================================
---
james/project/trunk/server/protocols/jmap/src/test/java/org/apache/james/jmap/crypto/SignedContinuationTokenManagerTest.java
(original)
+++
james/project/trunk/server/protocols/jmap/src/test/java/org/apache/james/jmap/crypto/SignedContinuationTokenManagerTest.java
Fri Dec 11 10:08:12 2015
@@ -24,6 +24,7 @@ import static org.mockito.Mockito.when;
import static org.assertj.core.api.Assertions.assertThat;
+import
org.apache.james.jmap.api.ContinuationTokenManager.ContinuationTokenStatus;
import org.apache.james.jmap.model.ContinuationToken;
import org.apache.james.jmap.utils.ZonedDateTimeProvider;
import org.junit.Before;
@@ -48,6 +49,11 @@ public class SignedContinuationTokenMana
toKenManager = new SignedContinuationTokenManager(signatureHandler,
zonedDateTimeProvider);
}
+ @Test(expected = NullPointerException.class)
+ public void isValidShouldThrowWhenTokenIsNull() throws Exception {
+ toKenManager.isValid(null);
+ }
+
@Test
public void isValidShouldRecognizeValidTokens() throws Exception {
when(zonedDateTimeProvider.provide()).thenAnswer(invocationOnMock ->
DATE);
@@ -95,21 +101,75 @@ public class SignedContinuationTokenMana
assertThat(toKenManager.isValid(continuationToken)).isFalse();
}
- @Test(expected = NullPointerException.class)
- public void generateTokenShouldThrowWhenUsernameIsNull() throws Exception {
- toKenManager.generateToken(null);
+ @Test
+ public void isValidShouldReturnFalseOnNonValidSignatures() throws
Exception {
+ when(zonedDateTimeProvider.provide()).thenAnswer(invocationOnMock ->
DATE);
+ ContinuationToken pirateContinuationToken = new
ContinuationToken("user", DATE.plusMinutes(15), "fake");
+ assertThat(toKenManager.isValid(pirateContinuationToken)).isFalse();
}
@Test(expected = NullPointerException.class)
- public void isValidShouldThrowWhenTokenIsNull() throws Exception {
- toKenManager.isValid(null);
+ public void getValidityShouldThrowWhenTokenIsNull() throws Exception {
+ toKenManager.getValidity(null);
}
@Test
- public void isValidShouldReturnFalseOnNonValidSignatures() throws
Exception {
+ public void getValidityShouldRecognizeValidTokens() throws Exception {
+ when(zonedDateTimeProvider.provide()).thenAnswer(invocationOnMock ->
DATE);
+ assertThat(
+ toKenManager.getValidity(
+ toKenManager.generateToken("user")))
+ .isEqualTo(ContinuationTokenStatus.OK);
+ }
+
+ @Test
+ public void getValidityShouldRecognizeTokenWhereUsernameIsModified()
throws Exception {
+ when(zonedDateTimeProvider.provide()).thenAnswer(invocationOnMock ->
DATE);
+ ContinuationToken continuationToken =
toKenManager.generateToken("user");
+ ContinuationToken pirateContinuationToken = new
ContinuationToken("pirate",
+ continuationToken.getExpirationDate(),
+ continuationToken.getSignature());
+
assertThat(toKenManager.getValidity(pirateContinuationToken)).isEqualTo(ContinuationTokenStatus.INVALID);
+ }
+
+ @Test
+ public void getValidityhouldRecognizeTokenWhereExpirationDateIsModified()
throws Exception {
+ when(zonedDateTimeProvider.provide()).thenAnswer(invocationOnMock ->
DATE);
+ ContinuationToken continuationToken =
toKenManager.generateToken("user");
+ ContinuationToken pirateContinuationToken = new
ContinuationToken(continuationToken.getUsername(),
+ continuationToken.getExpirationDate().plusHours(1),
+ continuationToken.getSignature());
+
assertThat(toKenManager.getValidity(pirateContinuationToken)).isEqualTo(ContinuationTokenStatus.INVALID);
+ }
+
+ @Test
+ public void getValidityShouldRecognizeTokenWhereSignatureIsModified()
throws Exception {
+ when(zonedDateTimeProvider.provide()).thenAnswer(invocationOnMock ->
DATE);
+ ContinuationToken continuationToken =
toKenManager.generateToken("user");
+ ContinuationToken pirateContinuationToken = new
ContinuationToken(continuationToken.getUsername(),
+ continuationToken.getExpirationDate(),
+ FAKE_SIGNATURE);
+
assertThat(toKenManager.getValidity(pirateContinuationToken)).isEqualTo(ContinuationTokenStatus.INVALID);
+ }
+
+ @Test
+ public void getValidityShouldReturnFalseWhenTokenIsOutdated() throws
Exception {
+ when(zonedDateTimeProvider.provide()).thenAnswer(invocationOnMock ->
DATE);
+ ContinuationToken continuationToken =
toKenManager.generateToken("user");
+ when(zonedDateTimeProvider.provide()).thenAnswer(invocationOnMock ->
DATE.plusHours(1));
+
assertThat(toKenManager.getValidity(continuationToken)).isEqualTo(ContinuationTokenStatus.EXPIRED);
+ }
+
+ @Test
+ public void getValidityShouldReturnFalseOnNonValidSignatures() throws
Exception {
when(zonedDateTimeProvider.provide()).thenAnswer(invocationOnMock ->
DATE);
ContinuationToken pirateContinuationToken = new
ContinuationToken("user", DATE.plusMinutes(15), "fake");
- assertThat(toKenManager.isValid(pirateContinuationToken)).isFalse();
+
assertThat(toKenManager.getValidity(pirateContinuationToken)).isEqualTo(ContinuationTokenStatus.INVALID);
+ }
+
+ @Test(expected = NullPointerException.class)
+ public void generateTokenShouldThrowWhenUsernameIsNull() throws Exception {
+ toKenManager.generateToken(null);
}
@Test
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]