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