1 /*
2  * JBoss, Home of Professional Open Source.
3  * Copyright 2014 Red Hat, Inc., and individual contributors
4  * as indicated by the @author tags.
5  *
6  * Licensed under the Apache License, Version 2.0 (the "License");
7  * you may not use this file except in compliance with the License.
8  * You may obtain a copy of the License at
9  *
10  *     http://www.apache.org/licenses/LICENSE-2.0
11  *
12  *  Unless required by applicable law or agreed to in writing, software
13  *  distributed under the License is distributed on an "AS IS" BASIS,
14  *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15  *  See the License for the specific language governing permissions and
16  *  limitations under the License.
17  */

18
19 package io.undertow.server.handlers.form;
20
21 import io.undertow.UndertowLogger;
22 import io.undertow.UndertowMessages;
23 import io.undertow.UndertowOptions;
24 import io.undertow.connector.PooledByteBuffer;
25 import io.undertow.server.HttpHandler;
26 import io.undertow.server.HttpServerExchange;
27 import io.undertow.util.UrlDecodeException;
28 import io.undertow.util.Headers;
29 import io.undertow.util.SameThreadExecutor;
30 import io.undertow.util.URLUtils;
31 import org.xnio.ChannelListener;
32 import org.xnio.IoUtils;
33 import org.xnio.channels.StreamSourceChannel;
34
35 import java.io.IOException;
36 import java.nio.ByteBuffer;
37
38 /**
39  * Parser definition for form encoded data. This handler takes effect for any request that has a mime type
40  * of application/x-www-form-urlencoded. The handler attaches a {@link FormDataParser} to the chain
41  * that can parse the underlying form data asynchronously.
42  *
43  * @author Stuart Douglas
44  */

45 public class FormEncodedDataDefinition implements FormParserFactory.ParserDefinition<FormEncodedDataDefinition> {
46
47     public static final String APPLICATION_X_WWW_FORM_URLENCODED = "application/x-www-form-urlencoded";
48     private static boolean parseExceptionLogAsDebug = false;
49     private String defaultEncoding = "ISO-8859-1";
50     private boolean forceCreation = false//if the parser should be created even if the correct headers are missing
51
52     public FormEncodedDataDefinition() {
53     }
54
55     @Override
56     public FormDataParser create(final HttpServerExchange exchange)  {
57         String mimeType = exchange.getRequestHeaders().getFirst(Headers.CONTENT_TYPE);
58         if (forceCreation || (mimeType != null && mimeType.startsWith(APPLICATION_X_WWW_FORM_URLENCODED))) {
59
60             String charset = defaultEncoding;
61             String contentType = exchange.getRequestHeaders().getFirst(Headers.CONTENT_TYPE);
62             if (contentType != null) {
63                 String cs = Headers.extractQuotedValueFromHeader(contentType, "charset");
64                 if (cs != null) {
65                     charset = cs;
66                 }
67             }
68             UndertowLogger.REQUEST_LOGGER.tracef("Created form encoded parser for %s", exchange);
69             return new FormEncodedDataParser(charset, exchange);
70         }
71         return null;
72     }
73
74     public String getDefaultEncoding() {
75         return defaultEncoding;
76     }
77
78     public boolean isForceCreation() {
79         return forceCreation;
80     }
81
82     public FormEncodedDataDefinition setForceCreation(boolean forceCreation) {
83         this.forceCreation = forceCreation;
84         return this;
85     }
86
87     public FormEncodedDataDefinition setDefaultEncoding(final String defaultEncoding) {
88         this.defaultEncoding = defaultEncoding;
89         return this;
90     }
91
92     private static final class FormEncodedDataParser implements ChannelListener<StreamSourceChannel>, FormDataParser {
93
94         private final HttpServerExchange exchange;
95         private final FormData data;
96         private final StringBuilder builder = new StringBuilder();
97         private String name = null;
98         private String charset;
99         private HttpHandler handler;
100
101         //0= parsing name
102         //1=parsing name, decode required
103         //2=parsing value
104         //3=parsing value, decode required
105         //4=finished
106         private int state = 0;
107
108         private FormEncodedDataParser(final String charset, final HttpServerExchange exchange) {
109             this.exchange = exchange;
110             this.charset = charset;
111             this.data = new FormData(exchange.getConnection().getUndertowOptions().get(UndertowOptions.MAX_PARAMETERS, 1000));
112         }
113
114         @Override
115         public void handleEvent(final StreamSourceChannel channel) {
116             try {
117                 doParse(channel);
118                 if (state == 4) {
119                     exchange.dispatch(SameThreadExecutor.INSTANCE, handler);
120                 }
121             } catch (IOException e) {
122                 IoUtils.safeClose(channel);
123                 UndertowLogger.REQUEST_IO_LOGGER.ioExceptionReadingFromChannel(e);
124                 exchange.endExchange();
125
126             }
127         }
128
129         private void doParse(final StreamSourceChannel channel) throws IOException {
130             int c = 0;
131             final PooledByteBuffer pooled = exchange.getConnection().getByteBufferPool().allocate();
132             try {
133                 final ByteBuffer buffer = pooled.getBuffer();
134                 do {
135                     buffer.clear();
136                     c = channel.read(buffer);
137                     if (c > 0) {
138                         buffer.flip();
139                         while (buffer.hasRemaining()) {
140                             byte n = buffer.get();
141                             switch (state) {
142                                 case 0: {
143                                     if (n == '=') {
144                                         name = builder.toString();
145                                         builder.setLength(0);
146                                         state = 2;
147                                     } else if (n == '&') {
148                                         addPair(builder.toString(), "");
149                                         builder.setLength(0);
150                                         state = 0;
151                                     } else if (n == '%' || n == '+') {
152                                         state = 1;
153                                         builder.append((char) n);
154                                     } else {
155                                         builder.append((char) n);
156                                     }
157                                     break;
158                                 }
159                                 case 1: {
160                                     if (n == '=') {
161                                         name = decodeParameterName(builder.toString(), charset, truenew StringBuilder());
162                                         builder.setLength(0);
163                                         state = 2;
164                                     } else if (n == '&') {
165                                         addPair(decodeParameterName(builder.toString(), charset, truenew StringBuilder()), "");
166                                         builder.setLength(0);
167                                         state = 0;
168                                     } else {
169                                         builder.append((char) n);
170                                     }
171                                     break;
172                                 }
173                                 case 2: {
174                                     if (n == '&') {
175                                         addPair(name, builder.toString());
176                                         builder.setLength(0);
177                                         state = 0;
178                                     } else if (n == '%' || n == '+') {
179                                         state = 3;
180                                         builder.append((char) n);
181                                     } else {
182                                         builder.append((char) n);
183                                     }
184                                     break;
185                                 }
186                                 case 3: {
187                                     if (n == '&') {
188                                         addPair(name, decodeParameterValue(name, builder.toString(), charset, truenew StringBuilder()));
189                                         builder.setLength(0);
190                                         state = 0;
191                                     } else {
192                                         builder.append((char) n);
193                                     }
194                                     break;
195                                 }
196                             }
197                         }
198                     }
199                 } while (c > 0);
200                 if (c == -1) {
201                     if (state == 2) {
202                         addPair(name, builder.toString());
203                     } else if (state == 3) {
204                         addPair(name, decodeParameterValue(name, builder.toString(), charset, truenew StringBuilder()));
205                     } else if(builder.length() > 0) {
206                         if(state == 1) {
207                             addPair(decodeParameterName(builder.toString(), charset, truenew StringBuilder()), "");
208                         } else {
209                             addPair(builder.toString(), "");
210                         }
211                     }
212                     state = 4;
213                     exchange.putAttachment(FORM_DATA, data);
214                 }
215             } finally {
216                 pooled.close();
217             }
218         }
219
220         private void addPair(String name, String value) {
221             //if there was exception during decoding ignore the parameter [UNDERTOW-1554]
222             if(name != null && value != null) {
223                 data.add(name, value);
224             }
225         }
226
227         private String decodeParameterValue(String name, String value, String charset, boolean decodeSlash, StringBuilder stringBuilder) {
228             String decodedValue = null;
229
230             try {
231                 decodedValue = URLUtils.decode(value, charset, decodeSlash, stringBuilder);
232             } catch (UrlDecodeException e) {
233                 if (!parseExceptionLogAsDebug) {
234                     UndertowLogger.REQUEST_LOGGER.errorf(UndertowMessages.MESSAGES.failedToDecodeParameterValue(name, value, e));
235                     parseExceptionLogAsDebug = true;
236                 } else {
237                     UndertowLogger.REQUEST_LOGGER.debugf(UndertowMessages.MESSAGES.failedToDecodeParameterValue(name, value, e));
238                 }
239             }
240
241             return decodedValue;
242         }
243
244         private String decodeParameterName(String name, String charset, boolean decodeSlash, StringBuilder stringBuilder) {
245             String decodedName = null;
246
247             try {
248                 decodedName = URLUtils.decode(name, charset, decodeSlash, stringBuilder);
249             } catch (UrlDecodeException e) {
250                 if (!parseExceptionLogAsDebug) {
251                     UndertowLogger.REQUEST_LOGGER.errorf(UndertowMessages.MESSAGES.failedToDecodeParameterName(name, e));
252                     parseExceptionLogAsDebug = true;
253                 } else {
254                     UndertowLogger.REQUEST_LOGGER.debugf(UndertowMessages.MESSAGES.failedToDecodeParameterName(name, e));
255                 }
256             }
257
258             return decodedName;
259         }
260
261         @Override
262         public void parse(HttpHandler handler) throws Exception {
263             if (exchange.getAttachment(FORM_DATA) != null) {
264                 handler.handleRequest(exchange);
265                 return;
266             }
267             this.handler = handler;
268             StreamSourceChannel channel = exchange.getRequestChannel();
269             if (channel == null) {
270                 throw new IOException(UndertowMessages.MESSAGES.requestChannelAlreadyProvided());
271             } else {
272                 doParse(channel);
273                 if (state != 4) {
274                     channel.getReadSetter().set(this);
275                     channel.resumeReads();
276                 } else {
277                     exchange.dispatch(SameThreadExecutor.INSTANCE, handler);
278                 }
279             }
280         }
281
282         @Override
283         public FormData parseBlocking() throws IOException {
284             final FormData existing = exchange.getAttachment(FORM_DATA);
285             if (existing != null) {
286                 return existing;
287             }
288
289             StreamSourceChannel channel = exchange.getRequestChannel();
290             if (channel == null) {
291                 throw new IOException(UndertowMessages.MESSAGES.requestChannelAlreadyProvided());
292             } else {
293                 while (state != 4) {
294                     doParse(channel);
295                     if (state != 4) {
296                         channel.awaitReadable();
297                     }
298                 }
299             }
300             return data;
301         }
302
303         @Override
304         public void close() throws IOException {
305
306         }
307
308         @Override
309         public void setCharacterEncoding(final String encoding) {
310             this.charset = encoding;
311         }
312     }
313
314 }
315