Version 1.3.8

This commit is contained in:
Privacy_Dragon 2025-01-23 20:13:39 +01:00
parent 2b634dcf75
commit e6899ed134
No known key found for this signature in database
GPG Key ID: B58E1E7FCA0E5F55
9 changed files with 242 additions and 77 deletions

View File

@ -9,8 +9,8 @@ android {
applicationId "nl.privacydragon.bookwyrm" applicationId "nl.privacydragon.bookwyrm"
minSdk 23 minSdk 23
targetSdk 34 targetSdk 34
versionCode 14 versionCode 15
versionName "1.3.7" versionName "1.3.8"
testInstrumentationRunner 'androidx.test.runner.AndroidJUnitRunner' testInstrumentationRunner 'androidx.test.runner.AndroidJUnitRunner'
} }
@ -43,5 +43,7 @@ dependencies {
androidTestImplementation 'androidx.test.espresso:espresso-core:3.6.1' androidTestImplementation 'androidx.test.espresso:espresso-core:3.6.1'
implementation 'com.google.zxing:core:3.5.3' implementation 'com.google.zxing:core:3.5.3'
implementation 'com.journeyapps:zxing-android-embedded:4.3.0@aar' implementation 'com.journeyapps:zxing-android-embedded:4.3.0@aar'
implementation 'org.conscrypt:conscrypt-android:2.5.2'
implementation 'com.squareup.okhttp3:okhttp:4.12.0'
} }

Binary file not shown.

Binary file not shown.

View File

@ -11,8 +11,8 @@
"type": "SINGLE", "type": "SINGLE",
"filters": [], "filters": [],
"attributes": [], "attributes": [],
"versionCode": 14, "versionCode": 15,
"versionName": "1.3.7", "versionName": "1.3.8",
"outputFile": "app-release.apk" "outputFile": "app-release.apk"
} }
], ],

View File

@ -10,11 +10,11 @@ import android.content.SharedPreferences;
import android.content.pm.PackageManager; import android.content.pm.PackageManager;
import android.graphics.Bitmap; import android.graphics.Bitmap;
import android.net.Uri; import android.net.Uri;
import android.os.Build;
import android.os.Bundle; import android.os.Bundle;
import android.util.Base64; import android.util.Base64;
import android.view.KeyEvent; import android.view.KeyEvent;
import android.view.View; import android.view.View;
import android.webkit.CookieManager;
import android.webkit.JavascriptInterface; import android.webkit.JavascriptInterface;
import android.webkit.ValueCallback; import android.webkit.ValueCallback;
import android.webkit.WebChromeClient; import android.webkit.WebChromeClient;
@ -33,17 +33,10 @@ import androidx.core.content.ContextCompat;
import com.journeyapps.barcodescanner.ScanContract; import com.journeyapps.barcodescanner.ScanContract;
import com.journeyapps.barcodescanner.ScanOptions; import com.journeyapps.barcodescanner.ScanOptions;
import java.io.BufferedInputStream; import org.conscrypt.Conscrypt;
import java.io.IOException; import java.io.IOException;
import java.io.InputStream;
import java.io.UnsupportedEncodingException; import java.io.UnsupportedEncodingException;
import java.net.CookieHandler;
import java.net.CookieManager;
import java.net.CookieStore;
import java.net.HttpCookie;
import java.net.HttpURLConnection;
import java.net.URI;
import java.net.URL;
import java.net.URLEncoder; import java.net.URLEncoder;
import java.nio.charset.StandardCharsets; import java.nio.charset.StandardCharsets;
import java.security.InvalidAlgorithmParameterException; import java.security.InvalidAlgorithmParameterException;
@ -52,10 +45,9 @@ import java.security.Key;
import java.security.KeyStore; import java.security.KeyStore;
import java.security.KeyStoreException; import java.security.KeyStoreException;
import java.security.NoSuchAlgorithmException; import java.security.NoSuchAlgorithmException;
import java.security.Security;
import java.security.UnrecoverableKeyException; import java.security.UnrecoverableKeyException;
import java.security.cert.CertificateException; import java.security.cert.CertificateException;
import java.util.List;
import java.util.Objects;
import javax.crypto.BadPaddingException; import javax.crypto.BadPaddingException;
import javax.crypto.Cipher; import javax.crypto.Cipher;
@ -63,10 +55,20 @@ import javax.crypto.IllegalBlockSizeException;
import javax.crypto.NoSuchPaddingException; import javax.crypto.NoSuchPaddingException;
import javax.crypto.spec.GCMParameterSpec; import javax.crypto.spec.GCMParameterSpec;
import okhttp3.FormBody;
import okhttp3.Headers;
import okhttp3.Interceptor;
import okhttp3.OkHttpClient;
import okhttp3.Request;
import okhttp3.RequestBody;
import okhttp3.Response;
public class StartActivity extends AppCompatActivity { public class StartActivity extends AppCompatActivity {
WebView myWebView; WebView myWebView;
ProgressBar LoadIndicator; ProgressBar LoadIndicator;
public ValueCallback<Uri[]> omhooglader; public ValueCallback<Uri[]> omhooglader;
String putje = "";
String sessie = "";
@SuppressLint("SetJavaScriptEnabled") @SuppressLint("SetJavaScriptEnabled")
@Override @Override
protected void onCreate(Bundle savedInstanceState) { protected void onCreate(Bundle savedInstanceState) {
@ -89,6 +91,7 @@ public class StartActivity extends AppCompatActivity {
}); });
myWebView = (WebView) findViewById(R.id.webview); myWebView = (WebView) findViewById(R.id.webview);
myWebView.setVisibility(View.GONE); myWebView.setVisibility(View.GONE);
myWebView.getSettings().setUserAgentString(getString(R.string.gebruikersagent));
myWebView.getSettings().setJavaScriptEnabled(true); myWebView.getSettings().setJavaScriptEnabled(true);
myWebView.addJavascriptInterface(new Object() myWebView.addJavascriptInterface(new Object()
{ {
@ -236,11 +239,32 @@ public class StartActivity extends AppCompatActivity {
// LoadIndicator.setVisibility(View.VISIBLE); // LoadIndicator.setVisibility(View.VISIBLE);
// android.webkit.CookieManager oven = android.webkit.CookieManager.getInstance(); // android.webkit.CookieManager oven = android.webkit.CookieManager.getInstance();
//myWebView.loadUrl("javascript:this.document.location.href = 'source://' + encodeURI(document.documentElement.outerHTML);"); //myWebView.loadUrl("javascript:this.document.location.href = 'source://' + encodeURI(document.documentElement.outerHTML);");
//try {
//See if we are already logged in.
CookieManager oven = CookieManager.getInstance();
String koek = oven.getCookie("https://" + server);
if (koek != null) {
if (koek.indexOf("sessionid") != -1) {
myWebView.loadUrl("https://" + server);
} else {
//This should get the login page, retreive the csrf-middlewaretoken, and then log the user in using a POST-request.
try { try {
getMiddleWareTokenAndLogIn(server, name, passw); //This should get the login page, retreive the csrf-middlewaretoken, and then log the user in using a POST-request. getMiddleWareTokenAndLogIn(server, name, passw);
} catch (IOException e) { } catch (IOException e) {
throw new RuntimeException(e); throw new RuntimeException(e);
} }
}
} else {
//This should get the login page, retreive the csrf-middlewaretoken, and then log the user in using a POST-request.
try {
getMiddleWareTokenAndLogIn(server, name, passw);
} catch (IOException e) {
throw new RuntimeException(e);
}
}
//} catch (IOException e) {
// throw new RuntimeException(e);
//}
} }
@ -254,79 +278,115 @@ public class StartActivity extends AppCompatActivity {
public void run() { public void run() {
try { try {
//Load the login page, and do not forget to take some cookies. //Load the login page, and do not forget to take some cookies.
URL url = new URL("https://" + server + "/login"); Security.insertProviderAt(Conscrypt.newProvider(), 1);
CookieManager koekManager = new CookieManager(); //URL url = new URL("https://" + server + "/");
CookieHandler.setDefault(koekManager); String speculaas = "";
CookieStore bakker = koekManager.getCookieStore(); String speculaasBeslag = "";
HttpURLConnection urlConnection = (HttpURLConnection) url.openConnection(); //The login page loading is done using OkHttpClient.
try { OkHttpClient client = new OkHttpClient();
//Get the input stream, and move it all into a byte array. Request aanvraag = new Request.Builder()
InputStream ina = new BufferedInputStream(urlConnection.getInputStream()); .url("https://" + server + "/")
byte[] pagina = null; .header("User-Agent", getString(R.string.gebruikersagent))
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { .build();
pagina = ina.readAllBytes(); //Get an answer!
} else { try (Response antwoord = client.newCall(aanvraag).execute()) {
//I truly hope that this byte array will always be big enough... if (!antwoord.isSuccessful()) throw new IOException("Unexpected code " + antwoord);
//The Tiramisu+ way is much better... //Search the headers for the 'set-cookie' header so we can eat a cookie!
pagina = new byte[30000]; Headers cenna = antwoord.headers();
ina.read(pagina); for (int i = 0; i < cenna.size(); i++) {
if (cenna.name(i).equals("set-cookie")) {
speculaas = cenna.value(i);
speculaasBeslag = speculaas.split(";")[0];
} }
try {
ina.close();
} catch (IOException e) {
throw new RuntimeException(e);
} }
//And now create a string out of the byte array, so we can retreive the middleware token. //And then get the HTML body.
String zooi = new String(pagina); assert antwoord.body() != null;
String zooi = antwoord.body().string();
// CookieManager koekManager = new CookieManager();
// CookieHandler.setDefault(koekManager);
// CookieStore bakker = koekManager.getCookieStore();
// HttpURLConnection urlConnection = (HttpURLConnection) url.openConnection();
// try {
// InputStream ina = new BufferedInputStream(urlConnection.getInputStream());
// byte[] pagina = null;
// if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
// pagina = ina.readAllBytes();
// } else {
// //I truly hope that this byte array will always be big enough...
// //The Tiramisu+ way is much better...
// pagina = new byte[30000];
// ina.read(pagina);
// }
// try {
// ina.close();
// } catch (IOException e) {
// throw new RuntimeException(e);
// }
// //We should not forget closing the connection we used for hearing what csrf cookie and token we needed.
// urlConnection.disconnect();
// //And now create a string out of the byte array, so we can retreive the middleware token.
// String zooi = new String(pagina);
//Very easy to get the token by taking the text that it is preceded by in the raw html as the regex for a split() function! //Very easy to get the token by taking the text that it is preceded by in the raw html as the regex for a split() function!
String[] opgebroken = zooi.split("name=\"csrfmiddlewaretoken\" value=\""); String[] opgebroken = zooi.split("name=\"csrfmiddlewaretoken\" value=\"");
//For that gives as second element the token, followed by all the following html code. Then strip that code off, using the immediately following characters as regex. //For that gives as second element the token, followed by all the following html code. Then strip that code off, using the immediately following characters as regex.
String[] breukjes = opgebroken[1].split("\">"); String[] breukjes = opgebroken[1].split("\">");
//Of course, the token is then the first element in our array. //Of course, the token is then the first element in our array.
String token = breukjes[0]; String token = breukjes[0];
//Log.d("botbreuk", token);
String gegevens = null; String gegevens = null;
//Initiate some strings to use for the delicious csrf cookie. //Initiate some strings to use for the delicious csrf cookie.
String speculaas = "", THT = ""; //String speculaas = "", THT = "";
//How to get the cookies? First get the cookie collection, the cookie box so to say, and then... // //How to get the cookies? First get the cookie collection, the cookie box so to say, and then...
List<HttpCookie> koektrommel = bakker.get(URI.create("https://" + server)); // List<HttpCookie> koektrommel = bakker.get(URI.create("https://" + server));
//Log.d("koek", koektrommel.toString()); // Log.d("koek", koektrommel.toString());
//... for every cookie in it check to see if it is the csrftoken named cookie. // //... for every cookie in it check to see if it is the csrftoken named cookie.
for (int i = 0; i < koektrommel.size(); ++i) { // for (int i = 0; i < koektrommel.size(); ++i) {
HttpCookie koekje = koektrommel.get(i); // HttpCookie koekje = koektrommel.get(i);
if (Objects.equals(koekje.getName(), "csrftoken")) { // if (Objects.equals(koekje.getName(), "csrftoken")) {
//If it is the csrftoken cookie, get the value of it, and the expiration date of it. // //If it is the csrftoken cookie, get the value of it, and the expiration date of it.
speculaas = koekje.toString(); // speculaas = koekje.toString();
THT = String.valueOf(koekje.getMaxAge()); // THT = String.valueOf(koekje.getMaxAge());
//Log.d("domein", koekje.getDomain()); // //Log.d("domein", koekje.getDomain());
} // }
} // }
//And then set the data string up for use in the POST request, with the csrf middleware token, the username, and the password. //And then set the data string up for use in the POST request, with the csrf middleware token, the username, and the password.
try { try {
gegevens = "csrfmiddlewaretoken=" + URLEncoder.encode(token, "UTF-8") + "&localname=" + URLEncoder.encode(name, "UTF-8") + "&password=" + URLEncoder.encode(passw, "UTF-8"); gegevens = "csrfmiddlewaretoken=" + URLEncoder.encode(token, "UTF-8") + "&localname=" + URLEncoder.encode(name, "UTF-8") + "&password=" + URLEncoder.encode(passw, "UTF-8");
} catch (UnsupportedEncodingException e) { } catch (UnsupportedEncodingException e) {
throw new RuntimeException(e); throw new RuntimeException(e);
} }
RequestBody keurslijf = new FormBody.Builder()
.add("csrfmiddlewaretoken", token)
.add("localname", name)
.add("password", passw)
.build();
String finalGegevens = gegevens; String finalGegevens = gegevens;
//Log.d("token", speculaas); //Log.d("gegevens", finalGegevens);
//Log.d("beslag", speculaasBeslag);
String finalSpeculaas = speculaas; String finalSpeculaas = speculaas;
String finalTHT = THT; //String finalTHT = THT;
logInAndGetHTML(server, keurslijf, speculaasBeslag);
//Then we have to run a bit of code on the main (UI) thread. To be able to work with the webview... //Then we have to run a bit of code on the main (UI) thread. To be able to work with the webview...
runOnUiThread(new Runnable() { runOnUiThread(new Runnable() {
@Override @Override
public void run() { public void run() {
//First we have to get the cookie manager of the webview, so we can hand it the csrf cookie. //First we have to get the cookie manager of the webview, so we can hand it the csrf cookie.
//Without being fed the correct csrf cookie, the Wyrm will refuse our request. The wyrm is a very picky eater! //Without being fed the correct csrf cookie, the Wyrm will refuse our request. The wyrm is a very picky eater!
android.webkit.CookieManager oven = android.webkit.CookieManager.getInstance(); CookieManager oven = CookieManager.getInstance();
//Bake the cookie into the webview. //Bake the cookie into the webview.
oven.setCookie("https://" + server, finalSpeculaas + "; Max-Age=" + finalTHT + "; Path=/; SameSite=Lax; Secure"); oven.setCookie("https://" + server, finalSpeculaas);
//And bake the session cookie as well.
oven.setCookie("https://" + server, sessie);
//And then finally it is time to send a POST request from the webview to log in. //And then finally it is time to send a POST request from the webview to log in.
myWebView.postUrl("https://" + server + "/login?next=/", finalGegevens.getBytes()); //myWebView.postUrl("https://" + server + "/login?next=/", finalGegevens.getBytes());
myWebView.loadDataWithBaseURL("https://" + server, putje, null, null, "https://" + server + "/login");
} }
}); });
} finally { } finally {
//We should not forget closing the connection we used for hearing what csrf cookie and token we needed. // //We should not forget closing the connection we used for hearing what csrf cookie and token we needed.
urlConnection.disconnect(); // urlConnection.disconnect();
} }
} catch (Exception e) { } catch (Exception e) {
throw new RuntimeException(e); throw new RuntimeException(e);
@ -336,7 +396,75 @@ public class StartActivity extends AppCompatActivity {
//^Here ends all that new Thread() code. //^Here ends all that new Thread() code.
//Run all the code in the thread. //Run all the code in the thread.
draadje.start(); draadje.start();
//return token; }
public void logInAndGetHTML(String server, RequestBody lichaam, String speculoos) throws IOException {
// Thread kabel = new Thread(new Runnable() {
// @Override
// public void run() {
// try {
// //Load the login page, and do not forget to take some cookies.
Security.insertProviderAt(Conscrypt.newProvider(), 1);
//Create a client using CookieMonster, so we can retrieve cookies from the redirect.
OkHttpClient client = new OkHttpClient.Builder()
.addNetworkInterceptor(new CookieMonster())
.build();
//URL url = new URL("https://" + server + "/");
//CookieManager koekManager = new CookieManager();
//CookieHandler.setDefault(koekManager);
//CookieStore bakker = koekManager.getCookieStore();
Request verzoek = new Request.Builder()
.url("https://" + server + "/login?next=/")
.header("User-Agent", getString(R.string.gebruikersagent))
.addHeader("origin", "https://" + server)
.addHeader("cookie", speculoos)
.post(lichaam)
.build();
try (Response reactie = client.newCall(verzoek).execute()) {
if (!reactie.isSuccessful())
throw new IOException("Unexpected code " + reactie);
assert reactie.body() != null;
putje = reactie.body().string();
}
// HttpURLConnection urlConnection = (HttpURLConnection) url.openConnection();
// urlConnection.setRequestProperty("origin", "https://" + server);
// byte[] paarden = gegevens.getBytes();
// try {
// urlConnection.setDoOutput(true);
// urlConnection.setChunkedStreamingMode(0);
//
// OutputStream out = new BufferedOutputStream(urlConnection.getOutputStream());
// out.write(paarden);
// out.flush();
//
// InputStream in = new BufferedInputStream(urlConnection.getInputStream());
// byte[] pagina = null;
// if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
// pagina = in.readAllBytes();
// } else {
// //I truly hope that this byte array will always be big enough...
// //The Tiramisu+ way is much better...
// pagina = new byte[30000];
// in.read(pagina);
// }
// try {
// in.close();
// } catch (IOException e) {
// throw new RuntimeException(e);
// }
// html[0] = new String(pagina);
// } finally {
// urlConnection.disconnect();
// }
// } catch (Exception e) {
// throw new RuntimeException(e);
// }
// }
// });
// kabel.start();
//Log.d("lichaam", putje);
//return putje;
} }
private final ActivityResultLauncher<ScanOptions> barcodeLanceerder = registerForActivityResult(new ScanContract(), private final ActivityResultLauncher<ScanOptions> barcodeLanceerder = registerForActivityResult(new ScanContract(),
result -> { result -> {
@ -394,6 +522,33 @@ public class StartActivity extends AppCompatActivity {
} }
return super.onKeyLongPress(keyCode, event); return super.onKeyLongPress(keyCode, event);
} }
final class CookieMonster implements Interceptor {
@Override public Response intercept(Interceptor.Chain chain) throws IOException {
//Om ingelogd te blijven moeten we het sessiekoekje aan kunnen bieden.
//Die moeten we dan wel eerst uit het koekblik pakken!
Request eersteVerzoek = chain.request();
//Eerst moeten we controleren of er al een sessiekoekje is. Als dat niet zo is, dan is dit het echte eerste verzoek.
if (sessie.isEmpty()) {
//In dat geval halen we de reactie op om het koekje te kunnen pakken!
Response eersteReactie = chain.proceed(chain.request());
Headers hoofden = eersteReactie.headers();
for (int i = 0; i < hoofden.size(); i++) {
if (hoofden.name(i).equals("set-cookie") && hoofden.value(i).startsWith("session")) {
sessie = hoofden.value(i);
}
}
//Nadat we het koekje hebben moet de reactie doorgebriefd worden aan de 'client',
//die dan het volgende verzoek zal gaan doen vanwege de 302-redirect bij het inloggen.
return eersteReactie;
}
//Het koekje is er! Hoera!
//Het nieuwe verzoek moet wel met het sessiekoekje verzonden worden, anders zijn we alsnog niet ingelogd!
Request nieuwVerzoek = eersteVerzoek.newBuilder()
.addHeader("cookie", sessie)
.build();
return chain.proceed(nieuwVerzoek);
}
}
//Here is code to make sure that links of the bookwyrm server are handled within the webview client, instead of having it open in the default browser. //Here is code to make sure that links of the bookwyrm server are handled within the webview client, instead of having it open in the default browser.
//Yes, I used the web for this too. //Yes, I used the web for this too.
private class MyWebViewClient extends WebViewClient { private class MyWebViewClient extends WebViewClient {
@ -415,6 +570,8 @@ public class StartActivity extends AppCompatActivity {
@Override @Override
public void onPageStarted(WebView view, String url, Bitmap favicon) { public void onPageStarted(WebView view, String url, Bitmap favicon) {
LoadIndicator.setVisibility(View.VISIBLE); LoadIndicator.setVisibility(View.VISIBLE);
//CookieManager oven = CookieManager.getInstance();
//Log.d("oven", oven.getCookie(url));
} }
} }
} }

View File

@ -4,4 +4,5 @@
<string name="name">blup</string> <string name="name">blup</string>
<string name="pw">gloep</string> <string name="pw">gloep</string>
<string name="q">wheeeee</string> <string name="q">wheeeee</string>
<string name="gebruikersagent">Bookwyrm Android/1.3.8</string>
</resources> </resources>

View File

@ -1,7 +1,7 @@
// Top-level build file where you can add configuration options common to all sub-projects/modules. // Top-level build file where you can add configuration options common to all sub-projects/modules.
plugins { plugins {
id 'com.android.application' version '8.7.3' apply false id 'com.android.application' version '8.8.0' apply false
id 'com.android.library' version '8.7.3' apply false id 'com.android.library' version '8.8.0' apply false
} }
task clean(type: Delete) { task clean(type: Delete) {

View File

@ -0,0 +1,5 @@
Fixing what turned out not to work for unreleased version 1.3.7:
* Significant log-in improvements, including better security.
Further changes:
* The app now uses a custom User-Agent string.
* Two new dependencies have been added: okhttp3, and conscrypt.

View File

@ -1,6 +1,6 @@
#Mon Feb 14 18:09:26 CET 2022 #Mon Feb 14 18:09:26 CET 2022
distributionBase=GRADLE_USER_HOME distributionBase=GRADLE_USER_HOME
distributionUrl=https\://services.gradle.org/distributions/gradle-8.9-bin.zip distributionUrl=https\://services.gradle.org/distributions/gradle-8.10.2-bin.zip
distributionPath=wrapper/dists distributionPath=wrapper/dists
zipStorePath=wrapper/dists zipStorePath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME zipStoreBase=GRADLE_USER_HOME