001package co.codewizards.cloudstore.rest.server.service;
002
003import static co.codewizards.cloudstore.core.util.Util.*;
004
005import java.io.ByteArrayInputStream;
006import java.io.File;
007import java.io.IOException;
008import java.io.InputStreamReader;
009import java.io.Reader;
010import java.net.MalformedURLException;
011import java.net.URL;
012import java.nio.CharBuffer;
013import java.util.Arrays;
014import java.util.UUID;
015
016import javax.servlet.http.HttpServletRequest;
017import javax.ws.rs.PathParam;
018import javax.ws.rs.WebApplicationException;
019import javax.ws.rs.core.Context;
020import javax.ws.rs.core.MediaType;
021import javax.ws.rs.core.Response;
022import javax.ws.rs.core.Response.Status;
023
024import org.glassfish.jersey.internal.util.Base64;
025import org.slf4j.Logger;
026import org.slf4j.LoggerFactory;
027
028import co.codewizards.cloudstore.core.auth.AuthConstants;
029import co.codewizards.cloudstore.core.dto.Error;
030import co.codewizards.cloudstore.core.repo.local.LocalRepoManager;
031import co.codewizards.cloudstore.core.repo.local.LocalRepoManagerFactory;
032import co.codewizards.cloudstore.core.repo.local.LocalRepoRegistry;
033import co.codewizards.cloudstore.core.repo.transport.RepoTransport;
034import co.codewizards.cloudstore.core.repo.transport.RepoTransportFactory;
035import co.codewizards.cloudstore.core.repo.transport.RepoTransportFactoryRegistry;
036import co.codewizards.cloudstore.core.util.IOUtil;
037import co.codewizards.cloudstore.rest.server.auth.Auth;
038import co.codewizards.cloudstore.rest.server.auth.TransientRepoPasswordManager;
039
040public abstract class AbstractServiceWithRepoToRepoAuth {
041
042        private static final Logger logger = LoggerFactory.getLogger(AbstractServiceWithRepoToRepoAuth.class);
043
044        protected @Context HttpServletRequest request;
045
046        protected @PathParam("repositoryName") String repositoryName;
047
048        private Auth auth;
049
050        /**
051         * Get the authentication information. This method does <b>not</b> verify, if the given authentication information
052         * is correct! It merely checks, if the client sent a 'Basic' authentication header. If it did not,
053         * this method throws a {@link WebApplicationException} with {@link Status#UNAUTHORIZED} or {@link Status#FORBIDDEN}.
054         * If it did, it extracts the information and puts it into an {@link Auth} instance.
055         * @return the {@link Auth} instance extracted from the client's headers. Never <code>null</code>.
056         * @throws WebApplicationException with {@link Status#UNAUTHORIZED}, if the client did not send an 'Authorization' header;
057         * with {@link Status#FORBIDDEN}, if there is an 'Authorization' header, but no 'Basic' authentication header (other authentication modes, like e.g. 'Digest'
058         * are not supported).
059         */
060        protected Auth getAuth()
061        throws WebApplicationException
062        {
063                if (auth == null) {
064                        String authorizationHeader = request.getHeader("Authorization");
065                        if (authorizationHeader == null || authorizationHeader.isEmpty()) {
066                                logger.debug("getAuth: There is no 'Authorization' header. Replying with a Status.UNAUTHORIZED response asking for 'Basic' authentication.");
067
068                                throw newUnauthorizedException();
069                        }
070
071                        logger.debug("getAuth: 'Authorization' header: {}", authorizationHeader);
072
073                        if (!authorizationHeader.startsWith("Basic"))
074                                throw new WebApplicationException(Response.status(Status.FORBIDDEN)
075                                                .type(MediaType.APPLICATION_XML)
076                                                .entity(new Error("Only 'Basic' authentication is supported!")).build());
077
078                        String basicAuthEncoded = authorizationHeader.substring("Basic".length()).trim();
079                        byte[] basicAuthDecodedBA = Base64.decode(basicAuthEncoded.getBytes(IOUtil.CHARSET_UTF_8));
080                        StringBuilder userNameSB = new StringBuilder();
081                        char[] password = null;
082
083                        ByteArrayInputStream in = new ByteArrayInputStream(basicAuthDecodedBA);
084                        CharBuffer cb = CharBuffer.allocate(basicAuthDecodedBA.length + 1);
085                        try {
086                                Reader r = new InputStreamReader(in, IOUtil.CHARSET_UTF_8);
087                                int charsReadTotal = 0;
088                                int charsRead;
089                                do {
090                                        charsRead = r.read(cb);
091
092                                        if (charsRead > 0)
093                                                charsReadTotal += charsRead;
094                                } while (charsRead >= 0);
095                                cb.position(0);
096
097                                while (cb.position() < charsReadTotal) {
098                                        char c = cb.get();
099                                        if (c == ':')
100                                                break;
101
102                                        userNameSB.append(c);
103                                }
104
105                                if (cb.position() < charsReadTotal) {
106                                        password = new char[charsReadTotal - cb.position()];
107                                        int idx = 0;
108                                        while (cb.position() < charsReadTotal)
109                                                password[idx++] = cb.get();
110                                }
111                        } catch (Exception e) {
112                                throw new WebApplicationException(Response.status(Status.INTERNAL_SERVER_ERROR).type(MediaType.APPLICATION_XML).entity(new Error(e)).build());
113                        } finally {
114                                // For extra safety: Overwrite all sensitive memory with 0.
115                                Arrays.fill(basicAuthDecodedBA, (byte)0);
116
117                                cb.position(0);
118                                for (int i = 0; i < cb.capacity(); ++i)
119                                        cb.put((char)0);
120                        }
121
122                        Auth auth = new Auth();
123                        auth.setUserName(userNameSB.toString());
124                        auth.setPassword(password);
125                        this.auth = auth;
126                }
127                return auth;
128        }
129
130        /**
131         * Get the {@link Auth} information via {@link #getAuth()} and verify, if they are valid.
132         * @return the {@link Auth} information via {@link #getAuth()}; never <code>null</code>.
133         * @throws WebApplicationException with {@link Status#UNAUTHORIZED}, if the client did not send an 'Authorization' header
134         * or if user-name / password is wrong;
135         * with {@link Status#FORBIDDEN}, if there is an 'Authorization' header, but no 'Basic' authentication header (other authentication modes, like e.g. 'Digest'
136         * are not supported); with {@link Status#INTERNAL_SERVER_ERROR}, if there was an {@link IOException}.
137         */
138        protected String authenticateAndReturnUserName()
139        throws WebApplicationException
140        {
141                UUID serverRepositoryId = LocalRepoRegistry.getInstance().getRepositoryId(repositoryName);
142                if (serverRepositoryId == null) {
143                        throw new WebApplicationException(Response.status(Status.NOT_FOUND)
144                                        .type(MediaType.APPLICATION_XML)
145                                        .entity(new Error(String.format("HTTP 404: repositoryName='%s' is neither an alias nor an ID of a known repository!", repositoryName))).build());
146                }
147
148                Auth auth = getAuth();
149                try {
150                        UUID clientRepositoryId = getClientRepositoryIdFromUserName(auth.getUserName());
151                        if (clientRepositoryId != null) {
152                                if (TransientRepoPasswordManager.getInstance().isPasswordValid(serverRepositoryId, clientRepositoryId, auth.getPassword()))
153                                        return auth.getUserName();
154                                else
155                                        throw newUnauthorizedException();
156                        }
157                } finally {
158                        // We clear auth, even though it is kept in this instance, because we need the password only for
159                        // authentication. We authenticate only once and don't need it later, again. Every service invocation
160                        // has its own new REST service object instance. Hence, this is clearing should be really no problem.
161                        auth.clear();
162                }
163                throw newUnauthorizedException();
164        }
165
166        protected UUID getClientRepositoryIdFromUserName(String userName) {
167                if (assertNotNull("userName", userName).startsWith(AuthConstants.USER_NAME_REPOSITORY_ID_PREFIX)) {
168                        String repositoryIdString = userName.substring(AuthConstants.USER_NAME_REPOSITORY_ID_PREFIX.length());
169                        UUID clientRepositoryId = UUID.fromString(repositoryIdString);
170                        return clientRepositoryId;
171                }
172                return null;
173        }
174
175        protected UUID getClientRepositoryIdFromUserNameOrFail(String userName) {
176                UUID clientRepositoryId = getClientRepositoryIdFromUserName(userName);
177                if (clientRepositoryId == null)
178                        throw new IllegalArgumentException(String.format("userName='%s' is not a repository!", userName));
179
180                return clientRepositoryId;
181        }
182
183        private WebApplicationException newUnauthorizedException() {
184                return new WebApplicationException(Response.status(Status.UNAUTHORIZED).header("WWW-Authenticate", "Basic realm=\"CloudStoreServer\"").build());
185        }
186
187        protected RepoTransport authenticateAndCreateLocalRepoTransport() {
188                String userName = authenticateAndReturnUserName();
189                UUID clientRepositoryId = getClientRepositoryIdFromUserNameOrFail(userName);
190                URL localRootURL = getLocalRootURL(clientRepositoryId);
191                RepoTransportFactory repoTransportFactory = RepoTransportFactoryRegistry.getInstance().getRepoTransportFactoryOrFail(localRootURL);
192                RepoTransport repoTransport = repoTransportFactory.createRepoTransport(localRootURL, clientRepositoryId);
193                return repoTransport;
194        }
195
196        protected URL authenticateAndGetLocalRootURL() {
197                String userName = authenticateAndReturnUserName();
198                UUID clientRepositoryId = getClientRepositoryIdFromUserNameOrFail(userName);
199                return getLocalRootURL(clientRepositoryId);
200        }
201
202        protected URL getLocalRootURL(UUID clientRepositoryId) {
203                assertNotNull("repositoryName", repositoryName);
204                final File localRoot = LocalRepoRegistry.getInstance().getLocalRootForRepositoryNameOrFail(repositoryName);
205                final LocalRepoManager localRepoManager = LocalRepoManagerFactory.Helper.getInstance().createLocalRepoManagerForExistingRepository(localRoot);
206                try {
207                        final String localPathPrefix = localRepoManager.getLocalPathPrefixOrFail(clientRepositoryId);
208                        URL localRootURL;
209                        try {
210                                localRootURL = localRoot.toURI().toURL();
211                        } catch (MalformedURLException e) {
212                                throw new RuntimeException(e);
213                        }
214                        if (!localPathPrefix.isEmpty()) {
215                                String localRootURLString = localRootURL.toExternalForm();
216                                if (localRootURLString.endsWith("/"))
217                                        localRootURLString = localRootURLString.substring(0, localRootURLString.length() - 1);
218
219                                // localPathPrefix is guaranteed to start with a '/'.
220                                localRootURLString += localPathPrefix;
221                                try {
222                                        localRootURL = new URL(localRootURLString);
223                                } catch (MalformedURLException e) {
224                                        throw new RuntimeException(e);
225                                }
226                        }
227                        return localRootURL;
228                } finally {
229                        localRepoManager.close();
230                }
231        }
232}