001package com.nimbusds.oauth2.sdk.http; 002 003 004import java.io.BufferedReader; 005import java.io.IOException; 006import java.io.PrintWriter; 007import java.net.MalformedURLException; 008import java.net.URL; 009import java.util.Enumeration; 010import java.util.Map; 011import javax.servlet.http.HttpServletRequest; 012import javax.servlet.http.HttpServletResponse; 013 014import com.nimbusds.oauth2.sdk.ParseException; 015import com.nimbusds.oauth2.sdk.util.URLUtils; 016import net.jcip.annotations.ThreadSafe; 017 018 019/** 020 * HTTP servlet utilities. 021 */ 022@ThreadSafe 023public class ServletUtils { 024 025 026 /** 027 * Reconstructs the request URL string for the specified servlet 028 * request. The host part is always the local IP address. The query 029 * string and fragment is always omitted. 030 * 031 * @param request The servlet request. Must not be {@code null}. 032 * 033 * @return The reconstructed request URL string. 034 */ 035 private static String reconstructRequestURLString(final HttpServletRequest request) { 036 037 StringBuilder sb = new StringBuilder("http"); 038 039 if (request.isSecure()) 040 sb.append('s'); 041 042 sb.append("://"); 043 044 String localAddress = request.getLocalAddr(); 045 046 if (localAddress.contains(".")) { 047 // IPv3 address 048 sb.append(localAddress); 049 } else if (localAddress.contains(":")) { 050 // IPv6 address, see RFC 2732 051 sb.append('['); 052 sb.append(localAddress); 053 sb.append(']'); 054 } else { 055 // Don't know what to do 056 } 057 058 if (! request.isSecure() && request.getLocalPort() != 80) { 059 // HTTP plain at port other than 80 060 sb.append(':'); 061 sb.append(request.getLocalPort()); 062 } 063 064 if (request.isSecure() && request.getLocalPort() != 443) { 065 // HTTPS at port other than 443 (default TLS) 066 sb.append(':'); 067 sb.append(request.getLocalPort()); 068 } 069 070 String path = request.getRequestURI(); 071 072 if (path != null) 073 sb.append(path); 074 075 return sb.toString(); 076 } 077 078 079 /** 080 * Creates a new HTTP request from the specified HTTP servlet request. 081 * 082 * <p><strong>Warning about servlet filters: </strong> Processing of 083 * HTTP POST and PUT requests requires the entity body to be available 084 * for reading from the {@link HttpServletRequest}. If you're getting 085 * unexpected exceptions, please ensure the entity body is not consumed 086 * or modified by an upstream servlet filter. 087 * 088 * @param sr The servlet request. Must not be {@code null}. 089 * 090 * @return The HTTP request. 091 * 092 * @throws IllegalArgumentException The the servlet request method is 093 * not GET, POST, PUT or DELETE or the 094 * content type header value couldn't 095 * be parsed. 096 * @throws IOException For a POST or PUT body that 097 * couldn't be read due to an I/O 098 * exception. 099 */ 100 public static HTTPRequest createHTTPRequest(final HttpServletRequest sr) 101 throws IOException { 102 103 return createHTTPRequest(sr, -1); 104 } 105 106 107 /** 108 * Creates a new HTTP request from the specified HTTP servlet request. 109 * 110 * <p><strong>Warning about servlet filters: </strong> Processing of 111 * HTTP POST and PUT requests requires the entity body to be available 112 * for reading from the {@link HttpServletRequest}. If you're getting 113 * unexpected exceptions, please ensure the entity body is not consumed 114 * or modified by an upstream servlet filter. 115 * 116 * @param sr The servlet request. Must not be 117 * {@code null}. 118 * @param maxEntityLength The maximum entity length to accept, -1 for 119 * no limit. 120 * 121 * @return The HTTP request. 122 * 123 * @throws IllegalArgumentException The the servlet request method is 124 * not GET, POST, PUT or DELETE or the 125 * content type header value couldn't 126 * be parsed. 127 * @throws IOException For a POST or PUT body that 128 * couldn't be read due to an I/O 129 * exception. 130 */ 131 public static HTTPRequest createHTTPRequest(final HttpServletRequest sr, final long maxEntityLength) 132 throws IOException { 133 134 HTTPRequest.Method method = HTTPRequest.Method.valueOf(sr.getMethod().toUpperCase()); 135 136 String urlString = reconstructRequestURLString(sr); 137 138 URL url; 139 140 try { 141 url = new URL(urlString); 142 143 } catch (MalformedURLException e) { 144 145 throw new IllegalArgumentException("Invalid request URL: " + e.getMessage() + ": " + urlString, e); 146 } 147 148 HTTPRequest request = new HTTPRequest(method, url); 149 150 try { 151 request.setContentType(sr.getContentType()); 152 153 } catch (ParseException e) { 154 155 throw new IllegalArgumentException("Invalid Content-Type header value: " + e.getMessage(), e); 156 } 157 158 Enumeration<String> headerNames = sr.getHeaderNames(); 159 160 while (headerNames.hasMoreElements()) { 161 final String headerName = headerNames.nextElement(); 162 request.setHeader(headerName, sr.getHeader(headerName)); 163 } 164 165 if (method.equals(HTTPRequest.Method.GET) || method.equals(HTTPRequest.Method.DELETE)) { 166 167 request.setQuery(sr.getQueryString()); 168 169 } else if (method.equals(HTTPRequest.Method.POST) || method.equals(HTTPRequest.Method.PUT)) { 170 171 // Impossible to read application/x-www-form-urlencoded request content on which parameters 172 // APIs have been used. To be safe we recreate the content based on the parameters in this case. 173 // See issues 174 // https://bitbucket.org/connect2id/oauth-2.0-sdk-with-openid-connect-extensions/issues/184 175 // https://bitbucket.org/connect2id/oauth-2.0-sdk-with-openid-connect-extensions/issues/186 176 if (request.getContentType() != null && request.getContentType() 177 .getBaseType().equals(CommonContentTypes.APPLICATION_URLENCODED.getBaseType())) { 178 179 // Recreate the content based on parameters 180 request.setQuery(URLUtils.serializeParametersAlt(sr.getParameterMap())); 181 } else { 182 // read body 183 StringBuilder body = new StringBuilder(256); 184 185 BufferedReader reader = sr.getReader(); 186 187 char[] cbuf = new char[256]; 188 189 int readChars; 190 191 while ((readChars = reader.read(cbuf)) != -1) { 192 193 body.append(cbuf, 0, readChars); 194 195 if (maxEntityLength > 0 && body.length() > maxEntityLength) { 196 throw new IOException( 197 "Request entity body is too large, limit is " + maxEntityLength + " chars"); 198 } 199 } 200 201 reader.close(); 202 request.setQuery(body.toString()); 203 } 204 } 205 206 return request; 207 } 208 209 210 /** 211 * Applies the status code, headers and content of the specified HTTP 212 * response to a HTTP servlet response. 213 * 214 * @param httpResponse The HTTP response. Must not be {@code null}. 215 * @param servletResponse The HTTP servlet response. Must not be 216 * {@code null}. 217 * 218 * @throws IOException If the response content couldn't be written. 219 */ 220 public static void applyHTTPResponse(final HTTPResponse httpResponse, 221 final HttpServletResponse servletResponse) 222 throws IOException { 223 224 // Set the status code 225 servletResponse.setStatus(httpResponse.getStatusCode()); 226 227 228 // Set the headers, but only if explicitly specified 229 for (Map.Entry<String,String> header : httpResponse.getHeaders().entrySet()) { 230 servletResponse.setHeader(header.getKey(), header.getValue()); 231 } 232 233 if (httpResponse.getContentType() != null) 234 servletResponse.setContentType(httpResponse.getContentType().toString()); 235 236 237 // Write out the content 238 239 if (httpResponse.getContent() != null) { 240 241 PrintWriter writer = servletResponse.getWriter(); 242 writer.print(httpResponse.getContent()); 243 writer.close(); 244 } 245 } 246 247 248 /** 249 * Prevents public instantiation. 250 */ 251 private ServletUtils() { 252 253 } 254}