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 -->
|
||||
|
||||
+26
-5
@@ -124,6 +124,23 @@ wrong address, server down, phone off the network. Either way the form opens
|
||||
**pre-filled with the address you are using**, and nothing is overwritten until
|
||||
you tap **SAVE & CONNECT**: a dropped connection never costs you the address.
|
||||
|
||||
### Servers that ask for a password
|
||||
|
||||
If your instance sits behind a reverse proxy configured to require a login
|
||||
(HTTP Basic authentication — for example an **Access List** in Nginx Proxy
|
||||
Manager), the app asks for it the first time it connects, from version 1.3:
|
||||
|
||||
> **Sign in** — *mc-webui.example.com asks for a username and password.*
|
||||
|
||||
Enter the username and password your proxy expects and tap **SIGN IN**. They are
|
||||
remembered per server address, so later launches go straight in. Get them wrong
|
||||
and the same prompt comes back on the next attempt, with the username filled in.
|
||||
Tapping **Cancel** returns to the address form.
|
||||
|
||||
This only ever appears when the server actually asks for a password. On an
|
||||
instance without one — local `http://` or a proxy with no access control — you
|
||||
will never see this screen, and nothing about the app changes.
|
||||
|
||||
---
|
||||
|
||||
## What works, and what doesn't
|
||||
@@ -149,13 +166,17 @@ A couple of things depend on how your instance is reachable:
|
||||
- **Install the `.apk` only from this repository** (or from the project's
|
||||
GitHub Releases page). An `.apk` from anywhere else is a different app, no
|
||||
matter what it is called
|
||||
- The app stores exactly one thing on the phone: **the server address you
|
||||
typed**. No messages, keys, or contacts — those stay on your server and your
|
||||
MeshCore device
|
||||
- The app stores two things on the phone: **the server address you typed**, and
|
||||
— only if your server asks for a login — **the username and password for it**.
|
||||
Both live in the app's private storage, which no other app can read. No
|
||||
messages, keys, or contacts — those stay on your server and your MeshCore
|
||||
device
|
||||
- **Plain `http://` is allowed** so that local instances work without a
|
||||
certificate. On a home network that is fine; over the internet, put mc-webui
|
||||
behind a reverse proxy with HTTPS and some form of access control — the app
|
||||
adds no authentication of its own
|
||||
behind a reverse proxy with HTTPS and some form of access control. The app has
|
||||
no login of its own, but it does **support a proxy that requires one** (see
|
||||
*Servers that ask for a password* above) — and over `https://` those
|
||||
credentials are encrypted in transit like everything else
|
||||
- **Permissions:** internet access; the camera, only when you use QR scanning
|
||||
and only after you allow it; notifications, only after you turn them on in
|
||||
the menu and allow them; and file storage on Android 9 and older, only for
|
||||
|
||||
Reference in new issue
Block a user