All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.google.apphosting.utils.security.urlfetch.URLFetchServiceStreamHandler Maven / Gradle / Ivy

There is a newer version: 2.0.32
Show newest version
/*
 * Copyright 2021 Google LLC
 *
 * 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
 *
 *     https://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.apphosting.utils.security.urlfetch;

import static com.google.common.collect.Maps.newLinkedHashMapWithExpectedSize;

import com.google.appengine.api.urlfetch.HTTPHeader;
import com.google.appengine.api.urlfetch.HTTPMethod;
import com.google.appengine.api.urlfetch.HTTPRequest;
import com.google.appengine.api.urlfetch.HTTPResponse;
import com.google.appengine.api.urlfetch.URLFetchService;
import com.google.appengine.api.urlfetch.URLFetchServiceFactory;
import com.google.common.base.Joiner;
import com.google.common.collect.ImmutableMap;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.lang.reflect.Constructor;
import java.net.HttpURLConnection;
import java.net.InetAddress;
import java.net.ProtocolException;
import java.net.Proxy;
import java.net.URL;
import java.net.URLConnection;
import java.net.URLStreamHandler;
import java.security.Permission;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.logging.Logger;
import java.util.regex.Pattern;

/**
 * Adapts the {@link URLFetchService} to the http and https protocol 
 * handlers for {@link URL}.
 *
 */
public class URLFetchServiceStreamHandler extends URLStreamHandler {
  /**
   * Property key for system property defining whether the Connection subclass can infer response
   * messages from the HTTP status code.
   */
  static final String DERIVE_RESPONSE_MESSAGE_PROPERTY =
      "appengine.urlfetch.deriveResponseMessage";

  /**
   * Property key for system property defining whether "internal" addresses such as the metadata
   * server are resolved natively even though other addresses are sent to the urlfetch service.
   *
   * 

This property is undocumented and subject to change. */ static final String RESOLVE_INTERNAL_NATIVELY_PROPERTY = "com.google.appengine.urlfetch.resolve.internal.addresses.natively"; /** * Default deadline for URLFetch calls. */ private static final int DEFAULT_DEADLINE_MS = 5000; private static final ImmutableMap HTTP_ERROR_CODES = new ImmutableMap.Builder() // HTTP 1.1 Status Codes (RFC 2616) .put(100, "CONTINUE") .put(101, "SWITCHING_PROTOCOLS") .put(200, "OK") .put(201, "CREATED") .put(202, "ACCEPTED") .put(203, "NON_AUTHORITATIVE_INFORMATION") .put(204, "NO_CONTENT") .put(205, "RESET_CONTENT") .put(206, "PARTIAL_CONTENT") .put(300, "MULTIPLE_CHOICES") .put(301, "MOVED_PERMANENTLY") .put(302, "FOUND") .put(303, "SEE_OTHER") .put(304, "NOT_MODIFIED") .put(305, "USE_PROXY") .put(307, "TEMPORARY_REDIRECT") .put(400, "BAD_REQUEST") .put(401, "UNAUTHORIZED") .put(402, "PAYMENT_REQUIRED") .put(403, "FORBIDDEN") .put(404, "NOT_FOUND") .put(405, "METHOD_NOT_ALLOWED") .put(406, "NOT_ACCEPTABLE") .put(407, "PROXY_AUTHENTICATION_REQUIRED") .put(408, "REQUEST_TIMEOUT") .put(409, "CONFLICT") .put(410, "GONE") .put(411, "LENGTH_REQUIRED") .put(412, "PRECONDITION_FAILED") .put(413, "REQUEST_ENTITY_TOO_LARGE") .put(414, "REQUEST_URI_TOO_LONG") .put(415, "UNSUPPORTED_MEDIA_TYPE") .put(416, "REQUESTED_RANGE_NOT_SATISFIABLE") .put(417, "EXPECTATION_FAILED") .put(500, "INTERNAL_SERVER_ERROR") .put(501, "NOT_IMPLEMENTED") .put(502, "BAD_GATEWAY") .put(503, "SERVICE_UNAVAILABLE") .put(504, "GATEWAY_TIMEOUT") .put(505, "HTTP_VERSION_NOT_SUPPORTED") // Additional HTTP 1.1 Status Code (RFC 7231) .put(426, "UPGRADE_REQUIRED") // Additional HTTP 1.1 Status Code (RFC 7538) .put(308, "PERMANENT_REDIRECT") // Webdav Status Codes (RFC 2518, RFC 4918) .put(102, "PROCESSING") .put(207, "MULTI_STATUS") .put(422, "UNPROCESSABLE_ENTITY") .put(423, "LOCKED") .put(424, "FAILED_DEPENDENCY") .put(507, "INSUFFICIENT_STORAGE") // Nonstandard Apache extension .put(509, "BANDWIDTH_LIMIT_EXCEEDED") // Additional HTTP Status Codes (RFC 6585) .put(428, "PRECONDITION_REQUIRED") .put(429, "TOO_MANY_REQUESTS") .put(431, "REQUEST_HEADER_FIELDS_TOO_LARGE") .put(511, "NETWORK_AUTHENTICATION_REQUIRED").build(); private static final Logger logger = Logger.getLogger(URLFetchServiceStreamHandler.class.getName()); private static class DeriveResponseMessageProperty { static final boolean INSTANCE = Boolean.getBoolean(DERIVE_RESPONSE_MESSAGE_PROPERTY); } private static Constructor httpURLConnectionConstructor; private static synchronized Constructor getHttpURLConnectionConstructor() throws ReflectiveOperationException { if (httpURLConnectionConstructor == null) { Class httpURLConnectionClass = Class.forName("sun.net.www.protocol.http.HttpURLConnection") .asSubclass(HttpURLConnection.class); httpURLConnectionConstructor = httpURLConnectionClass.getConstructor(URL.class, Proxy.class); } return httpURLConnectionConstructor; } private static HttpURLConnection openNativeConnection(URL u) throws IOException { try { // We use reflection here to avoid referencing sun.* classes from source code. That doesn't // work when compiling with a recent JDK and --release 8. return getHttpURLConnectionConstructor().newInstance(u, null); } catch (ReflectiveOperationException e) { throw new IOException("Could not get HttpURLConnection constructor", e); } } @Override protected HttpURLConnection openConnection(URL u) throws IOException { if (shouldOpenNatively(u)) { return openNativeConnection(u); } return new Connection(u); } @Override protected URLConnection openConnection(URL u, Proxy p) throws IOException { if (p == null) { throw new IllegalArgumentException("p may not be null"); } if (p.equals(Proxy.NO_PROXY)) { return openConnection(u); } throw new UnsupportedOperationException( "Google App Engine does not support the use of proxies."); } private boolean shouldOpenNatively(URL url) { return Boolean.getBoolean(RESOLVE_INTERNAL_NATIVELY_PROPERTY) && isInternalUrl(url); } // Pattern that must match an entire address for it to be internal. // ?x means that spaces are ignored in the pattern. private static final Pattern INTERNAL = Pattern.compile( "(?x: .+\\.internal | (.*:)? 169\\.254\\.\\d+\\.\\d+ )"); // @VisibleForTesting static boolean isInternalUrl(URL url) { String host = url.getHost(); if (host == null) { return false; } return INTERNAL.matcher(host).matches(); } /** * Do not resolve {@link InetAddress} objects. */ @Override protected synchronized InetAddress getHostAddress(URL u) { // N.B.: This is invoked by URL.equals() and // URL.hashCode() -- apparently with privileged permissions. return null; } /** * The HttpURLConnection wrapper around URLFetchService. */ // @VisibleForTesting static class Connection extends HttpURLConnection { /** * The service used to make the fetch. We need one per connection, * because {@link URLFetchService} makes no promises about MT-safety. */ private final URLFetchService service = URLFetchServiceFactory.getURLFetchService(); /** * The cached response. */ private HTTPResponse response; /** * The cached header fields of the response, in a more usable form. */ private LinkedHashMap> responseFields; /** * The current OutputStream. Will be null until {@link #getOutputStream} * is called. */ private BufferingOutputStream outputStream; /** * The current InputStream. Will be null until {@link #getInputStream} * is called. */ private InputStream inputStream; /** * The current request headers. */ private final LinkedHashMap> requestProperties = new LinkedHashMap<>(); // Holder class for global default URLFetch deadline. // @VisibleForTesting static class DeadlineParser { static final DeadlineParser INSTANCE = new DeadlineParser(); volatile int deadlineMs = -1; private DeadlineParser() { refresh(); } // @VisibleForTesting void refresh() { String globalDefault = System.getProperty(URLFetchService.DEFAULT_DEADLINE_PROPERTY); if (globalDefault != null) { try { deadlineMs = (int) (Double.parseDouble(globalDefault) * 1000); } catch (NumberFormatException e) { deadlineMs = -1; logger.warning("Cannot parse deadline: " + globalDefault); } } else { deadlineMs = -1; } } } public Connection(URL url) { super(url); // NB The JRE makes the default 0, which means "infinite timeout". // We don't want that to be the default (and it's not technically spec'd // to be the default), so we change it here. int deadlineMs = DeadlineParser.INSTANCE.deadlineMs; if (deadlineMs == -1) { deadlineMs = DEFAULT_DEADLINE_MS; } setConnectTimeout(deadlineMs); setReadTimeout(1); } @Override public void disconnect() { connected = false; } private boolean isConnected() { return connected; } @Override public boolean usingProxy() { // TODO We are using a proxy, but should we really return true? return false; } @Override public void setChunkedStreamingMode(int chunklen) { // TODO Is this relevant for URLFetchService? super.setChunkedStreamingMode(chunklen); } @Override public void setRequestMethod(String method) throws ProtocolException { method = method.toUpperCase(); try { HTTPMethod.valueOf(method); } catch (IllegalArgumentException e) { throw new ProtocolException(method + " is not one of the supported http methods: " + Arrays.asList(HTTPMethod.values())); } super.setRequestMethod(method); } @Override public int getResponseCode() throws IOException { getInputStream(); return responseCode; } @Override public String getResponseMessage() { return DeriveResponseMessageProperty.INSTANCE && HTTP_ERROR_CODES.containsKey(responseCode) ? HTTP_ERROR_CODES.get(responseCode) : "OK"; } @Override public InputStream getErrorStream() { if (connected && responseCode >= 400) { // Don't cause a fetch, if we haven't already fetched // Spec is to return null instead. return inputStream; } return null; } @Override public void connect() throws IOException { if (connected) { return; } connected = true; // There's no real connect phase for us, since we do the connect, // read, and write all in one single go with URLFetchService. // We can't really do anything here. // If it helps us at all, we do know that the "pre-connect" // settings can not change after this point: // // *

  • setAllowUserInteraction // *
  • setDoInput // *
  • setDoOutput // *
  • setIfModifiedSince // *
  • setUseCaches // * // *

    // * and the general request properties are modified using the method: // *

      // *
    • setRequestProperty } @Override public String getHeaderField(String name) { List fieldValues = getHeaderFields().get(name.toLowerCase()); if (fieldValues == null) { return null; } return fieldValues.get(fieldValues.size() - 1); } @Override public LinkedHashMap> getHeaderFields() { try { getInputStream(); // force a fetch } catch (IOException e) { // It's a bug in the spec that getHeaderFields doesn't throw // IOException or something similar, since it has to potentially // force a connection. throw new RuntimeException("Unable to complete the HTTP request", e); } return responseFields; } @Override public void setRequestProperty(String key, String value) { List values = new ArrayList<>(); values.add(value); requestProperties.put(key, values); super.setRequestProperty(key, value); } @Override public void addRequestProperty(String key, String value) { List values = requestProperties.get(key); if (values == null) { values = new ArrayList<>(); requestProperties.put(key, values); } values.add(value); super.addRequestProperty(key, value); } @Override public String getHeaderFieldKey(int n) { Map.Entry> entry = getNthEntry(n); if (entry != null) { return entry.getKey(); } return null; } @Override public String getHeaderField(int n) { Map.Entry> entry = getNthEntry(n); if (entry != null) { List values = entry.getValue(); if (values != null) { // N.B.: According to the javadoc, I think we're // actually supposed to be returning the final entry in values // here, not all of them. However, I don't want to break // anything now. return Joiner.on(",").useForNull("null").join(values); } } return null; } @Override public Permission getPermission() throws IOException { // Don't need any permissions to ask for any URL from URLFetchService return null; } /** * Returns the InputStream to be read from. *

      * This can be called to complete a PUT or a POST, or initiate a * GET, HEAD, DELETE when setDoInput is true. Calling this method * forces the request to take place, if it has not already. *

      * This method may be called many times by a client, which will * always expect to receive the same InputStream instance. * For example, {@link URLConnection} calls {@code getInputStream()} to * force the request to be committed before retrieving headers. */ @Override public InputStream getInputStream() throws IOException { if (inputStream != null) { return inputStream; } if (!getDoInput()) { String msg = "Input was not set on this URLConnection. Use \"setDoInput(true)\""; throw new IOException(msg); } fetchResponse(); byte[] content = response.getContent(); if (content == null) { content = new byte[]{}; } inputStream = new ByteArrayInputStream(content); return inputStream; } /** * Returns the OutputStream to be written to. *

      * This can be called to initiate a PUT or a POST, when setDoOutput is true. * There is no situation in which this may be called after * {@link #getInputStream}. This method may be called many times by a * client, which will always expect to receive the same OutputStream * instance. *

      * Closing the returned {@code OutputStream} will force the request to * take place, if it has not already. */ @Override public OutputStream getOutputStream() throws IOException { // It's possible we've been asked for the OutputStream before. if (outputStream != null) { return outputStream; } if (!getDoOutput()) { String msg = "Output was not set on this URLConnection. Use \"setDoOutput(true)\""; throw new IOException(msg); } // NB This maintains backwards compatibility with Sun's protocol // implementation. If the user forgot to set POST explicitly // (it defaults to GET), we transparently set it for them. if (method.equalsIgnoreCase(HTTPMethod.GET.name())) { method = HTTPMethod.POST.name(); } // Ensure we are "connected". connect(); // Don't do a fetch - we have to wait until after the client has done // all of his writing to the OutputStream / calls getInputStream. outputStream = new BufferingOutputStream(); return outputStream; } private Map.Entry> getNthEntry(int n) { Iterator>> iterator = getHeaderFields().entrySet().iterator(); Map.Entry> last = null; for (int i = 0; i <= n; ++i) { if (iterator.hasNext()) { last = iterator.next(); } else { return null; } } return last; } /** * Performs the actual fetch of the URL and retrieves the response. * This causes {@link #outputStream} to be closed, such * that any more writes throw an IOException. */ private HTTPResponse fetchResponse() throws IOException { if (response != null) { return response; } connect(); String method = getRequestMethod(); HTTPMethod httpMethod = HTTPMethod.valueOf(method); HTTPRequest request = new HTTPRequest(url, httpMethod); if (getInstanceFollowRedirects()) { request.getFetchOptions().followRedirects(); } else { request.getFetchOptions().doNotFollowRedirects(); } double deadlineSeconds; int connectTimeoutMillis = getConnectTimeout(); int readTimeoutMillis = getReadTimeout(); if (connectTimeoutMillis == 0 || readTimeoutMillis == 0) { // A value of 0 means an infinite deadline, so we use an arbitrarily large number // to represent that. deadlineSeconds = Integer.MAX_VALUE; } else { deadlineSeconds = (getConnectTimeout() + getReadTimeout()) / 1000.0; } if (deadlineSeconds > 0.0) { request.getFetchOptions().setDeadline(deadlineSeconds); } for (Map.Entry> entry : requestProperties.entrySet()) { String name = entry.getKey(); List values = new ArrayList(entry.getValue()); for (String value : values) { request.addHeader(new HTTPHeader(name, value)); } } if (outputStream != null) { byte[] output = outputStream.toByteArray(); outputStream.close(); request.setPayload(output); } // TODO // Consider support getIfModifiedSince() and getUseCaches() response = service.fetch(request); this.responseCode = response.getResponseCode(); if (response.getFinalUrl() != null) { this.url = response.getFinalUrl(); } List headers = response.getHeadersUncombined(); responseFields = newLinkedHashMapWithExpectedSize(headers.size()); for (HTTPHeader header : headers) { List values = responseFields.get(header.getName().toLowerCase()); if (values == null) { values = new ArrayList(); responseFields.put(header.getName().toLowerCase(), values); } values.add(header.getValue().trim()); } return response; } private class BufferingOutputStream extends OutputStream { private ByteArrayOutputStream buffer; private boolean closed; public BufferingOutputStream() { buffer = new ByteArrayOutputStream(); } @Override public void write(int b) throws IOException { checkOpen(); buffer.write(b); } @Override public void write(byte[] b) throws IOException { checkOpen(); buffer.write(b); } @Override public void write(byte[] b, int off, int len) throws IOException { checkOpen(); buffer.write(b, off, len); } @Override public void flush() throws IOException { checkOpen(); buffer.flush(); } @Override public void close() throws IOException { buffer.close(); closed = true; // Force the fetch if we know that no input will be done. if (!isConnected() && !getDoInput()) { fetchResponse(); } } public byte[] toByteArray() { return buffer.toByteArray(); } private void checkOpen() throws IOException { if (closed) { String msg = "The OutputStream has been committed and can no longer be written to."; throw new IOException(msg); } } } } /** * Trims whitespace from the front and back of {@code s}. */ // @VisibleForTesting static String trim(String s) { if (s == null) { return null; } int notWhitespaceChar = 0; for (int i = 0; i < s.length(); ++i) { char c = s.charAt(i); if (!Character.isWhitespace(c)) { notWhitespaceChar = i; break; } } if (notWhitespaceChar != 0) { s = s.substring(notWhitespaceChar); } return s.trim(); } }





  • © 2015 - 2025 Weber Informatics LLC | Privacy Policy