001/*
002 * Copyright 2002-2018 the original author or authors.
003 *
004 * Licensed under the Apache License, Version 2.0 (the "License");
005 * you may not use this file except in compliance with the License.
006 * You may obtain a copy of the License at
007 *
008 *      https://www.apache.org/licenses/LICENSE-2.0
009 *
010 * Unless required by applicable law or agreed to in writing, software
011 * distributed under the License is distributed on an "AS IS" BASIS,
012 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
013 * See the License for the specific language governing permissions and
014 * limitations under the License.
015 */
016
017package org.springframework.http.server;
018
019import java.io.ByteArrayInputStream;
020import java.io.ByteArrayOutputStream;
021import java.io.IOException;
022import java.io.InputStream;
023import java.io.OutputStreamWriter;
024import java.io.Writer;
025import java.net.InetSocketAddress;
026import java.net.URI;
027import java.net.URISyntaxException;
028import java.net.URLEncoder;
029import java.nio.charset.Charset;
030import java.nio.charset.StandardCharsets;
031import java.security.Principal;
032import java.util.Arrays;
033import java.util.Enumeration;
034import java.util.Iterator;
035import java.util.List;
036import java.util.Map;
037
038import javax.servlet.http.HttpServletRequest;
039
040import org.springframework.http.HttpHeaders;
041import org.springframework.http.HttpMethod;
042import org.springframework.http.InvalidMediaTypeException;
043import org.springframework.http.MediaType;
044import org.springframework.lang.Nullable;
045import org.springframework.util.Assert;
046import org.springframework.util.LinkedCaseInsensitiveMap;
047import org.springframework.util.StringUtils;
048
049/**
050 * {@link ServerHttpRequest} implementation that is based on a {@link HttpServletRequest}.
051 *
052 * @author Arjen Poutsma
053 * @author Rossen Stoyanchev
054 * @author Juergen Hoeller
055 * @since 3.0
056 */
057public class ServletServerHttpRequest implements ServerHttpRequest {
058
059        protected static final String FORM_CONTENT_TYPE = "application/x-www-form-urlencoded";
060
061        protected static final Charset FORM_CHARSET = StandardCharsets.UTF_8;
062
063
064        private final HttpServletRequest servletRequest;
065
066        @Nullable
067        private URI uri;
068
069        @Nullable
070        private HttpHeaders headers;
071
072        @Nullable
073        private ServerHttpAsyncRequestControl asyncRequestControl;
074
075
076        /**
077         * Construct a new instance of the ServletServerHttpRequest based on the
078         * given {@link HttpServletRequest}.
079         * @param servletRequest the servlet request
080         */
081        public ServletServerHttpRequest(HttpServletRequest servletRequest) {
082                Assert.notNull(servletRequest, "HttpServletRequest must not be null");
083                this.servletRequest = servletRequest;
084        }
085
086
087        /**
088         * Returns the {@code HttpServletRequest} this object is based on.
089         */
090        public HttpServletRequest getServletRequest() {
091                return this.servletRequest;
092        }
093
094        @Override
095        @Nullable
096        public HttpMethod getMethod() {
097                return HttpMethod.resolve(this.servletRequest.getMethod());
098        }
099
100        @Override
101        public String getMethodValue() {
102                return this.servletRequest.getMethod();
103        }
104
105        @Override
106        public URI getURI() {
107                if (this.uri == null) {
108                        String urlString = null;
109                        boolean hasQuery = false;
110                        try {
111                                StringBuffer url = this.servletRequest.getRequestURL();
112                                String query = this.servletRequest.getQueryString();
113                                hasQuery = StringUtils.hasText(query);
114                                if (hasQuery) {
115                                        url.append('?').append(query);
116                                }
117                                urlString = url.toString();
118                                this.uri = new URI(urlString);
119                        }
120                        catch (URISyntaxException ex) {
121                                if (!hasQuery) {
122                                        throw new IllegalStateException(
123                                                        "Could not resolve HttpServletRequest as URI: " + urlString, ex);
124                                }
125                                // Maybe a malformed query string... try plain request URL
126                                try {
127                                        urlString = this.servletRequest.getRequestURL().toString();
128                                        this.uri = new URI(urlString);
129                                }
130                                catch (URISyntaxException ex2) {
131                                        throw new IllegalStateException(
132                                                        "Could not resolve HttpServletRequest as URI: " + urlString, ex2);
133                                }
134                        }
135                }
136                return this.uri;
137        }
138
139        @Override
140        public HttpHeaders getHeaders() {
141                if (this.headers == null) {
142                        this.headers = new HttpHeaders();
143
144                        for (Enumeration<?> names = this.servletRequest.getHeaderNames(); names.hasMoreElements();) {
145                                String headerName = (String) names.nextElement();
146                                for (Enumeration<?> headerValues = this.servletRequest.getHeaders(headerName);
147                                                headerValues.hasMoreElements();) {
148                                        String headerValue = (String) headerValues.nextElement();
149                                        this.headers.add(headerName, headerValue);
150                                }
151                        }
152
153                        // HttpServletRequest exposes some headers as properties:
154                        // we should include those if not already present
155                        try {
156                                MediaType contentType = this.headers.getContentType();
157                                if (contentType == null) {
158                                        String requestContentType = this.servletRequest.getContentType();
159                                        if (StringUtils.hasLength(requestContentType)) {
160                                                contentType = MediaType.parseMediaType(requestContentType);
161                                                this.headers.setContentType(contentType);
162                                        }
163                                }
164                                if (contentType != null && contentType.getCharset() == null) {
165                                        String requestEncoding = this.servletRequest.getCharacterEncoding();
166                                        if (StringUtils.hasLength(requestEncoding)) {
167                                                Charset charSet = Charset.forName(requestEncoding);
168                                                Map<String, String> params = new LinkedCaseInsensitiveMap<>();
169                                                params.putAll(contentType.getParameters());
170                                                params.put("charset", charSet.toString());
171                                                MediaType mediaType = new MediaType(contentType.getType(), contentType.getSubtype(), params);
172                                                this.headers.setContentType(mediaType);
173                                        }
174                                }
175                        }
176                        catch (InvalidMediaTypeException ex) {
177                                // Ignore: simply not exposing an invalid content type in HttpHeaders...
178                        }
179
180                        if (this.headers.getContentLength() < 0) {
181                                int requestContentLength = this.servletRequest.getContentLength();
182                                if (requestContentLength != -1) {
183                                        this.headers.setContentLength(requestContentLength);
184                                }
185                        }
186                }
187
188                return this.headers;
189        }
190
191        @Override
192        public Principal getPrincipal() {
193                return this.servletRequest.getUserPrincipal();
194        }
195
196        @Override
197        public InetSocketAddress getLocalAddress() {
198                return new InetSocketAddress(this.servletRequest.getLocalName(), this.servletRequest.getLocalPort());
199        }
200
201        @Override
202        public InetSocketAddress getRemoteAddress() {
203                return new InetSocketAddress(this.servletRequest.getRemoteHost(), this.servletRequest.getRemotePort());
204        }
205
206        @Override
207        public InputStream getBody() throws IOException {
208                if (isFormPost(this.servletRequest)) {
209                        return getBodyFromServletRequestParameters(this.servletRequest);
210                }
211                else {
212                        return this.servletRequest.getInputStream();
213                }
214        }
215
216        @Override
217        public ServerHttpAsyncRequestControl getAsyncRequestControl(ServerHttpResponse response) {
218                if (this.asyncRequestControl == null) {
219                        if (!ServletServerHttpResponse.class.isInstance(response)) {
220                                throw new IllegalArgumentException(
221                                                "Response must be a ServletServerHttpResponse: " + response.getClass());
222                        }
223                        ServletServerHttpResponse servletServerResponse = (ServletServerHttpResponse) response;
224                        this.asyncRequestControl = new ServletServerHttpAsyncRequestControl(this, servletServerResponse);
225                }
226                return this.asyncRequestControl;
227        }
228
229
230        private static boolean isFormPost(HttpServletRequest request) {
231                String contentType = request.getContentType();
232                return (contentType != null && contentType.contains(FORM_CONTENT_TYPE) &&
233                                HttpMethod.POST.matches(request.getMethod()));
234        }
235
236        /**
237         * Use {@link javax.servlet.ServletRequest#getParameterMap()} to reconstruct the
238         * body of a form 'POST' providing a predictable outcome as opposed to reading
239         * from the body, which can fail if any other code has used the ServletRequest
240         * to access a parameter, thus causing the input stream to be "consumed".
241         */
242        private static InputStream getBodyFromServletRequestParameters(HttpServletRequest request) throws IOException {
243                ByteArrayOutputStream bos = new ByteArrayOutputStream(1024);
244                Writer writer = new OutputStreamWriter(bos, FORM_CHARSET);
245
246                Map<String, String[]> form = request.getParameterMap();
247                for (Iterator<String> nameIterator = form.keySet().iterator(); nameIterator.hasNext();) {
248                        String name = nameIterator.next();
249                        List<String> values = Arrays.asList(form.get(name));
250                        for (Iterator<String> valueIterator = values.iterator(); valueIterator.hasNext();) {
251                                String value = valueIterator.next();
252                                writer.write(URLEncoder.encode(name, FORM_CHARSET.name()));
253                                if (value != null) {
254                                        writer.write('=');
255                                        writer.write(URLEncoder.encode(value, FORM_CHARSET.name()));
256                                        if (valueIterator.hasNext()) {
257                                                writer.write('&');
258                                        }
259                                }
260                        }
261                        if (nameIterator.hasNext()) {
262                                writer.append('&');
263                        }
264                }
265                writer.flush();
266
267                return new ByteArrayInputStream(bos.toByteArray());
268        }
269
270}