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