001package co.codewizards.cloudstore.rest.client.transport; 002 003import java.net.MalformedURLException; 004import java.net.URL; 005import java.security.GeneralSecurityException; 006import java.util.Date; 007import java.util.HashMap; 008import java.util.Map; 009import java.util.UUID; 010 011import javax.ws.rs.client.ClientBuilder; 012 013import org.slf4j.Logger; 014import org.slf4j.LoggerFactory; 015 016import co.codewizards.cloudstore.core.auth.AuthConstants; 017import co.codewizards.cloudstore.core.auth.AuthToken; 018import co.codewizards.cloudstore.core.auth.AuthTokenIO; 019import co.codewizards.cloudstore.core.auth.AuthTokenVerifier; 020import co.codewizards.cloudstore.core.auth.EncryptedSignedAuthToken; 021import co.codewizards.cloudstore.core.auth.SignedAuthToken; 022import co.codewizards.cloudstore.core.auth.SignedAuthTokenDecrypter; 023import co.codewizards.cloudstore.core.auth.SignedAuthTokenIO; 024import co.codewizards.cloudstore.core.concurrent.DeferredCompletionException; 025import co.codewizards.cloudstore.core.dto.ChangeSetDto; 026import co.codewizards.cloudstore.core.dto.ConfigPropSetDto; 027import co.codewizards.cloudstore.core.dto.DateTime; 028import co.codewizards.cloudstore.core.dto.RepoFileDto; 029import co.codewizards.cloudstore.core.dto.RepositoryDto; 030import co.codewizards.cloudstore.core.dto.VersionInfoDto; 031import co.codewizards.cloudstore.core.io.TimeoutException; 032import co.codewizards.cloudstore.core.oio.File; 033import co.codewizards.cloudstore.core.repo.local.LocalRepoManager; 034import co.codewizards.cloudstore.core.repo.local.LocalRepoManagerFactory; 035import co.codewizards.cloudstore.core.repo.local.LocalRepoRegistryImpl; 036import co.codewizards.cloudstore.core.repo.transport.AbstractRepoTransport; 037import co.codewizards.cloudstore.core.util.AssertUtil; 038import co.codewizards.cloudstore.rest.client.ClientBuilderDefaultValuesDecorator; 039import co.codewizards.cloudstore.rest.client.CloudStoreRestClient; 040import co.codewizards.cloudstore.rest.client.CredentialsProvider; 041import co.codewizards.cloudstore.rest.client.request.BeginPutFile; 042import co.codewizards.cloudstore.rest.client.request.Copy; 043import co.codewizards.cloudstore.rest.client.request.Delete; 044import co.codewizards.cloudstore.rest.client.request.EndPutFile; 045import co.codewizards.cloudstore.rest.client.request.EndSyncFromRepository; 046import co.codewizards.cloudstore.rest.client.request.EndSyncToRepository; 047import co.codewizards.cloudstore.rest.client.request.GetChangeSetDto; 048import co.codewizards.cloudstore.rest.client.request.GetClientRepositoryDto; 049import co.codewizards.cloudstore.rest.client.request.GetEncryptedSignedAuthToken; 050import co.codewizards.cloudstore.rest.client.request.GetFileData; 051import co.codewizards.cloudstore.rest.client.request.GetRepoFileDto; 052import co.codewizards.cloudstore.rest.client.request.GetRepositoryDto; 053import co.codewizards.cloudstore.rest.client.request.GetVersionInfoDto; 054import co.codewizards.cloudstore.rest.client.request.MakeDirectory; 055import co.codewizards.cloudstore.rest.client.request.MakeSymlink; 056import co.codewizards.cloudstore.rest.client.request.Move; 057import co.codewizards.cloudstore.rest.client.request.PutFileData; 058import co.codewizards.cloudstore.rest.client.request.PutParentConfigPropSetDto; 059import co.codewizards.cloudstore.rest.client.request.RequestRepoConnection; 060import co.codewizards.cloudstore.rest.client.ssl.DynamicX509TrustManagerCallback; 061import co.codewizards.cloudstore.rest.client.ssl.SSLContextBuilder; 062 063public class RestRepoTransport extends AbstractRepoTransport implements CredentialsProvider { 064 private static final Logger logger = LoggerFactory.getLogger(RestRepoTransport.class); 065 066 private final long changeSetTimeout = 60L * 60L * 1000L; // TODO make configurable! 067 private final long fileChunkSetTimeout = 60L * 60L * 1000L; // TODO make configurable! 068 069 private UUID repositoryId; // server-repository 070 private byte[] publicKey; 071 private String repositoryName; // server-repository 072 private CloudStoreRestClient client; 073 private final Map<UUID, AuthToken> clientRepositoryId2AuthToken = new HashMap<UUID, AuthToken>(1); // should never be more ;-) 074 075 protected DynamicX509TrustManagerCallback getDynamicX509TrustManagerCallback() { 076 final RestRepoTransportFactory repoTransportFactory = (RestRepoTransportFactory) getRepoTransportFactory(); 077 final Class<? extends DynamicX509TrustManagerCallback> klass = repoTransportFactory.getDynamicX509TrustManagerCallbackClass(); 078 if (klass == null) 079 throw new IllegalStateException("dynamicX509TrustManagerCallbackClass is not set!"); 080 081 try { 082 final DynamicX509TrustManagerCallback instance = klass.newInstance(); 083 return instance; 084 } catch (final Exception e) { 085 throw new RuntimeException(String.format("Could not instantiate class %s: %s", klass.getName(), e.toString()), e); 086 } 087 } 088 089 public RestRepoTransport() { } 090 091 @Override 092 public UUID getRepositoryId() { 093 if (repositoryId == null) { 094 final RepositoryDto repositoryDto = getRepositoryDto(); 095 repositoryId = repositoryDto.getRepositoryId(); 096 publicKey = repositoryDto.getPublicKey(); 097 } 098 return repositoryId; 099 } 100 101 @Override 102 public byte[] getPublicKey() { 103 getRepositoryId(); // ensure, the public key is loaded 104 return AssertUtil.assertNotNull(publicKey, "publicKey"); 105 } 106 107 @Override 108 public RepositoryDto getRepositoryDto() { 109 return getClient().execute(new GetRepositoryDto(getRepositoryName())); 110 } 111 112 @Override 113 public RepositoryDto getClientRepositoryDto() { 114 getClientRepositoryIdOrFail(); 115 return getClient().execute(new GetClientRepositoryDto(getRepositoryName())); 116 } 117 118 @Override 119 public void requestRepoConnection(final byte[] publicKey) { 120 final RepositoryDto repositoryDto = new RepositoryDto(); 121 repositoryDto.setRepositoryId(getClientRepositoryIdOrFail()); 122 repositoryDto.setPublicKey(publicKey); 123 getClient().execute(new RequestRepoConnection(getRepositoryName(), getPathPrefix(), repositoryDto)); 124 } 125 126 @Override 127 public void close() { 128 client = null; 129 super.close(); 130 } 131 132 @Override 133 public ChangeSetDto getChangeSetDto(final boolean localSync, final Long lastSyncToRemoteRepoLocalRepositoryRevisionSynced) { 134 final long beginTimestamp = System.currentTimeMillis(); 135 while (true) { 136 try { 137 return getClient().execute(new GetChangeSetDto(getRepositoryId().toString(), localSync, lastSyncToRemoteRepoLocalRepositoryRevisionSynced)); 138 } catch (final DeferredCompletionException x) { 139 if (System.currentTimeMillis() > beginTimestamp + changeSetTimeout) 140 throw new TimeoutException(String.format("Could not get change-set within %s milliseconds!", changeSetTimeout), x); 141 142 logger.info("getChangeSet: Got DeferredCompletionException; will retry."); 143 } 144 } 145 } 146 147 @Override 148 public void prepareForChangeSetDto(ChangeSetDto changeSetDto) { 149 // nothing to do here. 150 } 151 152 @Override 153 public void makeDirectory(String path, final Date lastModified) { 154 path = prefixPath(path); 155 getClient().execute(new MakeDirectory(getRepositoryId().toString(), path, lastModified)); 156 } 157 158 @Override 159 public void makeSymlink(String path, final String target, final Date lastModified) { 160 path = prefixPath(path); 161 getClient().execute(new MakeSymlink(getRepositoryId().toString(), path, target, lastModified)); 162 } 163 164 @Override 165 public void copy(String fromPath, String toPath) { 166 fromPath = prefixPath(fromPath); 167 toPath = prefixPath(toPath); 168 getClient().execute(new Copy(getRepositoryId().toString(), fromPath, toPath)); 169 } 170 171 @Override 172 public void move(String fromPath, String toPath) { 173 fromPath = prefixPath(fromPath); 174 toPath = prefixPath(toPath); 175 getClient().execute(new Move(getRepositoryId().toString(), fromPath, toPath)); 176 } 177 178 @Override 179 public void delete(String path) { 180 path = prefixPath(path); 181 getClient().execute(new Delete(getRepositoryId().toString(), path)); 182 } 183 184 @Override 185 public RepoFileDto getRepoFileDto(String path) { 186 path = prefixPath(path); 187 final long beginTimestamp = System.currentTimeMillis(); 188 while (true) { 189 try { 190 return getClient().execute(new GetRepoFileDto(getRepositoryId().toString(), path)); 191 } catch (final DeferredCompletionException x) { 192 if (System.currentTimeMillis() > beginTimestamp + fileChunkSetTimeout) 193 throw new TimeoutException(String.format("Could not get file-chunk-set within %s milliseconds!", fileChunkSetTimeout), x); 194 195 logger.info("getFileChunkSet: Got DeferredCompletionException; will retry."); 196 } 197 } 198 } 199 200 @Override 201 public byte[] getFileData(String path, final long offset, final int length) { 202 path = prefixPath(path); 203 return getClient().execute(new GetFileData(getRepositoryId().toString(), path, offset, length)); 204 } 205 206 @Override 207 public void beginPutFile(String path) { 208 path = prefixPath(path); 209 getClient().execute(new BeginPutFile(getRepositoryId().toString(), path)); 210 } 211 212 @Override 213 public void putFileData(String path, final long offset, final byte[] fileData) { 214 path = prefixPath(path); 215 getClient().execute(new PutFileData(getRepositoryId().toString(), path, offset, fileData)); 216 } 217 218 @Override 219 public void endPutFile(String path, final Date lastModified, final long length, final String sha1) { 220 path = prefixPath(path); 221 getClient().execute(new EndPutFile(getRepositoryId().toString(), path, new DateTime(lastModified), length, sha1)); 222 } 223 224 @Override 225 public void endSyncFromRepository() { 226 getClient().execute(new EndSyncFromRepository(getRepositoryId().toString())); 227 } 228 229 @Override 230 public void endSyncToRepository(final long fromLocalRevision) { 231 getClient().execute(new EndSyncToRepository(getRepositoryId().toString(), fromLocalRevision)); 232 } 233 234 @Override 235 public void putParentConfigPropSetDto(ConfigPropSetDto parentConfigPropSetDto) { 236 getClient().execute(new PutParentConfigPropSetDto(getRepositoryId().toString(), parentConfigPropSetDto)); 237 } 238 239 @Override 240 public String getUserName() { 241 final UUID clientRepositoryId = getClientRepositoryIdOrFail(); 242 return AuthConstants.USER_NAME_REPOSITORY_ID_PREFIX + clientRepositoryId; 243 } 244 245 @Override 246 public String getPassword() { 247 final AuthToken authToken = getAuthToken(); 248 return authToken.getPassword(); 249 } 250 251 private AuthToken getAuthToken() { 252 final UUID clientRepositoryId = getClientRepositoryIdOrFail(); 253 AuthToken authToken = clientRepositoryId2AuthToken.get(clientRepositoryId); 254 if (authToken != null && isAfterRenewalDate(authToken)) { 255 logger.debug("getAuthToken: old AuthToken passed renewal-date: clientRepositoryId={} serverRepositoryId={} renewalDateTime={} expiryDateTime={}", 256 clientRepositoryId, getRepositoryId(), authToken.getRenewalDateTime(), authToken.getExpiryDateTime()); 257 258 authToken = null; 259 } 260 261 if (authToken == null) { 262 logger.debug("getAuthToken: getting new AuthToken: clientRepositoryId={} serverRepositoryId={}", 263 clientRepositoryId, getRepositoryId()); 264 265 final File localRoot = LocalRepoRegistryImpl.getInstance().getLocalRoot(clientRepositoryId); 266 final LocalRepoManager localRepoManager = LocalRepoManagerFactory.Helper.getInstance().createLocalRepoManagerForExistingRepository(localRoot); 267 try { 268 final EncryptedSignedAuthToken encryptedSignedAuthToken = getClient().execute(new GetEncryptedSignedAuthToken(getRepositoryName(), localRepoManager.getRepositoryId())); 269 270 final byte[] signedAuthTokenData = new SignedAuthTokenDecrypter(localRepoManager.getPrivateKey()).decrypt(encryptedSignedAuthToken); 271 272 final SignedAuthToken signedAuthToken = new SignedAuthTokenIO().deserialise(signedAuthTokenData); 273 274 final AuthTokenVerifier verifier = new AuthTokenVerifier(localRepoManager.getRemoteRepositoryPublicKeyOrFail(getRepositoryId())); 275 verifier.verify(signedAuthToken); 276 277 authToken = new AuthTokenIO().deserialise(signedAuthToken.getAuthTokenData()); 278 final Date expiryDate = AssertUtil.assertNotNull(authToken.getExpiryDateTime(), "authToken.expiryDateTime").toDate(); 279 final Date renewalDate = AssertUtil.assertNotNull(authToken.getRenewalDateTime(), "authToken.renewalDateTime").toDate(); 280 if (!renewalDate.before(expiryDate)) 281 throw new IllegalArgumentException( 282 String.format("Invalid AuthToken: renewalDateTime >= expiryDateTime :: renewalDateTime=%s expiryDateTime=%s", 283 authToken.getRenewalDateTime(), authToken.getExpiryDateTime())); 284 285 clientRepositoryId2AuthToken.put(clientRepositoryId, authToken); 286 } finally { 287 localRepoManager.close(); 288 } 289 290 logger.info("getAuthToken: got new AuthToken: clientRepositoryId={} serverRepositoryId={} renewalDateTime={} expiryDateTime={}", 291 clientRepositoryId, getRepositoryId(), authToken.getRenewalDateTime(), authToken.getExpiryDateTime()); 292 } 293 else 294 logger.trace("getAuthToken: old AuthToken still valid: clientRepositoryId={} serverRepositoryId={} renewalDateTime={} expiryDateTime={}", 295 clientRepositoryId, getRepositoryId(), authToken.getRenewalDateTime(), authToken.getExpiryDateTime()); 296 297 return authToken; 298 } 299 300 private boolean isAfterRenewalDate(final AuthToken authToken) { 301 AssertUtil.assertNotNull(authToken, "authToken"); 302 return System.currentTimeMillis() > authToken.getRenewalDateTime().getMillis(); 303 } 304 305 protected CloudStoreRestClient getClient() { 306 if (client == null) { 307 ClientBuilder clientBuilder = createClientBuilder(); 308 final CloudStoreRestClient c = new CloudStoreRestClient(getRemoteRoot(), clientBuilder); 309 c.setCredentialsProvider(this); 310 client = c; 311 } 312 return client; 313 } 314 315 @Override 316 protected URL determineRemoteRootWithoutPathPrefix() { 317 final String repositoryName = getRepositoryName(); 318 final String baseURL = getClient().getBaseUrl(); 319 if (!baseURL.endsWith("/")) 320 throw new IllegalStateException(String.format("baseURL does not end with a '/'! baseURL='%s'", baseURL)); 321 322 try { 323 return new URL(baseURL + repositoryName); 324 } catch (final MalformedURLException e) { 325 throw new RuntimeException(e); 326 } 327 } 328 329 public String getRepositoryName() { 330 if (repositoryName == null) { 331 final String pathAfterBaseURL = getPathAfterBaseURL(); 332 final int indexOfFirstSlash = pathAfterBaseURL.indexOf('/'); 333 if (indexOfFirstSlash < 0) { 334 repositoryName = pathAfterBaseURL; 335 } 336 else { 337 repositoryName = pathAfterBaseURL.substring(0, indexOfFirstSlash); 338 } 339 if (repositoryName.isEmpty()) 340 throw new IllegalStateException("repositoryName is empty!"); 341 } 342 return repositoryName; 343 } 344 345 private String pathAfterBaseURL; 346 347 protected String getPathAfterBaseURL() { 348 String pathAfterBaseURL = this.pathAfterBaseURL; 349 if (pathAfterBaseURL == null) { 350 final URL remoteRoot = getRemoteRoot(); 351 if (remoteRoot == null) 352 throw new IllegalStateException("remoteRoot not yet assigned!"); 353 354 final String baseURL = getClient().getBaseUrl(); 355 if (!baseURL.endsWith("/")) 356 throw new IllegalStateException(String.format("baseURL does not end with a '/'! remoteRoot='%s' baseURL='%s'", remoteRoot, baseURL)); 357 358 final String remoteRootString = remoteRoot.toExternalForm(); 359 if (!remoteRootString.startsWith(baseURL)) 360 throw new IllegalStateException(String.format("remoteRoot does not start with baseURL! remoteRoot='%s' baseURL='%s'", remoteRoot, baseURL)); 361 362 this.pathAfterBaseURL = pathAfterBaseURL = remoteRootString.substring(baseURL.length()); 363 } 364 return pathAfterBaseURL; 365 } 366 367 private ClientBuilder createClientBuilder(){ 368 final ClientBuilder builder = new ClientBuilderDefaultValuesDecorator(); 369 try { 370 builder.sslContext(SSLContextBuilder.create() 371 .remoteURL(getRemoteRoot()) 372 .callback(getDynamicX509TrustManagerCallback()).build()); 373 } catch (final GeneralSecurityException e) { 374 throw new RuntimeException(e); 375 } 376 return builder; 377 } 378 379 @Override 380 public VersionInfoDto getVersionInfoDto() { 381 final VersionInfoDto versionInfoDto = getClient().execute(new GetVersionInfoDto()); 382 return versionInfoDto; 383 } 384}