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 try { 170 url = new URL(urlString); 171 } catch (MalformedURLException e) { 172 throw new IllegalArgumentException("Invalid request URL: " + e.getMessage() + ": " + urlString, e); 173 } 174 175 HTTPRequest request = new HTTPRequest(method, url); 176 177 try { 178 request.setContentType(sr.getContentType()); 179 180 } catch (ParseException e) { 181 182 throw new IllegalArgumentException("Invalid Content-Type header value: " + e.getMessage(), e); 183 } 184 185 Enumeration<String> headerNames = sr.getHeaderNames(); 186 187 while (headerNames.hasMoreElements()) { 188 189 final String headerName = headerNames.nextElement(); 190 191 Enumeration<String> headerValues = sr.getHeaders(headerName); 192 193 if (headerValues == null || ! headerValues.hasMoreElements()) 194 continue; 195 196 List<String> headerValuesList = new LinkedList<>(); 197 while (headerValues.hasMoreElements()) { 198 headerValuesList.add(headerValues.nextElement()); 199 } 200 201 request.setHeader(headerName, headerValuesList.toArray(new String[0])); 202 } 203 204 if (method.equals(HTTPRequest.Method.GET) || method.equals(HTTPRequest.Method.DELETE)) { 205 206 request.setQuery(sr.getQueryString()); 207 208 } else if (method.equals(HTTPRequest.Method.POST) || method.equals(HTTPRequest.Method.PUT)) { 209 210 // Impossible to read application/x-www-form-urlencoded request content on which parameters 211 // APIs have been used. To be safe we recreate the content based on the parameters in this case. 212 // See issues 213 // https://bitbucket.org/connect2id/oauth-2.0-sdk-with-openid-connect-extensions/issues/184 214 // https://bitbucket.org/connect2id/oauth-2.0-sdk-with-openid-connect-extensions/issues/186 215 if (ContentType.APPLICATION_URLENCODED.matches(request.getEntityContentType())) { 216 217 // Recreate the content based on parameters 218 request.setQuery(URLUtils.serializeParametersAlt(sr.getParameterMap())); 219 } else { 220 // read body 221 StringBuilder body = new StringBuilder(256); 222 223 BufferedReader reader = sr.getReader(); 224 225 char[] cbuf = new char[256]; 226 227 int readChars; 228 229 while ((readChars = reader.read(cbuf)) != -1) { 230 231 body.append(cbuf, 0, readChars); 232 233 if (maxEntityLength > 0 && body.length() > maxEntityLength) { 234 throw new IOException( 235 "Request entity body is too large, limit is " + maxEntityLength + " chars"); 236 } 237 } 238 239 reader.close(); 240 request.setQuery(body.toString()); 241 } 242 } 243 244 // Extract validated client X.509 if we have mutual TLS 245 X509Certificate cert = extractClientX509Certificate(sr); 246 if (cert != null) { 247 request.setClientX509Certificate(cert); 248 request.setClientX509CertificateSubjectDN(cert.getSubjectDN() != null ? cert.getSubjectDN().getName() : null); 249 250 // The root DN cannot be reliably set for a CA-signed 251 // client cert from a servlet request, unless self-issued 252 if (X509CertificateUtils.hasMatchingIssuerAndSubject(cert)) { 253 request.setClientX509CertificateRootDN(cert.getIssuerDN() != null ? cert.getIssuerDN().getName() : null); 254 } 255 } 256 257 // Extract client IP address, X-Forwarded-For not checked 258 request.setClientIPAddress(sr.getRemoteAddr()); 259 260 return request; 261 } 262 263 264 /** 265 * Applies the status code, headers and content of the specified HTTP 266 * response to a HTTP servlet response. 267 * 268 * @param httpResponse The HTTP response. Must not be {@code null}. 269 * @param servletResponse The HTTP servlet response. Must not be 270 * {@code null}. 271 * 272 * @throws IOException If the response content couldn't be written. 273 */ 274 public static void applyHTTPResponse(final HTTPResponse httpResponse, 275 final HttpServletResponse servletResponse) 276 throws IOException { 277 278 // Set the status code 279 servletResponse.setStatus(httpResponse.getStatusCode()); 280 281 282 // Set the headers, but only if explicitly specified 283 for (Map.Entry<String,List<String>> header : httpResponse.getHeaderMap().entrySet()) { 284 for (String headerValue: header.getValue()) { 285 servletResponse.addHeader(header.getKey(), headerValue); 286 } 287 } 288 289 if (httpResponse.getEntityContentType() != null) 290 servletResponse.setContentType(httpResponse.getEntityContentType().toString()); 291 292 293 // Write out the content 294 295 if (httpResponse.getContent() != null) { 296 297 PrintWriter writer = servletResponse.getWriter(); 298 writer.print(httpResponse.getContent()); 299 writer.close(); 300 } 301 } 302 303 304 /** 305 * Extracts the client's X.509 certificate from the specified servlet 306 * request. The first found certificate is returned, if any. 307 * 308 * @param servletRequest The HTTP servlet request. Must not be 309 * {@code null}. 310 * 311 * @return The first client X.509 certificate, {@code null} if none is 312 * found. 313 */ 314 public static X509Certificate extractClientX509Certificate(final ServletRequest servletRequest) { 315 316 X509Certificate[] certs = (X509Certificate[]) servletRequest.getAttribute("javax.servlet.request.X509Certificate"); 317 318 if (certs == null || certs.length == 0) { 319 return null; 320 } 321 322 return certs[0]; 323 } 324 325 326 /** 327 * Prevents public instantiation. 328 */ 329 private ServletUtils() { } 330}