Move all files to natural position.

This commit is contained in:
Alan Evans
2020-01-06 10:52:48 -05:00
parent 0df36047e7
commit 9ebe920195
3016 changed files with 6 additions and 36 deletions

View File

@@ -0,0 +1,62 @@
package org.thoughtcrime.securesms.net;
import android.os.AsyncTask;
import androidx.annotation.NonNull;
import androidx.annotation.WorkerThread;
import org.thoughtcrime.securesms.util.Util;
import org.whispersystems.libsignal.util.guava.Optional;
import java.io.InputStream;
import okhttp3.Call;
public class CallRequestController implements RequestController {
private final Call call;
private InputStream stream;
private boolean canceled;
public CallRequestController(@NonNull Call call) {
this.call = call;
}
@Override
public void cancel() {
AsyncTask.THREAD_POOL_EXECUTOR.execute(() -> {
synchronized (CallRequestController.this) {
if (canceled) return;
call.cancel();
if (stream != null) {
Util.close(stream);
}
canceled = true;
}
});
}
public synchronized void setStream(@NonNull InputStream stream) {
if (canceled) {
Util.close(stream);
} else {
this.stream = stream;
}
notifyAll();
}
/**
* Blocks until the stream is available or until the request is canceled.
*/
@WorkerThread
public synchronized Optional<InputStream> getStream() {
while(stream == null && !canceled) {
Util.wait(this, 0);
}
return Optional.fromNullable(this.stream);
}
}

View File

@@ -0,0 +1,404 @@
package org.thoughtcrime.securesms.net;
import androidx.annotation.NonNull;
import android.text.TextUtils;
import com.annimon.stream.Stream;
import com.bumptech.glide.util.ContentLengthInputStream;
import org.thoughtcrime.securesms.logging.Log;
import org.thoughtcrime.securesms.util.Util;
import org.thoughtcrime.securesms.util.concurrent.SignalExecutors;
import org.whispersystems.libsignal.util.Pair;
import org.whispersystems.libsignal.util.guava.Optional;
import java.io.FilterInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.security.SecureRandom;
import java.util.ArrayList;
import java.util.LinkedList;
import java.util.List;
import okhttp3.CacheControl;
import okhttp3.Call;
import okhttp3.OkHttpClient;
import okhttp3.Request;
import okhttp3.Response;
public class ChunkedDataFetcher {
private static final String TAG = ChunkedDataFetcher.class.getSimpleName();
private static final CacheControl NO_CACHE = new CacheControl.Builder().noCache().build();
private static final long MB = 1024 * 1024;
private static final long KB = 1024;
private final OkHttpClient client;
public ChunkedDataFetcher(@NonNull OkHttpClient client) {
this.client = client;
}
public RequestController fetch(@NonNull String url, long contentLength, @NonNull Callback callback) {
if (contentLength <= 0) {
return fetchChunksWithUnknownTotalSize(url, callback);
}
CompositeRequestController compositeController = new CompositeRequestController();
fetchChunks(url, contentLength, Optional.absent(), compositeController, callback);
return compositeController;
}
private RequestController fetchChunksWithUnknownTotalSize(@NonNull String url, @NonNull Callback callback) {
CompositeRequestController compositeController = new CompositeRequestController();
long chunkSize = new SecureRandom().nextInt(1024) + 1024;
Request request = new Request.Builder()
.url(url)
.cacheControl(NO_CACHE)
.addHeader("Range", "bytes=0-" + (chunkSize - 1))
.addHeader("Accept-Encoding", "identity")
.build();
Call firstChunkCall = client.newCall(request);
compositeController.addController(new CallRequestController(firstChunkCall));
firstChunkCall.enqueue(new okhttp3.Callback() {
@Override
public void onFailure(@NonNull Call call, @NonNull IOException e) {
if (!compositeController.isCanceled()) {
callback.onFailure(e);
compositeController.cancel();
}
}
@Override
public void onResponse(@NonNull Call call, @NonNull Response response) {
String contentRange = response.header("Content-Range");
if (!response.isSuccessful()) {
Log.w(TAG, "Non-successful response code: " + response.code());
callback.onFailure(new IOException("Non-successful response code: " + response.code()));
compositeController.cancel();
if (response.body() != null) response.body().close();
return;
}
if (TextUtils.isEmpty(contentRange)) {
Log.w(TAG, "Missing Content-Range header.");
callback.onFailure(new IOException("Missing Content-Length header."));
compositeController.cancel();
if (response.body() != null) response.body().close();
return;
}
if (response.body() == null) {
Log.w(TAG, "Missing body.");
callback.onFailure(new IOException("Missing body on initial request."));
compositeController.cancel();
return;
}
Optional<Long> contentLength = parseLengthFromContentRange(contentRange);
if (!contentLength.isPresent()) {
Log.w(TAG, "Unable to parse length from Content-Range.");
callback.onFailure(new IOException("Unable to get parse length from Content-Range."));
compositeController.cancel();
return;
}
if (chunkSize >= contentLength.get()) {
try {
callback.onSuccess(response.body().byteStream());
} catch (IOException e) {
callback.onFailure(e);
compositeController.cancel();
}
} else {
InputStream stream = ContentLengthInputStream.obtain(response.body().byteStream(), chunkSize);
fetchChunks(url, contentLength.get(), Optional.of(new Pair<>(stream, chunkSize)), compositeController, callback);
}
}
});
return compositeController;
}
private void fetchChunks(@NonNull String url,
long contentLength,
Optional<Pair<InputStream, Long>> firstChunk,
CompositeRequestController compositeController,
Callback callback)
{
List<ByteRange> requestPattern;
try {
if (firstChunk.isPresent()) {
requestPattern = Stream.of(getRequestPattern(contentLength - firstChunk.get().second()))
.map(b -> new ByteRange(b.start + firstChunk.get().second(),
b.end + firstChunk.get().second(),
b.ignoreFirst))
.toList();
} else {
requestPattern = getRequestPattern(contentLength);
}
} catch (IOException e) {
callback.onFailure(e);
compositeController.cancel();
return;
}
SignalExecutors.UNBOUNDED.execute(() -> {
List<CallRequestController> controllers = Stream.of(requestPattern).map(range -> makeChunkRequest(client, url, range)).toList();
List<InputStream> streams = new ArrayList<>(controllers.size() + (firstChunk.isPresent() ? 1 : 0));
if (firstChunk.isPresent()) {
streams.add(firstChunk.get().first());
}
Stream.of(controllers).forEach(compositeController::addController);
for (CallRequestController controller : controllers) {
Optional<InputStream> stream = controller.getStream();
if (!stream.isPresent()) {
Log.w(TAG, "Stream was canceled.");
callback.onFailure(new IOException("Failure"));
compositeController.cancel();
return;
}
streams.add(stream.get());
}
try {
callback.onSuccess(new InputStreamList(streams));
} catch (IOException e) {
callback.onFailure(e);
compositeController.cancel();
}
});
}
private CallRequestController makeChunkRequest(@NonNull OkHttpClient client, @NonNull String url, @NonNull ByteRange range) {
Request request = new Request.Builder()
.url(url)
.cacheControl(NO_CACHE)
.addHeader("Range", "bytes=" + range.start + "-" + range.end)
.addHeader("Accept-Encoding", "identity")
.build();
Call call = client.newCall(request);
CallRequestController callController = new CallRequestController(call);
call.enqueue(new okhttp3.Callback() {
@Override
public void onFailure(@NonNull Call call, @NonNull IOException e) {
callController.cancel();
}
@Override
public void onResponse(@NonNull Call call, @NonNull Response response) {
if (!response.isSuccessful()) {
callController.cancel();
if (response.body() != null) response.body().close();
return;
}
if (response.body() == null) {
callController.cancel();
if (response.body() != null) response.body().close();
return;
}
InputStream stream = new SkippingInputStream(ContentLengthInputStream.obtain(response.body().byteStream(), response.body().contentLength()), range.ignoreFirst);
callController.setStream(stream);
}
});
return callController;
}
private Optional<Long> parseLengthFromContentRange(@NonNull String contentRange) {
int totalStartPos = contentRange.indexOf('/');
if (totalStartPos >= 0 && contentRange.length() > totalStartPos + 1) {
String totalString = contentRange.substring(totalStartPos + 1);
try {
return Optional.of(Long.parseLong(totalString));
} catch (NumberFormatException e) {
return Optional.absent();
}
}
return Optional.absent();
}
private List<ByteRange> getRequestPattern(long size) throws IOException {
if (size > MB) return getRequestPattern(size, MB);
else if (size > 500 * KB) return getRequestPattern(size, 500 * KB);
else if (size > 100 * KB) return getRequestPattern(size, 100 * KB);
else if (size > 50 * KB) return getRequestPattern(size, 50 * KB);
else if (size > 10 * KB) return getRequestPattern(size, 10 * KB);
else if (size > KB) return getRequestPattern(size, KB);
throw new IOException("Unsupported size: " + size);
}
private List<ByteRange> getRequestPattern(long size, long increment) {
List<ByteRange> results = new LinkedList<>();
long offset = 0;
while (size - offset > increment) {
results.add(new ByteRange(offset, offset + increment - 1, 0));
offset += increment;
}
if (size - offset > 0) {
results.add(new ByteRange(size - increment, size-1, increment - (size - offset)));
}
return results;
}
private static class ByteRange {
private final long start;
private final long end;
private final long ignoreFirst;
private ByteRange(long start, long end, long ignoreFirst) {
this.start = start;
this.end = end;
this.ignoreFirst = ignoreFirst;
}
}
private static class SkippingInputStream extends FilterInputStream {
private long skip;
SkippingInputStream(InputStream in, long skip) {
super(in);
this.skip = skip;
}
@Override
public int read() throws IOException {
if (skip != 0) {
skipFully(skip);
skip = 0;
}
return super.read();
}
@Override
public int read(@NonNull byte[] buffer) throws IOException {
if (skip != 0) {
skipFully(skip);
skip = 0;
}
return super.read(buffer);
}
@Override
public int read(@NonNull byte[] buffer, int offset, int length) throws IOException {
if (skip != 0) {
skipFully(skip);
skip = 0;
}
return super.read(buffer, offset, length);
}
@Override
public int available() throws IOException {
return Util.toIntExact(super.available() - skip);
}
private void skipFully(long amount) throws IOException {
byte[] buffer = new byte[4096];
while (amount > 0) {
int read = super.read(buffer, 0, Math.min(buffer.length, Util.toIntExact(amount)));
if (read != -1) amount -= read;
else return;
}
}
}
private static class InputStreamList extends InputStream {
private final List<InputStream> inputStreams;
private int currentStreamIndex = 0;
InputStreamList(List<InputStream> inputStreams) {
this.inputStreams = inputStreams;
}
@Override
public int read() throws IOException {
while (currentStreamIndex < inputStreams.size()) {
int result = inputStreams.get(currentStreamIndex).read();
if (result == -1) currentStreamIndex++;
else return result;
}
return -1;
}
@Override
public int read(@NonNull byte[] buffer, int offset, int length) throws IOException {
while (currentStreamIndex < inputStreams.size()) {
int result = inputStreams.get(currentStreamIndex).read(buffer, offset, length);
if (result == -1) currentStreamIndex++;
else return result;
}
return -1;
}
@Override
public int read(@NonNull byte[] buffer) throws IOException {
return read(buffer, 0, buffer.length);
}
@Override
public void close() throws IOException {
for (InputStream stream : inputStreams) {
try {
stream.close();
} catch (IOException ignored) {}
}
}
@Override
public int available() {
int total = 0;
for (int i=currentStreamIndex;i<inputStreams.size();i++) {
try {
int available = inputStreams.get(i).available();
if (available != -1) total += available;
} catch (IOException ignored) {}
}
return total;
}
}
public interface Callback {
void onSuccess(InputStream stream) throws IOException;
void onFailure(Exception e);
}
}

View File

@@ -0,0 +1,32 @@
package org.thoughtcrime.securesms.net;
import androidx.annotation.NonNull;
import com.annimon.stream.Stream;
import java.util.ArrayList;
import java.util.List;
public class CompositeRequestController implements RequestController {
private final List<RequestController> controllers = new ArrayList<>();
private boolean canceled = false;
public synchronized void addController(@NonNull RequestController controller) {
if (canceled) {
controller.cancel();
} else {
controllers.add(controller);
}
}
@Override
public synchronized void cancel() {
canceled = true;
Stream.of(controllers).forEach(RequestController::cancel);
}
public synchronized boolean isCanceled() {
return canceled;
}
}

View File

@@ -0,0 +1,58 @@
package org.thoughtcrime.securesms.net;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import org.thoughtcrime.securesms.linkpreview.LinkPreviewUtil;
import org.thoughtcrime.securesms.logging.Log;
import java.io.IOException;
import okhttp3.HttpUrl;
import okhttp3.Interceptor;
import okhttp3.Response;
/**
* Interceptor to do extra safety checks on requests through the {@link ContentProxySelector}
* to prevent non-whitelisted requests from getting to it. In particular, this guards against
* requests redirecting to non-whitelisted domains.
*
* Note that because of the way interceptors are ordered, OkHttp will hit the proxy with the
* bad-redirected-domain before we can intercept the request, so we have to "look ahead" by
* detecting a redirected response on the first pass.
*/
public class ContentProxySafetyInterceptor implements Interceptor {
private static final String TAG = Log.tag(ContentProxySafetyInterceptor.class);
@Override
public @NonNull Response intercept(@NonNull Chain chain) throws IOException {
if (isWhitelisted(chain.request().url())) {
Response response = chain.proceed(chain.request());
if (response.isRedirect()) {
if (isWhitelisted(response.header("Location"))) {
return response;
} else {
Log.w(TAG, "Tried to redirect to a non-whitelisted domain!");
chain.call().cancel();
throw new IOException("Tried to redirect to a non-whitelisted domain!");
}
} else {
return response;
}
} else {
Log.w(TAG, "Request was for a non-whitelisted domain!");
chain.call().cancel();
throw new IOException("Request was for a non-whitelisted domain!");
}
}
private static boolean isWhitelisted(@NonNull HttpUrl url) {
return isWhitelisted(url.toString());
}
private static boolean isWhitelisted(@Nullable String url) {
return LinkPreviewUtil.isWhitelistedLinkUrl(url) || LinkPreviewUtil.isWhitelistedMediaUrl(url);
}
}

View File

@@ -0,0 +1,57 @@
package org.thoughtcrime.securesms.net;
import android.os.AsyncTask;
import org.thoughtcrime.securesms.linkpreview.LinkPreviewDomains;
import org.thoughtcrime.securesms.logging.Log;
import org.thoughtcrime.securesms.BuildConfig;
import org.thoughtcrime.securesms.util.Util;
import java.io.IOException;
import java.net.InetSocketAddress;
import java.net.Proxy;
import java.net.ProxySelector;
import java.net.SocketAddress;
import java.net.SocketException;
import java.net.URI;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
public class ContentProxySelector extends ProxySelector {
private static final String TAG = ContentProxySelector.class.getSimpleName();
private static final Set<String> WHITELISTED_DOMAINS = new HashSet<>();
static {
WHITELISTED_DOMAINS.addAll(LinkPreviewDomains.LINKS);
WHITELISTED_DOMAINS.addAll(LinkPreviewDomains.IMAGES);
}
private final List<Proxy> CONTENT = new ArrayList<Proxy>(1) {{
add(new Proxy(Proxy.Type.HTTP, InetSocketAddress.createUnresolved(BuildConfig.CONTENT_PROXY_HOST,
BuildConfig.CONTENT_PROXY_PORT)));
}};
@Override
public List<Proxy> select(URI uri) {
for (String domain : WHITELISTED_DOMAINS) {
if (uri.getHost().endsWith(domain)) {
return CONTENT;
}
}
throw new IllegalArgumentException("Tried to proxy a non-whitelisted domain.");
}
@Override
public void connectFailed(URI uri, SocketAddress address, IOException failure) {
if (failure instanceof SocketException) {
Log.d(TAG, "Socket exception. Likely a cancellation.");
} else {
Log.w(TAG, "Connection failed.", failure);
}
}
}

View File

@@ -0,0 +1,10 @@
package org.thoughtcrime.securesms.net;
public interface RequestController {
/**
* Best-effort cancellation of any outstanding requests. Will also release any resources held by
* the underlying request.
*/
void cancel();
}