mirror of
https://github.com/MarekWo/mc-webui.git
synced 2026-10-30 23:51:57 +01:00
feat(android): sign in to servers that ask for a password
A reverse proxy can be told to require HTTP Basic authentication - an Access List in Nginx Proxy Manager, say - which is what makes publishing an instance to the internet reasonable. The wrapper had no answer for that: WebViewClient cancels the request by default, so the app showed nginx's bare 401 page and stopped there. Handle onReceivedHttpAuthRequest instead, asking for the username and password once and keeping them per host, so later launches go straight in. WebView only calls it after a 401 carrying WWW-Authenticate, so an instance that asks for no password never reaches any of this and behaves exactly as before. Credentials are stored before the server has accepted them; a wrong one simply comes back on the next 401 with the username filled in, since useHttpAuthUsernamePassword() tells a first ask from a retry. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0187QpThw1Y8n7aYDjHtMHax
This commit is contained in:
1 parent
e9029f02a4
commit
51f6674d6c
5 files changed
+156
-7
No files matched your search
@@ -11,8 +11,8 @@ android {
|
||||
applicationId = "it.wojtaszek.mc.wrapper"
|
||||
minSdk = 21
|
||||
targetSdk = 34
|
||||
versionCode = 3
|
||||
versionName = "1.2"
|
||||
versionCode = 4
|
||||
versionName = "1.3"
|
||||
}
|
||||
|
||||
buildTypes {
|
||||
|
||||
@@ -19,6 +19,7 @@ import android.os.Bundle
|
||||
import android.os.Environment
|
||||
import android.view.View
|
||||
import android.webkit.CookieManager
|
||||
import android.webkit.HttpAuthHandler
|
||||
import android.webkit.JavascriptInterface
|
||||
import android.webkit.PermissionRequest
|
||||
import android.webkit.SslErrorHandler
|
||||
@@ -207,6 +208,29 @@ class MainActivity : AppCompatActivity() {
|
||||
handler?.cancel()
|
||||
showConfig(getString(R.string.error_ssl))
|
||||
}
|
||||
|
||||
/**
|
||||
* Only ever called when the server answers 401 with a
|
||||
* `WWW-Authenticate` header - a server that asks for no password
|
||||
* never reaches this, so nothing changes for those users.
|
||||
*/
|
||||
override fun onReceivedHttpAuthRequest(
|
||||
view: WebView?,
|
||||
handler: HttpAuthHandler?,
|
||||
host: String?,
|
||||
realm: String?
|
||||
) {
|
||||
if (handler == null) return
|
||||
val saved = savedCredentials(host)
|
||||
// useHttpAuthUsernamePassword() is true only on the first ask for
|
||||
// a request; a second one means the server turned those exact
|
||||
// credentials down, so ask the user rather than retry forever
|
||||
if (handler.useHttpAuthUsernamePassword() && saved != null) {
|
||||
handler.proceed(saved.first, saved.second)
|
||||
return
|
||||
}
|
||||
askForCredentials(handler, host, saved?.first)
|
||||
}
|
||||
}
|
||||
|
||||
webView.webChromeClient = object : WebChromeClient() {
|
||||
@@ -264,6 +288,63 @@ class MainActivity : AppCompatActivity() {
|
||||
}
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------- http auth
|
||||
|
||||
/**
|
||||
* Asks for the username and password the server wants, then remembers them
|
||||
* so the next launch goes straight in.
|
||||
*
|
||||
* They are stored before the server has had a chance to accept them: a
|
||||
* wrong password simply comes back here on the next 401, with the username
|
||||
* filled in, and overwrites what was saved.
|
||||
*/
|
||||
private fun askForCredentials(handler: HttpAuthHandler, host: String?, knownUser: String?) {
|
||||
val form = layoutInflater.inflate(R.layout.dialog_auth, null)
|
||||
val userInput = form.findViewById<EditText>(R.id.authUser)
|
||||
val passwordInput = form.findViewById<EditText>(R.id.authPassword)
|
||||
form.findViewById<TextView>(R.id.authMessage).text =
|
||||
getString(R.string.auth_message, host ?: "")
|
||||
knownUser?.let { userInput.setText(it) }
|
||||
|
||||
val giveUp = {
|
||||
handler.cancel()
|
||||
showConfig(getString(R.string.error_auth_cancelled))
|
||||
}
|
||||
|
||||
AlertDialog.Builder(this)
|
||||
.setTitle(R.string.auth_title)
|
||||
.setView(form)
|
||||
.setPositiveButton(R.string.auth_sign_in) { _, _ ->
|
||||
val user = userInput.text.toString()
|
||||
val password = passwordInput.text.toString()
|
||||
saveCredentials(host, user, password)
|
||||
handler.proceed(user, password)
|
||||
}
|
||||
.setNegativeButton(R.string.cancel) { _, _ -> giveUp() }
|
||||
.setOnCancelListener { giveUp() }
|
||||
.show()
|
||||
}
|
||||
|
||||
/**
|
||||
* Credentials live in the app's own private preferences, keyed by host so
|
||||
* that switching servers does not carry them over. That file is readable
|
||||
* only by this app, which is as far as a wrapper can reasonably go.
|
||||
*/
|
||||
private fun savedCredentials(host: String?): Pair<String, String>? {
|
||||
if (host.isNullOrEmpty()) return null
|
||||
val user = prefs.getString(KEY_AUTH_USER + host, null) ?: return null
|
||||
val password = prefs.getString(KEY_AUTH_PASSWORD + host, null) ?: return null
|
||||
return user to password
|
||||
}
|
||||
|
||||
private fun saveCredentials(host: String?, user: String, password: String) {
|
||||
if (host.isNullOrEmpty()) return
|
||||
prefs.edit()
|
||||
.putString(KEY_AUTH_USER + host, user)
|
||||
.putString(KEY_AUTH_PASSWORD + host, password)
|
||||
.apply()
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------- notifications
|
||||
|
||||
/**
|
||||
@@ -471,6 +552,9 @@ class MainActivity : AppCompatActivity() {
|
||||
private const val PREFS = "MC_PREFS"
|
||||
private const val KEY_URL = "SERVER_URL"
|
||||
private const val KEY_ASKED_NOTIFICATIONS = "ASKED_NOTIFICATIONS"
|
||||
// Suffixed with the host, so each server keeps its own login
|
||||
private const val KEY_AUTH_USER = "AUTH_USER_"
|
||||
private const val KEY_AUTH_PASSWORD = "AUTH_PASSWORD_"
|
||||
private const val REQ_CAMERA = 1
|
||||
private const val REQ_STORAGE = 2
|
||||
private const val REQ_NOTIFICATIONS = 3
|
||||
|
||||
@@ -0,0 +1,36 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- Shown only when the server answers 401 and asks for a username/password -->
|
||||
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="vertical"
|
||||
android:paddingStart="24dp"
|
||||
android:paddingEnd="24dp"
|
||||
android:paddingTop="8dp"
|
||||
android:paddingBottom="8dp">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/authMessage"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:textSize="13sp"
|
||||
android:layout_marginBottom="16dp"/>
|
||||
|
||||
<EditText
|
||||
android:id="@+id/authUser"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:hint="@string/auth_user"
|
||||
android:inputType="text"
|
||||
android:autofillHints="username"
|
||||
android:layout_marginBottom="8dp"/>
|
||||
|
||||
<EditText
|
||||
android:id="@+id/authPassword"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:hint="@string/auth_password"
|
||||
android:inputType="textPassword"
|
||||
android:autofillHints="password"/>
|
||||
|
||||
</LinearLayout>
|
||||
@@ -14,9 +14,17 @@
|
||||
<string name="change_server">Change server</string>
|
||||
<string name="cancel">Cancel</string>
|
||||
|
||||
<!-- Sign-in, for servers that ask for a username and password -->
|
||||
<string name="auth_title">Sign in</string>
|
||||
<string name="auth_message">%1$s asks for a username and password.</string>
|
||||
<string name="auth_user">Username</string>
|
||||
<string name="auth_password">Password</string>
|
||||
<string name="auth_sign_in">SIGN IN</string>
|
||||
|
||||
<!-- Connection problems -->
|
||||
<string name="error_unreachable">Could not reach the server. Check the address, and that the phone is on the right network.</string>
|
||||
<string name="error_ssl">SSL error - the certificate was rejected. Use a valid HTTPS certificate, or plain http:// on a local network.</string>
|
||||
<string name="error_auth_cancelled">This server asks for a username and password. Connect again to enter them.</string>
|
||||
<string name="no_app_for_link">No app on the phone can open this link</string>
|
||||
|
||||
<!-- Downloads and permissions -->
|
||||
|
||||
Reference in new issue
Block a user