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