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}