diff --git a/app/build.gradle b/app/build.gradle index b45adb445..f48b64a62 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -13,8 +13,8 @@ android { applicationId "ceui.lisa.pixiv" minSdkVersion 21 targetSdkVersion 30 - versionCode 162 - versionName "2.7.0" + versionCode 163 + versionName "3.0.0" testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" javaCompileOptions { @@ -165,8 +165,6 @@ dependencies { implementation 'com.hjq:toast:8.8' - implementation 'com.github.franmontiel:PersistentCookieJar:v1.0.1' - testImplementation 'junit:junit:4.13' androidTestImplementation 'androidx.test:runner:1.2.0' androidTestImplementation 'androidx.test.ext:junit:1.1.1' diff --git a/app/src/main/java/ceui/lisa/activities/MainActivity.java b/app/src/main/java/ceui/lisa/activities/MainActivity.java index 695c073c4..a15e27971 100644 --- a/app/src/main/java/ceui/lisa/activities/MainActivity.java +++ b/app/src/main/java/ceui/lisa/activities/MainActivity.java @@ -288,8 +288,6 @@ public boolean onNavigationItemSelected(MenuItem item) { intent = new Intent(mContext, TemplateActivity.class); intent.putExtra(TemplateActivity.EXTRA_FRAGMENT, "网页链接"); intent.putExtra(Params.URL, "https://app-api.pixiv.net/web/v1/login?code_challenge=vMBcNztwMPd312YCAZNjat4Tf1xmqdZKV1eZJug24Nc&code_challenge_method=S256&client=pixiv-android"); -// intent.putExtra(Params.URL, "https://accounts.pixiv.net/login?prompt=select_account&return_to=https://app-api.pixiv.net/web/v1/users/auth/pixiv/start?code_challenge=Fqvio7cBvKtdLYeK-bNz8Cl7PvEcwI-OzkwBwxvC--k&code_challenge_method=S256&client=pixiv-ios&source=pixiv-ios&ref="); -// intent.putExtra(Params.URL, "https://accounts.pixiv.net/login?prompt=select_account&return_to=https://app-api.pixiv.net/web/v1/users/auth/pixiv/start?code_challenge=GwlGmGStY2GdW6UogqEGnUkKtPFIFZfAx6Fb4w9u2KE&code_challenge_method=S256&client=pixiv-android&source=pixiv-android&ref="); intent.putExtra(Params.TITLE, getString(R.string.now_login)); intent.putExtra(Params.PREFER_PRESERVE, true); } else { diff --git a/app/src/main/java/ceui/lisa/activities/OutWakeActivity.java b/app/src/main/java/ceui/lisa/activities/OutWakeActivity.java index c7bb5b927..dc558f25a 100644 --- a/app/src/main/java/ceui/lisa/activities/OutWakeActivity.java +++ b/app/src/main/java/ceui/lisa/activities/OutWakeActivity.java @@ -155,8 +155,8 @@ public void doSomething(Void t) { Common.showToast("尝试登陆"); String code = uri.getQueryParameter("code"); Retro.getAccountApi().newLogin( - FragmentLogin.IOS_CLIENT_ID, - FragmentLogin.IOS_CLIENT_SECRET, + FragmentLogin.CLIENT_ID, + FragmentLogin.CLIENT_SECRET, FragmentLogin.AUTH_CODE, code, HostManager.get().getPkceItem().getVerify(), diff --git a/app/src/main/java/ceui/lisa/fragments/FragmentLogin.java b/app/src/main/java/ceui/lisa/fragments/FragmentLogin.java index d180ad5a0..ad3ef61cb 100644 --- a/app/src/main/java/ceui/lisa/fragments/FragmentLogin.java +++ b/app/src/main/java/ceui/lisa/fragments/FragmentLogin.java @@ -176,20 +176,24 @@ public void onClick(View v) { intent.putExtra(TemplateActivity.EXTRA_FRAGMENT, "网页链接"); intent.putExtra(Params.URL, "https://app-api.pixiv.net/web/v1/login?code_challenge=" + HostManager.get().getPkceItem().getChallenge() + - "&code_challenge_method=S256&client=pixiv-ios"); + "&code_challenge_method=S256&client=pixiv-android"); intent.putExtra(Params.TITLE, getString(R.string.now_login)); intent.putExtra(Params.PREFER_PRESERVE, true); startActivity(intent); } }); + baseBind.sign.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { - if (baseBind.signUserName.getText().toString().length() != 0) { - sign(); - } else { - Common.showToast("请输入用户名", 3); - } + Intent intent = new Intent(mContext, TemplateActivity.class); + intent.putExtra(TemplateActivity.EXTRA_FRAGMENT, "网页链接"); + intent.putExtra(Params.URL, "https://app-api.pixiv.net/web/v1/provisional-accounts/create?code_challenge=" + + HostManager.get().getPkceItem().getChallenge() + + "&code_challenge_method=S256&client=pixiv-android"); + intent.putExtra(Params.TITLE, getString(R.string.now_sign)); + intent.putExtra(Params.PREFER_PRESERVE, true); + startActivity(intent); } }); baseBind.hasNoAccount.setOnClickListener(new View.OnClickListener() { diff --git a/app/src/main/java/ceui/lisa/fragments/FragmentWebView.java b/app/src/main/java/ceui/lisa/fragments/FragmentWebView.java index a032e6059..596ff2b47 100644 --- a/app/src/main/java/ceui/lisa/fragments/FragmentWebView.java +++ b/app/src/main/java/ceui/lisa/fragments/FragmentWebView.java @@ -1,13 +1,18 @@ package ceui.lisa.fragments; import android.content.Intent; +import android.net.SSLCertificateSocketFactory; import android.net.Uri; +import android.os.Build; import android.os.Bundle; +import android.text.TextUtils; +import android.util.Log; import android.view.ContextMenu; import android.view.Menu; import android.view.MenuItem; import android.view.View; import android.webkit.WebResourceRequest; +import android.webkit.WebResourceResponse; import android.webkit.WebSettings; import android.webkit.WebView; import android.widget.RelativeLayout; @@ -16,10 +21,32 @@ import com.just.agentweb.AgentWeb; import com.just.agentweb.WebViewClient; +import java.io.IOException; +import java.net.HttpURLConnection; +import java.net.InetAddress; +import java.net.MalformedURLException; +import java.net.Socket; +import java.net.URL; +import java.net.URLConnection; +import java.net.UnknownHostException; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Set; + +import javax.net.ssl.HostnameVerifier; +import javax.net.ssl.HttpsURLConnection; +import javax.net.ssl.SSLPeerUnverifiedException; +import javax.net.ssl.SSLSession; +import javax.net.ssl.SSLSocket; +import javax.net.ssl.SSLSocketFactory; + import ceui.lisa.R; import ceui.lisa.activities.OutWakeActivity; import ceui.lisa.activities.UserActivity; import ceui.lisa.databinding.FragmentWebviewBinding; +import ceui.lisa.http.HttpDns; +import ceui.lisa.http.RubySSLSocketFactory; import ceui.lisa.utils.ClipBoardUtils; import ceui.lisa.utils.Common; import ceui.lisa.utils.Params; @@ -34,6 +61,7 @@ public class FragmentWebView extends BaseFragment { private static final String USER_HEAD = "https://www.pixiv.net/member.php?id="; private static final String WORKS_HEAD = "https://www.pixiv.net/artworks/"; private static final String PIXIV_HEAD = "https://www.pixiv.net/"; + private static final String TAG = "FragmentWebView"; private String title; private String url; private String response = null; @@ -45,6 +73,7 @@ public class FragmentWebView extends BaseFragment { private WebView mWebView; private String mIntentUrl; private WebViewClickHandler handler = new WebViewClickHandler(); + private HttpDns httpDns = HttpDns.getInstance(); @Override public void initBundle(Bundle bundle) { @@ -107,24 +136,89 @@ protected void initData() { .setAgentWebParent(baseBind.webViewParent, new RelativeLayout.LayoutParams(-1, -1)) .useDefaultIndicator() .setWebViewClient(new WebViewClient() { +// @Override +// public WebResourceResponse shouldInterceptRequest(WebView view, WebResourceRequest request) { +// String scheme = request.getUrl().getScheme().trim(); +// String method = request.getMethod(); +// Map headerFields = request.getRequestHeaders(); +// String url = request.getUrl().toString(); +// Log.e(TAG, "url:" + url); +// // 无法拦截body,拦截方案只能正常处理不带body的请求; +// if ((scheme.equalsIgnoreCase("http") || scheme.equalsIgnoreCase("https")) +// && method.equalsIgnoreCase("get")) { +// try { +// URLConnection connection = recursiveRequest(url, headerFields, null); +// +// if (connection == null) { +// Log.e(TAG, "connection null"); +// return super.shouldInterceptRequest(view, request); +// } +// +// // 注*:对于POST请求的Body数据,WebResourceRequest接口中并没有提供,这里无法处理 +// String contentType = connection.getContentType(); +// String mime = getMime(contentType); +// String charset = getCharset(contentType); +// HttpURLConnection httpURLConnection = (HttpURLConnection)connection; +// int statusCode = httpURLConnection.getResponseCode(); +// String response = httpURLConnection.getResponseMessage(); +// Map> headers = httpURLConnection.getHeaderFields(); +// Set headerKeySet = headers.keySet(); +// Log.e(TAG, "code:" + httpURLConnection.getResponseCode()); +// Log.e(TAG, "mime:" + mime + "; charset:" + charset); +// +// +// // 无mime类型的请求不拦截 +// if (TextUtils.isEmpty(mime)) { +// Log.e(TAG, "no MIME"); +// return super.shouldInterceptRequest(view, request); +// } else { +// // 二进制资源无需编码信息 +// if (!TextUtils.isEmpty(charset) || (isBinaryRes(mime))) { +// WebResourceResponse resourceResponse = new WebResourceResponse(mime, charset, httpURLConnection.getInputStream()); +// resourceResponse.setStatusCodeAndReasonPhrase(statusCode, response); +// Map responseHeader = new HashMap(); +// for (String key: headerKeySet) { +// // HttpUrlConnection可能包含key为null的报头,指向该http请求状态码 +// responseHeader.put(key, httpURLConnection.getHeaderField(key)); +// } +// resourceResponse.setResponseHeaders(responseHeader); +// return resourceResponse; +// } else { +// Log.e(TAG, "non binary resource for " + mime); +// return super.shouldInterceptRequest(view, request); +// } +// } +// } catch (MalformedURLException e) { +// e.printStackTrace(); +// } catch (IOException e) { +// e.printStackTrace(); +// } +// } +// return super.shouldInterceptRequest(view, request); +// } + @Override public boolean shouldOverrideUrlLoading(WebView view, WebResourceRequest request) { - String destiny = request.getUrl().toString(); Common.showLog(className + "destiny " + destiny); if (destiny.contains(PIXIV_HEAD)) { - try { - Intent intent = new Intent(mContext, OutWakeActivity.class); - intent.setData(Uri.parse(destiny)); - startActivity(intent); - if (!preferPreserve) { - finish(); + if (destiny.contains("logout.php")) { + return false; + } else { + try { + + Intent intent = new Intent(mContext, OutWakeActivity.class); + intent.setData(Uri.parse(destiny)); + startActivity(intent); + if (!preferPreserve) { + finish(); + } + } catch (Exception e) { + Common.showToast(e.toString()); + e.printStackTrace(); } - } catch (Exception e) { - Common.showToast(e.toString()); - e.printStackTrace(); + return true; } - return true; } return super.shouldOverrideUrlLoading(view, request); @@ -250,4 +344,241 @@ public boolean onMenuItemClick(MenuItem item) { return true; } } + + + + + + /** + * 从contentType中获取MIME类型 + * @param contentType + * @return + */ + private String getMime(String contentType) { + if (contentType == null) { + return null; + } + return contentType.split(";")[0]; + } + + /** + * 从contentType中获取编码信息 + * @param contentType + * @return + */ + private String getCharset(String contentType) { + if (contentType == null) { + return null; + } + + String[] fields = contentType.split(";"); + if (fields.length <= 1) { + return null; + } + + String charset = fields[1]; + if (!charset.contains("=")) { + return null; + } + charset = charset.substring(charset.indexOf("=") + 1); + return charset; + } + + /** + * 是否是二进制资源,二进制资源可以不需要编码信息 + * @param mime + * @return + */ + private boolean isBinaryRes(String mime) { + if (mime.startsWith("image") + || mime.startsWith("audio") + || mime.startsWith("video")) { + return true; + } else { + return false; + } + } + + /** + * header中是否含有cookie + * @param headers + */ + private boolean containCookie(Map headers) { + for (Map.Entry headerField : headers.entrySet()) { + if (headerField.getKey().contains("Cookie")) { + return true; + } + } + return false; + } + + public URLConnection recursiveRequest(String path, Map headers, String reffer) { + HttpURLConnection conn; + URL url = null; + try { + url = new URL(path); + conn = (HttpURLConnection) url.openConnection(); + // 异步接口获取IP + String ip = "210.140.131.188"; + if (ip != null) { + // 通过HTTPDNS获取IP成功,进行URL替换和HOST头设置 + Log.d(TAG, "Get IP: " + ip + " for host: " + url.getHost() + " from HTTPDNS successfully!"); + String newUrl = path.replaceFirst(url.getHost(), ip); + conn = (HttpURLConnection) new URL(newUrl).openConnection(); + + if (headers != null) { + for (Map.Entry field : headers.entrySet()) { + conn.setRequestProperty(field.getKey(), field.getValue()); + } + } + // 设置HTTP请求头Host域 + conn.setRequestProperty("Host", url.getHost()); + } else { + return null; + } + conn.setConnectTimeout(30000); + conn.setReadTimeout(30000); + conn.setInstanceFollowRedirects(false); + if (conn instanceof HttpsURLConnection) { + final HttpsURLConnection httpsURLConnection = (HttpsURLConnection)conn; + // sni场景,创建SSLScocket + WebviewTlsSniSocketFactory sslSocketFactory = new WebviewTlsSniSocketFactory((HttpsURLConnection) conn); + httpsURLConnection.setSSLSocketFactory(sslSocketFactory); + // https场景,证书校验 + httpsURLConnection.setHostnameVerifier(new HostnameVerifier() { + @Override + public boolean verify(String hostname, SSLSession session) { + String host = httpsURLConnection.getRequestProperty("Host"); + if (null == host) { + host = httpsURLConnection.getURL().getHost(); + } + return HttpsURLConnection.getDefaultHostnameVerifier().verify(host, session); + } + }); + } + int code = conn.getResponseCode();// Network block + if (needRedirect(code)) { + // 原有报头中含有cookie,放弃拦截 + if (containCookie(headers)) { + return null; + } + + String location = conn.getHeaderField("Location"); + if (location == null) { + location = conn.getHeaderField("location"); + } + + if (location != null) { + if (!(location.startsWith("http://") || location + .startsWith("https://"))) { + //某些时候会省略host,只返回后面的path,所以需要补全url + URL originalUrl = new URL(path); + location = originalUrl.getProtocol() + "://" + + originalUrl.getHost() + location; + } + Log.e(TAG, "code:" + code + "; location:" + location + "; path" + path); + return recursiveRequest(location, headers, path); + } else { + // 无法获取location信息,让浏览器获取 + return null; + } + } else { + // redirect finish. + Log.e(TAG, "redirect finish"); + return conn; + } + } catch (MalformedURLException e) { + Log.w(TAG, "recursiveRequest MalformedURLException"); + } catch (IOException e) { + Log.w(TAG, "recursiveRequest IOException"); + } catch (Exception e) { + Log.w(TAG, "unknow exception"); + } + return null; + } + + private boolean needRedirect(int code) { + return code >= 300 && code < 400; + } + + class WebviewTlsSniSocketFactory extends SSLSocketFactory { + private final String TAG = WebviewTlsSniSocketFactory.class.getSimpleName(); + HostnameVerifier hostnameVerifier = HttpsURLConnection.getDefaultHostnameVerifier(); + private HttpsURLConnection conn; + + public WebviewTlsSniSocketFactory(HttpsURLConnection conn) { + this.conn = conn; + } + + @Override + public Socket createSocket() throws IOException { + return null; + } + + @Override + public Socket createSocket(String host, int port) throws IOException, UnknownHostException { + return null; + } + + @Override + public Socket createSocket(String host, int port, InetAddress localHost, int localPort) throws IOException, UnknownHostException { + return null; + } + + @Override + public Socket createSocket(InetAddress host, int port) throws IOException { + return null; + } + + @Override + public Socket createSocket(InetAddress address, int port, InetAddress localAddress, int localPort) throws IOException { + return null; + } + + // TLS layer + + @Override + public String[] getDefaultCipherSuites() { + return new String[0]; + } + + @Override + public String[] getSupportedCipherSuites() { + return new String[0]; + } + + @Override + public Socket createSocket(Socket plainSocket, String host, int port, boolean autoClose) throws IOException { + String peerHost = this.conn.getRequestProperty("Host"); + if (peerHost == null) + peerHost = host; + Log.i(TAG, "customized createSocket. host: " + peerHost); + InetAddress address = plainSocket.getInetAddress(); + if (autoClose) { + // we don't need the plainSocket + plainSocket.close(); + } + // create and connect SSL socket, but don't do hostname/certificate verification yet + SSLCertificateSocketFactory sslSocketFactory = (SSLCertificateSocketFactory) SSLCertificateSocketFactory.getDefault(0); + SSLSocket ssl = (SSLSocket) sslSocketFactory.createSocket(address, port); + + // enable TLSv1.1/1.2 if available + ssl.setEnabledProtocols(ssl.getSupportedProtocols()); + + // set up SNI before the handshake + Log.i(TAG, "Setting SNI hostname"); + sslSocketFactory.setHostname(ssl, peerHost); + + // verify hostname and certificate + SSLSession session = ssl.getSession(); + + if (!hostnameVerifier.verify(peerHost, session)) + throw new SSLPeerUnverifiedException("Cannot verify hostname: " + peerHost); + + Log.i(TAG, "Established " + session.getProtocol() + " connection with " + session.getPeerHost() + + " using " + session.getCipherSuite()); + + return ssl; + } + } } diff --git a/app/src/main/java/ceui/lisa/http/Retro.java b/app/src/main/java/ceui/lisa/http/Retro.java index 03c4c1d7e..30f52eaeb 100644 --- a/app/src/main/java/ceui/lisa/http/Retro.java +++ b/app/src/main/java/ceui/lisa/http/Retro.java @@ -3,10 +3,6 @@ import android.util.Log; import com.blankj.utilcode.util.DeviceUtils; -import com.franmontiel.persistentcookiejar.ClearableCookieJar; -import com.franmontiel.persistentcookiejar.PersistentCookieJar; -import com.franmontiel.persistentcookiejar.cache.SetCookieCache; -import com.franmontiel.persistentcookiejar.persistence.SharedPrefsCookiePersistor; import com.google.gson.Gson; import com.google.gson.GsonBuilder; import com.safframework.http.interceptor.LoggingInterceptor; @@ -126,11 +122,6 @@ public static OkHttpClient.Builder getLogClient() { loggingInterceptor.setLevel(HttpLoggingInterceptor.Level.BODY); return new OkHttpClient.Builder() .addInterceptor(loggingInterceptor) - .cookieJar(cookieJar) .protocols(Collections.singletonList(Protocol.HTTP_1_1)); } - - public static ClearableCookieJar cookieJar = new PersistentCookieJar(new SetCookieCache(), - new SharedPrefsCookiePersistor(Shaft.getContext())); - } \ No newline at end of file diff --git a/app/src/main/res/layout/activity_login.xml b/app/src/main/res/layout/activity_login.xml index 0bfb33fe1..9fd42e470 100644 --- a/app/src/main/res/layout/activity_login.xml +++ b/app/src/main/res/layout/activity_login.xml @@ -71,6 +71,7 @@ android:layout_height="wrap_content" android:singleLine="true" android:ellipsize="end" + android:visibility="invisible" android:layout_marginStart="@dimen/tweenty_four_dp" android:layout_marginEnd="@dimen/tweenty_four_dp" android:hint="@string/string_9" @@ -86,6 +87,7 @@ android:singleLine="true" android:ellipsize="end" android:textSize="12sp" + android:visibility="invisible" android:maxLines="1" android:layout_marginStart="@dimen/tweenty_four_dp" android:layout_marginEnd="@dimen/tweenty_four_dp" @@ -101,6 +103,7 @@ android:id="@+id/show_pwd" android:layout_marginBottom="@dimen/sixteen_dp" android:layout_marginStart="18dp" + android:visibility="invisible" android:layout_height="wrap_content"> @@ -211,6 +214,7 @@ android:layout_height="wrap_content" android:layout_marginStart="@dimen/tweenty_four_dp" android:layout_marginTop="@dimen/sixteen_dp" + android:visibility="invisible" android:layout_marginEnd="@dimen/tweenty_four_dp" android:hint="@string/string_12" app:met_floatingLabel="highlight" diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 790ebcac3..3dcbc12f5 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -34,10 +34,10 @@ 打开图片 在 Google 中搜索此图片 已复制 - 登录 + 登录(需要开代理) 没有账号? 我要登录 - 注册账号 + 注册(需要开代理) users