Skip to content

Commit 14af502

Browse files
committed
chore: add proper cli
1 parent 6ca61f7 commit 14af502

File tree

10 files changed

+325
-5
lines changed

10 files changed

+325
-5
lines changed

build.gradle.kts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,7 @@ plugins {
5050
apply(plugin = "kotlinx-atomicfu")
5151

5252
val JAVA_VERSION = JavaVersion.VERSION_17
53-
val VERSION = Version(1, 0, 0, 0, ReleaseType.Beta)
53+
val VERSION = Version(1, 0, 0, 0, ReleaseType.None)
5454
val COMMIT_HASH by lazy {
5555
val cmd = "git rev-parse --short HEAD".split("\\s".toRegex())
5656
val proc = ProcessBuilder(cmd)
@@ -164,6 +164,9 @@ dependencies {
164164
// Clikt (for CLI)
165165
implementation("com.github.ajalt.clikt:clikt:3.4.2")
166166

167+
// Argon2
168+
implementation("de.mkammerer:argon2-jvm:2.11")
169+
167170
// Testing utilities
168171
testImplementation("io.kotest:kotest-runner-junit5")
169172
testImplementation("io.kotest:kotest-assertions-core")

src/main/kotlin/dev/floofy/hazel/Main.kt

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,9 +17,11 @@
1717

1818
package dev.floofy.hazel
1919

20+
import dev.floofy.hazel.cli.HazelCli
21+
2022
object Main {
2123
@JvmStatic
2224
fun main(args: Array<String>) {
23-
Bootstrap.bootstrap(null)
25+
HazelCli.main(args)
2426
}
2527
}

src/main/kotlin/dev/floofy/hazel/cli/HazelCli.kt

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,3 +16,50 @@
1616
*/
1717

1818
package dev.floofy.hazel.cli
19+
20+
import com.github.ajalt.clikt.core.CliktCommand
21+
import com.github.ajalt.clikt.core.ProgramResult
22+
import com.github.ajalt.clikt.core.subcommands
23+
import com.github.ajalt.clikt.parameters.options.option
24+
import com.github.ajalt.clikt.parameters.options.versionOption
25+
import dev.floofy.hazel.Bootstrap
26+
import dev.floofy.hazel.HazelInfo
27+
import dev.floofy.hazel.cli.commands.GenerateCommand
28+
import dev.floofy.hazel.cli.commands.PingCommand
29+
import dev.floofy.hazel.cli.commands.keystore.KeystoreCommand
30+
31+
object HazelCli: CliktCommand(
32+
name = "hazel",
33+
invokeWithoutSubcommand = true,
34+
allowMultipleSubcommands = true,
35+
help = """
36+
|Hazel is a simple, reliable Content Delivery Network (CDN) microservice to handle
37+
|your needs without doing anything to your data.
38+
|
39+
|This command line is the command line utility to manage the keystore for user persistence,
40+
|secret key storage, and managing the Hazel server.
41+
""".trimMargin()
42+
) {
43+
private val configPath: String? by option(
44+
"-c", "--config",
45+
help = "The configuration path to run the server.",
46+
envvar = "HAZEL_CONFIG_PATH"
47+
)
48+
49+
init {
50+
versionOption("v${HazelInfo.version} (${HazelInfo.commitHash} - ${HazelInfo.buildDate})", names = setOf("--version", "-v"))
51+
subcommands(
52+
PingCommand,
53+
GenerateCommand,
54+
KeystoreCommand
55+
)
56+
}
57+
58+
override fun run() {
59+
// If we haven't invoked a subcommand with `hazel ...`, the server will run.
60+
if (currentContext.invokedSubcommand == null) {
61+
Bootstrap.bootstrap(configPath)
62+
throw ProgramResult(0)
63+
}
64+
}
65+
}

src/main/kotlin/dev/floofy/hazel/cli/abstractions/BaseKeystoreCommand.kt

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,3 +16,45 @@
1616
*/
1717

1818
package dev.floofy.hazel.cli.abstractions
19+
20+
import com.github.ajalt.clikt.core.CliktCommand
21+
import com.github.ajalt.clikt.parameters.options.option
22+
import com.github.ajalt.clikt.parameters.options.required
23+
import dev.floofy.hazel.core.KeystoreWrapper
24+
import dev.floofy.hazel.data.KeystoreConfig
25+
26+
abstract class BaseKeystoreCommand(
27+
name: String? = null,
28+
help: String = ""
29+
): CliktCommand(name = name, help = help) {
30+
private val keystorePath: String by option(
31+
"--path", "--ks-path", "--keystore-path", "-p",
32+
help = "The keystore path to consume."
33+
).required()
34+
35+
private val password: String? by option(
36+
"--password", "--ks-pwd", "--pwd",
37+
help = "The keystore password to use to unlock it."
38+
)
39+
40+
fun wrapper(): KeystoreWrapper {
41+
val cwd = System.getProperty("user.dir", "")
42+
val home = System.getProperty("user.home", "/")
43+
44+
val path = when {
45+
keystorePath.startsWith("~/") -> home + keystorePath.substring(1)
46+
keystorePath.startsWith("./") -> cwd + keystorePath.substring(1)
47+
else -> keystorePath
48+
}
49+
50+
val wrapper = KeystoreWrapper(
51+
KeystoreConfig(
52+
path,
53+
password
54+
)
55+
)
56+
57+
wrapper.initUnsafe()
58+
return wrapper
59+
}
60+
}

src/main/kotlin/dev/floofy/hazel/cli/commands/GenerateCommand.kt

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,3 +16,46 @@
1616
*/
1717

1818
package dev.floofy.hazel.cli.commands
19+
20+
import com.github.ajalt.clikt.core.CliktCommand
21+
import com.github.ajalt.clikt.parameters.arguments.argument
22+
import com.github.ajalt.clikt.parameters.types.file
23+
import java.nio.file.Files
24+
import java.nio.file.Paths
25+
26+
object GenerateCommand: CliktCommand(
27+
name = "generate",
28+
help = """
29+
|The "generate" subcommand generates a configuration file that can be executed to run the server.
30+
|
31+
|The `dest` argument is the source to put the file in. The file cannot be a directory since Hazel
32+
|loads it and runs it.
33+
""".trimMargin()
34+
) {
35+
private val dest by argument(
36+
"dest",
37+
help = "The destination of the file to output in"
38+
).file(mustExist = false, canBeFile = true, canBeDir = false)
39+
40+
override fun run() {
41+
if (dest.exists()) {
42+
println("[hazel:generate] File ${dest.path} already exists.")
43+
return
44+
}
45+
46+
println("[hazel:generate] Writing output to ${dest.path}...")
47+
val contents = """
48+
|[keystore]
49+
|file = "./data/keystore.jks"
50+
|
51+
|[storage]
52+
|class = "fs"
53+
|
54+
|[storage.fs]
55+
|directory = "./data/hazel"
56+
""".trimMargin()
57+
58+
Files.write(Paths.get(dest.toURI()), contents.toByteArray())
59+
println("[hazel:generate] Generated a default configuration file in ${dest.path}!")
60+
}
61+
}

src/main/kotlin/dev/floofy/hazel/cli/commands/PingCommand.kt

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,3 +16,42 @@
1616
*/
1717

1818
package dev.floofy.hazel.cli.commands
19+
20+
import com.github.ajalt.clikt.core.CliktCommand
21+
import com.github.ajalt.clikt.core.CliktError
22+
import com.github.ajalt.clikt.parameters.arguments.argument
23+
import io.ktor.client.*
24+
import io.ktor.client.engine.okhttp.*
25+
import io.ktor.client.request.*
26+
import io.ktor.client.statement.*
27+
import io.ktor.http.*
28+
import kotlinx.coroutines.runBlocking
29+
30+
object PingCommand: CliktCommand(
31+
name = "ping",
32+
help = "Pings the server with the [URL] provided."
33+
) {
34+
private val url: String by argument(
35+
"url",
36+
help = "The URL of the server to hit"
37+
)
38+
39+
override fun run() {
40+
val client = HttpClient(OkHttp)
41+
val res = runBlocking {
42+
client.get("$url/heartbeat")
43+
}
44+
45+
if (!res.status.isSuccess()) {
46+
throw CliktError("Couldn't reach server at $url, is it open?")
47+
}
48+
49+
val body = runBlocking {
50+
res.bodyAsText()
51+
}
52+
53+
if (body != "OK") {
54+
throw CliktError("Server running at $url is not a Hazel server! (GET /heartbeat = OK)")
55+
}
56+
}
57+
}
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
/*
2+
* 🪶 hazel: Minimal, simple, and open source content delivery network made in Kotlin
3+
* Copyright 2022 Noel <[email protected]>
4+
*
5+
* Licensed under the Apache License, Version 2.0 (the "License");
6+
* you may not use this file except in compliance with the License.
7+
* You may obtain a copy of the License at
8+
*
9+
* http://www.apache.org/licenses/LICENSE-2.0
10+
*
11+
* Unless required by applicable law or agreed to in writing, software
12+
* distributed under the License is distributed on an "AS IS" BASIS,
13+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14+
* See the License for the specific language governing permissions and
15+
* limitations under the License.
16+
*/
17+
18+
package dev.floofy.hazel.cli.commands.keystore
19+
20+
import com.github.ajalt.clikt.core.CliktCommand
21+
22+
object AddUserCommand: CliktCommand(
23+
name = "add-user",
24+
help = "Adds a user to the keystore."
25+
) {
26+
override fun run() {
27+
println("soon:tm:")
28+
}
29+
}
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
/*
2+
* 🪶 hazel: Minimal, simple, and open source content delivery network made in Kotlin
3+
* Copyright 2022 Noel <[email protected]>
4+
*
5+
* Licensed under the Apache License, Version 2.0 (the "License");
6+
* you may not use this file except in compliance with the License.
7+
* You may obtain a copy of the License at
8+
*
9+
* http://www.apache.org/licenses/LICENSE-2.0
10+
*
11+
* Unless required by applicable law or agreed to in writing, software
12+
* distributed under the License is distributed on an "AS IS" BASIS,
13+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14+
* See the License for the specific language governing permissions and
15+
* limitations under the License.
16+
*/
17+
18+
package dev.floofy.hazel.cli.commands.keystore
19+
20+
import com.github.ajalt.clikt.core.CliktCommand
21+
import com.github.ajalt.clikt.core.PrintHelpMessage
22+
import com.github.ajalt.clikt.core.subcommands
23+
24+
object KeystoreCommand: CliktCommand(
25+
name = "keystore",
26+
invokeWithoutSubcommand = true,
27+
help = """
28+
|The `keystore` command is the main management utility for managing Hazel's keystore.
29+
|
30+
|Hazel uses a Java keystore to handle secret key management, SSL certificates for the server,
31+
|and user persistence without bringing in an external database.
32+
|
33+
|To generate a keystore, you can run `hazel keystore generate` to create the keystore in the path
34+
|and you can load it using the `keystore.path` configuration key.
35+
""".trimMargin()
36+
) {
37+
init {
38+
subcommands(ListKeysCommand)
39+
}
40+
41+
override fun run() {
42+
if (currentContext.invokedSubcommand == null)
43+
throw PrintHelpMessage(this, false)
44+
}
45+
}
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
/*
2+
* 🪶 hazel: Minimal, simple, and open source content delivery network made in Kotlin
3+
* Copyright 2022 Noel <[email protected]>
4+
*
5+
* Licensed under the Apache License, Version 2.0 (the "License");
6+
* you may not use this file except in compliance with the License.
7+
* You may obtain a copy of the License at
8+
*
9+
* http://www.apache.org/licenses/LICENSE-2.0
10+
*
11+
* Unless required by applicable law or agreed to in writing, software
12+
* distributed under the License is distributed on an "AS IS" BASIS,
13+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14+
* See the License for the specific language governing permissions and
15+
* limitations under the License.
16+
*/
17+
18+
package dev.floofy.hazel.cli.commands.keystore
19+
20+
import dev.floofy.hazel.cli.abstractions.BaseKeystoreCommand
21+
22+
object ListKeysCommand: BaseKeystoreCommand(
23+
"list",
24+
"List all the keys in the keystore."
25+
) {
26+
override fun run() {
27+
val keystore = wrapper()
28+
val keys = keystore.keys()
29+
30+
for (key in keys) {
31+
val isUser = key.startsWith("users.")
32+
if (isUser) {
33+
val name = key.split(".").last()
34+
println("===> User $name | Use `hazel keystore update-pwd $name` to update their password,")
35+
println(" | or use `hazel keystore delete users.$name` to delete their account")
36+
println(" | in the keystore.")
37+
} else {
38+
println("===> Key $key")
39+
}
40+
}
41+
}
42+
}

0 commit comments

Comments
 (0)