Skip to content

Commit 0238d80

Browse files
committed
feat: internalize android native https runtime
1 parent f878396 commit 0238d80

18 files changed

Lines changed: 984 additions & 320 deletions

File tree

.github/workflows/yogurt-dev-release.yml

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -78,13 +78,15 @@ jobs:
7878
- name: Build Yogurt for ${{ matrix.target }}
7979
run: ./gradlew :yogurt:linkReleaseExecutable${{ matrix.gradle-target }} --no-daemon
8080

81-
- name: Bundle Android curl runtime
81+
- name: Bundle Android CA bundle
8282
if: matrix.target == 'androidNativeArm64'
8383
shell: bash
8484
run: |
8585
set -euo pipefail
86-
chmod +x scripts/prepare-android-curl.sh
87-
scripts/prepare-android-curl.sh yogurt/build/bin/${{ matrix.target }}/releaseExecutable
86+
./gradlew :android-https-native:prepareAndroidNativeCaBundle --no-daemon
87+
install -m 644 \
88+
android-https-native/build/generated/ca/cacert.pem \
89+
yogurt/build/bin/${{ matrix.target }}/releaseExecutable/cacert.pem
8890
8991
- name: Upload ${{ matrix.platform }} artifact
9092
uses: actions/upload-artifact@v4

acidify-core/build.gradle.kts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -66,7 +66,8 @@ kotlin {
6666
linuxMain.dependencies {
6767
implementation(libs.ktor.client.curl)
6868
}
69-
androidNativeArm64Main.dependencies {
69+
findByName("androidNativeArm64Main")?.dependencies {
70+
implementation(project(":android-https-native"))
7071
implementation(libs.ktor.client.cio)
7172
}
7273
all {
Lines changed: 17 additions & 171 deletions
Original file line numberDiff line numberDiff line change
@@ -1,22 +1,6 @@
1-
@file:OptIn(kotlinx.cinterop.ExperimentalForeignApi::class)
2-
31
package org.ntqqrev.acidify.internal.util
42

5-
import kotlinx.cinterop.*
6-
import platform.posix.X_OK
7-
import platform.posix.access
8-
import platform.posix.errno
9-
import platform.posix.fclose
10-
import platform.posix.fopen
11-
import platform.posix.fread
12-
import platform.posix.fwrite
13-
import platform.posix.getenv
14-
import platform.posix.readlink
15-
import platform.posix.remove
16-
import platform.posix.stat
17-
import platform.posix.strerror
18-
import platform.posix.system
19-
import kotlin.random.Random
3+
import org.ntqqrev.androidhttps.executeTextRequest
204

215
internal actual fun platformCurlTextRequestOrNull(
226
method: String,
@@ -27,158 +11,20 @@ internal actual fun platformCurlTextRequestOrNull(
2711
followRedirects: Boolean,
2812
proxy: String?,
2913
): PlatformCurlTextResponse? {
30-
val curlBinary = discoverCurlBinary()
31-
val stdoutPath = createTempPath("curl-stdout")
32-
val stderrPath = createTempPath("curl-stderr")
33-
val headerPath = createTempPath("curl-headers")
34-
val bodyPath = createTempPath("curl-body")
35-
val inputPath = body?.let {
36-
createTempPath("curl-input").also { path -> writeTextFile(path, body) }
37-
}
38-
return try {
39-
val args = buildList {
40-
add(curlBinary)
41-
add("--silent")
42-
add("--show-error")
43-
add("--http1.1")
44-
add("--request")
45-
add(method)
46-
add("--dump-header")
47-
add(headerPath)
48-
add("--output")
49-
add(bodyPath)
50-
add("--write-out")
51-
add("%{http_code}")
52-
if (followRedirects) {
53-
add("--location")
54-
}
55-
if (!proxy.isNullOrBlank()) {
56-
add("--proxy")
57-
add(proxy)
58-
}
59-
if (!contentType.isNullOrBlank()) {
60-
add("--header")
61-
add("Content-Type: $contentType")
62-
}
63-
headers.forEach { (key, value) ->
64-
add("--header")
65-
add("$key: $value")
66-
}
67-
if (inputPath != null) {
68-
add("--data-binary")
69-
add("@$inputPath")
70-
}
71-
add(url)
72-
}
73-
val status = decodePosixStatus(system(buildRedirectedCommand(args, stdoutPath, stderrPath)))
74-
val stdout = readTextFile(stdoutPath).trim()
75-
val stderr = readTextFile(stderrPath).trim()
76-
if (status != 0) {
77-
val message = stderr.ifBlank { "curl exited with code $status" }
78-
throw IllegalStateException(message)
79-
}
80-
PlatformCurlTextResponse(
81-
statusCode = stdout.toIntOrNull() ?: -1,
82-
headers = parseHeaders(readTextFile(headerPath)),
83-
body = readTextFile(bodyPath),
84-
)
85-
} finally {
86-
listOf(stdoutPath, stderrPath, headerPath, bodyPath, inputPath)
87-
.filterNotNull()
88-
.forEach { path -> remove(path) }
89-
}
90-
}
91-
92-
private fun discoverCurlBinary(): String {
93-
getenv("YOGURT_CURL_PATH")?.toKString()?.takeIf { access(it, X_OK) == 0 }?.let { return it }
94-
getenv("ACIDIFY_CURL_PATH")?.toKString()?.takeIf { access(it, X_OK) == 0 }?.let { return it }
95-
96-
currentProgramDirectory()?.let { programDir ->
97-
val candidate = "$programDir/curl"
98-
if (access(candidate, X_OK) == 0) {
99-
return candidate
100-
}
101-
}
102-
103-
if (access("./curl", X_OK) == 0) {
104-
return "./curl"
105-
}
106-
if (access("/system/bin/curl", X_OK) == 0) {
107-
return "/system/bin/curl"
108-
}
109-
return "curl"
110-
}
111-
112-
private fun currentProgramDirectory(): String? = memScoped {
113-
val bufferSize = 4096
114-
val buffer = allocArray<ByteVar>(bufferSize)
115-
val length = readlink("/proc/self/exe", buffer, (bufferSize - 1).convert())
116-
if (length <= 0) return@memScoped null
117-
buffer[length] = 0
118-
buffer.toKString().substringBeforeLast('/', "").ifBlank { null }
119-
}
120-
121-
private fun createTempPath(kind: String): String =
122-
"/data/local/tmp/acidify-$kind-${Random.nextLong().toULong().toString(16)}.tmp"
123-
124-
private fun buildRedirectedCommand(args: List<String>, stdoutPath: String, stderrPath: String): String =
125-
buildString {
126-
append(args.joinToString(" ") { quotePosixArgument(it) })
127-
append(" > ")
128-
append(quotePosixArgument(stdoutPath))
129-
append(" 2> ")
130-
append(quotePosixArgument(stderrPath))
131-
}
132-
133-
private fun quotePosixArgument(argument: String): String =
134-
"'" + argument.replace("'", "'\"'\"'") + "'"
135-
136-
private fun decodePosixStatus(status: Int): Int = when {
137-
status < 0 -> -1
138-
(status and 0x7f) == 0 -> (status ushr 8) and 0xff
139-
(status and 0x7f) != 0x7f -> 128 + (status and 0x7f)
140-
else -> -1
141-
}
142-
143-
private fun writeTextFile(path: String, text: String) {
144-
val file = fopen(path, "wb") ?: error(strerror(errno)?.toKString() ?: "Failed to open $path")
145-
try {
146-
val bytes = text.encodeToByteArray()
147-
if (bytes.isNotEmpty()) {
148-
bytes.usePinned {
149-
fwrite(it.addressOf(0), 1.convert(), bytes.size.convert(), file)
150-
}
151-
}
152-
} finally {
153-
fclose(file)
154-
}
155-
}
156-
157-
private fun readTextFile(path: String): String = memScoped {
158-
val st = alloc<stat>()
159-
if (platform.posix.stat(path, st.ptr) != 0) return@memScoped ""
160-
val size = st.st_size.toInt()
161-
if (size <= 0) return@memScoped ""
162-
val file = fopen(path, "rb") ?: return@memScoped ""
163-
try {
164-
val bytes = ByteArray(size)
165-
bytes.usePinned {
166-
fread(it.addressOf(0), 1.convert(), bytes.size.convert(), file)
167-
}
168-
bytes.decodeToString()
169-
} finally {
170-
fclose(file)
171-
}
172-
}
173-
174-
private fun parseHeaders(rawHeaders: String): Map<String, List<String>> {
175-
val result = linkedMapOf<String, MutableList<String>>()
176-
rawHeaders.lineSequence().forEach { line ->
177-
val separatorIndex = line.indexOf(':')
178-
if (separatorIndex <= 0) return@forEach
179-
val key = line.substring(0, separatorIndex).trim().lowercase()
180-
val value = line.substring(separatorIndex + 1).trim()
181-
result.getOrPut(key) { mutableListOf() }.add(value)
182-
}
183-
return result
14+
if (!proxy.isNullOrBlank()) {
15+
return null
16+
}
17+
val response = executeTextRequest(
18+
method = method,
19+
url = url,
20+
headers = headers,
21+
body = body,
22+
contentType = contentType,
23+
followRedirects = followRedirects,
24+
)
25+
return PlatformCurlTextResponse(
26+
statusCode = response.statusCode,
27+
headers = response.headers,
28+
body = response.body,
29+
)
18430
}

0 commit comments

Comments
 (0)