Skip to content

Commit f434acd

Browse files
authored
Strip DNS label automatically (#254)
1 parent 45d2adf commit f434acd

5 files changed

Lines changed: 194 additions & 14 deletions

File tree

src/commonMain/kotlin/me/devnatan/dockerkt/io/Http.kt

Lines changed: 1 addition & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -96,17 +96,12 @@ private fun HttpClientConfig<*>.configure(client: DockerClient) {
9696
}
9797
}
9898

99-
@OptIn(ExperimentalStdlibApi::class)
10099
private fun createUrlBuilder(socketPath: String): URLBuilder =
101100
if (isUnixSocket(socketPath)) {
102101
URLBuilder(
103102
protocol = URLProtocol.HTTP,
104103
port = DockerSocketPort,
105-
host =
106-
socketPath
107-
.substringAfter(UnixSocketPrefix)
108-
.encodeToByteArray()
109-
.toHexString() + EncodedHostnameSuffix,
104+
host = encodeSocketPathHostname(socketPath.substringAfter(UnixSocketPrefix)),
110105
)
111106
} else {
112107
val url = Url(socketPath)

src/commonMain/kotlin/me/devnatan/dockerkt/io/Sockets.kt

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ package me.devnatan.dockerkt.io
55
import kotlin.jvm.JvmName
66

77
internal const val EncodedHostnameSuffix = ".socket"
8+
internal const val MaxDnsLabelLength = 63
89

910
internal const val DockerSocketPort = 2375
1011
internal const val UnixSocketPrefix = "unix://"
@@ -17,3 +18,19 @@ public const val DefaultDockerUnixSocket: String = "$UnixSocketPrefix/var/run/do
1718
public const val DefaultDockerHttpSocket: String = "${HttpSocketPrefix}localhost:$DockerSocketPort"
1819

1920
internal fun isUnixSocket(input: String): Boolean = input.startsWith(UnixSocketPrefix)
21+
22+
@OptIn(ExperimentalStdlibApi::class)
23+
internal fun encodeSocketPathHostname(socketPath: String): String =
24+
socketPath
25+
.encodeToByteArray()
26+
.toHexString()
27+
.chunked(MaxDnsLabelLength)
28+
.joinToString(".") + EncodedHostnameSuffix
29+
30+
@OptIn(ExperimentalStdlibApi::class)
31+
internal fun decodeSocketPathHostname(hostname: String): String =
32+
hostname
33+
.substring(0, hostname.indexOf(EncodedHostnameSuffix))
34+
.replace(".", "")
35+
.hexToByteArray()
36+
.decodeToString()
Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
package me.devnatan.dockerkt.io
2+
3+
import kotlin.test.Test
4+
import kotlin.test.assertEquals
5+
import kotlin.test.assertTrue
6+
7+
class SocketsTest {
8+
@Test
9+
fun `encode and decode short socket path`() {
10+
val path = "/var/run/docker.sock"
11+
val encoded = encodeSocketPathHostname(path)
12+
val decoded = decodeSocketPathHostname(encoded)
13+
assertEquals(path, decoded)
14+
}
15+
16+
@Test
17+
fun `encode and decode long socket path`() {
18+
val path = "/Users/invoked/.docker/run/docker.sock"
19+
val encoded = encodeSocketPathHostname(path)
20+
val decoded = decodeSocketPathHostname(encoded)
21+
assertEquals(path, decoded)
22+
}
23+
24+
@Test
25+
fun `encoded hostname ends with socket suffix`() {
26+
val encoded = encodeSocketPathHostname("/var/run/docker.sock")
27+
assertTrue(encoded.endsWith(EncodedHostnameSuffix))
28+
}
29+
30+
@Test
31+
fun `all dns labels are within max length`() {
32+
val path = "/Users/invoked/.docker/run/docker.sock"
33+
val encoded = encodeSocketPathHostname(path)
34+
val labels = encoded.removeSuffix(EncodedHostnameSuffix).split(".")
35+
for (label in labels) {
36+
assertTrue(
37+
label.length <= MaxDnsLabelLength,
38+
"Label '$label' exceeds max DNS label length of $MaxDnsLabelLength (was ${label.length})",
39+
)
40+
}
41+
}
42+
43+
@Test
44+
fun `very long path produces valid dns labels`() {
45+
val path = "/very/long/path/to/some/deeply/nested/docker/socket/directory/structure/docker.sock"
46+
val encoded = encodeSocketPathHostname(path)
47+
val labels = encoded.removeSuffix(EncodedHostnameSuffix).split(".")
48+
for (label in labels) {
49+
assertTrue(
50+
label.length <= MaxDnsLabelLength,
51+
"Label '$label' exceeds max DNS label length of $MaxDnsLabelLength (was ${label.length})",
52+
)
53+
}
54+
assertEquals(path, decodeSocketPathHostname(encoded))
55+
}
56+
57+
@Test
58+
fun `path shorter than label limit produces single label`() {
59+
// "/a.sock" = 7 bytes = 14 hex chars, well under 63
60+
val path = "/a.sock"
61+
val encoded = encodeSocketPathHostname(path)
62+
val labels = encoded.removeSuffix(EncodedHostnameSuffix).split(".")
63+
assertEquals(1, labels.size)
64+
}
65+
66+
@Test
67+
fun `isUnixSocket returns true for unix prefix`() {
68+
assertTrue(isUnixSocket("unix:///var/run/docker.sock"))
69+
}
70+
71+
@Test
72+
fun `isUnixSocket returns false for tcp prefix`() {
73+
assertTrue(!isUnixSocket("tcp://localhost:2375"))
74+
}
75+
}

src/jvmMain/kotlin/me/devnatan/dockerkt/io/Sockets.jvm.kt

Lines changed: 1 addition & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -24,18 +24,11 @@ internal class SocketDns(
2424
}
2525

2626
internal class UnixSocketFactory : AFUNIXSocketFactory() {
27-
@OptIn(ExperimentalStdlibApi::class)
28-
private fun decodeHostname(hostname: String): String =
29-
hostname
30-
.substring(0, hostname.indexOf(EncodedHostnameSuffix))
31-
.hexToByteArray()
32-
.decodeToString()
33-
3427
override fun addressFromHost(
3528
host: String,
3629
port: Int,
3730
): AFUNIXSocketAddress {
38-
val socketPath = decodeHostname(host)
31+
val socketPath = decodeSocketPathHostname(host)
3932
val socketFile = Paths.get(socketPath) ?: error("Unable to connect to unix socket @ $socketPath")
4033

4134
if (!Files.exists(socketFile) || !Files.isWritable(socketFile)) {
Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,100 @@
1+
package me.devnatan.dockerkt.io
2+
3+
import okhttp3.HttpUrl
4+
import okhttp3.HttpUrl.Companion.toHttpUrl
5+
import okhttp3.OkHttpClient
6+
import okhttp3.Request
7+
import kotlin.test.Test
8+
import kotlin.test.assertEquals
9+
import kotlin.test.assertNotNull
10+
11+
class SocketHostnameOkHttpTest {
12+
private fun buildOkHttpUrl(socketPath: String): HttpUrl {
13+
val hostname = encodeSocketPathHostname(socketPath)
14+
return "http://$hostname:$DockerSocketPort/v1.41/version".toHttpUrl()
15+
}
16+
17+
@Test
18+
fun `OkHttp accepts encoded default socket path`() {
19+
val url = buildOkHttpUrl("/var/run/docker.sock")
20+
assertNotNull(url)
21+
assertEquals(DockerSocketPort, url.port)
22+
}
23+
24+
@Test
25+
fun `OkHttp accepts encoded long socket path from issue 253`() {
26+
val url = buildOkHttpUrl("/Users/invoked/.docker/run/docker.sock")
27+
assertNotNull(url)
28+
assertEquals(DockerSocketPort, url.port)
29+
}
30+
31+
@Test
32+
fun `OkHttp accepts encoded very long socket path`() {
33+
val url =
34+
buildOkHttpUrl(
35+
"/very/long/path/to/some/deeply/nested/docker/socket/directory/structure/docker.sock",
36+
)
37+
assertNotNull(url)
38+
assertEquals(DockerSocketPort, url.port)
39+
}
40+
41+
@Test
42+
fun `OkHttp builds valid request with encoded hostname`() {
43+
val hostname = encodeSocketPathHostname("/Users/invoked/.docker/run/docker.sock")
44+
val url = "http://$hostname:$DockerSocketPort/v1.41/version".toHttpUrl()
45+
val request = Request.Builder().url(url).build()
46+
assertEquals("GET", request.method)
47+
assertEquals("/v1.41/version", request.url.encodedPath)
48+
}
49+
50+
@Test
51+
fun `SocketDns resolves encoded hostname for unix socket`() {
52+
val hostname = encodeSocketPathHostname("/Users/invoked/.docker/run/docker.sock")
53+
val dns = SocketDns(isUnixSocket = true)
54+
val addresses = dns.lookup(hostname)
55+
assertEquals(1, addresses.size)
56+
assertEquals(hostname, addresses[0].hostName)
57+
}
58+
59+
@Test
60+
fun `UnixSocketFactory decodes chunked hostname back to original path`() {
61+
val originalPath = "/Users/invoked/.docker/run/docker.sock"
62+
val hostname = encodeSocketPathHostname(originalPath)
63+
val decoded = decodeSocketPathHostname(hostname)
64+
assertEquals(originalPath, decoded)
65+
}
66+
67+
@Test
68+
fun `OkHttpClient can be configured with SocketDns and encoded hostname`() {
69+
val hostname = encodeSocketPathHostname("/Users/invoked/.docker/run/docker.sock")
70+
val client =
71+
OkHttpClient
72+
.Builder()
73+
.dns(SocketDns(isUnixSocket = true))
74+
.build()
75+
76+
val url = "http://$hostname:$DockerSocketPort/v1.41/version".toHttpUrl()
77+
val request = Request.Builder().url(url).build()
78+
val call = client.newCall(request)
79+
assertNotNull(call)
80+
}
81+
82+
@Test
83+
fun `encoded hostname roundtrips through OkHttp URL parsing`() {
84+
val paths =
85+
listOf(
86+
"/var/run/docker.sock",
87+
"/Users/invoked/.docker/run/docker.sock",
88+
"/home/user/.local/share/docker/run/docker.sock",
89+
"/very/long/path/to/some/deeply/nested/docker/socket/directory/structure/docker.sock",
90+
)
91+
92+
for (path in paths) {
93+
val hostname = encodeSocketPathHostname(path)
94+
val url = "http://$hostname:$DockerSocketPort/v1.41/version".toHttpUrl()
95+
val parsedHost = url.host
96+
val decoded = decodeSocketPathHostname(parsedHost)
97+
assertEquals(path, decoded, "Roundtrip failed for path: $path")
98+
}
99+
}
100+
}

0 commit comments

Comments
 (0)