summaryrefslogtreecommitdiffstats
path: root/mobile/android/geckoview/src/main/java/org/mozilla/geckoview/GeckoWebExecutor.java
blob: 15464510569de3710b192aa922f3a16f18428737 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*-
 * vim: ts=4 sw=4 expandtab:
 * This Source Code Form is subject to the terms of the Mozilla Public
 * License, v. 2.0. If a copy of the MPL was not distributed with this
 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */

package org.mozilla.geckoview;

import androidx.annotation.AnyThread;
import androidx.annotation.IntDef;
import androidx.annotation.NonNull;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.net.InetAddress;
import java.nio.ByteBuffer;
import java.util.List;
import java.util.Locale;
import org.mozilla.gecko.GeckoThread;
import org.mozilla.gecko.annotation.WrapForJNI;

/**
 * GeckoWebExecutor is responsible for fetching a {@link WebRequest} and delivering a {@link
 * WebResponse} to the caller via {@link #fetch(WebRequest)}. Example:
 *
 * <pre>
 *     final GeckoWebExecutor executor = new GeckoWebExecutor();
 *
 *     final GeckoResult&lt;WebResponse&gt; result = executor.fetch(
 *             new WebRequest.Builder("https://example.org/json")
 *             .header("Accept", "application/json")
 *             .build());
 *
 *     result.then(response -&gt; {
 *         // Do something with response
 *     });
 * </pre>
 */
@AnyThread
public class GeckoWebExecutor {
  // We don't use this right now because we access GeckoThread directly, but
  // it's future-proofing for a world where we allow multiple GeckoRuntimes.
  private final GeckoRuntime mRuntime;

  @WrapForJNI(dispatchTo = "gecko", stubName = "Fetch")
  private static native void nativeFetch(
      WebRequest request, int flags, GeckoResult<WebResponse> result);

  @WrapForJNI(dispatchTo = "gecko", stubName = "Resolve")
  private static native void nativeResolve(String host, GeckoResult<InetAddress[]> result);

  @WrapForJNI(calledFrom = "gecko", exceptionMode = "nsresult")
  private static ByteBuffer createByteBuffer(final int capacity) {
    return ByteBuffer.allocateDirect(capacity);
  }

  @Retention(RetentionPolicy.SOURCE)
  @IntDef({
    FETCH_FLAGS_NONE,
    FETCH_FLAGS_ANONYMOUS,
    FETCH_FLAGS_NO_REDIRECTS,
    FETCH_FLAGS_PRIVATE,
    FETCH_FLAGS_STREAM_FAILURE_TEST,
  })
  public @interface FetchFlags {}

  /** No special treatment. */
  public static final int FETCH_FLAGS_NONE = 0;

  /** Don't send cookies or other user data along with the request. */
  @WrapForJNI public static final int FETCH_FLAGS_ANONYMOUS = 1;

  /** Don't automatically follow redirects. */
  @WrapForJNI public static final int FETCH_FLAGS_NO_REDIRECTS = 1 << 1;

  // There was supposed to be another flag, which we then decided not to implement.
  // That's the reason there's no value 1 << 2, and it can absolutely be used :)

  /** Associates this download with the current private browsing session */
  @WrapForJNI public static final int FETCH_FLAGS_PRIVATE = 1 << 3;

  /** This flag causes a read error in the {@link WebResponse} body. Useful for testing. */
  @WrapForJNI public static final int FETCH_FLAGS_STREAM_FAILURE_TEST = 1 << 10;

  /**
   * Create a new GeckoWebExecutor instance.
   *
   * @param runtime A GeckoRuntime instance
   */
  public GeckoWebExecutor(final @NonNull GeckoRuntime runtime) {
    mRuntime = runtime;
  }

  /**
   * Send the given {@link WebRequest}.
   *
   * @param request A {@link WebRequest} instance
   * @return A {@link GeckoResult} which will be completed with a {@link WebResponse}. If the
   *     request fails to complete, the {@link GeckoResult} will be completed exceptionally with a
   *     {@link WebRequestError}.
   * @throws IllegalArgumentException if request is null or otherwise unusable.
   */
  public @NonNull GeckoResult<WebResponse> fetch(final @NonNull WebRequest request) {
    return fetch(request, FETCH_FLAGS_NONE);
  }

  /**
   * Send the given {@link WebRequest} with specified flags.
   *
   * @param request A {@link WebRequest} instance
   * @param flags The specified flags. One or more of the {@link #FETCH_FLAGS_NONE FETCH_*} flags.
   * @return A {@link GeckoResult} which will be completed with a {@link WebResponse}. If the
   *     request fails to complete, the {@link GeckoResult} will be completed exceptionally with a
   *     {@link WebRequestError}.
   * @throws IllegalArgumentException if request is null or otherwise unusable.
   */
  public @NonNull GeckoResult<WebResponse> fetch(
      final @NonNull WebRequest request, final @FetchFlags int flags) {
    if (request.body != null && !request.body.isDirect()) {
      throw new IllegalArgumentException("Request body must be a direct ByteBuffer");
    }

    if (request.cacheMode < WebRequest.CACHE_MODE_FIRST
        || request.cacheMode > WebRequest.CACHE_MODE_LAST) {
      throw new IllegalArgumentException("Unknown cache mode");
    }

    final String uri = request.uri.toLowerCase(Locale.ROOT);
    // We don't need to fully validate the URI here, just a sanity check
    if (!uri.startsWith("http") && !uri.startsWith("blob")) {
      throw new IllegalArgumentException(
          "Unsupported URI scheme: " + (uri.length() > 10 ? uri.substring(0, 10) : uri));
    }

    final GeckoResult<WebResponse> result = new GeckoResult<>();

    if (GeckoThread.isStateAtLeast(GeckoThread.State.PROFILE_READY)) {
      nativeFetch(request, flags, result);
    } else {
      GeckoThread.queueNativeCallUntil(
          GeckoThread.State.PROFILE_READY,
          this,
          "nativeFetch",
          WebRequest.class,
          request,
          flags,
          GeckoResult.class,
          result);
    }

    return result;
  }

  /**
   * Resolves the specified host name.
   *
   * @param host An Internet host name, e.g. mozilla.org.
   * @return A {@link GeckoResult} which will be fulfilled with a {@link List} of {@link
   *     InetAddress}. In case of failure, the {@link GeckoResult} will be completed exceptionally
   *     with a {@link java.net.UnknownHostException}.
   */
  public @NonNull GeckoResult<InetAddress[]> resolve(final @NonNull String host) {
    final GeckoResult<InetAddress[]> result = new GeckoResult<>();

    if (GeckoThread.isStateAtLeast(GeckoThread.State.PROFILE_READY)) {
      nativeResolve(host, result);
    } else {
      GeckoThread.queueNativeCallUntil(
          GeckoThread.State.PROFILE_READY,
          this,
          "nativeResolve",
          String.class,
          host,
          GeckoResult.class,
          result);
    }
    return result;
  }

  /**
   * This causes a speculative connection to be made to the host in the specified URI. This is
   * useful if an app thinks it may be making a request to that host in the near future. If no
   * request is made, the connection will be cleaned up after an unspecified amount of time.
   *
   * @param uri A URI String.
   */
  public void speculativeConnect(final @NonNull String uri) {
    GeckoThread.speculativeConnect(uri);
  }
}