package co.codewizards.cloudstore.rest.client.transport;
import java.net.MalformedURLException;
import java.net.URL;
import java.security.GeneralSecurityException;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;
import java.util.UUID;
import javax.ws.rs.client.ClientBuilder;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import co.codewizards.cloudstore.core.auth.AuthConstants;
import co.codewizards.cloudstore.core.auth.AuthToken;
import co.codewizards.cloudstore.core.auth.AuthTokenIO;
import co.codewizards.cloudstore.core.auth.AuthTokenVerifier;
import co.codewizards.cloudstore.core.auth.EncryptedSignedAuthToken;
import co.codewizards.cloudstore.core.auth.SignedAuthToken;
import co.codewizards.cloudstore.core.auth.SignedAuthTokenDecrypter;
import co.codewizards.cloudstore.core.auth.SignedAuthTokenIO;
import co.codewizards.cloudstore.core.concurrent.DeferredCompletionException;
import co.codewizards.cloudstore.core.dto.ChangeSetDto;
import co.codewizards.cloudstore.core.dto.ConfigPropSetDto;
import co.codewizards.cloudstore.core.dto.DateTime;
import co.codewizards.cloudstore.core.dto.RepoFileDto;
import co.codewizards.cloudstore.core.dto.RepositoryDto;
import co.codewizards.cloudstore.core.dto.VersionInfoDto;
import co.codewizards.cloudstore.core.io.TimeoutException;
import co.codewizards.cloudstore.core.oio.File;
import co.codewizards.cloudstore.core.repo.local.LocalRepoManager;
import co.codewizards.cloudstore.core.repo.local.LocalRepoManagerFactory;
import co.codewizards.cloudstore.core.repo.local.LocalRepoRegistryImpl;
import co.codewizards.cloudstore.core.repo.transport.AbstractRepoTransport;
import co.codewizards.cloudstore.core.util.AssertUtil;
import co.codewizards.cloudstore.rest.client.ClientBuilderDefaultValuesDecorator;
import co.codewizards.cloudstore.rest.client.CloudStoreRestClient;
import co.codewizards.cloudstore.rest.client.CredentialsProvider;
import co.codewizards.cloudstore.rest.client.request.BeginPutFile;
import co.codewizards.cloudstore.rest.client.request.Copy;
import co.codewizards.cloudstore.rest.client.request.Delete;
import co.codewizards.cloudstore.rest.client.request.EndPutFile;
import co.codewizards.cloudstore.rest.client.request.EndSyncFromRepository;
import co.codewizards.cloudstore.rest.client.request.EndSyncToRepository;
import co.codewizards.cloudstore.rest.client.request.GetChangeSetDto;
import co.codewizards.cloudstore.rest.client.request.GetEncryptedSignedAuthToken;
import co.codewizards.cloudstore.rest.client.request.GetFileData;
import co.codewizards.cloudstore.rest.client.request.GetRepoFileDto;
import co.codewizards.cloudstore.rest.client.request.GetRepositoryDto;
import co.codewizards.cloudstore.rest.client.request.GetVersionInfoDto;
import co.codewizards.cloudstore.rest.client.request.MakeDirectory;
import co.codewizards.cloudstore.rest.client.request.MakeSymlink;
import co.codewizards.cloudstore.rest.client.request.Move;
import co.codewizards.cloudstore.rest.client.request.PutFileData;
import co.codewizards.cloudstore.rest.client.request.PutParentConfigPropSetDto;
import co.codewizards.cloudstore.rest.client.request.RequestRepoConnection;
import co.codewizards.cloudstore.rest.client.ssl.DynamicX509TrustManagerCallback;
import co.codewizards.cloudstore.rest.client.ssl.SSLContextBuilder;
public class RestRepoTransport extends AbstractRepoTransport implements CredentialsProvider {
private static final Logger logger = LoggerFactory.getLogger(RestRepoTransport.class);
private final long changeSetTimeout = 60L * 60L * 1000L; // TODO make configurable!
private final long fileChunkSetTimeout = 60L * 60L * 1000L; // TODO make configurable!
private UUID repositoryId; // server-repository
private byte[] publicKey;
private String repositoryName; // server-repository
private CloudStoreRestClient client;
private final Map<UUID, AuthToken> clientRepositoryId2AuthToken = new HashMap<UUID, AuthToken>(1); // should never be more ;-)
protected DynamicX509TrustManagerCallback getDynamicX509TrustManagerCallback() {
final RestRepoTransportFactory repoTransportFactory = (RestRepoTransportFactory) getRepoTransportFactory();
final Class<? extends DynamicX509TrustManagerCallback> klass = repoTransportFactory.getDynamicX509TrustManagerCallbackClass();
if (klass == null)
throw new IllegalStateException("dynamicX509TrustManagerCallbackClass is not set!");
try {
final DynamicX509TrustManagerCallback instance = klass.newInstance();
return instance;
} catch (final Exception e) {
throw new RuntimeException(String.format("Could not instantiate class %s: %s", klass.getName(), e.toString()), e);
}
}
public RestRepoTransport() { }
@Override
public UUID getRepositoryId() {
if (repositoryId == null) {
final RepositoryDto repositoryDto = getRepositoryDto();
repositoryId = repositoryDto.getRepositoryId();
publicKey = repositoryDto.getPublicKey();
}
return repositoryId;
}
@Override
public byte[] getPublicKey() {
getRepositoryId(); // ensure, the public key is loaded
return AssertUtil.assertNotNull(publicKey, "publicKey");
}
@Override
public RepositoryDto getRepositoryDto() {
return getClient().execute(new GetRepositoryDto(getRepositoryName()));
}
@Override
public void requestRepoConnection(final byte[] publicKey) {
final RepositoryDto repositoryDto = new RepositoryDto();
repositoryDto.setRepositoryId(getClientRepositoryIdOrFail());
repositoryDto.setPublicKey(publicKey);
getClient().execute(new RequestRepoConnection(getRepositoryName(), getPathPrefix(), repositoryDto));
}
@Override
public void close() {
client = null;
super.close();
}
@Override
public ChangeSetDto getChangeSetDto(final boolean localSync) {
final long beginTimestamp = System.currentTimeMillis();
while (true) {
try {
return getClient().execute(new GetChangeSetDto(getRepositoryId().toString(), localSync));
} catch (final DeferredCompletionException x) {
if (System.currentTimeMillis() > beginTimestamp + changeSetTimeout)
throw new TimeoutException(String.format("Could not get change-set within %s milliseconds!", changeSetTimeout), x);
logger.info("getChangeSet: Got DeferredCompletionException; will retry.");
}
}
}
@Override
public void prepareForChangeSetDto(ChangeSetDto changeSetDto) {
// nothing to do here.
}
@Override
public void makeDirectory(String path, final Date lastModified) {
path = prefixPath(path);
getClient().execute(new MakeDirectory(getRepositoryId().toString(), path, lastModified));
}
@Override
public void makeSymlink(String path, final String target, final Date lastModified) {
path = prefixPath(path);
getClient().execute(new MakeSymlink(getRepositoryId().toString(), path, target, lastModified));
}
@Override
public void copy(String fromPath, String toPath) {
fromPath = prefixPath(fromPath);
toPath = prefixPath(toPath);
getClient().execute(new Copy(getRepositoryId().toString(), fromPath, toPath));
}
@Override
public void move(String fromPath, String toPath) {
fromPath = prefixPath(fromPath);
toPath = prefixPath(toPath);
getClient().execute(new Move(getRepositoryId().toString(), fromPath, toPath));
}
@Override
public void delete(String path) {
path = prefixPath(path);
getClient().execute(new Delete(getRepositoryId().toString(), path));
}
@Override
public RepoFileDto getRepoFileDto(String path) {
path = prefixPath(path);
final long beginTimestamp = System.currentTimeMillis();
while (true) {
try {
return getClient().execute(new GetRepoFileDto(getRepositoryId().toString(), path));
} catch (final DeferredCompletionException x) {
if (System.currentTimeMillis() > beginTimestamp + fileChunkSetTimeout)
throw new TimeoutException(String.format("Could not get file-chunk-set within %s milliseconds!", fileChunkSetTimeout), x);
logger.info("getFileChunkSet: Got DeferredCompletionException; will retry.");
}
}
}
@Override
public byte[] getFileData(String path, final long offset, final int length) {
path = prefixPath(path);
return getClient().execute(new GetFileData(getRepositoryId().toString(), path, offset, length));
}
@Override
public void beginPutFile(String path) {
path = prefixPath(path);
getClient().execute(new BeginPutFile(getRepositoryId().toString(), path));
}
@Override
public void putFileData(String path, final long offset, final byte[] fileData) {
path = prefixPath(path);
getClient().execute(new PutFileData(getRepositoryId().toString(), path, offset, fileData));
}
@Override
public void endPutFile(String path, final Date lastModified, final long length, final String sha1) {
path = prefixPath(path);
getClient().execute(new EndPutFile(getRepositoryId().toString(), path, new DateTime(lastModified), length, sha1));
}
@Override
public void endSyncFromRepository() {
getClient().execute(new EndSyncFromRepository(getRepositoryId().toString()));
}
@Override
public void endSyncToRepository(final long fromLocalRevision) {
getClient().execute(new EndSyncToRepository(getRepositoryId().toString(), fromLocalRevision));
}
@Override
public void putParentConfigPropSetDto(ConfigPropSetDto parentConfigPropSetDto) {
getClient().execute(new PutParentConfigPropSetDto(getRepositoryId().toString(), parentConfigPropSetDto));
}
@Override
public String getUserName() {
final UUID clientRepositoryId = getClientRepositoryIdOrFail();
return AuthConstants.USER_NAME_REPOSITORY_ID_PREFIX + clientRepositoryId;
}
@Override
public String getPassword() {
final AuthToken authToken = getAuthToken();
return authToken.getPassword();
}
private AuthToken getAuthToken() {
final UUID clientRepositoryId = getClientRepositoryIdOrFail();
AuthToken authToken = clientRepositoryId2AuthToken.get(clientRepositoryId);
if (authToken != null && isAfterRenewalDate(authToken)) {
logger.debug("getAuthToken: old AuthToken passed renewal-date: clientRepositoryId={} serverRepositoryId={} renewalDateTime={} expiryDateTime={}",
clientRepositoryId, getRepositoryId(), authToken.getRenewalDateTime(), authToken.getExpiryDateTime());
authToken = null;
}
if (authToken == null) {
logger.debug("getAuthToken: getting new AuthToken: clientRepositoryId={} serverRepositoryId={}",
clientRepositoryId, getRepositoryId());
final File localRoot = LocalRepoRegistryImpl.getInstance().getLocalRoot(clientRepositoryId);
final LocalRepoManager localRepoManager = LocalRepoManagerFactory.Helper.getInstance().createLocalRepoManagerForExistingRepository(localRoot);
try {
final EncryptedSignedAuthToken encryptedSignedAuthToken = getClient().execute(new GetEncryptedSignedAuthToken(getRepositoryName(), localRepoManager.getRepositoryId()));
final byte[] signedAuthTokenData = new SignedAuthTokenDecrypter(localRepoManager.getPrivateKey()).decrypt(encryptedSignedAuthToken);
final SignedAuthToken signedAuthToken = new SignedAuthTokenIO().deserialise(signedAuthTokenData);
final AuthTokenVerifier verifier = new AuthTokenVerifier(localRepoManager.getRemoteRepositoryPublicKeyOrFail(getRepositoryId()));
verifier.verify(signedAuthToken);
authToken = new AuthTokenIO().deserialise(signedAuthToken.getAuthTokenData());
final Date expiryDate = AssertUtil.assertNotNull(authToken.getExpiryDateTime(), "authToken.expiryDateTime").toDate();
final Date renewalDate = AssertUtil.assertNotNull(authToken.getRenewalDateTime(), "authToken.renewalDateTime").toDate();
if (!renewalDate.before(expiryDate))
throw new IllegalArgumentException(
String.format("Invalid AuthToken: renewalDateTime >= expiryDateTime :: renewalDateTime=%s expiryDateTime=%s",
authToken.getRenewalDateTime(), authToken.getExpiryDateTime()));
clientRepositoryId2AuthToken.put(clientRepositoryId, authToken);
} finally {
localRepoManager.close();
}
logger.info("getAuthToken: got new AuthToken: clientRepositoryId={} serverRepositoryId={} renewalDateTime={} expiryDateTime={}",
clientRepositoryId, getRepositoryId(), authToken.getRenewalDateTime(), authToken.getExpiryDateTime());
}
else
logger.trace("getAuthToken: old AuthToken still valid: clientRepositoryId={} serverRepositoryId={} renewalDateTime={} expiryDateTime={}",
clientRepositoryId, getRepositoryId(), authToken.getRenewalDateTime(), authToken.getExpiryDateTime());
return authToken;
}
private boolean isAfterRenewalDate(final AuthToken authToken) {
AssertUtil.assertNotNull(authToken, "authToken");
return System.currentTimeMillis() > authToken.getRenewalDateTime().getMillis();
}
protected CloudStoreRestClient getClient() {
if (client == null) {
ClientBuilder clientBuilder = createClientBuilder();
final CloudStoreRestClient c = new CloudStoreRestClient(getRemoteRoot(), clientBuilder);
c.setCredentialsProvider(this);
client = c;
}
return client;
}
@Override
protected URL determineRemoteRootWithoutPathPrefix() {
final String repositoryName = getRepositoryName();
final String baseURL = getClient().getBaseUrl();
if (!baseURL.endsWith("/"))
throw new IllegalStateException(String.format("baseURL does not end with a '/'! baseURL='%s'", baseURL));
try {
return new URL(baseURL + repositoryName);
} catch (final MalformedURLException e) {
throw new RuntimeException(e);
}
}
public String getRepositoryName() {
if (repositoryName == null) {
final String pathAfterBaseURL = getPathAfterBaseURL();
final int indexOfFirstSlash = pathAfterBaseURL.indexOf('/');
if (indexOfFirstSlash < 0) {
repositoryName = pathAfterBaseURL;
}
else {
repositoryName = pathAfterBaseURL.substring(0, indexOfFirstSlash);
}
if (repositoryName.isEmpty())
throw new IllegalStateException("repositoryName is empty!");
}
return repositoryName;
}
private String pathAfterBaseURL;
protected String getPathAfterBaseURL() {
String pathAfterBaseURL = this.pathAfterBaseURL;
if (pathAfterBaseURL == null) {
final URL remoteRoot = getRemoteRoot();
if (remoteRoot == null)
throw new IllegalStateException("remoteRoot not yet assigned!");
final String baseURL = getClient().getBaseUrl();
if (!baseURL.endsWith("/"))
throw new IllegalStateException(String.format("baseURL does not end with a '/'! remoteRoot='%s' baseURL='%s'", remoteRoot, baseURL));
final String remoteRootString = remoteRoot.toExternalForm();
if (!remoteRootString.startsWith(baseURL))
throw new IllegalStateException(String.format("remoteRoot does not start with baseURL! remoteRoot='%s' baseURL='%s'", remoteRoot, baseURL));
this.pathAfterBaseURL = pathAfterBaseURL = remoteRootString.substring(baseURL.length());
}
return pathAfterBaseURL;
}
private ClientBuilder createClientBuilder(){
final ClientBuilder builder = new ClientBuilderDefaultValuesDecorator();
try {
builder.sslContext(SSLContextBuilder.create()
.remoteURL(getRemoteRoot())
.callback(getDynamicX509TrustManagerCallback()).build());
} catch (final GeneralSecurityException e) {
throw new RuntimeException(e);
}
return builder;
}
@Override
public VersionInfoDto getVersionInfoDto() {
final VersionInfoDto versionInfoDto = getClient().execute(new GetVersionInfoDto());
return versionInfoDto;
}
}