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