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