Standalone Shonar Desktop: vendor portable sources + local engine; decouple from ~/Projects/Shonar
- shared/ = portable Android-origin sources vendored from deferred/desktop-server (app/build.gradle.kts srcDir repointed; PlaybackController.kt excluded as Android-only) - backend/ = bundled-lite engine (SQLite + inline queue); .venv symlinked from the old checkout, PYTHONPATH pins THIS backend's code over any editable install - repoRoot() resolves this project dir (env SHONAR_REPO still wins); desktop-dev.sh watches shared/ + backend/ - Verified: :app:compileKotlin + :app:test green (23 tests); engine boots on :8010, self-migrates, /healthz ok
This commit is contained in:
commit
76c867fca4
136 changed files with 21099 additions and 0 deletions
91
shared/com/shonar/provider/ShonarHandshake.kt
Normal file
91
shared/com/shonar/provider/ShonarHandshake.kt
Normal file
|
|
@ -0,0 +1,91 @@
|
|||
package com.shonar.provider
|
||||
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.withContext
|
||||
import okhttp3.OkHttpClient
|
||||
import okhttp3.Request
|
||||
import java.util.concurrent.TimeUnit
|
||||
import javax.net.ssl.SSLHandshakeException
|
||||
|
||||
/**
|
||||
* Unauthenticated handshake against the SHONAR backend's
|
||||
* GET /api/v1/provider-info. Used by the provider-selection screen and by
|
||||
* Start9/Umbrel platform probes to identify SHONAR-compatible services.
|
||||
*
|
||||
* TLS policy (docs/server-providers.md §4):
|
||||
* - full system verification by default, no bypass, ever.
|
||||
* - a handshake failure yields ProbeResult.TlsFailure with the peer cert's
|
||||
* SPKI SHA-256 (recorded by the TOFU trust manager) so the UI can run
|
||||
* explicit trust-on-first-use approval; this client NEVER retries with
|
||||
* verification disabled.
|
||||
*/
|
||||
class ShonarHandshake(
|
||||
private val client: OkHttpClient = defaultClient(),
|
||||
private val tofu: TofuManager? = null,
|
||||
) {
|
||||
|
||||
suspend fun probe(url: ServerUrl): ProbeResult = withContext(Dispatchers.IO) {
|
||||
val endpoint = url.origin + "/api/v1/provider-info"
|
||||
val request = Request.Builder().url(endpoint).get().build()
|
||||
try {
|
||||
client.newCall(request).execute().use { resp ->
|
||||
if (resp.code == 200) {
|
||||
parseBody(resp.body?.string().orEmpty())
|
||||
} else {
|
||||
ProbeResult.Incompatible
|
||||
}
|
||||
}
|
||||
} catch (e: SSLHandshakeException) {
|
||||
ProbeResult.TlsFailure(fingerprintFor(url.host))
|
||||
} catch (e: javax.net.ssl.SSLPeerUnverifiedException) {
|
||||
// Hostname mismatch: strict verifier stays strict, but the UI
|
||||
// should still say *why* instead of a generic network error.
|
||||
ProbeResult.TlsFailure(fingerprintFor(url.host))
|
||||
} catch (e: Exception) {
|
||||
// message must stay generic: exception text can contain URLs but
|
||||
// never credentials (this call sends no credentials at all)
|
||||
ProbeResult.NetworkError(e.javaClass.simpleName)
|
||||
}
|
||||
}
|
||||
|
||||
private fun parseBody(json: String): ProbeResult = try {
|
||||
val root = org.json.JSONObject(json)
|
||||
if (root.optString("kind") != "shonar") {
|
||||
ProbeResult.Incompatible
|
||||
} else {
|
||||
val caps = root.optJSONObject("capabilities") ?: org.json.JSONObject()
|
||||
val set = mutableSetOf<ProviderDescriptor.Capability>()
|
||||
if (caps.optBoolean("chunked_upload")) set += ProviderDescriptor.Capability.CHUNKED_UPLOAD
|
||||
if (caps.optBoolean("server_transcription")) set += ProviderDescriptor.Capability.SERVER_TRANSCRIPTION
|
||||
if (caps.optBoolean("server_summary")) set += ProviderDescriptor.Capability.SERVER_SUMMARY
|
||||
if (caps.optBoolean("account_deletion")) set += ProviderDescriptor.Capability.ACCOUNT_DELETION
|
||||
ProbeResult.Compatible(
|
||||
descriptor = ProviderDescriptor(
|
||||
id = ProviderRegistry.CUSTOM_SHONAR_ID,
|
||||
displayName = "Custom SHONAR server",
|
||||
capabilities = set,
|
||||
),
|
||||
serverName = root.optString("storage_backend", "server"),
|
||||
version = root.optString("version", "unknown"),
|
||||
)
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
ProbeResult.Incompatible
|
||||
}
|
||||
|
||||
/**
|
||||
* SPKI SHA-256 recorded by the TOFU trust manager during the failed
|
||||
* handshake, or "unknown" when nothing was captured (plain HTTP,
|
||||
* pre-handshake failure, or no TOFU manager wired).
|
||||
*/
|
||||
private fun fingerprintFor(host: String): String =
|
||||
tofu?.failureFor(host)?.spkiHex ?: "unknown"
|
||||
|
||||
companion object {
|
||||
fun defaultClient(): OkHttpClient = OkHttpClient.Builder()
|
||||
.connectTimeout(10, TimeUnit.SECONDS)
|
||||
.readTimeout(15, TimeUnit.SECONDS)
|
||||
.followRedirects(false) // do not silently follow redirects to other hosts
|
||||
.build()
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue