Compare commits
4
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6395e73927 | ||
|
|
0c337c9384 | ||
|
|
ac1f42b7d5 | ||
|
|
88591fca89 |
@@ -8,6 +8,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/), and this
|
||||
|
||||
### Fixed
|
||||
|
||||
- Android Dashboard sign-in removes pasted line breaks from username and password fields, matching the browser login while preserving every other credential character. (#541)
|
||||
- **Relay tool availability avoids repeated Windows loopback delays and preserves multi-PC capabilities.** Host-local Android, Desktop, and Phone paths use explicit IPv4 loopback, while Desktop checks share a bounded health snapshot that preserves per-client advertisements and fails closed when Hermes-Relay is unavailable. (#562, #563)
|
||||
- Android keeps saved Dashboard sign-ins bound to their connection when switching gateways, rather than letting a stale resolver route invalidate another connection's session.
|
||||
- Bot Mode no longer crashes when different connections have bots with the same profile name. Both the conversation list and Active Now strip preserve each bot's connection, and opening progress appears only on the selected bot.
|
||||
|
||||
@@ -1525,10 +1525,14 @@ class DashboardApiClient(
|
||||
password: String,
|
||||
next: String = "/",
|
||||
): Result<DashboardLoginResponse> = withContext(Dispatchers.IO) {
|
||||
// Match the Dashboard's single-line HTML username/password controls:
|
||||
// remove only forbidden line breaks and preserve every other code point.
|
||||
val normalizedUsername = username.replace("\r", "").replace("\n", "")
|
||||
val normalizedPassword = password.replace("\r", "").replace("\n", "")
|
||||
val payload = buildJsonObject {
|
||||
put("provider", provider)
|
||||
put("username", username)
|
||||
put("password", password)
|
||||
put("username", normalizedUsername)
|
||||
put("password", normalizedPassword)
|
||||
put("next", next)
|
||||
}
|
||||
val httpUrl = resolveUrl("/auth/password-login")
|
||||
|
||||
+35
@@ -12,6 +12,7 @@ import kotlinx.coroutines.withTimeout
|
||||
import kotlinx.serialization.json.JsonArray
|
||||
import kotlinx.serialization.json.Json
|
||||
import kotlinx.serialization.json.jsonObject
|
||||
import kotlinx.serialization.json.jsonPrimitive
|
||||
import okhttp3.HttpUrl.Companion.toHttpUrl
|
||||
import okhttp3.MediaType.Companion.toMediaType
|
||||
import okhttp3.ResponseBody.Companion.toResponseBody
|
||||
@@ -659,6 +660,40 @@ class DashboardApiClientTest {
|
||||
assertEquals("basic", session.provider)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun passwordLogin_stripsOnlyBrowserForbiddenLineBreaksFromCredentials() = runTest {
|
||||
val preservedCredential = " \t\u00A0påss\u200B "
|
||||
val cases = listOf(
|
||||
listOf("user", "line\rbreak", "user", "linebreak"),
|
||||
listOf("user", "line\nbreak", "user", "linebreak"),
|
||||
listOf("user", "line\r\nbreak", "user", "linebreak"),
|
||||
listOf("us\r\ner", "secret", "user", "secret"),
|
||||
listOf(
|
||||
preservedCredential,
|
||||
preservedCredential,
|
||||
preservedCredential,
|
||||
preservedCredential,
|
||||
),
|
||||
)
|
||||
repeat(cases.size) {
|
||||
server.enqueue(
|
||||
MockResponse()
|
||||
.setResponseCode(200)
|
||||
.setHeader("Content-Type", "application/json")
|
||||
.setBody("""{"ok": true, "next": "/"}"""),
|
||||
)
|
||||
}
|
||||
|
||||
val client = DashboardApiClient(baseUrl = server.url("/").toString())
|
||||
cases.forEach { (username, password, expectedUsername, expectedPassword) ->
|
||||
client.loginPassword(username = username, password = password).getOrThrow()
|
||||
|
||||
val body = Json.parseToJsonElement(server.takeRequest().body.readUtf8()).jsonObject
|
||||
assertEquals(expectedUsername, body["username"]?.jsonPrimitive?.content)
|
||||
assertEquals(expectedPassword, body["password"]?.jsonPrimitive?.content)
|
||||
}
|
||||
}
|
||||
|
||||
private fun storedCookie(
|
||||
name: String,
|
||||
value: String,
|
||||
|
||||
Reference in New Issue
Block a user