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