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}