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