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>Requires 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}