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

package io.undertow.server.handlers.form;

import io.undertow.UndertowLogger;
import io.undertow.UndertowMessages;
import io.undertow.UndertowOptions;
import io.undertow.connector.PooledByteBuffer;
import io.undertow.server.HttpHandler;
import io.undertow.server.HttpServerExchange;
import io.undertow.util.UrlDecodeException;
import io.undertow.util.Headers;
import io.undertow.util.SameThreadExecutor;
import io.undertow.util.URLUtils;
import org.xnio.ChannelListener;
import org.xnio.IoUtils;
import org.xnio.channels.StreamSourceChannel;

import java.io.IOException;
import java.nio.ByteBuffer;

Parser definition for form encoded data. This handler takes effect for any request that has a mime type of application/x-www-form-urlencoded. The handler attaches a FormDataParser to the chain that can parse the underlying form data asynchronously.
Author:Stuart Douglas
/** * Parser definition for form encoded data. This handler takes effect for any request that has a mime type * of application/x-www-form-urlencoded. The handler attaches a {@link FormDataParser} to the chain * that can parse the underlying form data asynchronously. * * @author Stuart Douglas */
public class FormEncodedDataDefinition implements FormParserFactory.ParserDefinition<FormEncodedDataDefinition> { public static final String APPLICATION_X_WWW_FORM_URLENCODED = "application/x-www-form-urlencoded"; private static boolean parseExceptionLogAsDebug = false; private String defaultEncoding = "ISO-8859-1"; private boolean forceCreation = false; //if the parser should be created even if the correct headers are missing public FormEncodedDataDefinition() { } @Override public FormDataParser create(final HttpServerExchange exchange) { String mimeType = exchange.getRequestHeaders().getFirst(Headers.CONTENT_TYPE); if (forceCreation || (mimeType != null && mimeType.startsWith(APPLICATION_X_WWW_FORM_URLENCODED))) { String charset = defaultEncoding; String contentType = exchange.getRequestHeaders().getFirst(Headers.CONTENT_TYPE); if (contentType != null) { String cs = Headers.extractQuotedValueFromHeader(contentType, "charset"); if (cs != null) { charset = cs; } } UndertowLogger.REQUEST_LOGGER.tracef("Created form encoded parser for %s", exchange); return new FormEncodedDataParser(charset, exchange); } return null; } public String getDefaultEncoding() { return defaultEncoding; } public boolean isForceCreation() { return forceCreation; } public FormEncodedDataDefinition setForceCreation(boolean forceCreation) { this.forceCreation = forceCreation; return this; } public FormEncodedDataDefinition setDefaultEncoding(final String defaultEncoding) { this.defaultEncoding = defaultEncoding; return this; } private static final class FormEncodedDataParser implements ChannelListener<StreamSourceChannel>, FormDataParser { private final HttpServerExchange exchange; private final FormData data; private final StringBuilder builder = new StringBuilder(); private String name = null; private String charset; private HttpHandler handler; //0= parsing name //1=parsing name, decode required //2=parsing value //3=parsing value, decode required //4=finished private int state = 0; private FormEncodedDataParser(final String charset, final HttpServerExchange exchange) { this.exchange = exchange; this.charset = charset; this.data = new FormData(exchange.getConnection().getUndertowOptions().get(UndertowOptions.MAX_PARAMETERS, 1000)); } @Override public void handleEvent(final StreamSourceChannel channel) { try { doParse(channel); if (state == 4) { exchange.dispatch(SameThreadExecutor.INSTANCE, handler); } } catch (IOException e) { IoUtils.safeClose(channel); UndertowLogger.REQUEST_IO_LOGGER.ioExceptionReadingFromChannel(e); exchange.endExchange(); } } private void doParse(final StreamSourceChannel channel) throws IOException { int c = 0; final PooledByteBuffer pooled = exchange.getConnection().getByteBufferPool().allocate(); try { final ByteBuffer buffer = pooled.getBuffer(); do { buffer.clear(); c = channel.read(buffer); if (c > 0) { buffer.flip(); while (buffer.hasRemaining()) { byte n = buffer.get(); switch (state) { case 0: { if (n == '=') { name = builder.toString(); builder.setLength(0); state = 2; } else if (n == '&') { addPair(builder.toString(), ""); builder.setLength(0); state = 0; } else if (n == '%' || n == '+' || n < 0) { state = 1; builder.append((char) (n & 0xFF)); } else { builder.append((char) n); } break; } case 1: { if (n == '=') { name = decodeParameterName(builder.toString(), charset, true, new StringBuilder()); builder.setLength(0); state = 2; } else if (n == '&') { addPair(decodeParameterName(builder.toString(), charset, true, new StringBuilder()), ""); builder.setLength(0); state = 0; } else { builder.append((char) (n & 0xFF)); } break; } case 2: { if (n == '&') { addPair(name, builder.toString()); builder.setLength(0); state = 0; } else if (n == '%' || n == '+' || n < 0) { state = 3; builder.append((char) (n & 0xFF)); } else { builder.append((char) n); } break; } case 3: { if (n == '&') { addPair(name, decodeParameterValue(name, builder.toString(), charset, true, new StringBuilder())); builder.setLength(0); state = 0; } else { builder.append((char) (n & 0xFF)); } break; } } } } } while (c > 0); if (c == -1) { if (state == 2) { addPair(name, builder.toString()); } else if (state == 3) { addPair(name, decodeParameterValue(name, builder.toString(), charset, true, new StringBuilder())); } else if(builder.length() > 0) { if(state == 1) { addPair(decodeParameterName(builder.toString(), charset, true, new StringBuilder()), ""); } else { addPair(builder.toString(), ""); } } state = 4; exchange.putAttachment(FORM_DATA, data); } } finally { pooled.close(); } } private void addPair(String name, String value) { //if there was exception during decoding ignore the parameter [UNDERTOW-1554] if(name != null && value != null) { data.add(name, value); } } private String decodeParameterValue(String name, String value, String charset, boolean decodeSlash, StringBuilder stringBuilder) { String decodedValue = null; try { decodedValue = URLUtils.decode(value, charset, decodeSlash, stringBuilder); } catch (UrlDecodeException e) { if (!parseExceptionLogAsDebug) { UndertowLogger.REQUEST_LOGGER.errorf(UndertowMessages.MESSAGES.failedToDecodeParameterValue(name, value, e)); parseExceptionLogAsDebug = true; } else { UndertowLogger.REQUEST_LOGGER.debugf(UndertowMessages.MESSAGES.failedToDecodeParameterValue(name, value, e)); } } return decodedValue; } private String decodeParameterName(String name, String charset, boolean decodeSlash, StringBuilder stringBuilder) { String decodedName = null; try { decodedName = URLUtils.decode(name, charset, decodeSlash, stringBuilder); } catch (UrlDecodeException e) { if (!parseExceptionLogAsDebug) { UndertowLogger.REQUEST_LOGGER.errorf(UndertowMessages.MESSAGES.failedToDecodeParameterName(name, e)); parseExceptionLogAsDebug = true; } else { UndertowLogger.REQUEST_LOGGER.debugf(UndertowMessages.MESSAGES.failedToDecodeParameterName(name, e)); } } return decodedName; } @Override public void parse(HttpHandler handler) throws Exception { if (exchange.getAttachment(FORM_DATA) != null) { handler.handleRequest(exchange); return; } this.handler = handler; StreamSourceChannel channel = exchange.getRequestChannel(); if (channel == null) { throw new IOException(UndertowMessages.MESSAGES.requestChannelAlreadyProvided()); } else { doParse(channel); if (state != 4) { channel.getReadSetter().set(this); channel.resumeReads(); } else { exchange.dispatch(SameThreadExecutor.INSTANCE, handler); } } } @Override public FormData parseBlocking() throws IOException { final FormData existing = exchange.getAttachment(FORM_DATA); if (existing != null) { return existing; } StreamSourceChannel channel = exchange.getRequestChannel(); if (channel == null) { throw new IOException(UndertowMessages.MESSAGES.requestChannelAlreadyProvided()); } else { while (state != 4) { doParse(channel); if (state != 4) { channel.awaitReadable(); } } } return data; } @Override public void close() throws IOException { } @Override public void setCharacterEncoding(final String encoding) { this.charset = encoding; } } }