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}