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