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}