summaryrefslogtreecommitdiff
path: root/net/common/src
diff options
context:
space:
mode:
Diffstat (limited to 'net/common/src')
-rw-r--r--net/common/src/main/AndroidManifest.xml6
-rw-r--r--net/common/src/main/java/de/danoeh/antennapod/net/common/AntennapodHttpClient.java108
-rw-r--r--net/common/src/main/java/de/danoeh/antennapod/net/common/BasicAuthorizationInterceptor.java80
-rw-r--r--net/common/src/main/java/de/danoeh/antennapod/net/common/HttpCredentialEncoder.java18
-rw-r--r--net/common/src/main/java/de/danoeh/antennapod/net/common/NetworkUtils.java144
-rw-r--r--net/common/src/main/java/de/danoeh/antennapod/net/common/UriUtil.java28
-rw-r--r--net/common/src/main/java/de/danoeh/antennapod/net/common/UserAgentInterceptor.java17
-rw-r--r--net/common/src/test/java/de/danoeh/antennapod/net/common/UriUtilTest.java25
8 files changed, 426 insertions, 0 deletions
diff --git a/net/common/src/main/AndroidManifest.xml b/net/common/src/main/AndroidManifest.xml
new file mode 100644
index 000000000..1ffc73012
--- /dev/null
+++ b/net/common/src/main/AndroidManifest.xml
@@ -0,0 +1,6 @@
+<manifest xmlns:android="http://schemas.android.com/apk/res/android">
+
+ <uses-permission android:name="android.permission.INTERNET" />
+ <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
+ <uses-permission android:name="android.permission.ACCESS_WIFI_STATE" />
+</manifest>
diff --git a/net/common/src/main/java/de/danoeh/antennapod/net/common/AntennapodHttpClient.java b/net/common/src/main/java/de/danoeh/antennapod/net/common/AntennapodHttpClient.java
new file mode 100644
index 000000000..0a5231172
--- /dev/null
+++ b/net/common/src/main/java/de/danoeh/antennapod/net/common/AntennapodHttpClient.java
@@ -0,0 +1,108 @@
+package de.danoeh.antennapod.net.common;
+
+import android.text.TextUtils;
+import android.util.Log;
+import androidx.annotation.NonNull;
+import de.danoeh.antennapod.model.download.ProxyConfig;
+import de.danoeh.antennapod.net.ssl.SslClientSetup;
+import okhttp3.Cache;
+import okhttp3.Credentials;
+import okhttp3.JavaNetCookieJar;
+import okhttp3.OkHttpClient;
+import java.io.File;
+import java.net.CookieManager;
+import java.net.CookiePolicy;
+import java.net.InetSocketAddress;
+import java.net.Proxy;
+import java.net.SocketAddress;
+import java.util.concurrent.TimeUnit;
+
+/**
+ * Provides access to a HttpClient singleton.
+ */
+public class AntennapodHttpClient {
+ private static final String TAG = "AntennapodHttpClient";
+ private static final int CONNECTION_TIMEOUT = 10000;
+ private static final int READ_TIMEOUT = 30000;
+ private static final int MAX_CONNECTIONS = 8;
+ private static File cacheDirectory;
+ private static ProxyConfig proxyConfig;
+
+ private static volatile OkHttpClient httpClient = null;
+
+ private AntennapodHttpClient() {
+
+ }
+
+ /**
+ * Returns the HttpClient singleton.
+ */
+ public static synchronized OkHttpClient getHttpClient() {
+ if (httpClient == null) {
+ httpClient = newBuilder().build();
+ }
+ return httpClient;
+ }
+
+ public static synchronized void reinit() {
+ httpClient = newBuilder().build();
+ }
+
+ /**
+ * Creates a new HTTP client. Most users should just use
+ * getHttpClient() to get the standard AntennaPod client,
+ * but sometimes it's necessary for others to have their own
+ * copy so that the clients don't share state.
+ * @return http client
+ */
+ @NonNull
+ public static OkHttpClient.Builder newBuilder() {
+ Log.d(TAG, "Creating new instance of HTTP client");
+
+ System.setProperty("http.maxConnections", String.valueOf(MAX_CONNECTIONS));
+
+ OkHttpClient.Builder builder = new OkHttpClient.Builder();
+ builder.interceptors().add(new BasicAuthorizationInterceptor());
+ builder.networkInterceptors().add(new UserAgentInterceptor());
+
+ // set cookie handler
+ CookieManager cm = new CookieManager();
+ cm.setCookiePolicy(CookiePolicy.ACCEPT_ORIGINAL_SERVER);
+ builder.cookieJar(new JavaNetCookieJar(cm));
+
+ // set timeouts
+ builder.connectTimeout(CONNECTION_TIMEOUT, TimeUnit.MILLISECONDS);
+ builder.readTimeout(READ_TIMEOUT, TimeUnit.MILLISECONDS);
+ builder.writeTimeout(READ_TIMEOUT, TimeUnit.MILLISECONDS);
+ builder.cache(new Cache(cacheDirectory, 20L * 1000000)); // 20MB
+
+ // configure redirects
+ builder.followRedirects(true);
+ builder.followSslRedirects(true);
+
+ if (proxyConfig != null && proxyConfig.type != Proxy.Type.DIRECT && !TextUtils.isEmpty(proxyConfig.host)) {
+ int port = proxyConfig.port > 0 ? proxyConfig.port : ProxyConfig.DEFAULT_PORT;
+ SocketAddress address = InetSocketAddress.createUnresolved(proxyConfig.host, port);
+ builder.proxy(new Proxy(proxyConfig.type, address));
+ if (!TextUtils.isEmpty(proxyConfig.username) && proxyConfig.password != null) {
+ builder.proxyAuthenticator((route, response) -> {
+ String credentials = Credentials.basic(proxyConfig.username, proxyConfig.password);
+ return response.request().newBuilder()
+ .header("Proxy-Authorization", credentials)
+ .build();
+ });
+ }
+ }
+
+ SslClientSetup.installCertificates(builder);
+ return builder;
+ }
+
+ public static void setCacheDirectory(File cacheDirectory) {
+ AntennapodHttpClient.cacheDirectory = cacheDirectory;
+ }
+
+ public static void setProxyConfig(ProxyConfig proxyConfig) {
+ AntennapodHttpClient.proxyConfig = proxyConfig;
+ }
+}
diff --git a/net/common/src/main/java/de/danoeh/antennapod/net/common/BasicAuthorizationInterceptor.java b/net/common/src/main/java/de/danoeh/antennapod/net/common/BasicAuthorizationInterceptor.java
new file mode 100644
index 000000000..8e7b9a4f4
--- /dev/null
+++ b/net/common/src/main/java/de/danoeh/antennapod/net/common/BasicAuthorizationInterceptor.java
@@ -0,0 +1,80 @@
+package de.danoeh.antennapod.net.common;
+
+import android.text.TextUtils;
+import android.util.Log;
+import androidx.annotation.NonNull;
+import de.danoeh.antennapod.model.download.DownloadRequest;
+import de.danoeh.antennapod.net.common.HttpCredentialEncoder;
+import de.danoeh.antennapod.net.common.UriUtil;
+import java.io.IOException;
+import java.net.HttpURLConnection;
+import java.util.List;
+
+import okhttp3.Interceptor;
+import okhttp3.Request;
+import okhttp3.Response;
+
+public class BasicAuthorizationInterceptor implements Interceptor {
+ private static final String TAG = "BasicAuthInterceptor";
+ private static final String HEADER_AUTHORIZATION = "Authorization";
+
+ @Override
+ @NonNull
+ public Response intercept(Chain chain) throws IOException {
+ Request request = chain.request();
+
+ Response response = chain.proceed(request);
+
+ if (response.code() != HttpURLConnection.HTTP_UNAUTHORIZED) {
+ return response;
+ }
+
+ Request.Builder newRequest = request.newBuilder();
+ if (!TextUtils.equals(response.request().url().toString(), request.url().toString())) {
+ // Redirect detected. OkHTTP does not re-add the headers on redirect, so calling the new location directly.
+ newRequest.url(response.request().url());
+
+ List<String> authorizationHeaders = request.headers().values(HEADER_AUTHORIZATION);
+ if (!authorizationHeaders.isEmpty() && !TextUtils.isEmpty(authorizationHeaders.get(0))) {
+ // Call already had authorization headers. Try again with the same credentials.
+ newRequest.header(HEADER_AUTHORIZATION, authorizationHeaders.get(0));
+ return chain.proceed(newRequest.build());
+ }
+ }
+
+ String userInfo = null;
+ if (request.tag() instanceof DownloadRequest) {
+ DownloadRequest downloadRequest = (DownloadRequest) request.tag();
+ userInfo = UriUtil.getURIFromRequestUrl(downloadRequest.getSource()).getUserInfo();
+ if (TextUtils.isEmpty(userInfo)
+ && (!TextUtils.isEmpty(downloadRequest.getUsername())
+ || !TextUtils.isEmpty(downloadRequest.getPassword()))) {
+ userInfo = downloadRequest.getUsername() + ":" + downloadRequest.getPassword();
+ }
+ }
+
+ if (TextUtils.isEmpty(userInfo)) {
+ Log.d(TAG, "no credentials for '" + request.url() + "'");
+ return response;
+ }
+
+ if (!userInfo.contains(":")) {
+ Log.d(TAG, "Invalid credentials for '" + request.url() + "'");
+ return response;
+ }
+ String username = userInfo.substring(0, userInfo.indexOf(':'));
+ String password = userInfo.substring(userInfo.indexOf(':') + 1);
+
+ Log.d(TAG, "Authorization failed, re-trying with ISO-8859-1 encoded credentials");
+ newRequest.header(HEADER_AUTHORIZATION, HttpCredentialEncoder.encode(username, password, "ISO-8859-1"));
+ response = chain.proceed(newRequest.build());
+
+ if (response.code() != HttpURLConnection.HTTP_UNAUTHORIZED) {
+ return response;
+ }
+
+ Log.d(TAG, "Authorization failed, re-trying with UTF-8 encoded credentials");
+ newRequest.header(HEADER_AUTHORIZATION, HttpCredentialEncoder.encode(username, password, "UTF-8"));
+ return chain.proceed(newRequest.build());
+ }
+}
diff --git a/net/common/src/main/java/de/danoeh/antennapod/net/common/HttpCredentialEncoder.java b/net/common/src/main/java/de/danoeh/antennapod/net/common/HttpCredentialEncoder.java
new file mode 100644
index 000000000..9b2063ce6
--- /dev/null
+++ b/net/common/src/main/java/de/danoeh/antennapod/net/common/HttpCredentialEncoder.java
@@ -0,0 +1,18 @@
+package de.danoeh.antennapod.net.common;
+
+import android.util.Base64;
+
+import java.io.UnsupportedEncodingException;
+
+public abstract class HttpCredentialEncoder {
+ public static String encode(String username, String password, String charset) {
+ try {
+ String credentials = username + ":" + password;
+ byte[] bytes = credentials.getBytes(charset);
+ String encoded = Base64.encodeToString(bytes, Base64.NO_WRAP);
+ return "Basic " + encoded;
+ } catch (UnsupportedEncodingException e) {
+ throw new AssertionError(e);
+ }
+ }
+}
diff --git a/net/common/src/main/java/de/danoeh/antennapod/net/common/NetworkUtils.java b/net/common/src/main/java/de/danoeh/antennapod/net/common/NetworkUtils.java
new file mode 100644
index 000000000..179c4e13e
--- /dev/null
+++ b/net/common/src/main/java/de/danoeh/antennapod/net/common/NetworkUtils.java
@@ -0,0 +1,144 @@
+package de.danoeh.antennapod.net.common;
+
+import android.content.Context;
+import android.net.ConnectivityManager;
+import android.net.Network;
+import android.net.NetworkCapabilities;
+import android.net.NetworkInfo;
+import android.net.wifi.WifiManager;
+import android.os.Build;
+import java.util.Arrays;
+import java.util.List;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+import de.danoeh.antennapod.storage.preferences.UserPreferences;
+
+public class NetworkUtils {
+ private static final String REGEX_PATTERN_IP_ADDRESS = "([0-9]{1,3}[\\.]){3}[0-9]{1,3}";
+
+ private NetworkUtils(){}
+
+ private static Context context;
+
+ public static void init(Context context) {
+ NetworkUtils.context = context;
+ }
+
+ public static boolean isAutoDownloadAllowed() {
+ ConnectivityManager cm = (ConnectivityManager) context.getSystemService(Context.CONNECTIVITY_SERVICE);
+ NetworkInfo networkInfo = cm.getActiveNetworkInfo();
+ if (networkInfo == null) {
+ return false;
+ }
+ if (networkInfo.getType() == ConnectivityManager.TYPE_WIFI) {
+ if (UserPreferences.isEnableAutodownloadWifiFilter()) {
+ return isInAllowedWifiNetwork();
+ } else {
+ return !isNetworkMetered();
+ }
+ } else if (networkInfo.getType() == ConnectivityManager.TYPE_ETHERNET) {
+ return true;
+ } else {
+ return UserPreferences.isAllowMobileAutoDownload() || !NetworkUtils.isNetworkRestricted();
+ }
+ }
+
+ public static boolean networkAvailable() {
+ ConnectivityManager cm = (ConnectivityManager) context.getSystemService(Context.CONNECTIVITY_SERVICE);
+ NetworkInfo info = cm.getActiveNetworkInfo();
+ return info != null && info.isConnected();
+ }
+
+ public static boolean isEpisodeDownloadAllowed() {
+ return UserPreferences.isAllowMobileEpisodeDownload() || !NetworkUtils.isNetworkRestricted();
+ }
+
+ public static boolean isEpisodeHeadDownloadAllowed() {
+ // It is not an image but it is a similarly tiny request
+ // that is probably not even considered a download by most users
+ return isImageAllowed();
+ }
+
+ public static boolean isImageAllowed() {
+ return UserPreferences.isAllowMobileImages() || !NetworkUtils.isNetworkRestricted();
+ }
+
+ public static boolean isStreamingAllowed() {
+ return UserPreferences.isAllowMobileStreaming() || !NetworkUtils.isNetworkRestricted();
+ }
+
+ public static boolean isFeedRefreshAllowed() {
+ return UserPreferences.isAllowMobileFeedRefresh() || !NetworkUtils.isNetworkRestricted();
+ }
+
+ public static boolean isNetworkRestricted() {
+ return isNetworkMetered() || isNetworkCellular();
+ }
+
+ private static boolean isNetworkMetered() {
+ ConnectivityManager connManager = (ConnectivityManager) context.getSystemService(Context.CONNECTIVITY_SERVICE);
+ return connManager.isActiveNetworkMetered();
+ }
+
+ public static boolean isVpnOverWifi() {
+ if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M) {
+ return false;
+ }
+ ConnectivityManager connManager = (ConnectivityManager) context.getSystemService(Context.CONNECTIVITY_SERVICE);
+ NetworkCapabilities capabilities = connManager.getNetworkCapabilities(connManager.getActiveNetwork());
+ return capabilities != null
+ && capabilities.hasTransport(NetworkCapabilities.TRANSPORT_WIFI)
+ && capabilities.hasTransport(NetworkCapabilities.TRANSPORT_VPN);
+ }
+
+ private static boolean isNetworkCellular() {
+ ConnectivityManager connManager = (ConnectivityManager) context.getSystemService(Context.CONNECTIVITY_SERVICE);
+ if (Build.VERSION.SDK_INT >= 23) {
+ Network network = connManager.getActiveNetwork();
+ if (network == null) {
+ return false; // Nothing connected
+ }
+ NetworkInfo info = connManager.getNetworkInfo(network);
+ if (info == null) {
+ return true; // Better be safe than sorry
+ }
+ NetworkCapabilities capabilities = connManager.getNetworkCapabilities(network);
+ if (capabilities == null) {
+ return true; // Better be safe than sorry
+ }
+ return capabilities.hasTransport(NetworkCapabilities.TRANSPORT_CELLULAR);
+ } else {
+ // if the default network is a VPN,
+ // this method will return the NetworkInfo for one of its underlying networks
+ NetworkInfo info = connManager.getActiveNetworkInfo();
+ if (info == null) {
+ return false; // Nothing connected
+ }
+ //noinspection deprecation
+ return info.getType() == ConnectivityManager.TYPE_MOBILE;
+ }
+ }
+
+ private static boolean isInAllowedWifiNetwork() {
+ WifiManager wm = (WifiManager) context.getApplicationContext().getSystemService(Context.WIFI_SERVICE);
+ List<String> selectedNetworks = Arrays.asList(UserPreferences.getAutodownloadSelectedNetworks());
+ return selectedNetworks.contains(Integer.toString(wm.getConnectionInfo().getNetworkId()));
+ }
+
+ public static boolean wasDownloadBlocked(Throwable throwable) {
+ String message = throwable.getMessage();
+ if (message != null) {
+ Pattern pattern = Pattern.compile(REGEX_PATTERN_IP_ADDRESS);
+ Matcher matcher = pattern.matcher(message);
+ if (matcher.find()) {
+ String ip = matcher.group();
+ return ip.startsWith("127.") || ip.startsWith("0.");
+ }
+ }
+ if (throwable.getCause() != null) {
+ return wasDownloadBlocked(throwable.getCause());
+ }
+ return false;
+ }
+}
diff --git a/net/common/src/main/java/de/danoeh/antennapod/net/common/UriUtil.java b/net/common/src/main/java/de/danoeh/antennapod/net/common/UriUtil.java
new file mode 100644
index 000000000..63fc087d6
--- /dev/null
+++ b/net/common/src/main/java/de/danoeh/antennapod/net/common/UriUtil.java
@@ -0,0 +1,28 @@
+package de.danoeh.antennapod.net.common;
+
+import java.net.MalformedURLException;
+import java.net.URI;
+import java.net.URISyntaxException;
+import java.net.URL;
+
+/**
+ * Utility methods for dealing with URL encoding.
+ */
+public class UriUtil {
+ private UriUtil() {}
+
+ public static URI getURIFromRequestUrl(String source) {
+ // try without encoding the URI
+ try {
+ return new URI(source);
+ } catch (URISyntaxException ignore) {
+ System.out.println("Source is not encoded, encoding now");
+ }
+ try {
+ URL url = new URL(source);
+ return new URI(url.getProtocol(), url.getUserInfo(), url.getHost(), url.getPort(), url.getPath(), url.getQuery(), url.getRef());
+ } catch (MalformedURLException | URISyntaxException e) {
+ throw new IllegalArgumentException(e);
+ }
+ }
+}
diff --git a/net/common/src/main/java/de/danoeh/antennapod/net/common/UserAgentInterceptor.java b/net/common/src/main/java/de/danoeh/antennapod/net/common/UserAgentInterceptor.java
new file mode 100644
index 000000000..7a61aa070
--- /dev/null
+++ b/net/common/src/main/java/de/danoeh/antennapod/net/common/UserAgentInterceptor.java
@@ -0,0 +1,17 @@
+package de.danoeh.antennapod.net.common;
+
+import okhttp3.Interceptor;
+import okhttp3.Response;
+
+import java.io.IOException;
+
+public class UserAgentInterceptor implements Interceptor {
+ public static String USER_AGENT = "AntennaPod/0.0.0";
+
+ @Override
+ public Response intercept(Chain chain) throws IOException {
+ return chain.proceed(chain.request().newBuilder()
+ .header("User-Agent", USER_AGENT)
+ .build());
+ }
+}
diff --git a/net/common/src/test/java/de/danoeh/antennapod/net/common/UriUtilTest.java b/net/common/src/test/java/de/danoeh/antennapod/net/common/UriUtilTest.java
new file mode 100644
index 000000000..32a856284
--- /dev/null
+++ b/net/common/src/test/java/de/danoeh/antennapod/net/common/UriUtilTest.java
@@ -0,0 +1,25 @@
+package de.danoeh.antennapod.net.common;
+
+import de.danoeh.antennapod.net.common.UriUtil;
+import org.junit.Test;
+
+import static org.junit.Assert.assertEquals;
+
+/**
+ * Test class for URIUtil
+ */
+public class UriUtilTest {
+
+ @Test
+ public void testGetURIFromRequestUrlShouldNotEncode() {
+ final String testUrl = "http://example.com/this%20is%20encoded";
+ assertEquals(testUrl, UriUtil.getURIFromRequestUrl(testUrl).toString());
+ }
+
+ @Test
+ public void testGetURIFromRequestUrlShouldEncode() {
+ final String testUrl = "http://example.com/this is not encoded";
+ final String expected = "http://example.com/this%20is%20not%20encoded";
+ assertEquals(expected, UriUtil.getURIFromRequestUrl(testUrl).toString());
+ }
+}