Skip to content

Commit 97c67f9

Browse files
committed
feat: support both HTTP & HTTPS requests, persist keystore locally
1 parent f8dffba commit 97c67f9

5 files changed

Lines changed: 111 additions & 14 deletions

File tree

build.gradle.kts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,8 @@ dependencies {
5151
implementation("com.microsoft.sqlserver:mssql-jdbc:9.4.1.jre8")
5252
implementation("io.github.sakethpathike:kapsule:0.1.2")
5353
implementation("org.jetbrains:markdown:0.7.3")
54+
55+
implementation("io.ktor:ktor-network-tls-certificates-jvm")
5456
}
5557

5658
tasks.named<JavaExec>("run") {

src/main/kotlin/com/sakethh/linkora/Application.kt

Lines changed: 89 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -9,8 +9,8 @@ import com.sakethh.linkora.presentation.routing.configureRouting
99
import com.sakethh.linkora.presentation.routing.websocket.configureEventsWebSocket
1010
import com.sakethh.linkora.utils.SysEnvKey
1111
import com.sakethh.linkora.utils.useSysEnvValues
12-
import io.ktor.http.HttpHeaders
13-
import io.ktor.http.HttpMethod
12+
import io.ktor.http.*
13+
import io.ktor.network.tls.certificates.*
1414
import io.ktor.server.application.*
1515
import io.ktor.server.engine.*
1616
import io.ktor.server.netty.*
@@ -19,18 +19,42 @@ import io.ktor.server.websocket.*
1919
import kotlinx.serialization.encodeToString
2020
import kotlinx.serialization.json.Json
2121
import java.awt.Desktop
22+
import java.io.FileInputStream
23+
import java.net.Inet4Address
2224
import java.net.InetAddress
23-
import java.net.URI
2425
import java.nio.file.Files
2526
import java.nio.file.Paths
2627
import java.nio.file.StandardOpenOption
28+
import java.security.KeyStore
29+
import kotlin.io.path.exists
2730
import kotlin.time.Duration.Companion.seconds
2831

2932
fun main() {
3033
val serverConfig = ServerConfiguration.readConfig()
34+
val serverKeyStore = ServerConfiguration.createOrLoadServerKeystore(
35+
serverConfig
36+
)
37+
require(serverConfig.keyStorePassword != null && serverConfig.keyStorePassword.isNotBlank()) {
38+
"keyStorePassword value must be set in ServerConfig."
39+
}
3140
embeddedServer(
32-
Netty, port = serverConfig.serverPort, host = serverConfig.hostAddress, module = Application::module
33-
).start(wait = true)
41+
factory = Netty, configure = {
42+
sslConnector(builder = {
43+
this.port = serverConfig.httpsPort
44+
this.host = Inet4Address.getLocalHost().hostAddress
45+
enabledProtocols = listOf("TLSv1.3", "TLSv1.2")
46+
}, keyStore = serverKeyStore, keyAlias = Constants.KEY_STORE_ALIAS, keyStorePassword = {
47+
serverConfig.keyStorePassword.toCharArray()
48+
}, privateKeyPassword = {
49+
serverConfig.keyStorePassword.toCharArray()
50+
})
51+
52+
// for http connections
53+
connector {
54+
this.port = serverConfig.httpPort
55+
this.host = serverConfig.hostAddress
56+
}
57+
}, module = Application::module).start(wait = true)
3458
}
3559

3660
object ServerConfiguration {
@@ -73,7 +97,8 @@ object ServerConfiguration {
7397
databaseUrl = if (dataBaseUrl.endsWith("/linkora").not()) "$dataBaseUrl/linkora" else dataBaseUrl,
7498
databaseUser = dataBaseUserName,
7599
databasePassword = dataBasePassword,
76-
serverAuthToken = serverAuthToken
100+
serverAuthToken = serverAuthToken,
101+
keyStorePassword = ServerConfig.generateAToken()
77102
)
78103
val jsonConfigString = json.encodeToString(serverConfig)
79104
println(jsonConfigString)
@@ -85,6 +110,14 @@ object ServerConfiguration {
85110
}
86111
}
87112

113+
fun createConfig(serverConfig: ServerConfig) {
114+
require(serverConfig.keyStorePassword != null && serverConfig.keyStorePassword.isNotBlank()) {
115+
"keyStorePassword value must be set in ServerConfig."
116+
}
117+
val jsonConfigString = json.encodeToString(serverConfig)
118+
Files.writeString(configFilePath, jsonConfigString, StandardOpenOption.TRUNCATE_EXISTING)
119+
}
120+
88121
fun readConfig(): ServerConfig {
89122
return if (useSysEnvValues()) {
90123
ServerConfig(
@@ -97,19 +130,34 @@ object ServerConfiguration {
97130
} catch (_: Exception) {
98131
InetAddress.getLocalHost().hostAddress
99132
},
100-
serverPort = try {
133+
httpPort = try {
101134
System.getenv(SysEnvKey.LINKORA_SERVER_PORT.name).toInt()
102135
} catch (_: Exception) {
103136
45454
104137
},
105-
serverAuthToken = System.getenv(SysEnvKey.LINKORA_SERVER_AUTH_TOKEN.name)
138+
httpsPort = try {
139+
System.getenv(SysEnvKey.LINKORA_HTTPS_PORT.name).toInt()
140+
} catch (_: Exception) {
141+
54545
142+
},
143+
serverAuthToken = System.getenv(SysEnvKey.LINKORA_SERVER_AUTH_TOKEN.name),
144+
keyStorePassword = System.getenv(SysEnvKey.LINKORA_KEY_STORE_PASSWORD.name)
106145
)
107146
} else {
108147
createConfig(forceWrite = false)
109148
Files.readString(configFilePath).let {
110149
try {
111150
json.decodeFromString<ServerConfig>(it).let {
112-
it.copy(databaseUrl = "jdbc:" + it.databaseUrl)
151+
val newKeyPassword = ServerConfig.generateAToken()
152+
it.run {
153+
if (keyStorePassword == null) {
154+
createConfig(serverConfig = it.copy(keyStorePassword = newKeyPassword))
155+
}
156+
copy(
157+
databaseUrl = "jdbc:" + it.databaseUrl,
158+
keyStorePassword = keyStorePassword ?: newKeyPassword
159+
)
160+
}
113161
}
114162
} catch (_: Exception) {
115163
println("It seems you’ve manipulated `linkoraConfig.json` and messed things up a bit. No problemo, we’ll restart the configuration process to make sure things go smoothly.")
@@ -119,6 +167,37 @@ object ServerConfiguration {
119167
}
120168
}
121169
}
170+
171+
fun createOrLoadServerKeystore(serverConfig: ServerConfig, forceCreate: Boolean = false): KeyStore {
172+
require(serverConfig.keyStorePassword != null && serverConfig.keyStorePassword.isNotBlank()) {
173+
"keyStorePassword value must be set in ServerConfig."
174+
}
175+
val keyStore = buildKeyStore {
176+
this.certificate(
177+
alias = Constants.KEY_STORE_ALIAS, block = {
178+
this.password = serverConfig.keyStorePassword
179+
this.domains = listOf(Inet4Address.getLocalHost().hostAddress)
180+
this.ipAddresses = listOf(Inet4Address.getLocalHost())
181+
})
182+
}
183+
return jarDir.resolve("linkoraServerCert.jks").run {
184+
if (forceCreate.not() && exists()) {
185+
FileInputStream(toFile()).use { keyStoreFile ->
186+
KeyStore.getInstance(KeyStore.getDefaultType()).also {
187+
println("Loading existing keystore...")
188+
it.load(keyStoreFile, serverConfig.keyStorePassword.toCharArray())
189+
}
190+
}
191+
} else {
192+
keyStore.also {
193+
println("Creating new keystore...")
194+
it.saveToFile(
195+
output = toFile(), password = serverConfig.keyStorePassword
196+
)
197+
}
198+
}
199+
}
200+
}
122201
}
123202

124203
fun Application.module() {
@@ -142,8 +221,7 @@ fun Application.module() {
142221
maxFrameSize = Long.MAX_VALUE
143222
}
144223
configureEventsWebSocket()
145-
val serverConfiguredPage =
146-
"http://" + serverConfig.hostAddress + ":" + serverConfig.serverPort + "/" + Route.Sync.SERVER_IS_CONFIGURED.name
224+
"http://" + serverConfig.hostAddress + ":" + serverConfig.httpPort + "/" + Route.Sync.SERVER_IS_CONFIGURED.name
147225
if (useSysEnvValues().not() && Desktop.isDesktopSupported() && Desktop.getDesktop()
148226
.isSupported(Desktop.Action.BROWSE)
149227
) {

src/main/kotlin/com/sakethh/linkora/Constants.kt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,4 +2,5 @@ package com.sakethh.linkora
22

33
object Constants {
44
const val SERVER_VERSION = "0.1.0"
5+
const val KEY_STORE_ALIAS = "linkora-sync-server-cert"
56
}
Lines changed: 18 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
package com.sakethh.linkora.domain.model
22

3+
import kotlinx.serialization.SerialName
34
import kotlinx.serialization.Serializable
45
import java.net.InetAddress
56

@@ -9,6 +10,21 @@ data class ServerConfig(
910
val databaseUser: String = "database_user",
1011
val databasePassword: String = "database_password",
1112
val hostAddress: String = InetAddress.getLocalHost().hostAddress,
12-
val serverPort: Int = 45454,
13+
@SerialName("serverPort") val httpPort: Int = 45454,
14+
val httpsPort: Int = 54545,
1315
val serverAuthToken: String = "TOKEN",
14-
)
16+
val keyStorePassword: String? = null
17+
) {
18+
companion object {
19+
fun generateAToken(): String {
20+
return run {
21+
val chars = (0..9) + ('a'..'z') + ('A'..'Z') + listOf('!', '@', '#', '$', '%', '^', '&', '*')
22+
buildString {
23+
repeat(45) {
24+
append(chars.random())
25+
}
26+
}
27+
}
28+
}
29+
}
30+
}

src/main/kotlin/com/sakethh/linkora/utils/SysEnvKey.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,5 +2,5 @@ package com.sakethh.linkora.utils
22

33
enum class SysEnvKey {
44
LINKORA_SERVER_USE_ENV_VAL, LINKORA_DATABASE_URL, LINKORA_DATABASE_USER, LINKORA_DATABASE_PASSWORD, LINKORA_SERVER_AUTH_TOKEN,
5-
LINKORA_HOST_ADDRESS, LINKORA_SERVER_PORT
5+
LINKORA_HOST_ADDRESS, LINKORA_SERVER_PORT, LINKORA_HTTPS_PORT, LINKORA_KEY_STORE_PASSWORD
66
}

0 commit comments

Comments
 (0)