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}