____ _ _______ __
/ __ \ | /| / / __/ |/ /
/ /_/ / |/ |/ / _// /
\____/|__/|__/___/_||_/
owenclave / app/src/main/java/io/nekohasekai/sagernet/fmt/juicity/JuicityFmt.kt
/******************************************************************************
* *
* Copyright (C) 2024 dyhkwong *
* *
* This program is free software: you can redistribute it and/or modify *
* it under the terms of the GNU General Public License as published by *
* the Free Software Foundation, either version 3 of the License, or *
* (at your option) any later version. *
* *
* This program is distributed in the hope that it will be useful, *
* but WITHOUT ANY WARRANTY; without even the implied warranty of *
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the *
* GNU General Public License for more details. *
* *
* You should have received a copy of the GNU General Public License *
* along with this program. If not, see . *
* *
******************************************************************************/
package io.nekohasekai.sagernet.fmt.juicity
import io.nekohasekai.sagernet.ktx.*
import libexclavecore.Libexclavecore
import java.util.Base64
fun parseJuicity(url: String): JuicityBean {
val link = Libexclavecore.parseURL(url)
return JuicityBean().apply {
name = link.fragment
serverAddress = link.host.ifEmpty { error("empty host") }
serverPort = link.port
uuid = link.username
password = link.password
link.queryParameter("sni")?.also {
sni = it
}
link.queryParameter("allow_insecure")?.takeIf { it == "1" }?.also {
allowInsecure = true
}
link.queryParameter("pinned_certchain_sha256")?.also {
pinnedPeerCertificateChainSha256 = when {
it.length == 64 -> {
Base64.getUrlEncoder().encodeToString(it.chunked(2).map { it.toInt(16).toByte() }.toByteArray())
}
else -> {
it.replace('/', '_').replace('+', '-')
}
}
// match Juicity's behavior
// https://github.com/juicity/juicity/blob/412dbe43e091788c5464eb2d6e9c169bdf39f19c/cmd/client/run.go#L97
allowInsecure = true
}
}
}
fun JuicityBean.toUri(): String? {
val builder = Libexclavecore.newURL("juicity").apply {
setHostPort(serverAddress.ifEmpty { error("empty server address") }, serverPort)
username = uuid.ifEmpty { error("empty uuid") }
if (name.isNotEmpty()) {
fragment = name
}
}
if (password.isNotEmpty()) {
builder.password = password
}
builder.addQueryParameter("congestion_control", "bbr")
if (sni.isNotEmpty()) {
builder.addQueryParameter("sni", sni)
}
if (pinnedPeerCertificateChainSha256.isNotEmpty()) {
// https://github.com/juicity/juicity/blob/412dbe43e091788c5464eb2d6e9c169bdf39f19c/cmd/client/run.go#L87-L96
// it actually supports Base64 URL-safe encoding with padding, Base64 standard encoding with padding and Hex encoding
val certChainHash = pinnedPeerCertificateChainSha256.listByLineOrComma()[0].replace(":", "")
builder.addQueryParameter("pinned_certchain_sha256", when {
certChainHash.length == 64 -> {
Base64.getUrlEncoder().encodeToString(certChainHash.chunked(2).map { it.toInt(16).toByte() }.toByteArray())
}
else -> {
certChainHash.replace('/', '_').replace('+', '-')
}
})
}
// as `pinnedPeerCertificate(PublicKey)Sha256` is not exportable,
// only add `allow_insecure=1` if `pinnedPeerCertificate(PublicKey)Sha256` is not used
if (pinnedPeerCertificateChainSha256.isNotEmpty() ||
(allowInsecure && pinnedPeerCertificateSha256.isEmpty() &&
pinnedPeerCertificatePublicKeySha256.isEmpty() && serverNameToVerify.listByLineOrComma().isEmpty())
) {
builder.addQueryParameter("allow_insecure", "1")
}
return builder.string
}