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