feat(files): веб-сессия Nextcloud для .whiteboard в WebView
Доска (whiteboard) открывается через веб-viewer, которому мало Basic-auth: Vue-XHR ходят по session-cookie+CSRF. Теперь перед загрузкой web-файла логинимся формой /login app-паролем (NextcloudWebSession, обходит 2FA), переносим cookie в CookieManager и грузим страницу нативно — для web-файлов отключено OkHttp-проксирование (оно теряло тело POST/PROPFIND → «нет файлов»). Collabora-путь не тронут (WOPI-токен самодостаточен). +NetworkFactory.newBaseBuilder — билдер на общем пуле без auth.
This commit is contained in:
@@ -33,6 +33,14 @@ object NetworkFactory {
|
||||
clients.clear() // пересобрать производные клиенты уже с кэшем
|
||||
}
|
||||
|
||||
/** Билдер на основе базового клиента (пул/кэш) c опциональным trust-all TLS, без auth. */
|
||||
fun newBaseBuilder(trustAllCerts: Boolean = false): OkHttpClient.Builder =
|
||||
base.newBuilder()
|
||||
.callTimeout(30, TimeUnit.SECONDS)
|
||||
.connectTimeout(20, TimeUnit.SECONDS)
|
||||
.readTimeout(30, TimeUnit.SECONDS)
|
||||
.applyUnsafeSslIfNeeded(trustAllCerts)
|
||||
|
||||
fun newAuthedClient(
|
||||
username: String,
|
||||
appPassword: String,
|
||||
|
||||
@@ -0,0 +1,82 @@
|
||||
package ru.forbion.f7cloud.core.network
|
||||
|
||||
import okhttp3.Cookie
|
||||
import okhttp3.CookieJar
|
||||
import okhttp3.FormBody
|
||||
import okhttp3.HttpUrl
|
||||
import okhttp3.HttpUrl.Companion.toHttpUrl
|
||||
import okhttp3.OkHttpClient
|
||||
import okhttp3.Request
|
||||
import java.util.concurrent.ConcurrentHashMap
|
||||
|
||||
/**
|
||||
* Устанавливает НАСТОЯЩУЮ веб-сессию Nextcloud (cookie + CSRF) по форме `/login`, используя
|
||||
* app-пароль как пароль (он валиден для входа и обходит 2FA). Нужно для встраивания веб-viewer'ов
|
||||
* (доска Whiteboard и т.п.), которым мало Basic-auth: их Vue-XHR ходят по сессии.
|
||||
*/
|
||||
object NextcloudWebSession {
|
||||
private val requestTokenRegex = Regex(
|
||||
"""(?:name=["']requesttoken["']\s+value|<meta\s+name=["']requesttoken["']\s+content)=["']([^"']+)["']""",
|
||||
RegexOption.IGNORE_CASE,
|
||||
)
|
||||
|
||||
/** Cookie, собранные при логине (для переноса в WebView CookieManager). */
|
||||
fun login(
|
||||
serverUrl: String,
|
||||
username: String,
|
||||
appPassword: String,
|
||||
trustAllCerts: Boolean = false,
|
||||
): List<Cookie> {
|
||||
val base = serverUrl.trimEnd('/')
|
||||
val host = base.toHttpUrl().host
|
||||
val jar = MemoryCookieJar()
|
||||
val client = NetworkFactory.newBaseBuilder(trustAllCerts)
|
||||
.cookieJar(jar)
|
||||
.followRedirects(true)
|
||||
.followSslRedirects(true)
|
||||
.build()
|
||||
|
||||
// 1. GET страницы логина — начальные cookie + requesttoken.
|
||||
val loginHtml = client.newCall(
|
||||
Request.Builder().url("$base/login").header("User-Agent", USER_AGENT).get().build(),
|
||||
).execute().use { it.body?.string().orEmpty() }
|
||||
val token = requestTokenRegex.find(loginHtml)?.groupValues?.getOrNull(1).orEmpty()
|
||||
|
||||
// 2. POST формы логина app-паролем — сервер ставит session-cookie.
|
||||
val form = FormBody.Builder()
|
||||
.add("user", username)
|
||||
.add("password", appPassword)
|
||||
.apply { if (token.isNotBlank()) add("requesttoken", token) }
|
||||
.add("remember_login", "1")
|
||||
.add("timezone", "UTC")
|
||||
.add("timezone_offset", "0")
|
||||
.build()
|
||||
client.newCall(
|
||||
Request.Builder()
|
||||
.url("$base/login")
|
||||
.header("User-Agent", USER_AGENT)
|
||||
.apply { if (token.isNotBlank()) header("requesttoken", token) }
|
||||
.header("OCS-APIRequest", "true")
|
||||
.post(form)
|
||||
.build(),
|
||||
).execute().use { /* cookie осели в jar */ }
|
||||
|
||||
return jar.cookiesForHost(host)
|
||||
}
|
||||
|
||||
private const val USER_AGENT = "Mozilla/5.0 (Android) F7cloud-Mobile"
|
||||
|
||||
private class MemoryCookieJar : CookieJar {
|
||||
private val store = ConcurrentHashMap<String, MutableList<Cookie>>()
|
||||
override fun saveFromResponse(url: HttpUrl, cookies: List<Cookie>) {
|
||||
val list = store.getOrPut(url.host) { mutableListOf() }
|
||||
cookies.forEach { c ->
|
||||
list.removeAll { it.name == c.name }
|
||||
list += c
|
||||
}
|
||||
}
|
||||
override fun loadForRequest(url: HttpUrl): List<Cookie> =
|
||||
store[url.host].orEmpty().filter { it.matches(url) }
|
||||
fun cookiesForHost(host: String): List<Cookie> = store[host].orEmpty().toList()
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user