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}