protected void service()

in hawtio-system/src/main/java/io/hawt/web/proxy/ProxyServlet.java [204:362]


    protected void service(HttpServletRequest servletRequest, HttpServletResponse servletResponse)
        throws ServletException, IOException {
        // returns if enabled or not so that Connect plugin can turn on/off itself
        if ("/enabled".equals(servletRequest.getPathInfo())) {
            ServletHelpers.sendJSONResponse(servletResponse, enabled);
            return;
        }

        if (!enabled) {
            servletResponse.setStatus(HttpServletResponse.SC_NOT_FOUND);
            return;
        }

        // Make the Request
        //note: we won't transfer the protocol version because I'm not sure it would truly be compatible
        ProxyAddress proxyAddress = parseProxyAddress(servletRequest);
        if (proxyAddress == null || proxyAddress.getFullProxyUrl() == null) {
            servletResponse.setStatus(HttpServletResponse.SC_NOT_FOUND);
            return;
        }
        // TODO Implement allowlist protection for Kubernetes services as well
        if (proxyAddress instanceof ProxyDetails) {
            ProxyDetails details = (ProxyDetails) proxyAddress;
            if (!allowlist.isAllowed(details)) {
                LOG.debug("Rejecting {}", proxyAddress);
                ServletHelpers.doForbidden(servletResponse, ForbiddenReason.HOST_NOT_ALLOWED);
                return;
            }
        }

        String method = servletRequest.getMethod();
        String proxyRequestUri = proxyAddress.getFullProxyUrl();

        URI targetUriObj;
        try {
            targetUriObj = new URI(proxyRequestUri);
        } catch (URISyntaxException e) {
            LOG.error("URL '{}' is not valid: {}", proxyRequestUri, e.getMessage());
            servletResponse.setStatus(HttpServletResponse.SC_NOT_FOUND);
            return;
        }

        HttpRequest proxyRequest;
        //spec: RFC 2616, sec 4.3: either of these two headers signal that there is a message body.
        if (servletRequest.getHeader(HttpHeaders.CONTENT_LENGTH) != null ||
            servletRequest.getHeader(HttpHeaders.TRANSFER_ENCODING) != null) {
            HttpEntityEnclosingRequest eProxyRequest = new BasicHttpEntityEnclosingRequest(method, proxyRequestUri);
            // Add the input entity (streamed)
            //  note: we don't bother ensuring we close the servletInputStream since the container handles it
            eProxyRequest.setEntity(new InputStreamEntity(servletRequest.getInputStream(), servletRequest.getContentLength()));
            proxyRequest = eProxyRequest;
        } else {
            proxyRequest = new BasicHttpRequest(method, proxyRequestUri);
        }

        copyRequestHeaders(servletRequest, proxyRequest, targetUriObj);

        String username = proxyAddress.getUserName();
        String password = proxyAddress.getPassword();

        if (Strings.isNotBlank(username) && Strings.isNotBlank(password)) {
            String encodedCreds = Base64.encodeBase64String((username + ":" + password).getBytes());
            proxyRequest.setHeader("Authorization", "Basic " + encodedCreds);
        }

        Header proxyAuthHeader = proxyRequest.getFirstHeader("Authorization");
        if (proxyAuthHeader != null) {
            String proxyAuth = proxyAuthHeader.getValue();
            // if remote jolokia credentials have changed, we have to clear session cookies in http-client
            HttpSession session = servletRequest.getSession();
            if (session != null) {
                String previousProxyCredentials = (String) session.getAttribute("proxy-credentials");
                if (previousProxyCredentials != null && !previousProxyCredentials.equals(proxyAuth)) {
                    cookieStore.clear();
                }
                session.setAttribute("proxy-credentials", proxyAuth);
            }
        }

        try {
            setAuthHeaderWithRole(servletRequest, proxyRequest);
        } catch (NoSuchAlgorithmException e) {
            LOG.debug("Cannot set Authorization header for given role and user", e);
            throw new RuntimeException(e);
        }

        setXForwardedForHeader(servletRequest, proxyRequest);

        CloseableHttpResponse proxyResponse = null;
        int statusCode = 0;
        try {

            // Execute the request
            if (doLog) {
                log("proxy " + method + " uri: " + servletRequest.getRequestURI() + " -- " + proxyRequest.getRequestLine().getUri());
            }
            LOG.debug("proxy {} uri: {} -- {}", method, servletRequest.getRequestURI(), proxyRequest.getRequestLine().getUri());
            proxyResponse = proxyClient.execute(URIUtils.extractHost(targetUriObj), proxyRequest);

            // Process the response
            statusCode = proxyResponse.getStatusLine().getStatusCode();

            if (statusCode == 401 || statusCode == 403) {
                if (doLog) {
                    log("Authentication Failed on remote server " + proxyRequestUri);
                }
                LOG.debug("Authentication Failed on remote server {}", proxyRequestUri);
            } else if (doResponseRedirectOrNotModifiedLogic(servletRequest, servletResponse, proxyResponse, statusCode, targetUriObj)) {
                //the response is already "committed" now without any body to send
                //TODO copy response headers?
                return;
            }

            // Pass the response code. This method with the "reason phrase" is deprecated but it's the only way to pass the
            //  reason along too.
            //noinspection deprecation
            servletResponse.setStatus(statusCode, proxyResponse.getStatusLine().getReasonPhrase());

            copyResponseHeaders(proxyResponse, servletResponse);

            // Send the content to the client
            copyResponseEntity(proxyResponse, servletResponse);

        } catch (Exception e) {
            //abort request, according to best practice with HttpClient
            if (proxyRequest instanceof AbortableHttpRequest) {
                AbortableHttpRequest abortableHttpRequest = (AbortableHttpRequest) proxyRequest;
                abortableHttpRequest.abort();
            }
            // Exception needs to be suppressed for security reason
            LOG.debug("Proxy to " + proxyRequestUri + " failed", e);
            if (e instanceof ConnectException || e instanceof UnknownHostException) {
                // Target host refused connection or doesn't exist
                servletResponse.setStatus(HttpServletResponse.SC_NOT_FOUND);
            } else if (e instanceof ServletException) {
                // Redirect / Not Modified failed
                servletResponse.sendError(HttpServletResponse.SC_BAD_GATEWAY, e.getMessage());
            } else if (e instanceof SecurityException) {
                servletResponse.setHeader("WWW-Authenticate", "Basic");
                servletResponse.sendError(statusCode, e.getMessage());
            } else {
                servletResponse.sendError(HttpServletResponse.SC_INTERNAL_SERVER_ERROR, e.getMessage());
            }

        } finally {
            if (proxyResponse != null) {
                // Make sure the entire entity was consumed
                EntityUtils.consumeQuietly(proxyResponse.getEntity());

                try {
                    proxyResponse.close();
                } catch (IOException e) {
                    LOG.error("Error closing proxy client response: {}", e.getMessage());
                }
            }
            //Note: Don't need to close servlet outputStream:
            // http://stackoverflow.com/questions/1159168/should-one-call-close-on-httpservletresponse-getoutputstream-getwriter
        }
    }