org.apache.nifi.processors.standard.HandleHttpRequest.java Source code

Java tutorial

Introduction

Here is the source code for org.apache.nifi.processors.standard.HandleHttpRequest.java

Source

/*
 * Licensed to the Apache Software Foundation (ASF) under one or more
 * contributor license agreements.  See the NOTICE file distributed with
 * this work for additional information regarding copyright ownership.
 * The ASF licenses this file to You 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 org.apache.nifi.processors.standard;

import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.net.URI;
import java.net.URISyntaxException;
import java.net.URLDecoder;
import java.security.Principal;
import java.security.cert.X509Certificate;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.UUID;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.regex.Pattern;
import javax.servlet.AsyncContext;
import javax.servlet.DispatcherType;
import javax.servlet.ServletException;
import javax.servlet.http.Cookie;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.apache.commons.lang3.StringUtils;
import org.apache.nifi.annotation.behavior.InputRequirement;
import org.apache.nifi.annotation.behavior.InputRequirement.Requirement;
import org.apache.nifi.annotation.behavior.WritesAttribute;
import org.apache.nifi.annotation.behavior.WritesAttributes;
import org.apache.nifi.annotation.documentation.CapabilityDescription;
import org.apache.nifi.annotation.documentation.SeeAlso;
import org.apache.nifi.annotation.documentation.Tags;
import org.apache.nifi.annotation.lifecycle.OnScheduled;
import org.apache.nifi.annotation.lifecycle.OnStopped;
import org.apache.nifi.components.AllowableValue;
import org.apache.nifi.components.PropertyDescriptor;
import org.apache.nifi.flowfile.FlowFile;
import org.apache.nifi.http.HttpContextMap;
import org.apache.nifi.processor.AbstractProcessor;
import org.apache.nifi.processor.ProcessContext;
import org.apache.nifi.processor.ProcessSession;
import org.apache.nifi.processor.Relationship;
import org.apache.nifi.processor.exception.ProcessException;
import org.apache.nifi.processor.util.StandardValidators;
import org.apache.nifi.processors.standard.util.HTTPUtils;
import org.apache.nifi.ssl.SSLContextService;
import org.eclipse.jetty.server.Connector;
import org.eclipse.jetty.server.HttpConfiguration;
import org.eclipse.jetty.server.HttpConnectionFactory;
import org.eclipse.jetty.server.Request;
import org.eclipse.jetty.server.SecureRequestCustomizer;
import org.eclipse.jetty.server.Server;
import org.eclipse.jetty.server.ServerConnector;
import org.eclipse.jetty.server.SslConnectionFactory;
import org.eclipse.jetty.server.handler.AbstractHandler;
import org.eclipse.jetty.util.ssl.SslContextFactory;
import com.sun.jersey.api.client.ClientResponse.Status;

@InputRequirement(Requirement.INPUT_FORBIDDEN)
@Tags({ "http", "https", "request", "listen", "ingress", "web service" })
@CapabilityDescription("Starts an HTTP Server and listens for HTTP Requests. For each request, creates a FlowFile and transfers to 'success'. "
        + "This Processor is designed to be used in conjunction with the HandleHttpResponse Processor in order to create a Web Service")
@WritesAttributes({
        @WritesAttribute(attribute = HTTPUtils.HTTP_CONTEXT_ID, description = "An identifier that allows the HandleHttpRequest and HandleHttpResponse "
                + "to coordinate which FlowFile belongs to which HTTP Request/Response."),
        @WritesAttribute(attribute = "mime.type", description = "The MIME Type of the data, according to the HTTP Header \"Content-Type\""),
        @WritesAttribute(attribute = "http.servlet.path", description = "The part of the request URL that is considered the Servlet Path"),
        @WritesAttribute(attribute = "http.context.path", description = "The part of the request URL that is considered to be the Context Path"),
        @WritesAttribute(attribute = "http.method", description = "The HTTP Method that was used for the request, such as GET or POST"),
        @WritesAttribute(attribute = HTTPUtils.HTTP_LOCAL_NAME, description = "IP address/hostname of the server"),
        @WritesAttribute(attribute = HTTPUtils.HTTP_PORT, description = "Listening port of the server"),
        @WritesAttribute(attribute = "http.query.string", description = "The query string portion of hte Request URL"),
        @WritesAttribute(attribute = HTTPUtils.HTTP_REMOTE_HOST, description = "The hostname of the requestor"),
        @WritesAttribute(attribute = "http.remote.addr", description = "The hostname:port combination of the requestor"),
        @WritesAttribute(attribute = "http.remote.user", description = "The username of the requestor"),
        @WritesAttribute(attribute = HTTPUtils.HTTP_REQUEST_URI, description = "The full Request URL"),
        @WritesAttribute(attribute = "http.auth.type", description = "The type of HTTP Authorization used"),
        @WritesAttribute(attribute = "http.principal.name", description = "The name of the authenticated user making the request"),
        @WritesAttribute(attribute = HTTPUtils.HTTP_SSL_CERT, description = "The Distinguished Name of the requestor. This value will not be populated "
                + "unless the Processor is configured to use an SSLContext Service"),
        @WritesAttribute(attribute = "http.issuer.dn", description = "The Distinguished Name of the entity that issued the Subject's certificate. "
                + "This value will not be populated unless the Processor is configured to use an SSLContext Service"),
        @WritesAttribute(attribute = "http.headers.XXX", description = "Each of the HTTP Headers that is received in the request will be added as an "
                + "attribute, prefixed with \"http.headers.\" For example, if the request contains an HTTP Header named \"x-my-header\", then the value "
                + "will be added to an attribute named \"http.headers.x-my-header\"") })
@SeeAlso(value = { HandleHttpResponse.class }, classNames = { "org.apache.nifi.http.StandardHttpContextMap",
        "org.apache.nifi.ssl.StandardSSLContextService" })
public class HandleHttpRequest extends AbstractProcessor {

    private static final Pattern URL_QUERY_PARAM_DELIMITER = Pattern.compile("&");

    // Allowable values for client auth
    public static final AllowableValue CLIENT_NONE = new AllowableValue("No Authentication", "No Authentication",
            "Processor will not authenticate clients. Anyone can communicate with this Processor anonymously");
    public static final AllowableValue CLIENT_WANT = new AllowableValue("Want Authentication",
            "Want Authentication",
            "Processor will try to verify the client but if unable to verify will allow the client to communicate anonymously");
    public static final AllowableValue CLIENT_NEED = new AllowableValue("Need Authentication",
            "Need Authentication",
            "Processor will reject communications from any client unless the client provides a certificate that is trusted by the TrustStore"
                    + "specified in the SSL Context Service");

    public static final PropertyDescriptor PORT = new PropertyDescriptor.Builder().name("Listening Port")
            .description("The Port to listen on for incoming HTTP requests").required(true)
            .addValidator(StandardValidators.createLongValidator(0L, 65535L, true))
            .expressionLanguageSupported(false).defaultValue("80").build();
    public static final PropertyDescriptor HOSTNAME = new PropertyDescriptor.Builder().name("Hostname")
            .description("The Hostname to bind to. If not specified, will bind to all hosts").required(false)
            .addValidator(StandardValidators.NON_EMPTY_VALIDATOR).expressionLanguageSupported(false).build();
    public static final PropertyDescriptor HTTP_CONTEXT_MAP = new PropertyDescriptor.Builder()
            .name("HTTP Context Map")
            .description("The HTTP Context Map Controller Service to use for caching the HTTP Request Information")
            .required(true).identifiesControllerService(HttpContextMap.class).build();
    public static final PropertyDescriptor SSL_CONTEXT = new PropertyDescriptor.Builder()
            .name("SSL Context Service")
            .description(
                    "The SSL Context Service to use in order to secure the server. If specified, the server will accept only HTTPS requests; "
                            + "otherwise, the server will accept only HTTP requests")
            .required(false).identifiesControllerService(SSLContextService.class).build();
    public static final PropertyDescriptor URL_CHARACTER_SET = new PropertyDescriptor.Builder()
            .name("Default URL Character Set")
            .description(
                    "The character set to use for decoding URL parameters if the HTTP Request does not supply one")
            .required(true).defaultValue("UTF-8").addValidator(StandardValidators.CHARACTER_SET_VALIDATOR).build();
    public static final PropertyDescriptor PATH_REGEX = new PropertyDescriptor.Builder().name("Allowed Paths")
            .description(
                    "A Regular Expression that specifies the valid HTTP Paths that are allowed in the incoming URL Requests. If this value is "
                            + "specified and the path of the HTTP Requests does not match this Regular Expression, the Processor will respond with a "
                            + "404: NotFound")
            .required(false).addValidator(StandardValidators.REGULAR_EXPRESSION_VALIDATOR)
            .expressionLanguageSupported(false).build();
    public static final PropertyDescriptor ALLOW_GET = new PropertyDescriptor.Builder().name("Allow GET")
            .description("Allow HTTP GET Method").required(true).allowableValues("true", "false")
            .defaultValue("true").build();
    public static final PropertyDescriptor ALLOW_POST = new PropertyDescriptor.Builder().name("Allow POST")
            .description("Allow HTTP POST Method").required(true).allowableValues("true", "false")
            .defaultValue("true").build();
    public static final PropertyDescriptor ALLOW_PUT = new PropertyDescriptor.Builder().name("Allow PUT")
            .description("Allow HTTP PUT Method").required(true).allowableValues("true", "false")
            .defaultValue("true").build();
    public static final PropertyDescriptor ALLOW_DELETE = new PropertyDescriptor.Builder().name("Allow DELETE")
            .description("Allow HTTP DELETE Method").required(true).allowableValues("true", "false")
            .defaultValue("true").build();
    public static final PropertyDescriptor ALLOW_HEAD = new PropertyDescriptor.Builder().name("Allow HEAD")
            .description("Allow HTTP HEAD Method").required(true).allowableValues("true", "false")
            .defaultValue("false").build();
    public static final PropertyDescriptor ALLOW_OPTIONS = new PropertyDescriptor.Builder().name("Allow OPTIONS")
            .description("Allow HTTP OPTIONS Method").required(true).allowableValues("true", "false")
            .defaultValue("false").build();
    public static final PropertyDescriptor ADDITIONAL_METHODS = new PropertyDescriptor.Builder()
            .name("Additional HTTP Methods")
            .description("A comma-separated list of non-standard HTTP Methods that should be allowed")
            .required(false).addValidator(StandardValidators.NON_EMPTY_VALIDATOR).expressionLanguageSupported(false)
            .build();
    public static final PropertyDescriptor CLIENT_AUTH = new PropertyDescriptor.Builder()
            .name("Client Authentication")
            .description(
                    "Specifies whether or not the Processor should authenticate clients. This value is ignored if the <SSL Context Service> "
                            + "Property is not specified or the SSL Context provided uses only a KeyStore and not a TrustStore.")
            .required(true).allowableValues(CLIENT_NONE, CLIENT_WANT, CLIENT_NEED)
            .defaultValue(CLIENT_NONE.getValue()).build();
    public static final PropertyDescriptor CONTAINER_QUEUE_SIZE = new PropertyDescriptor.Builder()
            .name("container-queue-size").displayName("Container Queue Size")
            .description("The size of the queue for Http Request Containers").required(true)
            .addValidator(StandardValidators.POSITIVE_INTEGER_VALIDATOR).defaultValue("50").build();

    public static final Relationship REL_SUCCESS = new Relationship.Builder().name("success")
            .description("All content that is received is routed to the 'success' relationship").build();

    private static final List<PropertyDescriptor> propertyDescriptors;

    static {
        List<PropertyDescriptor> descriptors = new ArrayList<>();
        descriptors.add(PORT);
        descriptors.add(HOSTNAME);
        descriptors.add(SSL_CONTEXT);
        descriptors.add(HTTP_CONTEXT_MAP);
        descriptors.add(PATH_REGEX);
        descriptors.add(URL_CHARACTER_SET);
        descriptors.add(ALLOW_GET);
        descriptors.add(ALLOW_POST);
        descriptors.add(ALLOW_PUT);
        descriptors.add(ALLOW_DELETE);
        descriptors.add(ALLOW_HEAD);
        descriptors.add(ALLOW_OPTIONS);
        descriptors.add(ADDITIONAL_METHODS);
        descriptors.add(CLIENT_AUTH);
        descriptors.add(CONTAINER_QUEUE_SIZE);
        propertyDescriptors = Collections.unmodifiableList(descriptors);
    }

    private volatile Server server;
    private AtomicBoolean initialized = new AtomicBoolean(false);
    private volatile BlockingQueue<HttpRequestContainer> containerQueue;

    @Override
    protected List<PropertyDescriptor> getSupportedPropertyDescriptors() {
        return propertyDescriptors;
    }

    @Override
    public Set<Relationship> getRelationships() {
        return Collections.singleton(REL_SUCCESS);
    }

    @OnScheduled
    public void clearInit() {
        initialized.set(false);
    }

    private synchronized void initializeServer(final ProcessContext context) throws Exception {
        if (initialized.get()) {
            return;
        }
        this.containerQueue = new LinkedBlockingQueue<>(context.getProperty(CONTAINER_QUEUE_SIZE).asInteger());
        final String host = context.getProperty(HOSTNAME).getValue();
        final int port = context.getProperty(PORT).asInteger();
        final SSLContextService sslService = context.getProperty(SSL_CONTEXT)
                .asControllerService(SSLContextService.class);

        final String clientAuthValue = context.getProperty(CLIENT_AUTH).getValue();
        final boolean need;
        final boolean want;
        if (CLIENT_NEED.equals(clientAuthValue)) {
            need = true;
            want = false;
        } else if (CLIENT_WANT.equals(clientAuthValue)) {
            need = false;
            want = true;
        } else {
            need = false;
            want = false;
        }

        final SslContextFactory sslFactory = (sslService == null) ? null : createSslFactory(sslService, need, want);
        final Server server = new Server(port);

        // create the http configuration
        final HttpConfiguration httpConfiguration = new HttpConfiguration();
        if (sslFactory == null) {
            // create the connector
            final ServerConnector http = new ServerConnector(server, new HttpConnectionFactory(httpConfiguration));

            // set host and port
            if (StringUtils.isNotBlank(host)) {
                http.setHost(host);
            }
            http.setPort(port);

            // add this connector
            server.setConnectors(new Connector[] { http });
        } else {
            // add some secure config
            final HttpConfiguration httpsConfiguration = new HttpConfiguration(httpConfiguration);
            httpsConfiguration.setSecureScheme("https");
            httpsConfiguration.setSecurePort(port);
            httpsConfiguration.addCustomizer(new SecureRequestCustomizer());

            // build the connector
            final ServerConnector https = new ServerConnector(server,
                    new SslConnectionFactory(sslFactory, "http/1.1"),
                    new HttpConnectionFactory(httpsConfiguration));

            // set host and port
            if (StringUtils.isNotBlank(host)) {
                https.setHost(host);
            }
            https.setPort(port);

            // add this connector
            server.setConnectors(new Connector[] { https });
        }

        final Set<String> allowedMethods = new HashSet<>();
        if (context.getProperty(ALLOW_GET).asBoolean()) {
            allowedMethods.add("GET");
        }
        if (context.getProperty(ALLOW_POST).asBoolean()) {
            allowedMethods.add("POST");
        }
        if (context.getProperty(ALLOW_PUT).asBoolean()) {
            allowedMethods.add("PUT");
        }
        if (context.getProperty(ALLOW_DELETE).asBoolean()) {
            allowedMethods.add("DELETE");
        }
        if (context.getProperty(ALLOW_HEAD).asBoolean()) {
            allowedMethods.add("HEAD");
        }
        if (context.getProperty(ALLOW_OPTIONS).asBoolean()) {
            allowedMethods.add("OPTIONS");
        }

        final String additionalMethods = context.getProperty(ADDITIONAL_METHODS).getValue();
        if (additionalMethods != null) {
            for (final String additionalMethod : additionalMethods.split(",")) {
                final String trimmed = additionalMethod.trim();
                if (!trimmed.isEmpty()) {
                    allowedMethods.add(trimmed.toUpperCase());
                }
            }
        }

        final String pathRegex = context.getProperty(PATH_REGEX).getValue();
        final Pattern pathPattern = (pathRegex == null) ? null : Pattern.compile(pathRegex);

        server.setHandler(new AbstractHandler() {
            @Override
            public void handle(final String target, final Request baseRequest, final HttpServletRequest request,
                    final HttpServletResponse response) throws IOException, ServletException {

                final String requestUri = request.getRequestURI();
                if (!allowedMethods.contains(request.getMethod().toUpperCase())) {
                    getLogger().info(
                            "Sending back METHOD_NOT_ALLOWED response to {}; method was {}; request URI was {}",
                            new Object[] { request.getRemoteAddr(), request.getMethod(), requestUri });
                    response.sendError(Status.METHOD_NOT_ALLOWED.getStatusCode());
                    return;
                }

                if (pathPattern != null) {
                    final URI uri;
                    try {
                        uri = new URI(requestUri);
                    } catch (final URISyntaxException e) {
                        throw new ServletException(e);
                    }

                    if (!pathPattern.matcher(uri.getPath()).matches()) {
                        response.sendError(Status.NOT_FOUND.getStatusCode());
                        getLogger().info("Sending back NOT_FOUND response to {}; request was {} {}",
                                new Object[] { request.getRemoteAddr(), request.getMethod(), requestUri });
                        return;
                    }
                }

                // If destination queues full, send back a 503: Service Unavailable.
                if (context.getAvailableRelationships().isEmpty()) {
                    response.sendError(HttpServletResponse.SC_SERVICE_UNAVAILABLE);
                    return;
                }

                // Right now, that information, though, is only in the ProcessSession, not the ProcessContext,
                // so it is not known to us. Should see if it can be added to the ProcessContext.
                final AsyncContext async = baseRequest.startAsync();
                async.setTimeout(Long.MAX_VALUE); // timeout is handled by HttpContextMap
                final boolean added = containerQueue.offer(new HttpRequestContainer(request, response, async));

                if (added) {
                    getLogger().debug("Added Http Request to queue for {} {} from {}",
                            new Object[] { request.getMethod(), requestUri, request.getRemoteAddr() });
                } else {
                    getLogger().info("Sending back a SERVICE_UNAVAILABLE response to {}; request was {} {}",
                            new Object[] { request.getRemoteAddr(), request.getMethod(), request.getRemoteAddr() });

                    response.sendError(Status.SERVICE_UNAVAILABLE.getStatusCode());
                    response.flushBuffer();
                    async.complete();
                }
            }
        });

        this.server = server;
        server.start();

        getLogger().info("Server started and listening on port " + getPort());

        initialized.set(true);
    }

    protected int getPort() {
        for (final Connector connector : server.getConnectors()) {
            if (connector instanceof ServerConnector) {
                return ((ServerConnector) connector).getLocalPort();
            }
        }

        throw new IllegalStateException("Server is not listening on any ports");
    }

    protected int getRequestQueueSize() {
        return containerQueue.size();
    }

    private SslContextFactory createSslFactory(final SSLContextService sslService, final boolean needClientAuth,
            final boolean wantClientAuth) {
        final SslContextFactory sslFactory = new SslContextFactory();

        sslFactory.setNeedClientAuth(needClientAuth);
        sslFactory.setWantClientAuth(wantClientAuth);

        if (sslService.isKeyStoreConfigured()) {
            sslFactory.setKeyStorePath(sslService.getKeyStoreFile());
            sslFactory.setKeyStorePassword(sslService.getKeyStorePassword());
            sslFactory.setKeyStoreType(sslService.getKeyStoreType());
        }

        if (sslService.isTrustStoreConfigured()) {
            sslFactory.setTrustStorePath(sslService.getTrustStoreFile());
            sslFactory.setTrustStorePassword(sslService.getTrustStorePassword());
            sslFactory.setTrustStoreType(sslService.getTrustStoreType());
        }

        return sslFactory;
    }

    @OnStopped
    public void shutdown() throws Exception {
        if (server != null) {
            getLogger().debug("Shutting down server");
            server.stop();
            server.destroy();
            server.join();
            getLogger().info("Shut down {}", new Object[] { server });
        }
    }

    @Override
    public void onTrigger(final ProcessContext context, final ProcessSession session) throws ProcessException {
        try {
            if (!initialized.get()) {
                initializeServer(context);
            }
        } catch (Exception e) {
            context.yield();
            throw new ProcessException("Failed to initialize the server", e);
        }

        final HttpRequestContainer container = containerQueue.poll();
        if (container == null) {
            return;
        }

        final long start = System.nanoTime();
        final HttpServletRequest request = container.getRequest();
        FlowFile flowFile = session.create();
        try {
            flowFile = session.importFrom(request.getInputStream(), flowFile);
        } catch (final IOException e) {
            getLogger().error("Failed to receive content from HTTP Request from {} due to {}",
                    new Object[] { request.getRemoteAddr(), e });
            session.remove(flowFile);
            return;
        }

        final String charset = request.getCharacterEncoding() == null
                ? context.getProperty(URL_CHARACTER_SET).getValue()
                : request.getCharacterEncoding();

        final String contextIdentifier = UUID.randomUUID().toString();
        final Map<String, String> attributes = new HashMap<>();
        try {
            putAttribute(attributes, HTTPUtils.HTTP_CONTEXT_ID, contextIdentifier);
            putAttribute(attributes, "mime.type", request.getContentType());
            putAttribute(attributes, "http.servlet.path", request.getServletPath());
            putAttribute(attributes, "http.context.path", request.getContextPath());
            putAttribute(attributes, "http.method", request.getMethod());
            putAttribute(attributes, "http.local.addr", request.getLocalAddr());
            putAttribute(attributes, HTTPUtils.HTTP_LOCAL_NAME, request.getLocalName());
            final String queryString = request.getQueryString();
            if (queryString != null) {
                putAttribute(attributes, "http.query.string", URLDecoder.decode(queryString, charset));
            }
            putAttribute(attributes, HTTPUtils.HTTP_REMOTE_HOST, request.getRemoteHost());
            putAttribute(attributes, "http.remote.addr", request.getRemoteAddr());
            putAttribute(attributes, "http.remote.user", request.getRemoteUser());
            putAttribute(attributes, HTTPUtils.HTTP_REQUEST_URI, request.getRequestURI());
            putAttribute(attributes, "http.request.url", request.getRequestURL().toString());
            putAttribute(attributes, "http.auth.type", request.getAuthType());

            putAttribute(attributes, "http.requested.session.id", request.getRequestedSessionId());
            final DispatcherType dispatcherType = request.getDispatcherType();
            if (dispatcherType != null) {
                putAttribute(attributes, "http.dispatcher.type", dispatcherType.name());
            }
            putAttribute(attributes, "http.character.encoding", request.getCharacterEncoding());
            putAttribute(attributes, "http.locale", request.getLocale());
            putAttribute(attributes, "http.server.name", request.getServerName());
            putAttribute(attributes, HTTPUtils.HTTP_PORT, request.getServerPort());

            final Enumeration<String> paramEnumeration = request.getParameterNames();
            while (paramEnumeration.hasMoreElements()) {
                final String paramName = paramEnumeration.nextElement();
                final String value = request.getParameter(paramName);
                attributes.put("http.param." + paramName, value);
            }

            final Cookie[] cookies = request.getCookies();
            if (cookies != null) {
                for (final Cookie cookie : cookies) {
                    final String name = cookie.getName();
                    final String cookiePrefix = "http.cookie." + name + ".";
                    attributes.put(cookiePrefix + "value", cookie.getValue());
                    attributes.put(cookiePrefix + "domain", cookie.getDomain());
                    attributes.put(cookiePrefix + "path", cookie.getPath());
                    attributes.put(cookiePrefix + "max.age", String.valueOf(cookie.getMaxAge()));
                    attributes.put(cookiePrefix + "version", String.valueOf(cookie.getVersion()));
                    attributes.put(cookiePrefix + "secure", String.valueOf(cookie.getSecure()));
                }
            }

            if (queryString != null) {
                final String[] params = URL_QUERY_PARAM_DELIMITER.split(queryString);
                for (final String keyValueString : params) {
                    final int indexOf = keyValueString.indexOf("=");
                    if (indexOf < 0) {
                        // no =, then it's just a key with no value
                        attributes.put("http.query.param." + URLDecoder.decode(keyValueString, charset), "");
                    } else {
                        final String key = keyValueString.substring(0, indexOf);
                        final String value;

                        if (indexOf == keyValueString.length() - 1) {
                            value = "";
                        } else {
                            value = keyValueString.substring(indexOf + 1);
                        }

                        attributes.put("http.query.param." + URLDecoder.decode(key, charset),
                                URLDecoder.decode(value, charset));
                    }
                }
            }
        } catch (final UnsupportedEncodingException uee) {
            throw new ProcessException("Invalid character encoding", uee); // won't happen because charset has been validated
        }

        final Enumeration<String> headerNames = request.getHeaderNames();
        while (headerNames.hasMoreElements()) {
            final String headerName = headerNames.nextElement();
            final String headerValue = request.getHeader(headerName);
            putAttribute(attributes, "http.headers." + headerName, headerValue);
        }

        final Principal principal = request.getUserPrincipal();
        if (principal != null) {
            putAttribute(attributes, "http.principal.name", principal.getName());
        }

        final X509Certificate certs[] = (X509Certificate[]) request
                .getAttribute("javax.servlet.request.X509Certificate");
        final String subjectDn;
        if (certs != null && certs.length > 0) {
            final X509Certificate cert = certs[0];
            subjectDn = cert.getSubjectDN().getName();
            final String issuerDn = cert.getIssuerDN().getName();

            putAttribute(attributes, HTTPUtils.HTTP_SSL_CERT, subjectDn);
            putAttribute(attributes, "http.issuer.dn", issuerDn);
        } else {
            subjectDn = null;
        }

        flowFile = session.putAllAttributes(flowFile, attributes);

        final HttpContextMap contextMap = context.getProperty(HTTP_CONTEXT_MAP)
                .asControllerService(HttpContextMap.class);
        final boolean registered = contextMap.register(contextIdentifier, request, container.getResponse(),
                container.getContext());

        if (!registered) {
            getLogger().warn(
                    "Received request from {} but could not process it because too many requests are already outstanding; responding with SERVICE_UNAVAILABLE",
                    new Object[] { request.getRemoteAddr() });

            try {
                container.getResponse().setStatus(Status.SERVICE_UNAVAILABLE.getStatusCode());
                container.getResponse().flushBuffer();
                container.getContext().complete();
            } catch (final Exception e) {
                getLogger().warn("Failed to respond with SERVICE_UNAVAILABLE message to {} due to {}",
                        new Object[] { request.getRemoteAddr(), e });
            }

            session.remove(flowFile);
            return;
        }

        final long receiveMillis = TimeUnit.NANOSECONDS.toMillis(System.nanoTime() - start);
        session.getProvenanceReporter().receive(flowFile, HTTPUtils.getURI(attributes),
                "Received from " + request.getRemoteAddr() + (subjectDn == null ? "" : " with DN=" + subjectDn),
                receiveMillis);
        session.transfer(flowFile, REL_SUCCESS);
        getLogger().info("Transferring {} to 'success'; received from {}",
                new Object[] { flowFile, request.getRemoteAddr() });
    }

    private void putAttribute(final Map<String, String> map, final String key, final Object value) {
        if (value == null) {
            return;
        }

        putAttribute(map, key, value.toString());
    }

    private void putAttribute(final Map<String, String> map, final String key, final String value) {
        if (value == null) {
            return;
        }

        map.put(key, value);
    }

    private static class HttpRequestContainer {

        private final HttpServletRequest request;
        private final HttpServletResponse response;
        private final AsyncContext context;

        public HttpRequestContainer(final HttpServletRequest request, final HttpServletResponse response,
                final AsyncContext async) {
            this.request = request;
            this.response = response;
            this.context = async;
        }

        public HttpServletRequest getRequest() {
            return request;
        }

        public HttpServletResponse getResponse() {
            return response;
        }

        public AsyncContext getContext() {
            return context;
        }
    }
}