| // Copyright 2011 Google Inc. All Rights Reserved. |
| // |
| // 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 com.google.enterprise.adaptor; |
| |
| import com.google.common.annotations.VisibleForTesting; |
| import com.sun.net.httpserver.HttpExchange; |
| import com.sun.net.httpserver.HttpsServer; |
| |
| import java.io.IOException; |
| import java.io.OutputStream; |
| import java.io.UnsupportedEncodingException; |
| import java.net.HttpURLConnection; |
| import java.net.URI; |
| import java.net.URISyntaxException; |
| import java.net.URLDecoder; |
| import java.nio.charset.Charset; |
| import java.text.DateFormat; |
| import java.text.SimpleDateFormat; |
| import java.util.*; |
| import java.util.logging.Level; |
| import java.util.logging.Logger; |
| import java.util.zip.GZIPOutputStream; |
| |
| /** Utility class for working with {@link HttpExchange}s. */ |
| public final class HttpExchanges { |
| private static final Logger log |
| = Logger.getLogger(HttpExchanges.class.getName()); |
| private static final TimeZone GMT = TimeZone.getTimeZone("GMT"); |
| /** |
| * Default encoding to encode simple response messages. |
| */ |
| private static final Charset ENCODING = Charset.forName("UTF-8"); |
| |
| // DateFormats are relatively expensive to create, and cannot be used from |
| // multiple threads |
| /** RFC 822 date format, as updated by RFC 1123. */ |
| private static final ThreadLocal<DateFormat> dateFormatRfc1123 |
| = new ThreadLocal<DateFormat>() { |
| @Override |
| protected DateFormat initialValue() { |
| DateFormat df = new SimpleDateFormat( |
| "EEE, dd MMM yyyy HH:mm:ss zzz", Locale.ENGLISH); |
| df.setTimeZone(GMT); |
| return df; |
| } |
| }; |
| /** RFC 1036 date format. */ |
| private static final ThreadLocal<DateFormat> dateFormatRfc1036 |
| = new ThreadLocal<DateFormat>() { |
| @Override |
| protected DateFormat initialValue() { |
| DateFormat df = new SimpleDateFormat( |
| "EEEE, dd-MMM-yy HH:mm:ss zzz", Locale.ENGLISH); |
| df.setTimeZone(GMT); |
| return df; |
| } |
| }; |
| /** ANSI C's {@code asctime()} format. */ |
| private static final ThreadLocal<DateFormat> dateFormatAsctime |
| = new ThreadLocal<DateFormat>() { |
| @Override |
| protected DateFormat initialValue() { |
| DateFormat df = new SimpleDateFormat( |
| "EEE MMM d HH:mm:ss yyyy", Locale.ENGLISH); |
| df.setTimeZone(GMT); |
| return df; |
| } |
| }; |
| /** The various date formats as required by RFC 2616 3.3.1. */ |
| private static final List<ThreadLocal<DateFormat>> dateFormatsRfc2616; |
| /** |
| * When thread-local value is not {@code null}, signals that {@link #handle} |
| * should abort immediately with an error. This is a hack required because the |
| * HttpServer can't handle the Executor rejecting execution of a runnable. |
| */ |
| static final ThreadLocal<Object> abortImmediately |
| = new ThreadLocal<Object>(); |
| |
| static { |
| List<ThreadLocal<DateFormat>> tmpList |
| = new ArrayList<ThreadLocal<DateFormat>>(); |
| tmpList.add(dateFormatRfc1123); |
| tmpList.add(dateFormatRfc1036); |
| tmpList.add(dateFormatAsctime); |
| dateFormatsRfc2616 = Collections.unmodifiableList(tmpList); |
| } |
| |
| // Prevent initialization. |
| private HttpExchanges() {} |
| |
| /** Clear ThreadLocal state to test construction of those variables */ |
| @VisibleForTesting |
| static void resetThread() { |
| dateFormatRfc1123.remove(); |
| dateFormatRfc1036.remove(); |
| dateFormatAsctime.remove(); |
| abortImmediately.remove(); |
| } |
| |
| /** |
| * Best-effort attempt to reform the identical URI the client used to |
| * contact the server. |
| */ |
| public static URI getRequestUri(HttpExchange ex) { |
| String host = ex.getRequestHeaders().getFirst("Host"); |
| if (host == null) { |
| // Client must be using HTTP/1.0 |
| log.warning( |
| "Request did not provide Host header, using 'localhost' as hostname"); |
| int port = ex.getHttpContext().getServer().getAddress().getPort(); |
| host = "localhost:" + port; |
| } |
| String protocol = (ex.getHttpContext().getServer() instanceof HttpsServer) |
| ? "https" : "http"; |
| URI base; |
| try { |
| base = new URI(protocol, host, "/", null, null); |
| } catch (URISyntaxException e) { |
| throw new IllegalStateException(e); |
| } |
| URI requestedUri = ex.getRequestURI(); |
| // If uri is already absolute (e.g., a proxy is involved), then this |
| // does nothing, otherwise it resolves the URI for us based on who we |
| // think we are |
| requestedUri = base.resolve(requestedUri); |
| log.log(Level.FINER, "Resolved original URI to: {0}", requestedUri); |
| return requestedUri; |
| } |
| |
| /** |
| * Sends response to GSA. Should only be used when the request method is |
| * HEAD. |
| */ |
| static void respondToHead(HttpExchange ex, int code, |
| String contentType) throws IOException { |
| ex.getResponseHeaders().set("Transfer-Encoding", "chunked"); |
| respond(ex, code, contentType, (byte[]) null); |
| } |
| |
| /** |
| * Sends cheaply-generated response message to GSA. This is intended for use |
| * with pre-build, canned messages. It automatically handles not sending the |
| * actual content when the request method is HEAD. If the content requires |
| * a moderate amount of work to produce, then you should manually call |
| * {@link #respond} or {@link #respondToHead} depending on the situation. |
| */ |
| static void cannedRespond(HttpExchange ex, int code, Translation response) |
| throws IOException { |
| // TODO(ejona): use exchange to decide on response language |
| cannedRespond(ex, code, "text/plain", response.toString()); |
| } |
| |
| /** |
| * Sends cheaply-generated response message to GSA. This is intended for use |
| * with pre-build, canned messages. It automatically handles not sending the |
| * actual content when the request method is HEAD. If the content requires |
| * a moderate amount of work to produce, then you should manually call |
| * {@link #respond} or {@link #respondToHead} depending on the situation. |
| */ |
| static void cannedRespond(HttpExchange ex, int code, Translation response, |
| Object... params) throws IOException { |
| // TODO(ejona): use exchange to decide on response language |
| cannedRespond(ex, code, "text/plain", response.toString(params)); |
| } |
| |
| private static void cannedRespond(HttpExchange ex, int code, |
| String contentType, String response) throws IOException { |
| if ("HEAD".equals(ex.getRequestMethod())) { |
| respondToHead(ex, code, contentType); |
| } else { |
| respond(ex, code, contentType, response.getBytes(ENCODING)); |
| } |
| } |
| |
| /** |
| * Sends headers and configures {@code ex} for (possibly) sending content. |
| * Completing the request is the caller's responsibility. |
| */ |
| static void startResponse(HttpExchange ex, int code, |
| String contentType, boolean hasBody) throws IOException { |
| log.finest("Starting response"); |
| if (contentType != null) { |
| ex.getResponseHeaders().set("Content-Type", contentType); |
| } |
| if (!hasBody) { |
| // No body. Required for HEAD requests |
| ex.sendResponseHeaders(code, -1); |
| } else { |
| // Chuncked encoding |
| ex.sendResponseHeaders(code, 0); |
| } |
| } |
| |
| /** |
| * Sends response to GSA. Should not be used directly if the request method |
| * is HEAD. |
| */ |
| static void respond(HttpExchange ex, int code, String contentType, |
| byte response[]) throws IOException { |
| startResponse(ex, code, contentType, response != null); |
| if (response != null) { |
| OutputStream responseBody = ex.getResponseBody(); |
| log.finest("before writing response"); |
| responseBody.write(response); |
| responseBody.flush(); |
| // This shouldn't be needed, but without it one developer had trouble |
| responseBody.close(); |
| log.finest("after writing response"); |
| } |
| ex.close(); |
| log.finest("after closing exchange"); |
| } |
| |
| /** |
| * Redirect client to {@code location}. The client should retrieve the |
| * referred location via GET, independent of the method of this request. |
| */ |
| public static void sendRedirect(HttpExchange ex, URI location) |
| throws IOException { |
| ex.getResponseHeaders().set("Location", location.toString()); |
| respond(ex, HttpURLConnection.HTTP_SEE_OTHER, null, null); |
| } |
| |
| /** |
| * If the client supports it, set the correct headers and streams to provide |
| * GZIPed response data to the client. Because the content may become |
| * compressed, users of this method should generally use a {@code |
| * responseLength} of {@code 0} when calling {@link |
| * HttpExchange#sendResponseHeaders}. The exception is when responding to a |
| * HEAD request, in which {@code -1} is required. |
| */ |
| public static void enableCompressionIfSupported(HttpExchange ex) |
| throws IOException { |
| String encodingList = ex.getRequestHeaders().getFirst("Accept-Encoding"); |
| if (encodingList == null) { |
| return; |
| } |
| Collection<String> encodings = Arrays.asList(encodingList.split(",")); |
| if (encodings.contains("gzip")) { |
| log.finer("Enabling gzip compression for response"); |
| ex.getResponseHeaders().set("Content-Encoding", "gzip"); |
| // Although the documentation states that getResponseBody() can only be |
| // called after sendResponseHeaders(), this is not actually the case. |
| // Being able to call getResponseBody() before sendResponseHeaders() is |
| // the only way for filters to function and so is supported even though |
| // the documentation says otherwise. |
| final OutputStream os = ex.getResponseBody(); |
| ex.setStreams(null, new AbstractLazyOutputStream() { |
| @Override |
| protected OutputStream retrieveOs() throws IOException { |
| // Creating the GZIPOutputStream must happen after sendResponseHeaders |
| // since the constructor writes data to the provided OutputStream. |
| return new GZIPOutputStream(os); |
| } |
| }); |
| } |
| } |
| |
| /** |
| * Retrieves and parses the If-Modified-Since from the request, returning null |
| * if there was no such header or there was an error. |
| */ |
| public static Date getIfModifiedSince(HttpExchange ex) { |
| String ifModifiedSince |
| = ex.getRequestHeaders().getFirst("If-Modified-Since"); |
| if (ifModifiedSince == null) { |
| return null; |
| } |
| for (ThreadLocal<DateFormat> threadLocal : dateFormatsRfc2616) { |
| try { |
| return threadLocal.get().parse(ifModifiedSince); |
| } catch (java.text.ParseException e) { |
| // Ignore and try another format. We expect only to encounter the first |
| // format, however (the other formats are pre-HTTP/1.1). |
| log.log(Level.FINE, "Exception when parsing If-Modified-Since", e); |
| } |
| } |
| log.log(Level.WARNING, "Could not parse If-Modified-Since: {0}", |
| ifModifiedSince); |
| return null; |
| } |
| |
| /** |
| * Determines if the headers have already been sent for the exchange. |
| * |
| * <p>This implementation currently uses an imperfect heuristic, but should |
| * work well in most cases. It checks to see if the Date header is present, |
| * which is added during {@link HttpExchange#sendResponseHeaders}. |
| */ |
| public static boolean headersSent(HttpExchange ex) { |
| return ex.getResponseHeaders().getFirst("Date") != null; |
| } |
| |
| public static void setLastModified(HttpExchange ex, Date lastModified) { |
| ex.getResponseHeaders().set("Last-Modified", |
| dateFormatRfc1123.get().format(lastModified)); |
| |
| } |
| |
| /** |
| * Parse request GET query parameters of {@code ex} into its parts, correctly |
| * taking into account {@code charset}. The encoding of the GET parameters is |
| * not specified in the request parameters, so it must be negotiated elsewhere |
| * (i.e., via hard-coding). ISO 8859-1 (Latin-1) and UTF-8 are the only |
| * commonly used encodings for query parameters. |
| * |
| * @param ex exchange whose request query string is to be parsed |
| * @param charset character set used during encoding |
| * @return fully-decoded parameter values |
| */ |
| public static Map<String, List<String>> parseQueryParameters(HttpExchange ex, |
| Charset charset) { |
| String queryString = ex.getRequestURI().getRawQuery(); |
| if (queryString == null || queryString.isEmpty()) { |
| return Collections.emptyMap(); |
| } |
| Map<String, List<String>> parsedParams |
| = new TreeMap<String, List<String>>(); |
| for (String param : queryString.split("&")) { |
| String[] parts = param.split("=", 2); |
| String key = parts[0]; |
| String value = parts.length == 2 ? parts[1] : ""; |
| try { |
| key = URLDecoder.decode(key, charset.name()); |
| value = URLDecoder.decode(value, charset.name()); |
| } catch (UnsupportedEncodingException e) { |
| throw new AssertionError(e); |
| } |
| List<String> values = parsedParams.get(key); |
| if (values == null) { |
| values = new LinkedList<String>(); |
| parsedParams.put(key, values); |
| } |
| values.add(value); |
| } |
| |
| for (Map.Entry<String, List<String>> me : parsedParams.entrySet()) { |
| me.setValue(Collections.unmodifiableList(me.getValue())); |
| } |
| return Collections.unmodifiableMap(parsedParams); |
| } |
| } |