001/*
002 * oauth2-oidc-sdk
003 *
004 * Copyright 2012-2016, Connect2id Ltd and contributors.
005 *
006 * Licensed under the Apache License, Version 2.0 (the "License"); you may not use
007 * this file except in compliance with the License. You may obtain a copy of the
008 * License at
009 *
010 *    http://www.apache.org/licenses/LICENSE-2.0
011 *
012 * Unless required by applicable law or agreed to in writing, software distributed
013 * under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR
014 * CONDITIONS OF ANY KIND, either express or implied. See the License for the
015 * specific language governing permissions and limitations under the License.
016 */
017
018package com.nimbusds.oauth2.sdk.http;
019
020
021import java.io.BufferedReader;
022import java.io.IOException;
023import java.io.PrintWriter;
024import java.net.MalformedURLException;
025import java.net.URL;
026import java.security.cert.X509Certificate;
027import java.util.Enumeration;
028import java.util.LinkedList;
029import java.util.List;
030import java.util.Map;
031import javax.servlet.ServletRequest;
032import javax.servlet.http.HttpServletRequest;
033import javax.servlet.http.HttpServletResponse;
034
035import com.nimbusds.oauth2.sdk.ParseException;
036import com.nimbusds.oauth2.sdk.util.URLUtils;
037import com.nimbusds.oauth2.sdk.util.X509CertificateUtils;
038import net.jcip.annotations.ThreadSafe;
039
040
041/**
042 * HTTP servlet utilities.
043 */
044@ThreadSafe
045public class ServletUtils {
046
047
048        /**
049         * Reconstructs the request URL string for the specified servlet
050         * request. The host part is always the local IP address. The query
051         * string and fragment is always omitted.
052         *
053         * @param request The servlet request. Must not be {@code null}.
054         *
055         * @return The reconstructed request URL string.
056         */
057        private static String reconstructRequestURLString(final HttpServletRequest request) {
058
059                StringBuilder sb = new StringBuilder("http");
060
061                if (request.isSecure())
062                        sb.append('s');
063
064                sb.append("://");
065
066                String localAddress = request.getLocalAddr();
067                
068                if (localAddress == null || localAddress.trim().isEmpty()) {
069                        // Unknown local address (hostname / IP address)
070                } else if (localAddress.contains(".")) {
071                        // IPv3 address
072                        sb.append(localAddress);
073                } else if (localAddress.contains(":")) {
074                        // IPv6 address, see RFC 2732
075                        sb.append('[');
076                        sb.append(localAddress);
077                        sb.append(']');
078                }
079
080                if (! request.isSecure() && request.getLocalPort() != 80) {
081                        // HTTP plain at port other than 80
082                        sb.append(':');
083                        sb.append(request.getLocalPort());
084                }
085
086                if (request.isSecure() && request.getLocalPort() != 443) {
087                        // HTTPS at port other than 443 (default TLS)
088                        sb.append(':');
089                        sb.append(request.getLocalPort());
090                }
091
092                String path = request.getRequestURI();
093
094                if (path != null)
095                        sb.append(path);
096
097                return sb.toString();
098        }
099
100
101        /**
102         * Creates a new HTTP request from the specified HTTP servlet request.
103         *
104         * <p><strong>Warning about servlet filters: </strong> Processing of
105         * HTTP POST and PUT requests requires the entity body to be available
106         * for reading from the {@link HttpServletRequest}. If you're getting
107         * unexpected exceptions, please ensure the entity body is not consumed
108         * or modified by an upstream servlet filter.
109         *
110         * @param sr The servlet request. Must not be {@code null}.
111         *
112         * @return The HTTP request.
113         *
114         * @throws IllegalArgumentException The the servlet request method is
115         *                                  not GET, POST, PUT or DELETE or the
116         *                                  content type header value couldn't
117         *                                  be parsed.
118         * @throws IOException              For a POST or PUT body that
119         *                                  couldn't be read due to an I/O
120         *                                  exception.
121         */
122        public static HTTPRequest createHTTPRequest(final HttpServletRequest sr)
123                throws IOException {
124
125                return createHTTPRequest(sr, -1);
126        }
127
128
129        /**
130         * Creates a new HTTP request from the specified HTTP servlet request.
131         *
132         * <p><strong>Warning about servlet filters: </strong> Processing of
133         * HTTP POST and PUT requests requires the entity body to be available
134         * for reading from the {@link HttpServletRequest}. If you're getting
135         * unexpected exceptions, please ensure the entity body is not consumed
136         * or modified by an upstream servlet filter.
137         *
138         * @param sr              The servlet request. Must not be
139         *                        {@code null}.
140         * @param maxEntityLength The maximum entity length to accept, -1 for
141         *                        no limit.
142         *
143         * @return The HTTP request.
144         *
145         * @throws IllegalArgumentException The the servlet request method is
146         *                                  not GET, POST, PUT or DELETE or the
147         *                                  content type header value couldn't
148         *                                  be parsed.
149         * @throws IOException              For a POST or PUT body that
150         *                                  couldn't be read due to an I/O
151         *                                  exception.
152         */
153        public static HTTPRequest createHTTPRequest(final HttpServletRequest sr, final long maxEntityLength)
154                throws IOException {
155
156                HTTPRequest.Method method = HTTPRequest.Method.valueOf(sr.getMethod().toUpperCase());
157
158                String urlString = reconstructRequestURLString(sr);
159
160                URL url;
161
162                try {
163                        url = new URL(urlString);
164
165                } catch (MalformedURLException e) {
166
167                        throw new IllegalArgumentException("Invalid request URL: " + e.getMessage() + ": " + urlString, e);
168                }
169
170                HTTPRequest request = new HTTPRequest(method, url);
171
172                try {
173                        request.setContentType(sr.getContentType());
174
175                } catch (ParseException e) {
176
177                        throw new IllegalArgumentException("Invalid Content-Type header value: " + e.getMessage(), e);
178                }
179
180                Enumeration<String> headerNames = sr.getHeaderNames();
181
182                while (headerNames.hasMoreElements()) {
183                        
184                        final String headerName = headerNames.nextElement();
185                        
186                        Enumeration<String> headerValues = sr.getHeaders(headerName);
187                        
188                        if (headerValues == null || ! headerValues.hasMoreElements())
189                                continue;
190                        
191                        List<String> headerValuesList = new LinkedList<>();
192                        while (headerValues.hasMoreElements()) {
193                                headerValuesList.add(headerValues.nextElement());
194                        }
195                        
196                        request.setHeader(headerName, headerValuesList.toArray(new String[0]));
197                }
198
199                if (method.equals(HTTPRequest.Method.GET) || method.equals(HTTPRequest.Method.DELETE)) {
200
201                        request.setQuery(sr.getQueryString());
202
203                } else if (method.equals(HTTPRequest.Method.POST) || method.equals(HTTPRequest.Method.PUT)) {
204
205                        // Impossible to read application/x-www-form-urlencoded request content on which parameters
206                        // APIs have been used. To be safe we recreate the content based on the parameters in this case.
207                        // See issues
208                        // https://bitbucket.org/connect2id/oauth-2.0-sdk-with-openid-connect-extensions/issues/184
209                        // https://bitbucket.org/connect2id/oauth-2.0-sdk-with-openid-connect-extensions/issues/186
210                        if (request.getContentType() != null && request.getContentType()
211                                .getBaseType().equals(CommonContentTypes.APPLICATION_URLENCODED.getBaseType())) {
212
213                                // Recreate the content based on parameters
214                                request.setQuery(URLUtils.serializeParametersAlt(sr.getParameterMap()));
215                        } else {
216                                // read body
217                                StringBuilder body = new StringBuilder(256);
218
219                                BufferedReader reader = sr.getReader();
220
221                                char[] cbuf = new char[256];
222
223                                int readChars;
224
225                                while ((readChars = reader.read(cbuf)) != -1) {
226
227                                        body.append(cbuf, 0, readChars);
228
229                                        if (maxEntityLength > 0 && body.length() > maxEntityLength) {
230                                                throw new IOException(
231                                                        "Request entity body is too large, limit is " + maxEntityLength + " chars");
232                                        }
233                                }
234
235                                reader.close();
236                                request.setQuery(body.toString());
237                        }
238                }
239                
240                // Extract validated client X.509 if we have mutual TLS
241                X509Certificate cert = extractClientX509Certificate(sr);
242                if (cert != null) {
243                        request.setClientX509Certificate(cert);
244                        request.setClientX509CertificateSubjectDN(cert.getSubjectDN() != null ? cert.getSubjectDN().getName() : null);
245                        
246                        // The root DN cannot be reliably set for a CA-signed
247                        // client cert from a servlet request, unless self-issued
248                        if (X509CertificateUtils.hasMatchingIssuerAndSubject(cert)) {
249                                request.setClientX509CertificateRootDN(cert.getIssuerDN() != null ? cert.getIssuerDN().getName() : null);
250                        }
251                }
252                
253                // Extract client IP address, X-Forwarded-For not checked
254                request.setClientIPAddress(sr.getRemoteAddr());
255
256                return request;
257        }
258
259
260        /**
261         * Applies the status code, headers and content of the specified HTTP
262         * response to a HTTP servlet response.
263         *
264         * @param httpResponse    The HTTP response. Must not be {@code null}.
265         * @param servletResponse The HTTP servlet response. Must not be
266         *                        {@code null}.
267         *
268         * @throws IOException If the response content couldn't be written.
269         */
270        public static void applyHTTPResponse(final HTTPResponse httpResponse,
271                                             final HttpServletResponse servletResponse)
272                throws IOException {
273
274                // Set the status code
275                servletResponse.setStatus(httpResponse.getStatusCode());
276
277
278                // Set the headers, but only if explicitly specified
279                for (Map.Entry<String,List<String>> header : httpResponse.getHeaderMap().entrySet()) {
280                        for (String headerValue: header.getValue()) {
281                                servletResponse.addHeader(header.getKey(), headerValue);
282                        }
283                }
284
285                if (httpResponse.getContentType() != null)
286                        servletResponse.setContentType(httpResponse.getContentType().toString());
287
288
289                // Write out the content
290
291                if (httpResponse.getContent() != null) {
292
293                        PrintWriter writer = servletResponse.getWriter();
294                        writer.print(httpResponse.getContent());
295                        writer.close();
296                }
297        }
298        
299        
300        /**
301         * Extracts the client's X.509 certificate from the specified servlet
302         * request. The first found certificate is returned, if any.
303         *
304         * @param servletRequest The HTTP servlet request. Must not be
305         *                       {@code null}.
306         *
307         * @return The first client X.509 certificate, {@code null} if none is
308         *         found.
309         */
310        public static X509Certificate extractClientX509Certificate(final ServletRequest servletRequest) {
311                
312                X509Certificate[] certs = (X509Certificate[]) servletRequest.getAttribute("javax.servlet.request.X509Certificate");
313                
314                if (certs == null || certs.length == 0) {
315                        return null;
316                }
317                
318                return certs[0];
319        }
320
321
322        /**
323         * Prevents public instantiation.
324         */
325        private ServletUtils() { }
326}