From bd4778063b6ca36c3773078724b95bbf276522d4 Mon Sep 17 00:00:00 2001 From: Cuke <> Date: Wed, 29 Jun 2022 16:31:12 +0800 Subject: [PATCH] Add dns over https. --- .../com/github/catvod/crawler/Spider.java | 8 + .../tvbox/osc/server/ControlManager.java | 3 + .../github/tvbox/osc/server/RemoteServer.java | 11 + .../osc/ui/fragment/ModelSettingFragment.java | 43 ++ .../github/tvbox/osc/util/CrashHandler.java | 190 --------- .../com/github/tvbox/osc/util/HawkConfig.java | 1 + .../com/github/tvbox/osc/util/OkGoHelper.java | 55 +++ .../okhttp3/dnsoverhttps/BootstrapDns.java | 45 ++ .../okhttp3/dnsoverhttps/DnsOverHttps.java | 399 ++++++++++++++++++ .../okhttp3/dnsoverhttps/DnsRecordCodec.java | 139 ++++++ app/src/main/res/layout/fragment_model.xml | 2 +- 11 files changed, 705 insertions(+), 191 deletions(-) delete mode 100644 app/src/main/java/com/github/tvbox/osc/util/CrashHandler.java create mode 100644 app/src/main/java/okhttp3/dnsoverhttps/BootstrapDns.java create mode 100644 app/src/main/java/okhttp3/dnsoverhttps/DnsOverHttps.java create mode 100644 app/src/main/java/okhttp3/dnsoverhttps/DnsRecordCodec.java diff --git a/app/src/main/java/com/github/catvod/crawler/Spider.java b/app/src/main/java/com/github/catvod/crawler/Spider.java index 9f3bf4ab..e67abb6b 100644 --- a/app/src/main/java/com/github/catvod/crawler/Spider.java +++ b/app/src/main/java/com/github/catvod/crawler/Spider.java @@ -2,11 +2,15 @@ package com.github.catvod.crawler; import android.content.Context; +import com.github.tvbox.osc.util.OkGoHelper; + import org.json.JSONObject; import java.util.HashMap; import java.util.List; +import okhttp3.Dns; + public abstract class Spider { public static JSONObject empty = new JSONObject(); @@ -100,4 +104,8 @@ public abstract class Spider { public boolean manualVideoCheck() { return false; } + + public static Dns safeDns() { + return OkGoHelper.dnsOverHttps; + } } diff --git a/app/src/main/java/com/github/tvbox/osc/server/ControlManager.java b/app/src/main/java/com/github/tvbox/osc/server/ControlManager.java index a3f72200..5337055c 100644 --- a/app/src/main/java/com/github/tvbox/osc/server/ControlManager.java +++ b/app/src/main/java/com/github/tvbox/osc/server/ControlManager.java @@ -15,6 +15,8 @@ import org.greenrobot.eventbus.EventBus; import java.io.IOException; +import tv.danmaku.ijk.media.player.IjkMediaPlayer; + /** * @author pj567 * @date :2021/1/4 @@ -81,6 +83,7 @@ public class ControlManager { }); try { mServer.start(); + IjkMediaPlayer.setDotPort(Hawk.get(HawkConfig.DOH_URL, 0) > 0, RemoteServer.serverPort); break; } catch (IOException ex) { RemoteServer.serverPort++; diff --git a/app/src/main/java/com/github/tvbox/osc/server/RemoteServer.java b/app/src/main/java/com/github/tvbox/osc/server/RemoteServer.java index 7619ad76..cb9970a1 100644 --- a/app/src/main/java/com/github/tvbox/osc/server/RemoteServer.java +++ b/app/src/main/java/com/github/tvbox/osc/server/RemoteServer.java @@ -8,12 +8,14 @@ import android.os.Environment; import com.github.tvbox.osc.R; import com.github.tvbox.osc.api.ApiConfig; import com.github.tvbox.osc.event.ServerEvent; +import com.github.tvbox.osc.util.OkGoHelper; import com.google.gson.JsonArray; import com.google.gson.JsonObject; import org.greenrobot.eventbus.EventBus; import java.io.BufferedOutputStream; +import java.io.ByteArrayInputStream; import java.io.File; import java.io.FileInputStream; import java.io.FileOutputStream; @@ -137,6 +139,15 @@ public class RemoteServer extends NanoHTTPD { } catch (Throwable th) { return NanoHTTPD.newFixedLengthResponse(NanoHTTPD.Response.Status.INTERNAL_ERROR, NanoHTTPD.MIME_PLAINTEXT, th.getMessage()); } + } else if (fileName.equals("/dns-query")) { + String name = session.getParms().get("name"); + byte[] rs = null; + try { + rs = OkGoHelper.dnsOverHttps.lookupHttpsForwardSync(name); + } catch (Throwable th) { + rs = new byte[0]; + } + return NanoHTTPD.newFixedLengthResponse(NanoHTTPD.Response.Status.OK, "application/dns-message", new ByteArrayInputStream(rs), rs.length); } } else if (session.getMethod() == Method.POST) { Map files = new HashMap(); diff --git a/app/src/main/java/com/github/tvbox/osc/ui/fragment/ModelSettingFragment.java b/app/src/main/java/com/github/tvbox/osc/ui/fragment/ModelSettingFragment.java index d16eac17..9ae3261f 100644 --- a/app/src/main/java/com/github/tvbox/osc/ui/fragment/ModelSettingFragment.java +++ b/app/src/main/java/com/github/tvbox/osc/ui/fragment/ModelSettingFragment.java @@ -22,6 +22,7 @@ import com.github.tvbox.osc.ui.dialog.SelectDialog; import com.github.tvbox.osc.ui.dialog.XWalkInitDialog; import com.github.tvbox.osc.util.FastClickCheckUtil; import com.github.tvbox.osc.util.HawkConfig; +import com.github.tvbox.osc.util.OkGoHelper; import com.github.tvbox.osc.util.PlayerHelper; import com.github.tvbox.osc.util.XWalkUtils; import com.orhanobut.hawk.Hawk; @@ -32,6 +33,9 @@ import org.jetbrains.annotations.NotNull; import java.util.ArrayList; import java.util.List; +import okhttp3.HttpUrl; +import tv.danmaku.ijk.media.player.IjkMediaPlayer; + /** * @author pj567 * @date :2020/12/23 @@ -47,6 +51,7 @@ public class ModelSettingFragment extends BaseLazyFragment { private TextView tvXWalkDown; private TextView tvApi; private TextView tvHomeApi; + private TextView tvDns; public static ModelSettingFragment newInstance() { return new ModelSettingFragment().setArguments(); @@ -72,11 +77,13 @@ public class ModelSettingFragment extends BaseLazyFragment { tvXWalkDown = findViewById(R.id.tvXWalkDown); tvApi = findViewById(R.id.tvApi); tvHomeApi = findViewById(R.id.tvHomeApi); + tvDns = findViewById(R.id.tvDns); tvMediaCodec.setText(Hawk.get(HawkConfig.IJK_CODEC, "")); tvDebugOpen.setText(Hawk.get(HawkConfig.DEBUG_OPEN, false) ? "已打开" : "已关闭"); tvParseWebView.setText(Hawk.get(HawkConfig.PARSE_WEBVIEW, true) ? "系统自带" : "XWalkView"); tvXWalkDown.setText(XWalkUtils.xWalkLibExist(mContext) ? "已下载" : "未下载"); tvApi.setText(Hawk.get(HawkConfig.API_URL, "")); + tvDns.setText(OkGoHelper.dnsHttpsList.get(Hawk.get(HawkConfig.DOH_URL, 0))); tvHomeApi.setText(ApiConfig.get().getHomeSourceBean().getName()); tvScale.setText(PlayerHelper.getScaleName(Hawk.get(HawkConfig.PLAY_SCALE, 0))); tvPlay.setText(PlayerHelper.getPlayerName(Hawk.get(HawkConfig.PLAY_TYPE, 0))); @@ -169,6 +176,42 @@ public class ModelSettingFragment extends BaseLazyFragment { } } }); + findViewById(R.id.llDns).setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + FastClickCheckUtil.check(v); + int dohUrl = Hawk.get(HawkConfig.DOH_URL, 0); + + SelectDialog dialog = new SelectDialog<>(mActivity); + dialog.setTip("请选择安全DNS"); + dialog.setAdapter(new SelectDialogAdapter.SelectDialogInterface() { + @Override + public void click(String value, int pos) { + tvDns.setText(OkGoHelper.dnsHttpsList.get(pos)); + Hawk.put(HawkConfig.DOH_URL, pos); + String url = OkGoHelper.getDohUrl(pos); + OkGoHelper.dnsOverHttps.setUrl(url.isEmpty() ? null : HttpUrl.get(url)); + IjkMediaPlayer.toggleDotPort(pos > 0); + } + + @Override + public String getDisplay(String val) { + return val; + } + }, new DiffUtil.ItemCallback() { + @Override + public boolean areItemsTheSame(@NonNull @NotNull String oldItem, @NonNull @NotNull String newItem) { + return oldItem.equals(newItem); + } + + @Override + public boolean areContentsTheSame(@NonNull @NotNull String oldItem, @NonNull @NotNull String newItem) { + return oldItem.equals(newItem); + } + }, OkGoHelper.dnsHttpsList, dohUrl); + dialog.show(); + } + }); findViewById(R.id.llApi).setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { diff --git a/app/src/main/java/com/github/tvbox/osc/util/CrashHandler.java b/app/src/main/java/com/github/tvbox/osc/util/CrashHandler.java deleted file mode 100644 index 20d07b7d..00000000 --- a/app/src/main/java/com/github/tvbox/osc/util/CrashHandler.java +++ /dev/null @@ -1,190 +0,0 @@ -package com.github.tvbox.osc.util; - -import android.app.AlarmManager; -import android.app.PendingIntent; -import android.content.Context; -import android.content.pm.PackageInfo; -import android.content.pm.PackageManager; -import android.os.Build; -import android.os.Environment; - -import androidx.annotation.NonNull; - -import java.io.File; -import java.io.FileOutputStream; -import java.io.PrintWriter; -import java.io.StringWriter; -import java.io.Writer; -import java.lang.reflect.Field; -import java.text.DateFormat; -import java.text.SimpleDateFormat; -import java.util.Date; -import java.util.Map; -import java.util.concurrent.ConcurrentHashMap; - - -/** - * @author acer - * @date 2018/9/10 - */ - -public class CrashHandler implements Thread.UncaughtExceptionHandler { - private static volatile CrashHandler instance; - private Context mContext; - private PendingIntent restartIntent; - private Thread.UncaughtExceptionHandler mDefaultHandler; - private String mExceptionInfo; - private DateFormat formatter = new SimpleDateFormat("yyyy-MM-dd-HH-mm-ss"); - private DateFormat formatterFodder = new SimpleDateFormat("yyyyMMdd"); - private ConcurrentHashMap mCrashInfo = new ConcurrentHashMap<>(); - - private CrashHandler() { - - } - - public static CrashHandler getInstance() { - if (instance == null) { - synchronized (CrashHandler.class) { - if (instance == null) { - instance = new CrashHandler(); - } - } - } - return instance; - } - - public void init(Context context, PendingIntent pendingIntent) { - mContext = context; - restartIntent = pendingIntent; - //保存一份系统默认的CrashHandler - mDefaultHandler = Thread.getDefaultUncaughtExceptionHandler(); - //使用我们自定义的异常处理器替换程序默认的 - Thread.setDefaultUncaughtExceptionHandler(this); - } - - @Override - public void uncaughtException(Thread t, Throwable e) { - if (!catchCrashException(e) && mDefaultHandler != null) { - //没有自定义的CrashHandler的时候就调用系统默认的异常处理方式 - mDefaultHandler.uncaughtException(t, e); - } else { - //退出应用 - try { - Thread.sleep(2000); - } catch (InterruptedException e1) { - e1.printStackTrace(); - } - // 退出程序并在2s后重启 - AlarmManager mgr = (AlarmManager) mContext.getSystemService(Context.ALARM_SERVICE); - mgr.set(AlarmManager.RTC, System.currentTimeMillis() + 2000, restartIntent); - AppManager.getInstance().appExit(1); - } - } - - private boolean catchCrashException(Throwable ex) { - if (ex == null) { - return false; - } - collectInfo(ex); - //上传崩溃信息 - uploadCrashInfo(); - return true; - } - - /** - * 获取异常信息和设备参数信息 - */ - private void collectInfo(Throwable ex) { - mExceptionInfo = collectExceptionInfo(ex); - try { - // 获得包管理器 - PackageManager mPackageManager = mContext.getPackageManager(); - // 得到该应用的信息,即主Activity - PackageInfo mPackageInfo = mPackageManager.getPackageInfo(mContext.getPackageName(), PackageManager.GET_ACTIVITIES); - this.mCrashInfo.put("PackageName", mContext.getPackageName()); - if (mPackageInfo != null) { - String versionName = mPackageInfo.versionName == null ? "null" : mPackageInfo.versionName; - String versionCode = mPackageInfo.versionCode + ""; - this.mCrashInfo.put("VersionName", versionName); - this.mCrashInfo.put("VersionCode", versionCode); - } - } catch (PackageManager.NameNotFoundException e) { - e.printStackTrace(); - } - // 反射机制 - Field[] mFields = Build.class.getDeclaredFields(); - // 迭代Build的字段key-value 此处的信息主要是为了在服务器端手机各种版本手机报错的原因 - for (Field field : mFields) { - try { - field.setAccessible(true); - mCrashInfo.put(field.getName(), field.get("").toString()); - } catch (IllegalArgumentException e) { - e.printStackTrace(); - } catch (IllegalAccessException e) { - e.printStackTrace(); - } - } - } - - private String saveCrashInfo2File(StringBuilder sb) { - try { - long timestamp = System.currentTimeMillis(); - String time = formatter.format(new Date()); - String fileName = "crash-" + time + "-" + timestamp + ".log"; - if (Environment.getExternalStorageState().equals(Environment.MEDIA_MOUNTED)) { - String path = "/sdcard/movie/crash/" + formatterFodder.format(new Date()) + "/"; - File dir = new File(path); - if (!dir.exists()) { - dir.mkdirs(); - } - FileOutputStream fos = new FileOutputStream(path + fileName); - fos.write(sb.toString().getBytes()); - fos.close(); - } - return fileName; - } catch (Exception e) { - } - return null; - } - - /** - * 获取捕获异常的信息 - */ - private String collectExceptionInfo(Throwable ex) { - Writer mWriter = new StringWriter(); - PrintWriter mPrintWriter = new PrintWriter(mWriter); - ex.printStackTrace(mPrintWriter); - ex.printStackTrace(); - Throwable mThrowable = ex.getCause(); - // 迭代栈队列把所有的异常信息写入writer中 - while (mThrowable != null) { - mThrowable.printStackTrace(mPrintWriter); - // 换行 每个个异常栈之间换行 - mPrintWriter.append("\r\n"); - mThrowable = mThrowable.getCause(); - } - // 记得关闭 - mPrintWriter.close(); - return mWriter.toString(); - } - - /** - * 将HashMap遍历转换成StringBuffer - */ - @NonNull - private static StringBuilder getInfoStr(ConcurrentHashMap info) { - StringBuilder mStringBuilder = new StringBuilder(); - for (Map.Entry entry : info.entrySet()) { - String key = entry.getKey(); - String value = entry.getValue(); - mStringBuilder.append(key + "=" + value + "\r\n"); - } - return mStringBuilder; - } - - private void uploadCrashInfo() { - StringBuilder mStringBuilder = getInfoStr(mCrashInfo); - mStringBuilder.append(mExceptionInfo); - saveCrashInfo2File(mStringBuilder); - } -} diff --git a/app/src/main/java/com/github/tvbox/osc/util/HawkConfig.java b/app/src/main/java/com/github/tvbox/osc/util/HawkConfig.java index bf3fda37..85af37fb 100644 --- a/app/src/main/java/com/github/tvbox/osc/util/HawkConfig.java +++ b/app/src/main/java/com/github/tvbox/osc/util/HawkConfig.java @@ -18,4 +18,5 @@ public class HawkConfig { public static final String PLAY_RENDER = "play_render"; //0 texture 2 public static final String PLAY_SCALE = "play_scale"; //0 texture 2 public static final String PLAY_TIME_STEP = "play_time_step"; //0 texture 2 + public static final String DOH_URL = "doh_url"; } \ No newline at end of file diff --git a/app/src/main/java/com/github/tvbox/osc/util/OkGoHelper.java b/app/src/main/java/com/github/tvbox/osc/util/OkGoHelper.java index 63e81e65..0b295395 100644 --- a/app/src/main/java/com/github/tvbox/osc/util/OkGoHelper.java +++ b/app/src/main/java/com/github/tvbox/osc/util/OkGoHelper.java @@ -7,14 +7,19 @@ import com.lzy.okgo.https.HttpsUtils; import com.lzy.okgo.interceptor.HttpLoggingInterceptor; import com.orhanobut.hawk.Hawk; +import java.io.File; import java.security.cert.CertificateException; +import java.util.ArrayList; import java.util.concurrent.TimeUnit; import java.util.logging.Level; import javax.net.ssl.SSLSocketFactory; import javax.net.ssl.X509TrustManager; +import okhttp3.Cache; +import okhttp3.HttpUrl; import okhttp3.OkHttpClient; +import okhttp3.dnsoverhttps.DnsOverHttps; import xyz.doikki.videoplayer.exo.ExoMediaSourceHelper; public class OkGoHelper { @@ -43,11 +48,59 @@ public class OkGoHelper { } catch (Throwable th) { th.printStackTrace(); } + builder.dns(dnsOverHttps); ExoMediaSourceHelper.getInstance(App.getInstance()).setOkClient(builder.build()); } + public static DnsOverHttps dnsOverHttps = null; + + public static ArrayList dnsHttpsList = new ArrayList<>(); + + + public static String getDohUrl(int type) { + switch (type) { + case 1: { + return "https://doh.pub/dns-query"; + } + case 2: { + return "https://dns.alidns.com/dns-query"; + } + case 3: { + return "https://doh.360.cn/dns-query"; + } + } + return ""; + } + + static void initDnsOverHttps() { + dnsHttpsList.add("关闭"); + dnsHttpsList.add("腾讯"); + dnsHttpsList.add("阿里"); + dnsHttpsList.add("360"); + OkHttpClient.Builder builder = new OkHttpClient.Builder(); + HttpLoggingInterceptor loggingInterceptor = new HttpLoggingInterceptor("OkExoPlayer"); + if (Hawk.get(HawkConfig.DEBUG_OPEN, false)) { + loggingInterceptor.setPrintLevel(HttpLoggingInterceptor.Level.BODY); + loggingInterceptor.setColorLevel(Level.INFO); + } else { + loggingInterceptor.setPrintLevel(HttpLoggingInterceptor.Level.NONE); + loggingInterceptor.setColorLevel(Level.OFF); + } + builder.addInterceptor(loggingInterceptor); + try { + setOkHttpSsl(builder); + } catch (Throwable th) { + th.printStackTrace(); + } + builder.cache(new Cache(new File(App.getInstance().getCacheDir().getAbsolutePath(), "dohcache"), 10 * 1024 * 1024)); + OkHttpClient dohClient = builder.build(); + String dohUrl = getDohUrl(Hawk.get(HawkConfig.DOH_URL, 0)); + dnsOverHttps = new DnsOverHttps.Builder().client(dohClient).url(dohUrl.isEmpty() ? null : HttpUrl.get(dohUrl)).build(); + } + public static void init() { + initDnsOverHttps(); OkHttpClient.Builder builder = new OkHttpClient.Builder(); HttpLoggingInterceptor loggingInterceptor = new HttpLoggingInterceptor("OkGo"); @@ -68,6 +121,7 @@ public class OkGoHelper { builder.writeTimeout(DEFAULT_MILLISECONDS, TimeUnit.MILLISECONDS); builder.connectTimeout(DEFAULT_MILLISECONDS, TimeUnit.MILLISECONDS); + builder.dns(dnsOverHttps); try { setOkHttpSsl(builder); } catch (Throwable th) { @@ -77,6 +131,7 @@ public class OkGoHelper { OkHttpClient okHttpClient = builder.build(); OkGo.getInstance().setOkHttpClient(okHttpClient); + initExoOkHttpClient(); } diff --git a/app/src/main/java/okhttp3/dnsoverhttps/BootstrapDns.java b/app/src/main/java/okhttp3/dnsoverhttps/BootstrapDns.java new file mode 100644 index 00000000..2cd474d3 --- /dev/null +++ b/app/src/main/java/okhttp3/dnsoverhttps/BootstrapDns.java @@ -0,0 +1,45 @@ +/* + * Copyright (C) 2018 Square, Inc. + * + * 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 okhttp3.dnsoverhttps; + +import java.net.InetAddress; +import java.net.UnknownHostException; +import java.util.List; +import okhttp3.Dns; + +/** + * Internal Bootstrap DNS implementation for handling initial connection to DNS over HTTPS server. + * + * Returns hardcoded results for the known host. + */ +final class BootstrapDns implements Dns { + private final String dnsHostname; + private final List dnsServers; + + BootstrapDns(String dnsHostname, List dnsServers) { + this.dnsHostname = dnsHostname; + this.dnsServers = dnsServers; + } + + @Override public List lookup(String hostname) throws UnknownHostException { + if (!this.dnsHostname.equals(hostname)) { + throw new UnknownHostException( + "BootstrapDns called for " + hostname + " instead of " + dnsHostname); + } + + return dnsServers; + } +} diff --git a/app/src/main/java/okhttp3/dnsoverhttps/DnsOverHttps.java b/app/src/main/java/okhttp3/dnsoverhttps/DnsOverHttps.java new file mode 100644 index 00000000..65d8452e --- /dev/null +++ b/app/src/main/java/okhttp3/dnsoverhttps/DnsOverHttps.java @@ -0,0 +1,399 @@ +/* + * Copyright (C) 2018 Square, Inc. + * + * 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 okhttp3.dnsoverhttps; + +import androidx.annotation.Nullable; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.net.InetAddress; +import java.net.UnknownHostException; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.concurrent.CountDownLatch; + +import okhttp3.CacheControl; +import okhttp3.Call; +import okhttp3.Callback; +import okhttp3.Dns; +import okhttp3.HttpUrl; +import okhttp3.MediaType; +import okhttp3.OkHttpClient; +import okhttp3.Protocol; +import okhttp3.Request; +import okhttp3.RequestBody; +import okhttp3.Response; +import okhttp3.ResponseBody; +import okhttp3.internal.Util; +import okhttp3.internal.platform.Platform; +import okhttp3.internal.publicsuffix.PublicSuffixDatabase; +import okio.ByteString; + +/** + * DNS over HTTPS implementation. + *

+ * Implementation of https://tools.ietf.org/html/draft-ietf-doh-dns-over-https-13 + * + *

A DNS API client encodes a single DNS query into an HTTP request + * using either the HTTP GET or POST method and the other requirements + * of this section. The DNS API server defines the URI used by the + * request through the use of a URI Template.
+ * + *

Warning: This is a non-final API.

+ * + *

As of OkHttp 3.11, this feature is an unstable preview: the API is subject to change, + * and the implementation is incomplete. We expect that OkHttp 3.12 or 3.13 will finalize this API. + * Until then, expect API and behavior changes when you update your OkHttp dependency. + */ +public class DnsOverHttps implements Dns { + public static final MediaType DNS_MESSAGE = MediaType.get("application/dns-message"); + public static final int MAX_RESPONSE_SIZE = 64 * 1024; + private final OkHttpClient client; + private HttpUrl url; + private final boolean includeIPv6; + private final boolean post; + private final boolean resolvePrivateAddresses; + private final boolean resolvePublicAddresses; + + DnsOverHttps(Builder builder) { + if (builder.client == null) { + throw new NullPointerException("client not set"); + } + if (builder.url == null) { + // throw new NullPointerException("url not set"); + } + + this.url = builder.url; + this.includeIPv6 = builder.includeIPv6; + this.post = builder.post; + this.resolvePrivateAddresses = builder.resolvePrivateAddresses; + this.resolvePublicAddresses = builder.resolvePublicAddresses; + this.client = builder.client.newBuilder().dns(buildBootstrapClient(builder)).build(); + } + + public void setUrl(HttpUrl newUrl) { + this.url = newUrl; + } + + private static Dns buildBootstrapClient(Builder builder) { + List hosts = builder.bootstrapDnsHosts; + + if (hosts != null) { + return new BootstrapDns(builder.url.host(), hosts); + } else { + return builder.systemDns; + } + } + + public HttpUrl url() { + return url; + } + + public boolean post() { + return post; + } + + public boolean includeIPv6() { + return includeIPv6; + } + + public OkHttpClient client() { + return client; + } + + public boolean resolvePrivateAddresses() { + return resolvePrivateAddresses; + } + + public boolean resolvePublicAddresses() { + return resolvePublicAddresses; + } + + @Override + public List lookup(String hostname) throws UnknownHostException { + if (this.url == null) + return Dns.SYSTEM.lookup(hostname); + if (!resolvePrivateAddresses || !resolvePublicAddresses) { + boolean privateHost = isPrivateHost(hostname); + + if (privateHost && !resolvePrivateAddresses) { + throw new UnknownHostException("private hosts not resolved"); + } + + if (!privateHost && !resolvePublicAddresses) { + throw new UnknownHostException("public hosts not resolved"); + } + } + return lookupHttps(hostname); + } + + public byte[] lookupHttpsForwardSync(String hostname) throws Throwable { + ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream(); + try { + byteArrayOutputStream.write(executeRequestsSync(hostname, DnsRecordCodec.TYPE_A)); + } finally { + + } + try { + byteArrayOutputStream.write(executeRequestsSync(hostname, DnsRecordCodec.TYPE_AAAA)); + } finally { + + } + return byteArrayOutputStream.toByteArray(); + } + + private List lookupHttps(String hostname) throws UnknownHostException { + List networkRequests = new ArrayList<>(2); + List failures = new ArrayList<>(2); + List results = new ArrayList<>(5); + + buildRequest(hostname, networkRequests, results, failures, DnsRecordCodec.TYPE_A); + + if (includeIPv6) { + buildRequest(hostname, networkRequests, results, failures, DnsRecordCodec.TYPE_AAAA); + } + + executeRequests(hostname, networkRequests, results, failures); + + if (!results.isEmpty()) { + return results; + } + return Dns.SYSTEM.lookup(hostname); + // return throwBestFailure(hostname, failures); + } + + private void buildRequest(String hostname, List networkRequests, List results, + List failures, int type) { + Request request = buildRequest(hostname, type); + Response response = getCacheOnlyResponse(request); + + if (response != null) { + processResponse(response, hostname, results, failures); + } else { + networkRequests.add(client.newCall(request)); + } + } + + private byte[] executeRequestsSync(String hostname, int type) throws IOException { + Request request = buildRequest(hostname, type); + Response response = getCacheOnlyResponse(request); + + if (response == null) { + response = client.newCall(request).execute(); + } + return response.body().bytes(); + } + + private void executeRequests(final String hostname, List networkRequests, + final List responses, final List failures) { + final CountDownLatch latch = new CountDownLatch(networkRequests.size()); + + for (Call call : networkRequests) { + call.enqueue(new Callback() { + @Override + public void onFailure(Call call, IOException e) { + synchronized (failures) { + failures.add(e); + } + latch.countDown(); + } + + @Override + public void onResponse(Call call, Response response) { + processResponse(response, hostname, responses, failures); + latch.countDown(); + } + }); + } + + try { + latch.await(); + } catch (InterruptedException e) { + failures.add(e); + } + } + + private void processResponse(Response response, String hostname, List results, + List failures) { + try { + List addresses = readResponse(hostname, response); + synchronized (results) { + results.addAll(addresses); + } + } catch (Exception e) { + synchronized (failures) { + failures.add(e); + } + } + } + + private List throwBestFailure(String hostname, List failures) + throws UnknownHostException { + if (failures.size() == 0) { + throw new UnknownHostException(hostname); + } + + Exception failure = failures.get(0); + + if (failure instanceof UnknownHostException) { + throw (UnknownHostException) failure; + } + + UnknownHostException unknownHostException = new UnknownHostException(hostname); + unknownHostException.initCause(failure); + + for (int i = 1; i < failures.size(); i++) { + Util.addSuppressedIfPossible(unknownHostException, failures.get(i)); + } + + throw unknownHostException; + } + + private @Nullable + Response getCacheOnlyResponse(Request request) { + if (!post && client.cache() != null) { + try { + Request cacheRequest = request.newBuilder().cacheControl(CacheControl.FORCE_CACHE).build(); + + Response cacheResponse = client.newCall(cacheRequest).execute(); + + if (cacheResponse.code() != 504) { + return cacheResponse; + } + } catch (IOException ioe) { + // Failures are ignored as we can fallback to the network + // and hopefully repopulate the cache. + } + } + + return null; + } + + private List readResponse(String hostname, Response response) throws Exception { + if (response.cacheResponse() == null && response.protocol() != Protocol.HTTP_2) { + Platform.get().log(Platform.WARN, "Incorrect protocol: " + response.protocol(), null); + } + + try { + if (!response.isSuccessful()) { + throw new IOException("response: " + response.code() + " " + response.message()); + } + + ResponseBody body = response.body(); + + if (body.contentLength() > MAX_RESPONSE_SIZE) { + throw new IOException("response size exceeds limit (" + + MAX_RESPONSE_SIZE + + " bytes): " + + body.contentLength() + + " bytes"); + } + + ByteString responseBytes = body.source().readByteString(); + + return DnsRecordCodec.decodeAnswers(hostname, responseBytes); + } finally { + response.close(); + } + } + + private Request buildRequest(String hostname, int type) { + Request.Builder requestBuilder = new Request.Builder().header("Accept", DNS_MESSAGE.toString()); + + ByteString query = DnsRecordCodec.encodeQuery(hostname, type); + + if (post) { + requestBuilder = requestBuilder.url(url).post(RequestBody.create(DNS_MESSAGE, query)); + } else { + String encoded = query.base64Url().replace("=", ""); + HttpUrl requestUrl = url.newBuilder().addQueryParameter("dns", encoded).build(); + + requestBuilder = requestBuilder.url(requestUrl); + } + + return requestBuilder.build(); + } + + static boolean isPrivateHost(String host) { + return PublicSuffixDatabase.get().getEffectiveTldPlusOne(host) == null; + } + + public static final class Builder { + @Nullable + OkHttpClient client = null; + @Nullable + HttpUrl url = null; + boolean includeIPv6 = true; + boolean post = false; + Dns systemDns = Dns.SYSTEM; + @Nullable + List bootstrapDnsHosts = null; + boolean resolvePrivateAddresses = false; + boolean resolvePublicAddresses = true; + + public Builder() { + } + + public DnsOverHttps build() { + return new DnsOverHttps(this); + } + + public Builder client(OkHttpClient client) { + this.client = client; + return this; + } + + public Builder url(HttpUrl url) { + this.url = url; + return this; + } + + public Builder includeIPv6(boolean includeIPv6) { + this.includeIPv6 = includeIPv6; + return this; + } + + public Builder post(boolean post) { + this.post = post; + return this; + } + + public Builder resolvePrivateAddresses(boolean resolvePrivateAddresses) { + this.resolvePrivateAddresses = resolvePrivateAddresses; + return this; + } + + public Builder resolvePublicAddresses(boolean resolvePublicAddresses) { + this.resolvePublicAddresses = resolvePublicAddresses; + return this; + } + + public Builder bootstrapDnsHosts(@Nullable List bootstrapDnsHosts) { + this.bootstrapDnsHosts = bootstrapDnsHosts; + return this; + } + + public Builder bootstrapDnsHosts(InetAddress... bootstrapDnsHosts) { + return bootstrapDnsHosts(Arrays.asList(bootstrapDnsHosts)); + } + + public Builder systemDns(Dns systemDns) { + this.systemDns = systemDns; + return this; + } + } +} diff --git a/app/src/main/java/okhttp3/dnsoverhttps/DnsRecordCodec.java b/app/src/main/java/okhttp3/dnsoverhttps/DnsRecordCodec.java new file mode 100644 index 00000000..4c1cfc48 --- /dev/null +++ b/app/src/main/java/okhttp3/dnsoverhttps/DnsRecordCodec.java @@ -0,0 +1,139 @@ +/* + * Copyright 2016 The Netty Project + * + * The Netty Project licenses this file to you 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 okhttp3.dnsoverhttps; + +import java.io.EOFException; +import java.net.InetAddress; +import java.net.UnknownHostException; +import java.nio.charset.Charset; +import java.util.ArrayList; +import java.util.List; +import okio.Buffer; +import okio.ByteString; +import okio.Utf8; + +/** + * Trivial Dns Encoder/Decoder, basically ripped from Netty full implementation. + */ +class DnsRecordCodec { + private static final byte SERVFAIL = 2; + private static final byte NXDOMAIN = 3; + public static final int TYPE_A = 0x0001; + public static final int TYPE_AAAA = 0x001c; + private static final int TYPE_PTR = 0x000c; + private static final Charset ASCII = Charset.forName("ASCII"); + + private DnsRecordCodec() { + } + + public static ByteString encodeQuery(String host, int type) { + Buffer buf = new Buffer(); + + buf.writeShort(0); // query id + buf.writeShort(256); // flags with recursion + buf.writeShort(1); // question count + buf.writeShort(0); // answerCount + buf.writeShort(0); // authorityResourceCount + buf.writeShort(0); // additional + + Buffer nameBuf = new Buffer(); + final String[] labels = host.split("\\."); + for (String label : labels) { + long utf8ByteCount = Utf8.size(label); + if (utf8ByteCount != label.length()) { + throw new IllegalArgumentException("non-ascii hostname: " + host); + } + nameBuf.writeByte((byte) utf8ByteCount); + nameBuf.writeUtf8(label); + } + nameBuf.writeByte(0); // end + + nameBuf.copyTo(buf, 0, nameBuf.size()); + buf.writeShort(type); + buf.writeShort(1); // CLASS_IN + + return buf.readByteString(); + } + + public static List decodeAnswers(String hostname, ByteString byteString) + throws Exception { + List result = new ArrayList<>(); + + Buffer buf = new Buffer(); + buf.write(byteString); + buf.readShort(); // query id + + final int flags = buf.readShort() & 0xffff; + if (flags >> 15 == 0) { + throw new IllegalArgumentException("not a response"); + } + + byte responseCode = (byte) (flags & 0xf); + + if (responseCode == NXDOMAIN) { + throw new UnknownHostException(hostname + ": NXDOMAIN"); + } else if (responseCode == SERVFAIL) { + throw new UnknownHostException(hostname + ": SERVFAIL"); + } + + final int questionCount = buf.readShort() & 0xffff; + final int answerCount = buf.readShort() & 0xffff; + buf.readShort(); // authority record count + buf.readShort(); // additional record count + + for (int i = 0; i < questionCount; i++) { + skipName(buf); // name + buf.readShort(); // type + buf.readShort(); // class + } + + for (int i = 0; i < answerCount; i++) { + skipName(buf); // name + + int type = buf.readShort() & 0xffff; + buf.readShort(); // class + final long ttl = buf.readInt() & 0xffffffffL; // ttl + final int length = buf.readShort() & 0xffff; + + if (type == TYPE_A || type == TYPE_AAAA) { + byte[] bytes = new byte[length]; + buf.read(bytes); + result.add(InetAddress.getByAddress(bytes)); + } else { + buf.skip(length); + } + } + + return result; + } + + private static void skipName(Buffer in) throws EOFException { + // 0 - 63 bytes + int length = in.readByte(); + + if (length < 0) { + // compressed name pointer, first two bits are 1 + // drop second byte of compression offset + in.skip(1); + } else { + while (length > 0) { + // skip each part of the domain name + in.skip(length); + length = in.readByte(); + } + } + } +} diff --git a/app/src/main/res/layout/fragment_model.xml b/app/src/main/res/layout/fragment_model.xml index 9b9a8caa..d9fa783e 100644 --- a/app/src/main/res/layout/fragment_model.xml +++ b/app/src/main/res/layout/fragment_model.xml @@ -433,7 +433,7 @@ android:layout_weight="1" />