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