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