Author SHA1 Message Date
sergeych 18c878dfb5 release 0.2.4 2024-08-01 02:36:37 +02:00
sergeych 1f7e7f88fa release 0.2.4 2024-08-01 02:36:16 +02:00
sergeych e0b6dea168 release 0.2.3 2024-08-01 02:31:32 +02:00
sergeych 26db60a211 readme update 2024-08-01 02:17:45 +02:00
sergeych 7c0edfb898 extending functionality for kilogin 2024-07-24 22:49:30 +03:00
sergeych 5df6143c75 adopted crypto2 0.4.* 2024-06-25 10:39:00 +07:00
sergeych 38fbca955c fixed automatic connection reestablishing for web and tcp 2024-06-18 16:11:13 +07:00
sergeych 825c0bd5f7 fixed automatic reconnection in TCP client/server 2024-06-17 17:42:16 +07:00
sergeych 0d3a8ae95c separate BreakConnectionException and more correct processing for ClosedException. 2024-06-17 13:01:09 +07:00
sergeych 4f6bc3c77e missing docs on builder#addErrors 2024-06-17 10:49:01 +07:00
sergeych 38d800c7ac added method to import errors into client interface KiloClient.Builder#addErrors 2024-06-17 10:44:12 +07:00
sergeych ae624ee051 packed nonce is now unsigned 2024-06-17 10:00:41 +07:00
sergeych 3dd1654f70 simplifies nonce calculation 2024-06-14 11:52:43 +07:00
sergeych 9660379891 migrated to crypto2 and kitlin2, no direct libsodium. Better session 2024-06-13 11:57:32 +07:00
sergeych 51873aa9b1 work in progress: restore connection 2024-04-23 21:49:15 +03:00
sergeych 93dc66acc5 work in porgress: fixing close connection 2024-03-23 01:29:48 +01:00
sergeych c0ca802a30 use mp bintools and added more macos apple targets 2024-03-16 20:33:22 +01:00
sergeych 745eb9ccdf small fix in client, more docs 2024-02-20 02:09:25 +03:00
sergeych 192f7e135f WS layer with tests 2023-11-23 10:58:37 +03:00
sergeych 3a56e67c24 remove cmd prefix from commands by delegate 2023-11-23 01:21:01 +03:00
sergeych ae3af68dab crypto2 as a separate library 2023-11-23 01:01:44 +03:00
sergeych 67c0009b5b use top-level Seal class 2023-11-22 12:38:57 +03:00
sergeych 7042e41d70 refactoring signing keys 2023-11-22 11:52:43 +03:00
sergeych e619b45485 removed nullable array from transport device, as channels close() is more than enough 2023-11-20 15:49:58 +03:00
sergeych e7abbe6d1d KiloSession.onConnected handler added 2023-11-17 17:43:38 +03:00
sergeych c6ac6f5907 fixed local host with accept and added tests 2023-11-17 17:22:09 +03:00
sergeych f02b390ed4 tcp optimization and graceful close 2023-11-15 11:47:56 +03:00
sergeych f92431a281 network transport with remote address, universal server 2023-11-15 02:25:57 +03:00
sergeych 96edbb2040 fixes, tcp/ip optimizations 2023-11-15 02:11:52 +03:00
sergeych bcf0140edb more readme 2023-11-14 03:36:58 +03:00
sergeych 4e2748f2ae added readme 2023-11-14 03:27:36 +03:00
sergeych 814263c11d removed debug noise 2023-11-14 03:08:37 +03:00
sergeych 0c05cce13b cancellability experiments in tests 2023-11-14 02:54:18 +03:00
sergeych 75e91f8092 async socket closing logic 2023-11-14 02:50:42 +03:00
sergeych 1814572a04 working suspended TCP block device on JVM (server and client) 2023-11-14 02:27:10 +03:00
sergeych fe29bec1b0 basic tcp connect async inmplementation for JVM/NIO 2023-11-14 01:47:30 +03:00
sergeych 8fc24567f0 splitting bracnhes 2023-11-13 22:38:11 +03:00
82 changed files with 1554 additions and 1488 deletions

No files matched your search

+4
View File
@@ -40,3 +40,7 @@ bin/
### Mac OS ###
.DS_Store
# More
.kotlin
/.idea/workspace.xml
-8
View File
@@ -1,8 +0,0 @@
# Default ignored files
/shelf/
/workspace.xml
# Editor-based HTTP Client requests
/httpRequests/
# Datasource local storage ignored files
/dataSources/
/dataSources.local.xml
+6
View File
@@ -0,0 +1,6 @@
<component name="ArtifactManager">
<artifact type="jar" name="kiloparsec-js-0.1.2-SNAPSHOT">
<output-path>$PROJECT_DIR$/build/libs</output-path>
<root id="archive" name="kiloparsec-js-0.1.2-SNAPSHOT.jar" />
</artifact>
</component>
@@ -1,7 +1,7 @@
<component name="ArtifactManager">
<artifact type="jar" name="kiloparsec-js-0.1.0-SNAPSHOT">
<artifact type="jar" name="kiloparsec-js-0.2.1-SNAPSHOT">
<output-path>$PROJECT_DIR$/build/libs</output-path>
<root id="archive" name="kiloparsec-js-0.1.0-SNAPSHOT.jar">
<root id="archive" name="kiloparsec-js-0.2.1-SNAPSHOT.jar">
<element id="module-output" name="kiloparsec.jsMain" />
</root>
</artifact>
+8
View File
@@ -0,0 +1,8 @@
<component name="ArtifactManager">
<artifact type="jar" name="kiloparsec-js-0.2.2-SNAPSHOT">
<output-path>$PROJECT_DIR$/build/libs</output-path>
<root id="archive" name="kiloparsec-js-0.2.2-SNAPSHOT.jar">
<element id="module-output" name="kiloparsec.jsMain" />
</root>
</artifact>
</component>
+8
View File
@@ -0,0 +1,8 @@
<component name="ArtifactManager">
<artifact type="jar" name="kiloparsec-js-0.2.3">
<output-path>$PROJECT_DIR$/build/libs</output-path>
<root id="archive" name="kiloparsec-js-0.2.3.jar">
<element id="module-output" name="kiloparsec.jsMain" />
</root>
</artifact>
</component>
+6
View File
@@ -0,0 +1,6 @@
<component name="ArtifactManager">
<artifact type="jar" name="kiloparsec-jvm-0.1.2-SNAPSHOT">
<output-path>$PROJECT_DIR$/build/libs</output-path>
<root id="archive" name="kiloparsec-jvm-0.1.2-SNAPSHOT.jar" />
</artifact>
</component>
@@ -1,7 +1,7 @@
<component name="ArtifactManager">
<artifact type="jar" name="kiloparsec-jvm-0.1.0-SNAPSHOT">
<artifact type="jar" name="kiloparsec-jvm-0.2.1-SNAPSHOT">
<output-path>$PROJECT_DIR$/build/libs</output-path>
<root id="archive" name="kiloparsec-jvm-0.1.0-SNAPSHOT.jar">
<root id="archive" name="kiloparsec-jvm-0.2.1-SNAPSHOT.jar">
<element id="module-output" name="kiloparsec.jvmMain" />
</root>
</artifact>
+8
View File
@@ -0,0 +1,8 @@
<component name="ArtifactManager">
<artifact type="jar" name="kiloparsec-jvm-0.2.2-SNAPSHOT">
<output-path>$PROJECT_DIR$/build/libs</output-path>
<root id="archive" name="kiloparsec-jvm-0.2.2-SNAPSHOT.jar">
<element id="module-output" name="kiloparsec.jvmMain" />
</root>
</artifact>
</component>
+8
View File
@@ -0,0 +1,8 @@
<component name="ArtifactManager">
<artifact type="jar" name="kiloparsec-jvm-0.2.3">
<output-path>$PROJECT_DIR$/build/libs</output-path>
<root id="archive" name="kiloparsec-jvm-0.2.3.jar">
<element id="module-output" name="kiloparsec.jvmMain" />
</root>
</artifact>
</component>
-6
View File
@@ -1,6 +0,0 @@
<component name="InspectionProjectProfileManager">
<profile version="1.0">
<option name="myName" value="Project Default" />
<inspection_tool class="ReplaceUntilWithRangeUntil" enabled="true" level="WEAK WARNING" enabled_by_default="true" />
</profile>
</component>
-6
View File
@@ -1,6 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="KotlinJpsPluginSettings">
<option name="version" value="1.9.20" />
</component>
</project>
+1 -1
View File
@@ -4,7 +4,7 @@
<component name="FrameworkDetectionExcludesConfiguration">
<file type="web" url="file://$PROJECT_DIR$" />
</component>
<component name="ProjectRootManager" version="2" languageLevel="JDK_1_8" project-jdk-name="17 (5)" project-jdk-type="JavaSDK">
<component name="ProjectRootManager" version="2" languageLevel="JDK_17" default="true" project-jdk-name="17 (5)" project-jdk-type="JavaSDK">
<output url="file://$PROJECT_DIR$/out" />
</component>
</project>
-124
View File
@@ -1,124 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="Palette2">
<group name="Swing">
<item class="com.intellij.uiDesigner.HSpacer" tooltip-text="Horizontal Spacer" icon="/com/intellij/uiDesigner/icons/hspacer.svg" removable="false" auto-create-binding="false" can-attach-label="false">
<default-constraints vsize-policy="1" hsize-policy="6" anchor="0" fill="1" />
</item>
<item class="com.intellij.uiDesigner.VSpacer" tooltip-text="Vertical Spacer" icon="/com/intellij/uiDesigner/icons/vspacer.svg" removable="false" auto-create-binding="false" can-attach-label="false">
<default-constraints vsize-policy="6" hsize-policy="1" anchor="0" fill="2" />
</item>
<item class="javax.swing.JPanel" icon="/com/intellij/uiDesigner/icons/panel.svg" removable="false" auto-create-binding="false" can-attach-label="false">
<default-constraints vsize-policy="3" hsize-policy="3" anchor="0" fill="3" />
</item>
<item class="javax.swing.JScrollPane" icon="/com/intellij/uiDesigner/icons/scrollPane.svg" removable="false" auto-create-binding="false" can-attach-label="true">
<default-constraints vsize-policy="7" hsize-policy="7" anchor="0" fill="3" />
</item>
<item class="javax.swing.JButton" icon="/com/intellij/uiDesigner/icons/button.svg" removable="false" auto-create-binding="true" can-attach-label="false">
<default-constraints vsize-policy="0" hsize-policy="3" anchor="0" fill="1" />
<initial-values>
<property name="text" value="Button" />
</initial-values>
</item>
<item class="javax.swing.JRadioButton" icon="/com/intellij/uiDesigner/icons/radioButton.svg" removable="false" auto-create-binding="true" can-attach-label="false">
<default-constraints vsize-policy="0" hsize-policy="3" anchor="8" fill="0" />
<initial-values>
<property name="text" value="RadioButton" />
</initial-values>
</item>
<item class="javax.swing.JCheckBox" icon="/com/intellij/uiDesigner/icons/checkBox.svg" removable="false" auto-create-binding="true" can-attach-label="false">
<default-constraints vsize-policy="0" hsize-policy="3" anchor="8" fill="0" />
<initial-values>
<property name="text" value="CheckBox" />
</initial-values>
</item>
<item class="javax.swing.JLabel" icon="/com/intellij/uiDesigner/icons/label.svg" removable="false" auto-create-binding="false" can-attach-label="false">
<default-constraints vsize-policy="0" hsize-policy="0" anchor="8" fill="0" />
<initial-values>
<property name="text" value="Label" />
</initial-values>
</item>
<item class="javax.swing.JTextField" icon="/com/intellij/uiDesigner/icons/textField.svg" removable="false" auto-create-binding="true" can-attach-label="true">
<default-constraints vsize-policy="0" hsize-policy="6" anchor="8" fill="1">
<preferred-size width="150" height="-1" />
</default-constraints>
</item>
<item class="javax.swing.JPasswordField" icon="/com/intellij/uiDesigner/icons/passwordField.svg" removable="false" auto-create-binding="true" can-attach-label="true">
<default-constraints vsize-policy="0" hsize-policy="6" anchor="8" fill="1">
<preferred-size width="150" height="-1" />
</default-constraints>
</item>
<item class="javax.swing.JFormattedTextField" icon="/com/intellij/uiDesigner/icons/formattedTextField.svg" removable="false" auto-create-binding="true" can-attach-label="true">
<default-constraints vsize-policy="0" hsize-policy="6" anchor="8" fill="1">
<preferred-size width="150" height="-1" />
</default-constraints>
</item>
<item class="javax.swing.JTextArea" icon="/com/intellij/uiDesigner/icons/textArea.svg" removable="false" auto-create-binding="true" can-attach-label="true">
<default-constraints vsize-policy="6" hsize-policy="6" anchor="0" fill="3">
<preferred-size width="150" height="50" />
</default-constraints>
</item>
<item class="javax.swing.JTextPane" icon="/com/intellij/uiDesigner/icons/textPane.svg" removable="false" auto-create-binding="true" can-attach-label="true">
<default-constraints vsize-policy="6" hsize-policy="6" anchor="0" fill="3">
<preferred-size width="150" height="50" />
</default-constraints>
</item>
<item class="javax.swing.JEditorPane" icon="/com/intellij/uiDesigner/icons/editorPane.svg" removable="false" auto-create-binding="true" can-attach-label="true">
<default-constraints vsize-policy="6" hsize-policy="6" anchor="0" fill="3">
<preferred-size width="150" height="50" />
</default-constraints>
</item>
<item class="javax.swing.JComboBox" icon="/com/intellij/uiDesigner/icons/comboBox.svg" removable="false" auto-create-binding="true" can-attach-label="true">
<default-constraints vsize-policy="0" hsize-policy="2" anchor="8" fill="1" />
</item>
<item class="javax.swing.JTable" icon="/com/intellij/uiDesigner/icons/table.svg" removable="false" auto-create-binding="true" can-attach-label="false">
<default-constraints vsize-policy="6" hsize-policy="6" anchor="0" fill="3">
<preferred-size width="150" height="50" />
</default-constraints>
</item>
<item class="javax.swing.JList" icon="/com/intellij/uiDesigner/icons/list.svg" removable="false" auto-create-binding="true" can-attach-label="false">
<default-constraints vsize-policy="6" hsize-policy="2" anchor="0" fill="3">
<preferred-size width="150" height="50" />
</default-constraints>
</item>
<item class="javax.swing.JTree" icon="/com/intellij/uiDesigner/icons/tree.svg" removable="false" auto-create-binding="true" can-attach-label="false">
<default-constraints vsize-policy="6" hsize-policy="6" anchor="0" fill="3">
<preferred-size width="150" height="50" />
</default-constraints>
</item>
<item class="javax.swing.JTabbedPane" icon="/com/intellij/uiDesigner/icons/tabbedPane.svg" removable="false" auto-create-binding="true" can-attach-label="false">
<default-constraints vsize-policy="3" hsize-policy="3" anchor="0" fill="3">
<preferred-size width="200" height="200" />
</default-constraints>
</item>
<item class="javax.swing.JSplitPane" icon="/com/intellij/uiDesigner/icons/splitPane.svg" removable="false" auto-create-binding="false" can-attach-label="false">
<default-constraints vsize-policy="3" hsize-policy="3" anchor="0" fill="3">
<preferred-size width="200" height="200" />
</default-constraints>
</item>
<item class="javax.swing.JSpinner" icon="/com/intellij/uiDesigner/icons/spinner.svg" removable="false" auto-create-binding="true" can-attach-label="true">
<default-constraints vsize-policy="0" hsize-policy="6" anchor="8" fill="1" />
</item>
<item class="javax.swing.JSlider" icon="/com/intellij/uiDesigner/icons/slider.svg" removable="false" auto-create-binding="true" can-attach-label="false">
<default-constraints vsize-policy="0" hsize-policy="6" anchor="8" fill="1" />
</item>
<item class="javax.swing.JSeparator" icon="/com/intellij/uiDesigner/icons/separator.svg" removable="false" auto-create-binding="false" can-attach-label="false">
<default-constraints vsize-policy="6" hsize-policy="6" anchor="0" fill="3" />
</item>
<item class="javax.swing.JProgressBar" icon="/com/intellij/uiDesigner/icons/progressbar.svg" removable="false" auto-create-binding="true" can-attach-label="false">
<default-constraints vsize-policy="0" hsize-policy="6" anchor="0" fill="1" />
</item>
<item class="javax.swing.JToolBar" icon="/com/intellij/uiDesigner/icons/toolbar.svg" removable="false" auto-create-binding="false" can-attach-label="false">
<default-constraints vsize-policy="0" hsize-policy="6" anchor="0" fill="1">
<preferred-size width="-1" height="20" />
</default-constraints>
</item>
<item class="javax.swing.JToolBar$Separator" icon="/com/intellij/uiDesigner/icons/toolbarSeparator.svg" removable="false" auto-create-binding="false" can-attach-label="false">
<default-constraints vsize-policy="0" hsize-policy="0" anchor="0" fill="1" />
</item>
<item class="javax.swing.JScrollBar" icon="/com/intellij/uiDesigner/icons/scrollbar.svg" removable="false" auto-create-binding="true" can-attach-label="false">
<default-constraints vsize-policy="6" hsize-policy="0" anchor="0" fill="2" />
</item>
</group>
</component>
</project>
Generated
+1 -1
View File
@@ -1,6 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="VcsDirectoryMappings">
<mapping directory="$PROJECT_DIR$" vcs="Git" />
<mapping directory="" vcs="Git" />
</component>
</project>
+171
View File
@@ -0,0 +1,171 @@
# Kiloparsec
The new generation of __PARanoid SECurity__ protocol, advanced, faster, more secure. It also allows connecting any "block device" transport to the same local interface. Out if the box it
provides the following transports:
| name | JVM | JS | native |
|----------------|-----|----|--------|
| TCP/IP server | * | | |
| TCP/IP client | * | | |
| Websock server | * | | |
| Websock client | * | * | * |
At the moment we're working on supporting TCP/IP on most native targets. This feature is planned to rach public beta in August and production in early september 2024.
## TCP/IP transport
It is the fastest. JVM implementation uses nio2 async sockets and optimizes TCP socket to play
well with blocks (smart NO_DELAY mode). It is multiplatform, nut lacks of async TCP/IP support
on natvic targetm this is where I need help having little time. I'd prefer to use something asyn like UV on native targets.
I know no existing way to implement it in KotlinJS for the modern browsers.
## Websock server
While it is much slower than pure TCP, it is still faster than any http-based transport. It uses binary frames based on the Ktor server framework to easily integrate with web services. We recommend using it instead of a classic HTTP API as it beats it in terms of speed and server load even with HTTP/2.
We recommend to create the `KiloInterface<S>` instance and connect it to the websock and tcp servers in real applications to get easy access from anywhere.
# Usage
Th elibrary should be used as maven dependency, not as source.
## Adding dependency
### Declare maven repository:
Add the private repository to your `build.gradle.kts`, like:
```kotlin
repositories {
maven("https://gitea.sergeych.net/api/packages/SergeychWorks/maven")
}
```
### Add dependency block
It could be, depending on your project structure, something like:
```kotlin
val commonMain by getting {
dependencies {
api("net.sergeych:kiloparsec:0.2.4")
}
}
```
## Create shared interface for your server and the client
It could be a multiplatform library that exports it or just a shared or copied source file declaring structures
and functions available, like:
```kotlin
// Api.kt
@Serializable
class FooArgs(val text: String,val number: Int = 42)
// Server-side interface
val cmdSetFoo by command<FooArgs,Unit>()
val cmdGetFoo by command<Unit,FooArgs>()
val cmdPing by command<String,String>()
val cmdCheckConnected by command<Unit,Boolean>()
// client-side interface (called from the server)
val cmdPushClient by command<String,Unit>()
```
## Call it from the client:
Remember, we need to implement client interface `cmdPushClient` in our example, so we need to provide
local interace too:
```kotlin
// Unit: no session on the client:
val client = websocketClient<Unit>("wss://your.host.com/kp") {
// This is server-callable function we export:
on(cmdPushClient) {
"server push: $it"
}
}
// If we want to collect connected state changes (this is optional)
launch {
client.connectedStateFlow.collect {
if( it )
println("I am connected")
else
println("trying to connect...")
}
}
// now we can call server's functions
client.call(cmdSetFoo, FooArgs("bar", 117))
assertEquals(FooArgs("bar", 117), client.call(cmdGetFoo))
```
## Create ktor-based server
Normally server side needs some session. It is convenient and avoid sending repeating data on each request speeding up
the protocol. With KILOPARSEC it is rather basic operation:\
~~~kotlin
// Our session just keeps Foo for cmd{Get|Set}Foo:
data class Session(var fooState: FooArgs?=null)
// Let's now provide interface we export, it will be used on each connection automatically:
// Note server interface uses Session:
val serverInterface = KiloInterface<Session>().apply {
onConnected {
// Do some initialization
session.fooState = null
}
// Exceptions are passed through the network and re-created (re-thrown) on other side:
on(cmdGetFoo) { session.fooState ?: throw IllegalStateException("foo is not yet set") }
on(cmdSetFoo) { session.fooState = it }
}
// now create server using ktor (see ktor project for more):
val ns: NettyApplicationEngine = embeddedServer(Netty, port = 8080, host = "0.0.0.0", module = {
setupWebsocketServer(serverInterface) { Session() }
}).start(wait = false)
~~~
# Details
It is not compatible with parsec family and no more based on an Universa crypto library. To better fit
the modern state of threats and rate of cyber crimes, KiloParsec uses more encryption and random key exchange on each
and every connection (while parsec caches session keys to avoid time-consuming keys exchange). For the same reason,
keys cryptography for session is shifted to use ed25519 curves which are supposed to provide agreeable strength with
enough speed to protect every connection with a unique new keys. Also, we completely get rid of SHA2.
Kiloparsec also uses a denser binary format [bipack](https://gitea.sergeych.net/SergeychWorks/mp_bintools), no more key-values,
which reveals much less on the inner data structure, providing advanced
typed RPC interfaces with kotlinx.serialization. There is also Rust implementation [bipack_ru](https://gitea.sergeych.net/DiWAN/bipack_ru).
The architecture allows connecting same functional interfaces to several various type channels at once.
Also, the difference from parsecs is that there are no more unencrypted layer commands available to users.
All RPC is performed over the encrypted connection.
# Technical description
Kiloparsec is a dull-duplex fully async (coroutine based) Remote Procedure Call protocol with typed parameters
and support for serializing exceptions (e.g. exception thrown while executing remote command will be caught and
rethrown at the caller context).
Kiloparsec is not REST, it _has advanced session mechanisms_ and built-in authentication based on the same curve keys.
Integrated tools to prevent MITM attacks include also non-transferred independently generated token that is calculated
independently on the ends and is never transferred with the network. Comparing it somehow (visually, with QR code, etc)
could add a very robust guarantee of the connection safety and ingenuity.
Kiloparsec has built-in completely asynchronous (coroutine based top-down) transport layer based on TCP (JVM only as for now) and the same async Websocket-based transport based on KTOR. Websocket client is multiplatform, though the server is JVM only insofar.
# Licensing
When the project reaches some stable state, it will be available under Apache or MIT license. I promise ;) I do
write opensource ;)
+75 -41
View File
@@ -1,51 +1,48 @@
import org.jetbrains.kotlin.gradle.plugin.KotlinJsCompilerType
plugins {
kotlin("multiplatform") version "1.9.20"
id("org.jetbrains.kotlin.plugin.serialization") version "1.9.20"
kotlin("multiplatform") version "2.0.0"
id("org.jetbrains.kotlin.plugin.serialization") version "2.0.0"
`maven-publish`
}
group = "net.sergeych"
version = "0.1.0-SNAPSHOT"
version = "0.2.4"
repositories {
mavenCentral()
mavenLocal()
maven("https://maven.universablockchain.com/")
maven("https://gitea.sergeych.net/api/packages/SergeychWorks/maven")
}
kotlin {
jvm {
jvmToolchain(8)
withJava()
testRuns.named("test") {
executionTask.configure {
useJUnitPlatform()
}
}
}
js(KotlinJsCompilerType.IR) {
jvm()
js {
browser {
// commonWebpackConfig {
// cssSupport {
// enabled.set(true)
// }
// }
}
}
val hostOs = System.getProperty("os.name")
val isArm64 = System.getProperty("os.arch") == "aarch64"
val isMingwX64 = hostOs.startsWith("Windows")
val nativeTarget = when {
hostOs == "Mac OS X" && isArm64 -> macosArm64("native")
hostOs == "Mac OS X" && !isArm64 -> macosX64("native")
hostOs == "Linux" && isArm64 -> linuxArm64("native")
hostOs == "Linux" && !isArm64 -> linuxX64("native")
isMingwX64 -> mingwX64("native")
else -> throw GradleException("Host OS is not supported in Kotlin/Native.")
}
// val hostOs = System.getProperty("os.name")
// val isArm64 = System.getProperty("os.arch") == "aarch64"
// val isMingwX64 = hostOs.startsWith("Windows")
// @Suppress("UNUSED_VARIABLE")
// val nativeTarget = when {
// hostOs == "Mac OS X" && isArm64 -> macosArm64("native")
// hostOs == "Mac OS X" && !isArm64 -> macosX64("native")
// hostOs == "Linux" && isArm64 -> linuxArm64("native")
// hostOs == "Linux" && !isArm64 -> linuxX64("native")
// isMingwX64 -> mingwX64("native")
// else -> throw GradleException("Host OS is not supported in Kotlin/Native.")
// }
macosArm64()
iosX64()
iosArm64()
iosSimulatorArm64()
linuxX64()
linuxArm64()
macosX64()
val ktor_version = "2.3.12"
sourceSets {
all {
@@ -57,27 +54,64 @@ kotlin {
val commonMain by getting {
dependencies {
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.7.3")
implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.5.1")
implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.7.1")
implementation("com.ionspin.kotlin:multiplatform-crypto-libsodium-bindings:0.9.0")
api("com.ionspin.kotlin:bignum:0.3.8")
api("net.sergeych:mp_bintools:0.0.6-SNAPSHOT")
api("net.sergeych:mp_stools:1.4.1")
// api("com.ionspin.kotlin:bignum:0.3.9")
api("io.ktor:ktor-client-core:$ktor_version")
api("net.sergeych:crypto2:0.4.2")
}
}
// val ktorSocketMain by creating {
// dependsOn(commonMain)
// dependencies {
// implementation("io.ktor:ktor-network:$ktor_version")
// }
// }
val commonTest by getting {
dependencies {
implementation(kotlin("test"))
implementation("org.slf4j:slf4j-simple:2.0.9")
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-test:1.7.3")
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-test:1.8.1")
}
}
val jvmMain by getting
val jvmMain by getting {
dependencies {
implementation("io.ktor:ktor-server-core:$ktor_version")
implementation("io.ktor:ktor-server-core-jvm:$ktor_version")
implementation("io.ktor:ktor-server-websockets-jvm:$ktor_version")
implementation("io.ktor:ktor-server-netty:$ktor_version")
api("io.ktor:ktor-client-cio:$ktor_version")
}
// dependsOn(ktorSocketMain)
}
val jvmTest by getting
val jsMain by getting
val jsMain by getting {
dependencies {
implementation("io.ktor:ktor-client-js:$ktor_version")
}
}
val jsTest by getting
val nativeMain by getting
val nativeTest by getting
// for (pm in listOf(linuxMain, macosMain, iosMain, mingwMain))
// pm { dependsOn(ktorSocketMain) }
}
publishing {
val mavenToken by lazy {
File("${System.getProperty("user.home")}/.gitea_token").readText()
}
repositories {
maven {
credentials(HttpHeaderCredentials::class) {
name = "Authorization"
value = mavenToken
}
url = uri("https://gitea.sergeych.net/api/packages/SergeychWorks/maven")
authentication {
create("Authorization", HttpHeaderAuthentication::class)
}
}
}
}
}
@@ -1,27 +0,0 @@
package net.sergeych.crypto
import com.ionspin.kotlin.crypto.LibsodiumInitializer
import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock
private var isReady = false
private val readyAccess = Mutex()
/**
* Library initialization: should be called before all other calls.
* It is safe and with little performance penalty to call it multiple times.
*/
suspend fun initCrypto() {
// faster to check with no lock
if( !isReady) {
readyAccess.withLock {
// recheck with lock, it could be ready by now
if( !isReady ) {
LibsodiumInitializer.initialize()
isReady = true
}
}
}
}
@@ -1,81 +0,0 @@
package net.sergeych.crypto
import kotlinx.serialization.Serializable
import kotlinx.serialization.Transient
/**
* Multi-signed data box. Use [SignedBox.invoke] to easily create
* instances and [SignedBox.plus] to add more signatures (signing keys), and
* [SignedBox.contains] to check for a specific key signature presence.
*
* It is serializable and checks integrity on deserialization. If any of seals does not
* match the signed [message], it throws [IllegalSignatureException] _on deserialization_.
* E.g., if you have it deserialized, it is ok, check it contains all needed keys among
* signers.
*
* __The main constructor is used for deserializing only__. Don't use it directly unless you
* know what you are doing as it may be dangerous.Use one of the above to create or change it.
*/
@Serializable
class SignedBox(
val message: UByteArray,
private val seals: List<Seal>,
@Transient
private val checkOnInit: Boolean = true
) {
/**
* If this instance is not signed by a given key, return new instance signed also by this
* key, or return unchanged (same) object if it is already signed by this key; you
* _can't assume it always returns a copied object!_
*/
operator fun plus(key: Key.Signing): SignedBox =
if (key.verifying in this) this
else SignedBox(message, seals + Seal.create(key, message), false)
/**
* Check that it is signed with a specified key.
*/
operator fun contains(verifyingKey: Key.Verifying): Boolean {
return seals.any { it.key == verifyingKey }
}
init {
if (seals.isEmpty()) throw IllegalArgumentException("there should be at least one seal")
if (checkOnInit) {
if (!seals.all { it.verify(message) }) throw IllegalSignatureException()
}
}
/**
* A key + signature pair for [SignedBox] boxes, usually you don't use it
* directly bug call [SignedBox] constructor or [SignedBox.plus] to
* add seals.
*/
@Serializable
data class Seal(val key: Key.Verifying, val signature: UByteArray) {
fun verify(message: UByteArray): Boolean = key.verify(signature, message)
companion object {
fun create(key: Key.Signing, message: UByteArray): Seal {
return Seal(key.verifying, key.sign(message))
}
}
}
companion object {
/**
* Create a new instance with a specific data sealed by one or more
* keys. At least one key is required to disallow providing not-signed
* instances, e.g. [SignedBox] is guaranteed to be properly sealed when
* successfully instantiated.
*
* @param data a message to sign
* @param keys a list of keys to sign with, should be at least one key.
* @throws IllegalArgumentException if keys are not specified.
*/
operator fun invoke(data: UByteArray, vararg keys: Key.Signing): SignedBox =
SignedBox(data, keys.map { Seal.create(it, data) }, false)
}
}
@@ -1,7 +0,0 @@
package net.sergeych.crypto
import net.sergeych.bintools.CRC
fun isValidContrail(data: UByteArray): Boolean = CRC.crc8(data.copyOfRange(1, data.size)) == data[0]
fun createContrail(data: UByteArray): UByteArray = ubyteArrayOf(CRC.crc8(data)) + data
@@ -1,75 +0,0 @@
package net.sergeych.crypto
import com.ionspin.kotlin.crypto.signature.InvalidSignatureException
import com.ionspin.kotlin.crypto.signature.Signature
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
import net.sergeych.crypto.Key.Signing
/**
* Keys in general: public, secret and later symmetric too.
* Keys could be compared to each other for equality and used
* as a Map keys (not sure about js).
*
* Use [Signing.pair] to create new keys.
*/
@Serializable
sealed class Key {
abstract val packed: UByteArray
override fun equals(other: Any?): Boolean {
return other is Key && other.packed contentEquals packed
}
override fun hashCode(): Int {
return packed.contentHashCode()
}
override fun toString(): String = packed.encodeToBase64Url()
/**
* Public key to verify signatures only
*/
@Serializable
@SerialName("pvk")
class Verifying(override val packed: UByteArray) : Key() {
/**
* Verify the signature and return true if it is correct.
*/
fun verify(signature: UByteArray, message: UByteArray): Boolean = try {
Signature.verifyDetached(signature, message, packed)
true
} catch (_: InvalidSignatureException) {
false
}
override fun toString(): String = "Pub:${super.toString()}"
}
/**
* Secret key to sign only
*/
@Serializable
@SerialName("ssk")
class Signing(override val packed: UByteArray) : Key() {
val verifying: Verifying by lazy {
Verifying(Signature.ed25519SkToPk(packed))
}
fun sign(message: UByteArray): UByteArray = Signature.detached(message, packed)
override fun toString(): String = "Sct:${super.toString()}"
companion object {
data class Pair(val signing: Signing, val verifying: Verifying)
fun pair(): Pair {
val p = Signature.keypair()
return Pair(Signing(p.secretKey), Verifying(p.publicKey))
}
}
}
}
class IllegalSignatureException: RuntimeException("signed data is tampered or signature is corrupted")
@@ -1,85 +0,0 @@
@file:Suppress("unused")
package net.sergeych.crypto
import com.ionspin.kotlin.crypto.secretbox.SecretBox
import com.ionspin.kotlin.crypto.secretbox.crypto_secretbox_NONCEBYTES
import com.ionspin.kotlin.crypto.util.LibsodiumRandom
import kotlinx.serialization.Serializable
import net.sergeych.bintools.toDataSource
import net.sergeych.bipack.BipackDecoder
import net.sergeych.bipack.BipackEncoder
class DecryptionFailedException : RuntimeException("can't encrypt: wrong key or tampered message")
@Serializable
data class WithNonce(
val cipherData: UByteArray,
val nonce: UByteArray,
)
@Serializable
data class WithFill(
val data: UByteArray,
val safetyFill: UByteArray? = null
) {
constructor(data: UByteArray, fillSize: Int) : this(data, randomBytes(fillSize))
}
fun randomBytes(n: Int): UByteArray = if (n > 0) LibsodiumRandom.buf(n) else ubyteArrayOf()
fun randomBytes(n: UInt): UByteArray = if (n > 0u) LibsodiumRandom.buf(n.toInt()) else ubyteArrayOf()
/**
* Uniform random in `0 ..< max` range
*/
fun randomUInt(max: UInt) = LibsodiumRandom.uniform(max)
fun randomUInt(max: Int) = LibsodiumRandom.uniform(max.toUInt())
fun <T: Comparable<T>>T.limit(range: ClosedRange<T>) = when {
this < range.start -> range.start
this > range.endInclusive -> range.endInclusive
else -> this
}
fun <T: Comparable<T>>T.limitMax(max: T) = if( this < max ) this else max
fun <T: Comparable<T>>T.limitMin(min: T) = if( this > min ) this else min
fun randomNonce(): UByteArray = randomBytes(crypto_secretbox_NONCEBYTES)
/**
* Secret-key encrypt with authentication.
* Generates random nonce and add some random fill to protect
* against some analysis attacks. Nonce is included in the result. To be
* used with [decrypt].
* @param secretKey a _secret_ key, see [SecretBox.keygen()] or like.
* @param plain data to encrypt
* @param fillSize number of random fill data to add. Use random value or default.
*/
fun encrypt(
secretKey: UByteArray,
plain: UByteArray,
fillSize: Int = randomUInt((plain.size * 3 / 10).limitMin(3)).toInt()
): UByteArray {
val filled = BipackEncoder.encode(WithFill(plain, fillSize))
val nonce = randomNonce()
val encrypted = SecretBox.easy(filled.toUByteArray(), nonce, secretKey)
return BipackEncoder.encode(WithNonce(encrypted, nonce)).toUByteArray()
}
/**
* Decrypt a secret-key-based message, normally encrypted with [encrypt].
* @throws DecryptionFailedException if the key is wrong or a message is tampered with (MAC
* check failed).
*/
fun decrypt(secretKey: UByteArray, cipher: UByteArray): UByteArray {
val wn: WithNonce = BipackDecoder.decode(cipher.toDataSource())
try {
return BipackDecoder.decode<WithFill>(
SecretBox.openEasy(wn.cipherData, wn.nonce, secretKey).toDataSource()
).data
}
catch(_: com.ionspin.kotlin.crypto.secretbox.SecretBoxCorruptedOrTamperedDataExceptionOrInvalidKey) {
throw DecryptionFailedException()
}
}
@@ -1,8 +0,0 @@
package net.sergeych.crypto
import net.sergeych.bintools.toDump
import net.sergeych.mp_tools.encodeToBase64Url
fun UByteArray.toDump(wide: Boolean = false) = toByteArray().toDump(wide)
fun UByteArray.encodeToBase64Url(): String = toByteArray().encodeToBase64Url()
@@ -0,0 +1,37 @@
package net.sergeych.kiloparsec
import io.ktor.utils.io.*
object AsyncVarint {
suspend fun encodeUnsigned(value: ULong, output: ByteWriteChannel) {
var rest = value
do {
val x = (rest and 127u).toInt()
rest = rest shr 7
if (rest > 0u)
output.writeByte(x or 0x80)
else
output.writeByte(x)
} while (rest > 0u)
}
suspend fun decodeUnsigned(source: ByteReadChannel): ULong {
var result: ULong = 0u
var count = 0
while (true) {
val x = source.readUByte().toInt()
result = result or ((x and 0x7F).toULong() shl count)
if ((x and 0x80) == 0)
break
count += 7
}
return result
}
}
suspend fun ByteReadChannel.readUByte(): UByte = this.readByte().toUByte()
suspend fun ByteWriteChannel.writeByte(byte: Int) {
this.writeByte(byte.toByte())
}
@@ -31,7 +31,8 @@ class Command<A, R>(
suspend fun exec(packedArgs: UByteArray, handler: suspend (A) -> R): UByteArray =
BipackEncoder.encode(
resultSerializer,
handler(BipackDecoder.decode(packedArgs.toDataSource(), argsSerializer))
handler(
BipackDecoder.decode(packedArgs.toDataSource(), argsSerializer))
).toUByteArray()
companion object {
@@ -7,6 +7,9 @@ import kotlin.reflect.KProperty
/**
* delegate returning function that creates a [Command] in the current context which by default has the name of
* the property.
*
* The Default name is a property name except the "cmd" prefix if present, which will be
* removed automatically.
*/
inline fun <reified A, reified R> command(overrideName: String? = null): CommandDelegate<A, R> {
return CommandDelegate(
@@ -22,13 +25,22 @@ inline fun <reified A, reified R> command(overrideName: String? = null): Command
class CommandDelegate<A, R>(
private val argsSerializer: KSerializer<A>,
private val resultSerializer: KSerializer<R>,
private val overrideName: String? = null
private val overrideName: String? = null,
) {
private var name: String = ""
operator fun getValue(nothing: Nothing?, property: KProperty<*>): Command<A, R> {
if (name.isEmpty()) {
name = overrideName ?: removeCmd(property.name)
}
return Command(
overrideName ?: property.name,
name,
argsSerializer,
resultSerializer
)
}
}
private fun removeCmd(name: String) =
if (name.startsWith("cmd"))
name.substring(3)
else name
@@ -2,10 +2,14 @@ package net.sergeych.kiloparsec
import kotlinx.coroutines.CancellationException
import kotlinx.coroutines.CompletableDeferred
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.isActive
import net.sergeych.crypto.Key
import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock
import net.sergeych.crypto2.SigningKey
import net.sergeych.crypto2.VerifyingPublicKey
import net.sergeych.mp_logger.LogTag
import net.sergeych.mp_logger.Loggable
import net.sergeych.mp_logger.debug
@@ -19,8 +23,8 @@ import net.sergeych.mp_tools.globalLaunch
* it manually when it is not needed, otherwise it will continue to reconnect.
*/
class KiloClient<S>(
localInterface: KiloInterface<S>,
secretKey: Key.Signing? = null,
val localInterface: KiloInterface<S>,
secretKey: SigningKey? = null,
connectionDataFactory: ConnectionDataFactory<S>,
) : RemoteInterface,
Loggable by LogTag("CLIF") {
@@ -32,7 +36,7 @@ class KiloClient<S>(
* to authenticate a client on connection restore, for example.
*/
@Suppress("unused")
val state = _state.asStateFlow()
val connectedStateFlow = _state.asStateFlow()
private var deferredClient = CompletableDeferred<KiloClientConnection<S>>()
@@ -44,30 +48,51 @@ class KiloClient<S>(
debug { "getting connection" }
val kc = connectionDataFactory()
debug { "get device and session" }
val client = KiloClientConnection(localInterface, kc,secretKey)
val client = KiloClientConnection(localInterface, kc, secretKey)
deferredClient.complete(client)
client.run {
_state.value = false
_state.value = it
}
resetDeferredClient()
debug { "client run finished" }
} catch (_: RemoteInterface.ClosedException) {
debug { "remote closed" }
delay(1000)
} catch (_: CancellationException) {
debug { "cancelled" }
break
} catch (t: Throwable) {
exception { "unexpected exception" to t }
delay(1000)
}
_state.value = false
if (deferredClient.isActive)
deferredClient = CompletableDeferred()
resetDeferredClient()
delay(100)
}
}
fun close() {
job.cancel()
debug { "client is closed" }
}
override suspend fun <A, R> call(cmd: Command<A, R>, args: A): R = deferredClient.await().call(cmd, args)
private val defMutex = Mutex()
private suspend fun resetDeferredClient() {
defMutex.withLock {
if (!deferredClient.isActive) {
deferredClient = CompletableDeferred()
}
}
}
override suspend fun <A, R> call(cmd: Command<A, R>, args: A): R =
try {
deferredClient.await().call(cmd, args)
} catch (t: RemoteInterface.ClosedException) {
resetDeferredClient()
throw t
}
/**
* Current session token. This is a per-connection unique random value same on the client and server part so
@@ -77,14 +102,14 @@ class KiloClient<S>(
suspend fun token() = deferredClient.await().token()
/**
* Remote party shared key ([Key.Verifying]]), could be used ti ensure server is what we expected and
* Remote party shared key ([VerifyingPublicKey]), could be used ti ensure server is what we expected and
* there is no active MITM attack.
*
* Non-null value means the key was successfully authenticated, null means remote party did not provide
* a key. Connection is established either with a properly authenticated key or no key at all.
*/
@Suppress("unused")
suspend fun remoteId() = deferredClient.await().remoteId()
suspend fun remoteId(): VerifyingPublicKey? = deferredClient.await().remoteId()
companion object {
class Builder<S>() {
@@ -96,7 +121,7 @@ class KiloClient<S>(
}
private var connectionBuilder: (suspend () -> Transport.Device)? = null
var secretIdKey: Key.Signing? = null
var secretIdKey: SigningKey? = null
/**
* Build local command implementations (remotely callable ones), exception
@@ -106,6 +131,21 @@ class KiloClient<S>(
interfaceBuilder = f
}
/**
* Import exception (error) registrations from a shared LocalInterface (usually, server side one) to
* DRY error registration on both server and client sides.
* Normally you need to:
*
* - Register all your errors in server's LocalInterface
*
* - In the client builder call [addErrors(serverLocalInterface]
*
* See also [LocalInterface.registerError] for exception registration
*/
fun addErrors(from: LocalInterface<*>) {
errorProviders += from
}
/**
* Create a new session object, otherwise Unit session will be used
*/
@@ -117,12 +157,15 @@ class KiloClient<S>(
connectionBuilder = f
}
val errorProviders = mutableListOf<LocalInterface<*>>()
internal fun build(): KiloClient<S> {
val i = KiloInterface<S>()
for (ep in errorProviders) i.addErrorProvider(ep)
interfaceBuilder?.let { i.it() }
val connector = connectionBuilder ?: throw IllegalArgumentException("connect handler was not set")
return KiloClient(i,secretIdKey) {
KiloConnectionData(connector(),sessionBuilder())
return KiloClient(i, secretIdKey) {
KiloConnectionData(connector(), sessionBuilder())
}
}
}
@@ -1,8 +1,9 @@
package net.sergeych.kiloparsec
import com.ionspin.kotlin.crypto.keyexchange.KeyExchange
import kotlinx.coroutines.*
import net.sergeych.crypto.Key
import net.sergeych.crypto2.SafeKeyExchange
import net.sergeych.crypto2.SigningKey
import net.sergeych.crypto2.VerifyingPublicKey
import net.sergeych.mp_logger.LogTag
import net.sergeych.mp_logger.Loggable
import net.sergeych.mp_logger.debug
@@ -12,20 +13,20 @@ import net.sergeych.utools.pack
private var clientIds = 0
class KiloClientConnection<S>(
private val clientInterface: LocalInterface<KiloScope<S>>,
private val clientInterface: KiloInterface<S>,
private val device: Transport.Device,
private val session: S,
private val secretIdKey: Key.Signing? = null,
private val secretIdKey: SigningKey? = null,
) : RemoteInterface, Loggable by LogTag("KPC:${++clientIds}") {
constructor(localInterface: KiloInterface<S>, connection: KiloConnectionData<S>, secretIdKey: Key.Signing? = null)
constructor(localInterface: KiloInterface<S>, connection: KiloConnectionData<S>, secretIdKey: SigningKey? = null)
: this(localInterface, connection.device, connection.session, secretIdKey)
private val kiloRemoteInterface = CompletableDeferred<KiloRemoteInterface<S>>()
private val deferredParams = CompletableDeferred<KiloParams<S>>()
suspend fun remoteId(): Key.Verifying? = deferredParams.await().remoteIdentity
suspend fun remoteId(): VerifyingPublicKey? = deferredParams.await().remoteIdentity
/**
* Run the client, blocking until the device is closed, or some critical exception
@@ -37,7 +38,7 @@ class KiloClientConnection<S>(
var job: Job? = null
try {
// in parallel: keys and connection
val deferredKeyPair = async { KeyExchange.keypair() }
val deferredKeyPair = async { SafeKeyExchange() }
debug { "opening device" }
debug { "got a transport device $device" }
@@ -55,23 +56,21 @@ class KiloClientConnection<S>(
val serverHe = transport.call(L0Request, Handshake(1u, pair.publicKey))
val sk = KeyExchange.clientSessionKeys(pair.publicKey, pair.secretKey, serverHe.publicKey)
val sk = pair.clientSessionKey(serverHe.publicKey)
var params = KiloParams(false, transport, sk, session, null, this@KiloClientConnection)
// Check ID if any
serverHe.serverSharedKey?.let { k ->
if (serverHe.signature == null)
throw RemoteInterface.SecurityException("missing signature")
if (!k.verify(serverHe.signature, params.token))
serverHe.signature?.let { s ->
if (!s.isValid(params.token))
throw RemoteInterface.SecurityException("wrong signature")
params = params.copy(remoteIdentity = k)
params = params.copy(remoteIdentity = s.publicKey)
}
transport.call(
L0ClientId, params.encrypt(
pack(
ClientIdentity(
secretIdKey?.verifying,
secretIdKey?.verifyingKey,
secretIdKey?.sign(params.token)
)
)
@@ -81,12 +80,14 @@ class KiloClientConnection<S>(
kiloRemoteInterface.complete(
KiloRemoteInterface(deferredParams, clientInterface)
)
clientInterface.onConnectHandler?.invoke(params.scope)
onConnectedStateChanged?.invoke(true)
job.join()
} catch (x: CancellationException) {
info { "client is cancelled" }
} catch (x: RemoteInterface.ClosedException) {
x.printStackTrace()
info { "connection closed by remote" }
} finally {
onConnectedStateChanged?.invoke(false)
@@ -1,16 +1,22 @@
package net.sergeych.kiloparsec
/**
* The local interface to provice functions, register errors for Kiloparsec users. Use it
* The local interface to provide functions, register errors for Kiloparsec users. Use it
* with [KiloClient], [KiloClientConnection], [KiloServerConnection], etc.
*
* BAse implementation registers relevant exceptions.
*/
class KiloInterface<S> : LocalInterface<KiloScope<S>>() {
open class KiloInterface<S> : LocalInterface<KiloScope<S>>() {
internal var onConnectHandler: (KiloScope<S>.()->Unit) ? = null
fun onConnected(f: KiloScope<S>.()->Unit) { onConnectHandler = f }
init {
registerError { RemoteInterface.UnknownCommand() }
registerError { RemoteInterface.InternalError(it) }
registerError { RemoteInterface.ClosedException(it) }
registerError { RemoteInterface.SecurityException(it) }
// registerError { RemoteInterface.SecurityException(it) }
registerError { RemoteInterface.InvalidDataException(it) }
registerError { RemoteInterface.RemoteException(it) }
registerError { IllegalStateException() }
@@ -1,8 +1,11 @@
package net.sergeych.kiloparsec
import kotlinx.coroutines.CompletableDeferred
import net.sergeych.tools.AtomicCounter
import net.sergeych.utools.pack
private val idCounter = AtomicCounter(0)
/**
* This class is not normally used directly. This is a local interface that supports
* secure transport command layer (encrypted calls/results) to work with [KiloRemoteInterface].
@@ -12,7 +15,10 @@ import net.sergeych.utools.pack
internal class KiloL0Interface<T>(
private val clientInterface: LocalInterface<KiloScope<T>>,
private val deferredParams: CompletableDeferred<KiloParams<T>>,
): LocalInterface<Unit>() {
) : LocalInterface<Unit>() {
override var logTag: String = "KL0:${idCounter.incrementAndGet()}"
init {
// local interface uses the same session as a client:
addErrorProvider(clientInterface)
@@ -27,7 +33,10 @@ internal class KiloL0Interface<T>(
0u,
clientInterface.execute(params.scope, call.name, call.serializedArgs)
)
} catch (t: Throwable) {
} catch(t: BreakConnectionException) {
throw t
}
catch (t: Throwable) {
clientInterface.encodeError(0u, t)
}
params.encrypt(pack(result))
@@ -1,23 +1,13 @@
package net.sergeych.kiloparsec
import com.ionspin.kotlin.crypto.keyexchange.KeyExchangeSessionKeyPair
import com.ionspin.kotlin.crypto.secretbox.SecretBox
import com.ionspin.kotlin.crypto.secretbox.crypto_secretbox_NONCEBYTES
import com.ionspin.kotlin.crypto.util.decodeFromUByteArray
import com.ionspin.kotlin.crypto.util.encodeToUByteArray
import kotlinx.serialization.Serializable
import net.sergeych.bintools.toDataSource
import net.sergeych.bipack.BipackDecoder
import net.sergeych.bipack.BipackEncoder
import net.sergeych.crypto.DecryptionFailedException
import net.sergeych.crypto.Key
import net.sergeych.crypto.randomBytes
import net.sergeych.crypto.randomUInt
import net.sergeych.tools.ProtectedOp
import net.sergeych.bipack.Unsigned
import net.sergeych.crypto2.*
import net.sergeych.synctools.ProtectedOp
import net.sergeych.synctools.invoke
import net.sergeych.utools.pack
import net.sergeych.utools.unpack
import org.komputing.khash.keccak.Keccak
import org.komputing.khash.keccak.KeccakParameter
import kotlin.math.roundToInt
/**
@@ -32,23 +22,18 @@ import kotlin.math.roundToInt
data class KiloParams<S>(
val isServer: Boolean,
val transport: RemoteInterface,
val sessionKeyPair: KeyExchangeSessionKeyPair,
val sessionKey: SafeKeyExchange.SessionKey,
val scopeSession: S,
val remoteIdentity: Key.Verifying?,
val remoteTransport: RemoteInterface
val remoteIdentity: VerifyingPublicKey?,
val remoteTransport: RemoteInterface,
) {
@Serializable
data class Package(
@Unsigned
val nonce: ULong,
val encryptedMessage: UByteArray,
)
@Serializable
data class FilledData(
val message: UByteArray,
val fill: UByteArray,
)
private var nonce = 0UL
val scope: KiloScope<S> by lazy {
@@ -56,81 +41,39 @@ data class KiloParams<S>(
override val session = scopeSession
override val remote: RemoteInterface = remoteTransport
override val sessionToken: UByteArray = token
override val remoteIdentity: Key.Verifying? = this@KiloParams.remoteIdentity
override val remoteIdentity: VerifyingPublicKey? = this@KiloParams.remoteIdentity
}
}
val token: UByteArray by lazy {
val base = if (isServer) sessionKeyPair.sendKey + sessionKeyPair.receiveKey
else sessionKeyPair.receiveKey + sessionKeyPair.sendKey
Keccak.digest(
base.toByteArray(), KeccakParameter.KECCAK_256
).toUByteArray().sliceArray(0..<crypto_secretbox_NONCEBYTES)
blake2b("token_".encodeToUByteArray() + sessionKey.sessionTag).sliceArray(0..<SymmetricKey.nonceLength)
}
private val sendBase by lazy {
Keccak.digest(
sessionKeyPair.sendKey.toByteArray(), KeccakParameter.KECCAK_256
).toUByteArray().sliceArray(0..<crypto_secretbox_NONCEBYTES)
}
private val numericNonce = NumericNonce(token)
private val receiveBase by lazy {
Keccak.digest(
sessionKeyPair.receiveKey.toByteArray(), KeccakParameter.KECCAK_256
).toUByteArray().sliceArray(0..<crypto_secretbox_NONCEBYTES)
}
private inline fun encodeNonce(base: UByteArray, nonce: ULong): UByteArray {
val result = base.copyOf()
var x = nonce
var i = 0
while (x > 0u) {
result[i] = result[i] xor (x and 0xFFu).toUByte()
x = x shr 8
i++
}
return result
}
private inline fun encodeSendNonce(nonce: ULong): UByteArray = encodeNonce(sendBase, nonce)
private inline fun encodeReceiveNonce(nonce: ULong): UByteArray = encodeNonce(receiveBase, nonce)
fun encrypt(plainText: String): UByteArray = encrypt(plainText.encodeToUByteArray())
private val proptectedOp = ProtectedOp()
private val protectedOp = ProtectedOp()
/**
* Encrypt using send keys and proper nonce
*/
fun encrypt(message: UByteArray, fillFactor: Float = 0f): UByteArray {
val fill: UByteArray = if (fillFactor > 0f)
randomBytes(randomUInt((message.size * fillFactor).roundToInt()))
val fill = if (fillFactor > 0f)
0..(message.size * fillFactor).roundToInt()
else
ubyteArrayOf()
null
val withFill = BipackEncoder.encode(FilledData(message, fill)).toUByteArray()
val n = proptectedOp { nonce++ }
val n = protectedOp.invoke { nonce++ }
return pack(
Package(n, SecretBox.easy(withFill, encodeSendNonce(n), sessionKeyPair.sendKey))
Package(n, sessionKey.encryptWithNonce(message, numericNonce.withULong(n), fill))
)
}
fun decryptString(cipherText: UByteArray): String = decrypt(cipherText).decodeFromUByteArray()
fun decrypt(encryptedMessage: UByteArray): UByteArray {
val p: Package = BipackDecoder.decode(encryptedMessage.toDataSource())
try {
return unpack<FilledData>(
SecretBox.openEasy(
p.encryptedMessage,
encodeReceiveNonce(p.nonce),
sessionKeyPair.receiveKey
)
).message
} catch (_: com.ionspin.kotlin.crypto.secretbox.SecretBoxCorruptedOrTamperedDataExceptionOrInvalidKey) {
throw DecryptionFailedException()
// fun decryptString(cipherText: UByteArray): String = decrypt(cipherText).decodeFromUByteArray()
fun decrypt(encryptedMessage: UByteArray): UByteArray =
protectDecryption {
val p: Package = BipackDecoder.decode(encryptedMessage.toDataSource())
sessionKey.decryptWithNonce(p.encryptedMessage, numericNonce.withULong(p.nonce))
}
}
}
@@ -7,7 +7,13 @@ import net.sergeych.utools.unpack
private var L1IdCounter = 0
// todo: We don't need it deferred here
/**
* Represents a remote interface for interacting with a Kiloparsec service.
*
* @param S the scope type of the Kiloparsec service which will be used to call local functions remotely
* @property deferredParams a [CompletableDeferred] that resolves to the parameters required for making remote calls
* @property clientInterface a [LocalInterface] used to communicate with the local client
*/
class KiloRemoteInterface<S>(
private val deferredParams: CompletableDeferred<KiloParams<S>>,
private val clientInterface: LocalInterface<KiloScope<S>>,
@@ -1,6 +1,7 @@
package net.sergeych.kiloparsec
import net.sergeych.crypto.Key
import net.sergeych.crypto2.SigningKey
import net.sergeych.crypto2.VerifyingPublicKey
/**
* Scope for Kiloparsec client/server commands execution, contain per-connection specific data. The scope
@@ -26,8 +27,8 @@ interface KiloScope<S> {
val sessionToken: UByteArray
/**
* If the remote part has provided a secret key, e.g., gave non-null [Key.Signing] on construction,
* the kiloparsec checks it in the MITM-safe way and provides its [Key.Verifying] shared key here.
* If the remote part has provided a secret key, e.g., gave non-null [SigningKey] on construction,
* the kiloparsec checks it in the MITM-safe way and provides its [VerifyingPublicKey] shared key here.
* Knowing a remote party shared key, it is possible to be sure that the connection is made directly
* to this party with no middle point intruders.
*
@@ -37,6 +38,6 @@ interface KiloScope<S> {
* In spite of the above said, which means, non-null value in this field means the key is authorized, but
* It is up to the caller to ensure it is expected key of the remote party.
*/
val remoteIdentity: Key.Verifying?
val remoteIdentity: VerifyingPublicKey?
}
@@ -0,0 +1,48 @@
package net.sergeych.kiloparsec
import kotlinx.coroutines.CancellationException
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.launch
import net.sergeych.crypto2.SigningKey
import net.sergeych.kiloparsec.adapter.InetTransportDevice
import net.sergeych.mp_logger.LogTag
import net.sergeych.mp_logger.debug
import net.sergeych.mp_logger.exception
import net.sergeych.mp_logger.info
import net.sergeych.mp_tools.globalLaunch
import net.sergeych.tools.AtomicCounter
private val instances = AtomicCounter()
@Suppress("unused")
class KiloServer<S>(
private val clientInterface: KiloInterface<S>,
private val connections: Flow<InetTransportDevice>,
private val serverSecretKey: SigningKey? = null,
private val sessionBuilder: ()->S,
): LogTag("KS:${instances.incrementAndGet()}") {
private val job = globalLaunch {
connections.collect { device ->
launch {
try {
info { "connected ${device}" }
KiloServerConnection(clientInterface, device, sessionBuilder(), serverSecretKey)
.apply { debug { "server connection is ready" }}
.run()
}
catch(_: CancellationException) {
}
catch(cce: LocalInterface.BreakConnectionException) {
info { "Closed exception caught, closing (${cce.flushSendQueue}" }
}
catch (t: Throwable) {
exception { "unexpected while creating kiloclient" to t }
}
}
}
}
fun close() {
job.cancel()
}
}
@@ -1,8 +1,8 @@
package net.sergeych.kiloparsec
import com.ionspin.kotlin.crypto.keyexchange.KeyExchange
import kotlinx.coroutines.CompletableDeferred
import net.sergeych.crypto.Key
import net.sergeych.crypto2.SafeKeyExchange
import net.sergeych.crypto2.SigningKey
import net.sergeych.mp_logger.LogTag
import net.sergeych.mp_logger.Loggable
import net.sergeych.mp_logger.debug
@@ -23,15 +23,15 @@ class KiloServerConnection<S>(
private val clientInterface: KiloInterface<S>,
private val device: Transport.Device,
private val session: S,
private val serverSigningKey: Key.Signing? = null
private val serverSigningKey: SigningKey? = null
) : RemoteInterface, Loggable by LogTag("SRV${++serverIds}") {
/**
* Shortcut to construct with [KiloConnectionData] intance
*/
@Suppress("unused")
constructor(localInterface: KiloInterface<S>, connection: KiloConnectionData<S>, serverSigningKey: Key.Signing? = null)
: this(localInterface, connection.device, connection.session, serverSigningKey)
constructor(localInterface: KiloInterface<S>, connection: KiloConnectionData<S>, serverSecretKey: SigningKey? = null)
: this(localInterface, connection.device, connection.session, serverSecretKey)
private val kiloRemoteInterface = CompletableDeferred<KiloRemoteInterface<S>>()
@@ -47,9 +47,7 @@ class KiloServerConnection<S>(
val l0Interface = KiloL0Interface(clientInterface, deferredParams).apply {
var params: KiloParams<S>? = null
on(L0Request) {
val sk = KeyExchange.serverSessionKeys(
pair.publicKey, pair.secretKey, it.publicKey
)
val sk = pair.serverSessionKey(it.publicKey)
params = KiloParams(
true,
@@ -59,15 +57,9 @@ class KiloServerConnection<S>(
null,
this@KiloServerConnection
)
var verifying: Key.Verifying? = null
var signature: UByteArray? = null
if( serverSigningKey != null ) {
verifying = serverSigningKey.verifying
signature = serverSigningKey.sign(params!!.token)
}
Handshake(1u, pair.publicKey, verifying, signature)
Handshake(1u, pair.publicKey, serverSigningKey?.seal(params!!.token))
}
on(L0ClientId) {
var p = params ?: throw RemoteInterface.ClosedException("wrong handshake sequence")
val ci = unpack<ClientIdentity>(p.decrypt(it))
@@ -83,19 +75,20 @@ class KiloServerConnection<S>(
kiloRemoteInterface.complete(
KiloRemoteInterface(deferredParams, clientInterface)
)
clientInterface.onConnectHandler?.invoke(p.scope)
}
}
val transport = Transport(device, l0Interface, Unit)
deferredTransport.complete(transport)
kiloRemoteInterface.complete(KiloRemoteInterface(deferredParams,clientInterface))
debug { "starintg the transport"}
debug { "starting the transport"}
transport.run()
debug { "server transport finished" }
}
companion object {
val pair = KeyExchange.keypair()
val pair = SafeKeyExchange()
}
override suspend fun <A, R> call(cmd: Command<A, R>, args: A): R {
@@ -1,14 +1,41 @@
package net.sergeych.kiloparsec
import net.sergeych.mp_logger.LogTag
import net.sergeych.mp_logger.Loggable
import net.sergeych.mp_logger.exception
import net.sergeych.mp_logger.info
import net.sergeych.tools.AtomicCounter
import net.sergeych.utools.firstNonNull
import kotlin.reflect.KClass
private typealias RawCommandHandler<C> = suspend (C, UByteArray) -> UByteArray
open class LocalInterface<S> {
private val idCounter = AtomicCounter()
open class LocalInterface<S> : Loggable by LogTag("LocalInterface${idCounter.incrementAndGet()}") {
private val commands = mutableMapOf<String, RawCommandHandler<S>>()
/**
* Instruct the transport to immediately break the connection.
* This exception is not passed to the remote end, instead, transport device breaks
* connection to remote when receiving it.
*
* Remote interface will throw [RemoteInterface.ClosedException] as the break will be detected. As reaction time
* it depends on the transport in use, we recommend sending some registered exception first if you need
* to pass important data, or implement special commands on both sides.
*
* __Important note:__ _it is not allowed to throw [RemoteInterface.ClosedException] directly!_
* This exception is processed internally and can't be sent over the network.
*/
open class BreakConnectionException(
text: String = "break connection request",
val flushSendQueue: Boolean = true,
) : RuntimeException(text) {
override val message: String?
get() = super.message + " (flush=$flushSendQueue)"
}
/**
* New session creator. Rarely needed directlym it can be used for delegation
* of local interfaces.
@@ -71,15 +98,22 @@ open class LocalInterface<S> {
errorByClass[t::class] ?: errorProviders.firstNonNull { it.getErrorCode(t) }
fun encodeError(forId: UInt, t: Throwable): Transport.Block.Error =
getErrorCode(t)?.let { Transport.Block.Error(forId, it, t.message) }
?: Transport.Block.Error(forId, "UnknownError", t.message)
if (t is RemoteInterface.ClosedException) {
exception { "Illegal attempt to send ClosedException" to t }
encodeError(forId, RemoteInterface.InternalError("TCE"))
}
else
getErrorCode(t)?.let { Transport.Block.Error(forId, it, t.message) }
?: Transport.Block.Error(forId, "UnknownError", "${t::class.simpleName}: ${t.message}")
open fun getErrorBuilder(code: String): ((String, UByteArray?) -> Throwable)? =
errorBuilder[code] ?: errorProviders.firstNonNull { it.getErrorBuilder(code) }
fun decodeError(tbe: Transport.Block.Error): Throwable =
getErrorBuilder(tbe.code)?.invoke(tbe.message, tbe.extra)
?: RemoteInterface.RemoteException(tbe)
?: RemoteInterface.RemoteException(tbe).also {
info { "can't decode error ${tbe.code}: ${tbe.message}" }
}
fun decodeAndThrow(tbe: Transport.Block.Error): Nothing {
throw decodeError(tbe)
@@ -1,12 +0,0 @@
package net.sergeych.kiloparsec
import kotlinx.coroutines.channels.Channel
class ProxyDevice(capacity: Int=1024) : Transport.Device {
override val input = Channel<UByteArray?>(capacity)
override val output= Channel<UByteArray?>(capacity)
override suspend fun close() {
runCatching { input.close() }
runCatching { output.close() }
}
}
@@ -15,11 +15,12 @@ interface RemoteInterface {
/**
* Is thrown when the channel is closed, in an attempt to execute a command, also to all pending
* calls (see [call]).
* calls (see [call]). Client code should never throw it. If command handler needs to break connection
* it should throw [LocalInterface.BreakConnectionException]
*/
open class ClosedException(t: String = "connection is closed") : Exception(t)
open class SecurityException(t: String = "invalid remote id and signature") : ClosedException(t)
open class SecurityException(t: String = "invalid remote id and signature") : LocalInterface.BreakConnectionException(t)
open class InvalidDataException(msg: String="invalid data, can't unpack") : Exception(msg)
@@ -41,6 +42,8 @@ interface RemoteInterface {
*/
class UnknownCommand : RemoteException("UnknownCommand")
open class InternalError(code: String="0"): RemoteException("Internal error: $code")
suspend fun <R> call(cmd: Command<Unit, R>): R = call(cmd, Unit)
/**
@@ -1,6 +1,8 @@
package net.sergeych.kiloparsec
import kotlinx.coroutines.*
import kotlinx.coroutines.channels.ClosedReceiveChannelException
import kotlinx.coroutines.channels.ClosedSendChannelException
import kotlinx.coroutines.channels.ReceiveChannel
import kotlinx.coroutines.channels.SendChannel
import kotlinx.coroutines.sync.Mutex
@@ -13,7 +15,7 @@ import kotlinx.serialization.descriptors.SerialDescriptor
import kotlinx.serialization.encoding.Decoder
import kotlinx.serialization.encoding.Encoder
import kotlinx.serialization.serializer
import net.sergeych.crypto.toDump
import net.sergeych.crypto2.toDump
import net.sergeych.kiloparsec.Transport.Device
import net.sergeych.mp_logger.*
import net.sergeych.utools.pack
@@ -41,7 +43,7 @@ class Transport<S>(
* Input blocks. When the device is disconnected, it should send one null to this channel
* to notify the owner. When [close] is called, the channel should be closed.
*/
val input: ReceiveChannel<UByteArray?>
val input: ReceiveChannel<UByteArray>
/**
* Send a binary block to a remote party where it should be received and put into [input]
@@ -54,6 +56,8 @@ class Transport<S>(
* possible. This method must not throw exceptions.
*/
suspend fun close()
suspend fun flush() {}
}
@Serializable(TransportBlockSerializer::class)
@@ -112,8 +116,16 @@ class Transport<S>(
}
// now we have mutex freed so we can call:
val r = device.output.trySend(pack(b).also { debug { ">>>\n${it.toDump()}" } })
if (!r.isSuccess) deferred.completeExceptionally(RemoteInterface.ClosedException())
val r = runCatching { device.output.send(pack(b).also { debug { ">>>\n${it.toDump()}" } }) }
if (!r.isSuccess) {
r.exceptionOrNull()?.let {
exception { "failed to send output block" to it }
} ?: run {
error { "It should not happen: empty exception on block send failure" }
throw RuntimeException("unexpected failure in sending block")
}
deferred.completeExceptionally(RemoteInterface.ClosedException())
}
// it returns packed result or throws a proper error:
return deferred.await()
@@ -137,9 +149,10 @@ class Transport<S>(
suspend fun run() {
coroutineScope {
debug { "awaiting incoming blocks" }
// todo: rewrite it to close the job with no exceptions at all, always
while (isActive && !isClosed) {
try {
device.input.receive()?.let { packed ->
device.input.receive().let { packed ->
debug { "<<<\n${packed.toDump()}" }
val b = unpack<Block>(packed)
debug { "<<$ $b" }
@@ -169,10 +182,12 @@ class Transport<S>(
localInterface.execute(commandContext, b.name, b.packedArgs)
)
)
} catch (x: RemoteInterface.ClosedException) {
// strange case: handler throws closed?
error { "not supported: command handler for $b has thrown ClosedException" }
send(Block.Error(b.id, "UnexpectedException", x.message))
} catch (x: LocalInterface.BreakConnectionException) {
// handler forced close
warning { "handler requested closing of the connection (${x.flushSendQueue}"}
isClosed = true
if( x.flushSendQueue ) device.flush()
device.close()
} catch (x: RemoteInterface.RemoteException) {
send(Block.Error(b.id, x.code, x.text, x.extra))
} catch (t: Throwable) {
@@ -181,24 +196,44 @@ class Transport<S>(
.also { debug { "command executed: ${b.name}" } }
}
}
} ?: run {
debug { "remote channel close received" }
isClosed = true
debug { "=---------------------------------------------"}
}
} catch (_: CancellationException) {
info { "loop is cancelled" }
debug { "input step performed closed=$isClosed active=$isActive"}
} catch (_: ClosedSendChannelException) {
info { "closed send channel" }
isClosed = true
} catch (_: ClosedReceiveChannelException) {
info { "closed receive channel"}
isClosed = true
}
catch(cce: LocalInterface.BreakConnectionException) {
info { "closing connection by local request ($cce)"}
device.close()
}
catch(t: RemoteInterface.ClosedException) {
// it is ok: we just exit the coroutine normally
// and mark we're closing
isClosed = true
}
catch (_: CancellationException) {
info { "loop is cancelled with CancellationException" }
isClosed = true
} catch (t: Throwable) {
exception { "channel closed on error" to t }
info { "isa? $isActive / $isClosed" }
runCatching { device.close() }
isClosed = true
}
}
debug { "leaving transport loop" }
access.withLock {
debug { "access lock obtained"}
isClosed = true
for (c in calls.values) c.completeExceptionally(RemoteInterface.ClosedException())
debug { "closing device $device, calls in queue ${calls.size}" }
runCatching { device.close() }
for (c in calls.values)
c.completeExceptionally(RemoteInterface.ClosedException())
calls.clear()
debug { "calls clear has been called" }
}
debug { "no more active: $isActive / ${calls.size}" }
}
@@ -206,7 +241,12 @@ class Transport<S>(
}
private suspend fun send(block: Block) {
device.output.send(pack(block))
try {
device.output.send(pack(block))
}
catch(_: ClosedSendChannelException) {
throw RemoteInterface.ClosedException()
}
}
}
@@ -1,6 +0,0 @@
package net.sergeych.kiloparsec.adapter
internal data class ConnectionId(
val address: NetworkAddress,
val id: UInt,
)
@@ -1,117 +0,0 @@
package net.sergeych.kiloparsec.adapter
import kotlinx.coroutines.*
import net.sergeych.crypto.Key
import net.sergeych.kiloparsec.KiloClientConnection
import net.sergeych.kiloparsec.KiloInterface
import net.sergeych.kiloparsec.ProxyDevice
import net.sergeych.mp_logger.*
import net.sergeych.mp_tools.globalLaunch
import net.sergeych.tools.AtomicCounter
import net.sergeych.utools.pack
import net.sergeych.utools.unpack
private val instanceCounter = AtomicCounter()
class DatagramClient<S>(
private val connector: DatagramConnector,
val serverAddress: NetworkAddress,
private val localInterface: KiloInterface<S>,
private val clientSigningKey: Key.Signing? = null,
private val createSession: () -> S,
) : LogTag("DGC:${instanceCounter.next()}") {
private var sessionId = 0u
private var isClosed = false
val job = globalLaunch {
while (isActive && !isClosed) {
openSession()
pumpSession()
debug { "closing session $sessionId" }
sessionId = 0u
}
}
private suspend fun pumpSession() {
try {
val proxyDevice = ProxyDevice()
val client = KiloClientConnection<S>(localInterface, proxyDevice, createSession(), clientSigningKey)
coroutineScope {
fun stop() {
cancel()
}
launch {
while (isActive) {
if (sessionId == 0u) {
error { "invalid zero sessionId in pump" }
stop()
} else {
val data = proxyDevice.output.receive()
if (data == null) {
warning { "kiloconnection send empty frame: closing" }
stop()
} else {
send(OuterPacket(sessionId, data))
}
}
}
}
while (isActive) {
val packet: OuterPacket = unpack(connector.incoming.receive().message)
if (packet.id == 0u) {
val result: DatagramResult = unpack(packet.payload)
if (result is DatagramResult.Error) {
if (result.error == DatagramError.Closed) {
info { "connection is closed on the remote part" }
} else {
warning { "unexpected error while pumping: ${result.error}" }
}
} else {
warning { "unexpected result: $result" }
}
stop()
} else {
proxyDevice.input.send(packet.payload)
}
}
}
proxyDevice.close()
} catch (x: CancellationException) {
throw x
} catch (x: Exception) {
x.printStackTrace()
}
}
private suspend fun openSession() {
while (!isClosed) {
send(OuterPacket(0u, ubyteArrayOf()))
val packet: OuterPacket = unpack(connector.incoming.receive().message)
if (packet.id == 0u) {
when (val result: DatagramResult = unpack(packet.payload)) {
is DatagramResult.Error -> {
warning { "error opening connection: ${result.error}" }
}
is DatagramResult.Ok -> {
debug { "connection established, our id is ${result.id}" }
sessionId = result.id
break
}
}
} else {
warning { "received unexpected openSession id: ${packet.id}" }
}
delay(500)
}
}
private suspend fun send(packet: OuterPacket) {
connector.send(pack(packet), serverAddress)
}
fun close() {
job.cancel()
}
}
@@ -1,7 +0,0 @@
package net.sergeych.kiloparsec.adapter
enum class DatagramError {
Closed,
NotExists,
Busy
}
@@ -1,15 +0,0 @@
package net.sergeych.kiloparsec.adapter
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
@Serializable
sealed class DatagramResult {
@Serializable
@SerialName("ok")
data class Ok(val id: UInt) : DatagramResult()
@Serializable
@SerialName("err")
data class Error(val error: DatagramError) : DatagramResult()
}
@@ -1,136 +0,0 @@
package net.sergeych.kiloparsec.adapter
import kotlinx.coroutines.channels.ClosedReceiveChannelException
import kotlinx.coroutines.isActive
import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock
import kotlinx.datetime.Instant
import net.sergeych.crypto.Key
import net.sergeych.kiloparsec.KiloInterface
import net.sergeych.kiloparsec.KiloServerConnection
import net.sergeych.kiloparsec.ProxyDevice
import net.sergeych.mp_logger.LogTag
import net.sergeych.mp_logger.debug
import net.sergeych.mp_tools.globalLaunch
import net.sergeych.tools.AtomicCounter
import net.sergeych.utools.now
import net.sergeych.utools.pack
import net.sergeych.utools.unpack
private val instanceCounter = AtomicCounter()
private class Connection<S>(
val server: DatagramServer<S>,
val connection: KiloServerConnection<S>,
val device: ProxyDevice,
val connectionId: ConnectionId,
var lastActiveAt: Instant = now(),
) {
suspend fun proceedFrame(frame: OuterPacket) {
lastActiveAt = now()
if (frame.payload.isEmpty()) {
// ping
server.sendData(connectionId,ubyteArrayOf())
} else
device.input.send(frame.payload)
}
private val job = globalLaunch {
while (isActive) {
val x = device.output.receive() ?: break
server.sendData(connectionId, x)
}
}
suspend fun close() {
job.cancel()
server.remove(connectionId)
}
}
class DatagramServer<S>(
private val connector: DatagramConnector,
private val localInterface: KiloInterface<S>,
private val serverSigningKey: Key.Signing? = null,
private val createSession: () -> S,
) : LogTag("DGS:${instanceCounter.next()}") {
private val job = globalLaunch {
while (isActive) {
try {
val datagram = connector.incoming.receive()
val outer = unpack<OuterPacket>(datagram.message)
if (outer.id == 0u)
setupNewConnection(datagram.address, outer)
else
proceedExistingConnection(datagram.address, outer)
} catch (_: ClosedReceiveChannelException) {
}
}
}
private val access = Mutex()
private val connections = mutableMapOf<ConnectionId, Connection<S>>()
private fun findFreeId(address: NetworkAddress): UInt {
val existingIds = connections.keys.filter { it.address == address }.map { it.id }.toSet()
debug { "existing connections from $address: $existingIds" }
var i = 0u
while (i in existingIds) i++
debug { "found the free id: $i" }
return i
}
private suspend fun setupNewConnection(address: NetworkAddress, packet: OuterPacket) {
val id = access.withLock {
val connectionId = ConnectionId(address, findFreeId(address))
val proxy = ProxyDevice()
connections[connectionId] = Connection(
this,
KiloServerConnection(localInterface, proxy, createSession(), serverSigningKey),
proxy,
connectionId,
)
connectionId
}
sendResult(id,DatagramResult.Ok(id.id))
}
private suspend fun proceedExistingConnection(address: NetworkAddress, outerPacket: OuterPacket) {
val connectionId = ConnectionId(address, outerPacket.id)
val c = access.withLock { connections[connectionId] }
if (c == null) {
sendResult(connectionId, DatagramResult.Error(DatagramError.NotExists))
} else {
c.proceedFrame(outerPacket)
}
}
/**
* Important. Closing the server also closes the connector which is supposed to be "owned"
* exclusively by `DatagramServer`
*/
fun close() {
job.cancel()
runCatching {
connector.close()
}
}
internal suspend fun remove(connectionId: ConnectionId) {
access.withLock {
connections.remove(connectionId)
}
}
internal suspend fun sendData(connectionId: ConnectionId,data: UByteArray) {
connector.send(pack(OuterPacket(connectionId.id, data)), connectionId.address)
}
internal suspend fun sendResult(connectionId: ConnectionId, result: DatagramResult) {
connector.send(pack(OuterPacket(0u, pack(result))), connectionId.address)
}
}
@@ -0,0 +1,17 @@
package net.sergeych.kiloparsec.adapter
import kotlinx.coroutines.channels.Channel
/**
* Transport device for inet protocol family with graceful shutdown on [close]
*/
@Suppress("unused")
class InetTransportDevice(
inputChannel: Channel<UByteArray>,
outputChannel: Channel<UByteArray>,
val remoteAddress: NetworkAddress,
doClose: (suspend ()->Unit)? = null,
doFlush: (suspend ()->Unit)? = null,
) : ProxyDevice(inputChannel, outputChannel, doClose, doFlush) {
override fun toString(): String = "@$remoteAddress"
}
@@ -1,6 +1,7 @@
package net.sergeych.kiloparsec.adapter
import kotlinx.coroutines.channels.ReceiveChannel
import kotlinx.coroutines.flow.Flow
/**
* Multiplatform implementation of an internet address.
@@ -24,12 +25,6 @@ interface Datagram {
* Address from where the message was sent
*/
val address: NetworkAddress
/**
* Send a datagram in response, e.g., to the [address].
* This method is optimized per single per-datagram use. If you need to send many datagram, use [DatagramConnector].
*/
suspend fun respondWith(message: UByteArray)
}
@OptIn(ExperimentalStdlibApi::class)
@@ -39,7 +34,7 @@ interface DatagramConnector: AutoCloseable {
suspend fun send(message: UByteArray, networkAddress: NetworkAddress)
@Suppress("unused")
suspend fun send(message: UByteArray, datagramAddress: String) {
send(message, networkAddressOf(datagramAddress))
send(message, datagramAddress.toNetworkAddress())
}
suspend fun send(message: UByteArray,host: String,port: Int) =
@@ -47,5 +42,15 @@ interface DatagramConnector: AutoCloseable {
override fun close()
}
expect fun networkAddressOf(address: String): NetworkAddress
expect fun NetworkAddress(host: String,port: Int): NetworkAddress
fun String.toNetworkAddress() : NetworkAddress {
val (host, port) = this.split(":").map { it.trim()}
return NetworkAddress(host, port.toInt())
}
expect fun acceptTcpDevice(port: Int): Flow<InetTransportDevice>
expect suspend fun connectTcpDevice(address: NetworkAddress): InetTransportDevice
suspend fun connectTcpDevice(address: String) = connectTcpDevice(address.toNetworkAddress())
@@ -1,9 +0,0 @@
package net.sergeych.kiloparsec.adapter
import kotlinx.serialization.Serializable
@Serializable
internal data class OuterPacket(
val id: UInt,
val payload: UByteArray,
)
@@ -0,0 +1,40 @@
package net.sergeych.kiloparsec.adapter
import kotlinx.coroutines.channels.Channel
import kotlinx.coroutines.channels.ReceiveChannel
import kotlinx.coroutines.channels.SendChannel
import kotlinx.coroutines.delay
import net.sergeych.kiloparsec.Transport
import net.sergeych.tools.AtomicCounter
private val counter = AtomicCounter()
open class ProxyDevice(
private val inputChannel: Channel<UByteArray>,
private val outputChannel: Channel<UByteArray>,
private val doClose: (suspend ()->Unit)? = null,
private val doFlush: (suspend ()->Unit)? = null,
): Transport.Device {
override val input: ReceiveChannel<UByteArray> = inputChannel
override val output: SendChannel<UByteArray> = outputChannel
override suspend fun close() {
kotlin.runCatching { doClose?.invoke() }
runCatching { inputChannel.close() }
runCatching { outputChannel.close() }
}
override suspend fun flush() {
doFlush?.invoke()
var cnt = 10
while(!outputChannel.isEmpty) {
if (cnt-- < 0) break
delay(50)
}
super.flush()
}
private val id = counter.incrementAndGet()
override fun toString(): String = "PX$id"
}
@@ -0,0 +1,114 @@
package net.sergeych.kiloparsec.adapter
import io.ktor.client.*
import io.ktor.client.plugins.websocket.*
import io.ktor.http.*
import io.ktor.websocket.*
import kotlinx.coroutines.CancellationException
import kotlinx.coroutines.CompletableDeferred
import kotlinx.coroutines.channels.Channel
import kotlinx.coroutines.channels.ClosedReceiveChannelException
import kotlinx.coroutines.channels.ClosedSendChannelException
import kotlinx.coroutines.launch
import net.sergeych.crypto2.SigningKey
import net.sergeych.kiloparsec.KiloClient
import net.sergeych.kiloparsec.KiloConnectionData
import net.sergeych.kiloparsec.KiloInterface
import net.sergeych.kiloparsec.RemoteInterface
import net.sergeych.mp_logger.LogTag
import net.sergeych.mp_logger.exception
import net.sergeych.mp_logger.info
import net.sergeych.mp_logger.warning
import net.sergeych.mp_tools.globalLaunch
import net.sergeych.tools.AtomicCounter
private val counter = AtomicCounter()
fun <S> websocketClient(
path: String,
clientInterface: KiloInterface<S> = KiloInterface(),
client: HttpClient = HttpClient { install(WebSockets) },
secretKey: SigningKey? = null,
sessionMaker: () -> S = {
@Suppress("UNCHECKED_CAST")
Unit as S
},
): KiloClient<S> {
var u = Url(path)
if (u.encodedPath.length <= 1)
u = URLBuilder(u).apply {
encodedPath = "/kp"
}.build()
return KiloClient(clientInterface, secretKey) {
val input = Channel<UByteArray>()
val output = Channel<UByteArray>()
val closeHandle = CompletableDeferred<Boolean>()
globalLaunch {
val log = LogTag("KC:${counter.incrementAndGet()}")
client.webSocket({
url.protocol = u.protocol
url.host = u.host
url.port = u.port
url.encodedPath = u.encodedPath
url.parameters.appendAll(u.parameters)
log.info { "kiloparsec server URL: $url" }
}) {
log.info { "connected to the server" }
// println("SENDING!!!")
// send("Helluva")
launch {
try {
for (block in output) {
send(block.toByteArray())
}
log.info { "input is closed, closing the websocket" }
if (closeHandle.isActive) closeHandle.complete(true)
} catch (_: ClosedSendChannelException) {
log.info { "send channel closed" }
}
catch(_: CancellationException) {}
catch(t: Throwable) {
log.info { "unexpected exception in websock sender: ${t.stackTraceToString()}" }
closeHandle.completeExceptionally(t)
}
if (closeHandle.isActive) closeHandle.complete(false)
}
launch {
try {
for (f in incoming) {
if (f is Frame.Binary) {
input.send(f.readBytes().toUByteArray())
} else {
log.warning { "ignoring unexpected frame of type ${f.frameType}" }
}
}
if (closeHandle.isActive) closeHandle.complete(true)
} catch (_: CancellationException) {
if (closeHandle.isActive) closeHandle.complete(false)
} catch (_: ClosedReceiveChannelException) {
log.warning { "receive channel closed unexpectedly" }
if (closeHandle.isActive) closeHandle.complete(false)
} catch (t: Throwable) {
log.exception { "unexpected error" to t }
if (closeHandle.isActive) closeHandle.complete(false)
}
}
if(!closeHandle.await()) {
log.warning { "Client is closing with error" }
throw RemoteInterface.ClosedException()
}
output.close()
input.close()
}
log.info { "closing connection" }
}
val device = ProxyDevice(input, output) {
// we need to explicitly close the coroutine job, or it can hang for a long time
// leaking resources.
closeHandle.complete(true)
// job.cancel()
}
KiloConnectionData(device, sessionMaker())
}
}
@@ -1,16 +1,17 @@
package net.sergeych.kiloparsec
import kotlinx.serialization.Serializable
import net.sergeych.crypto.Key
import net.sergeych.crypto2.SafeKeyExchange
import net.sergeych.crypto2.Seal
import net.sergeych.crypto2.VerifyingPublicKey
// L0 commands - key exchange and check:
@Serializable
data class Handshake(val version: UInt, val publicKey: UByteArray,
val serverSharedKey: Key.Verifying? = null,
val signature: UByteArray? = null)
data class Handshake(val version: UInt, val publicKey: SafeKeyExchange.PublicKey,
val signature: Seal? = null)
@Serializable
data class ClientIdentity(val clientIdKey: Key.Verifying?, val signature: UByteArray?)
data class ClientIdentity(val clientIdKey: VerifyingPublicKey?, val signature: UByteArray?)
// Level 0 command: request key exchange
internal val L0Request by command<Handshake, Handshake>()
@@ -0,0 +1,6 @@
package net.sergeych.kiloparsec
fun String.encodeToUByteArray() =
encodeToByteArray().toUByteArray()
fun UByteArray.decodeFromUByteArray(): String = toByteArray().decodeToString()
@@ -1,8 +0,0 @@
package net.sergeych.tools
class AtomicCounter(startWith: Long=0) {
private var op = ProtectedOp()
private var counter = startWith
fun next(): Long = op { counter++ }
}
@@ -1,21 +0,0 @@
package net.sergeych.tools
/**
* Multiplatform interface to perform a regular (not suspend) operation
* protected by a platform mutex (where necessary). Get real implementation
* with [ProtectedOp]
*/
interface ProtectedOpImplementation {
/**
* Call [f] iin mutually exclusive mode, it means that only one invocation
* can be active at a time, all the rest are waiting until the current operation
* will finish.
*/
operator fun <T>invoke(f: ()->T): T
}
/**
* Get the platform-depended implementation of a mutex-protected operation.
*/
expect fun ProtectedOp(): ProtectedOpImplementation
@@ -1,12 +0,0 @@
package net.sergeych.utools
/**
* Scan the collection and return the first non-null result of the [predicate] on it.
* If all the elements give null with predicate call, returns null.
*
* Note that collection is scanned only to the first non-null predicate result.
*/
fun <T,R>Collection<T>.firstNonNull(predicate: (T)->R?): R? {
for( x in this ) predicate(x)?.let { return it }
return null
}
@@ -1,46 +0,0 @@
package net.sergeych.utools
import kotlinx.serialization.KSerializer
import kotlinx.serialization.serializer
import net.sergeych.bintools.toDataSource
import net.sergeych.bipack.BipackDecoder
import net.sergeych.bipack.BipackEncoder
/**
* Effectively pack anyk nullable object. The result could be effectively packed
* in turn as a part of a more complex structure.
*
* To avoid packing non-null mark,
* we use a zero-size array, which, if in turn encoded, packs into a single
* zero byte. Thus, we avoid extra byte spending for unnecessary null
* check.
*/
inline fun <reified T> pack(element: T?): UByteArray = pack(serializer<T>(), element)
/**
* Unpack nullable data packed with [pack]
*/
inline fun <reified T: Any?> unpack(encoded: UByteArray): T =
unpack(serializer<T>(), encoded)
/**
* Effectively pack anyk nullable object. The result could be effectively packed
* in turn as a part of a more complex structure.
*
* To avoid packing non-null mark,
* we use a zero-size array, which, if in turn encoded, packs into a single
* zero byte. Thus, we avoid extra byte spending for unnecessary null
* check.
*/
fun <T>pack(serializer: KSerializer<T>, element: T?): UByteArray =
if (element == null) ubyteArrayOf()
else BipackEncoder.encode(serializer,element).toUByteArray()
/**
* Unpack nullable data packed with [pack]
*/
@Suppress("UNCHECKED_CAST")
fun <T: Any?> unpack(serializer: KSerializer<T>, encoded: UByteArray): T =
if (encoded.isEmpty()) null as T
else BipackDecoder.decode(encoded.toByteArray().toDataSource(),serializer)
@@ -1,12 +0,0 @@
@file:Suppress("unused")
package net.sergeych.utools
import kotlinx.datetime.Clock
import kotlinx.datetime.Instant
fun now(): Instant = Clock.System.now()
fun nowToSeconds(): Instant = Clock.System.now().truncateToSeconds()
fun Instant.truncateToSeconds(): Instant =
Instant.fromEpochSeconds(toEpochMilliseconds()/1000)
@@ -1,183 +0,0 @@
package org.komputing.khash.keccak
import com.ionspin.kotlin.bignum.integer.BigInteger
import org.komputing.khash.keccak.extensions.fillWith
import kotlin.math.min
object Keccak {
private val BIT_65 = BigInteger.ONE shl (64)
private val MAX_64_BITS = BIT_65 - BigInteger.ONE
fun digest(value: ByteArray, parameter: KeccakParameter): ByteArray {
val uState = IntArray(200)
val uMessage = convertToUInt(value)
var blockSize = 0
var inputOffset = 0
// Absorbing phase
while (inputOffset < uMessage.size) {
blockSize = min(uMessage.size - inputOffset, parameter.rateInBytes)
for (i in 0 until blockSize) {
uState[i] = uState[i] xor uMessage[i + inputOffset]
}
inputOffset += blockSize
if (blockSize == parameter.rateInBytes) {
doF(uState)
blockSize = 0
}
}
// Padding phase
uState[blockSize] = uState[blockSize] xor parameter.d
if (parameter.d and 0x80 != 0 && blockSize == parameter.rateInBytes - 1) {
doF(uState)
}
uState[parameter.rateInBytes - 1] = uState[parameter.rateInBytes - 1] xor 0x80
doF(uState)
// Squeezing phase
val byteResults = mutableListOf<Byte>()
var tOutputLen = parameter.outputLengthInBytes
while (tOutputLen > 0) {
blockSize = min(tOutputLen, parameter.rateInBytes)
for (i in 0 until blockSize) {
byteResults.add(uState[i].toByte().toInt().toByte())
}
tOutputLen -= blockSize
if (tOutputLen > 0) {
doF(uState)
}
}
return byteResults.toByteArray()
}
private fun doF(uState: IntArray) {
val lState = Array(5) { Array(5) { BigInteger.ZERO } }
for (i in 0..4) {
for (j in 0..4) {
val data = IntArray(8)
val index = 8 * (i + 5 * j)
uState.copyInto(data, 0, index, index + data.size)
lState[i][j] = convertFromLittleEndianTo64(data)
}
}
roundB(lState)
uState.fillWith(0)
for (i in 0..4) {
for (j in 0..4) {
val data = convertFrom64ToLittleEndian(lState[i][j])
data.copyInto(uState, 8 * (i + 5 * j))
}
}
}
/**
* Permutation on the given state.
*/
private fun roundB(state: Array<Array<BigInteger>>) {
var lfsrState = 1
for (round in 0..23) {
val c = arrayOfNulls<BigInteger>(5)
val d = arrayOfNulls<BigInteger>(5)
// θ step
for (i in 0..4) {
c[i] = state[i][0].xor(state[i][1]).xor(state[i][2]).xor(state[i][3]).xor(state[i][4])
}
for (i in 0..4) {
d[i] = c[(i + 4) % 5]!!.xor(c[(i + 1) % 5]!!.leftRotate64(1))
}
for (i in 0..4) {
for (j in 0..4) {
state[i][j] = state[i][j].xor(d[i]!!)
}
}
// ρ and π steps
var x = 1
var y = 0
var current = state[x][y]
for (i in 0..23) {
val tX = x
x = y
y = (2 * tX + 3 * y) % 5
val shiftValue = current
current = state[x][y]
state[x][y] = shiftValue.leftRotate64Safely((i + 1) * (i + 2) / 2)
}
// χ step
for (j in 0..4) {
val t = arrayOfNulls<BigInteger>(5)
for (i in 0..4) {
t[i] = state[i][j]
}
for (i in 0..4) {
// ~t[(i + 1) % 5]
val invertVal = t[(i + 1) % 5]!!.xor(MAX_64_BITS)
// t[i] ^ ((~t[(i + 1) % 5]) & t[(i + 2) % 5])
state[i][j] = t[i]!!.xor(invertVal.and(t[(i + 2) % 5]!!))
}
}
// ι step
for (i in 0..6) {
lfsrState = (lfsrState shl 1 xor (lfsrState shr 7) * 0x71) % 256
// pow(2, i) - 1
val bitPosition = (1 shl i) - 1
if (lfsrState and 2 != 0) {
state[0][0] = state[0][0].xor(BigInteger.ONE shl bitPosition)
}
}
}
}
/**
* Converts the given [data] array to an [IntArray] containing UInt values.
*/
private fun convertToUInt(data: ByteArray) = IntArray(data.size) {
data[it].toInt() and 0xFF
}
/**
* Converts the given [data] array containing the little endian representation of a number to a [BigInteger].
*/
private fun convertFromLittleEndianTo64(data: IntArray): BigInteger {
val value = data.map { it.toString(16) }
.map { if (it.length == 2) it else "0$it" }
.reversed()
.joinToString("")
return BigInteger.parseString(value, 16)
}
/**
* Converts the given [BigInteger] to a little endian representation as an [IntArray].
*/
private fun convertFrom64ToLittleEndian(uLong: BigInteger): IntArray {
val asHex = uLong.toString(16)
val asHexPadded = "0".repeat((8 * 2) - asHex.length) + asHex
return IntArray(8) {
((7 - it) * 2).let { pos ->
asHexPadded.substring(pos, pos + 2).toInt(16)
}
}
}
private fun BigInteger.leftRotate64Safely(rotate: Int) = leftRotate64(rotate % 64)
private fun BigInteger.leftRotate64(rotate: Int) = (this shr (64 - rotate)).add(this shl rotate).mod(BIT_65)
}
@@ -1,22 +0,0 @@
@file:Suppress("unused")
package org.komputing.khash.keccak
/**
* Parameters defining the FIPS 202 standard.
*/
enum class KeccakParameter(val rateInBytes: Int,val outputLengthInBytes: Int, val d: Int) {
KECCAK_224(144, 28, 0x01),
KECCAK_256(136, 32, 0x01),
KECCAK_384(104, 48, 0x01),
KECCAK_512(72, 64, 0x01),
SHA3_224(144, 28, 0x06),
SHA3_256(136, 32, 0x06),
SHA3_384(104, 48, 0x06),
SHA3_512(72, 64, 0x06),
SHAKE128(168, 32, 0x1F),
SHAKE256(136, 64, 0x1F)
}
@@ -1,42 +0,0 @@
package org.komputing.khash.keccak.extensions
/**
* Assigns the specified int value to each element of the specified
* range in the specified array of ints. The range to be filled
* extends from index <tt>fromIndex</tt>, inclusive, to index
* <tt>toIndex</tt>, exclusive. (If <tt>fromIndex==toIndex</tt>, the
* range to be filled is empty.)
*
* @param fromIndex the index of the first element (inclusive) to be
* filled with the specified value
* @param toIndex the index of the last element (exclusive) to be
* filled with the specified value
* @param value the value to be stored in all elements of the array
* @throws IllegalArgumentException if <tt>fromIndex &gt; toIndex</tt>
* @throws ArrayIndexOutOfBoundsException if <tt>fromIndex &lt; 0</tt> or
* <tt>toIndex &gt; a.length</tt>
*/
internal fun IntArray.fillWith(value: Int, fromIndex: Int = 0, toIndex: Int = this.size) {
if (fromIndex > toIndex) {
throw IllegalArgumentException(
"fromIndex($fromIndex) > toIndex($toIndex)"
)
}
if (fromIndex < 0) {
throw ArrayIndexOutOfBoundsException(fromIndex)
}
if (toIndex > this.size) {
throw ArrayIndexOutOfBoundsException(toIndex)
}
for (i in fromIndex until toIndex)
this[i] = value
}
/**
* Constructs a new [ArrayIndexOutOfBoundsException]
* class with an argument indicating the illegal index.
* @param index the illegal index.
*/
internal class ArrayIndexOutOfBoundsException(index: Int) : Throwable("Array index out of range: $index")
@@ -1,19 +0,0 @@
@file:Suppress("unused")
package org.komputing.khash.keccak.extensions
import org.komputing.khash.keccak.Keccak
import org.komputing.khash.keccak.KeccakParameter
/**
* Computes the proper Keccak digest of [this] byte array based on the given [parameter]
*/
fun ByteArray.digestKeccak(parameter: KeccakParameter): ByteArray {
return Keccak.digest(this, parameter)
}
/**
* Computes the proper Keccak digest of [this] string based on the given [parameter]
*/
fun String.digestKeccak(parameter: KeccakParameter): ByteArray {
return Keccak.digest(encodeToByteArray(), parameter)
}
+17 -27
View File
@@ -1,8 +1,9 @@
import com.ionspin.kotlin.crypto.secretbox.SecretBox
import com.ionspin.kotlin.crypto.util.decodeFromUByteArray
import com.ionspin.kotlin.crypto.util.encodeToUByteArray
import kotlinx.coroutines.test.runTest
import net.sergeych.crypto.*
import net.sergeych.crypto2.IllegalSignatureException
import net.sergeych.crypto2.SealedBox
import net.sergeych.crypto2.SigningSecretKey
import net.sergeych.crypto2.initCrypto
import net.sergeych.kiloparsec.encodeToUByteArray
import net.sergeych.utools.pack
import net.sergeych.utools.unpack
import kotlin.test.*
@@ -11,47 +12,36 @@ class KeysTest {
@Test
fun testCreationAndMap() = runTest {
initCrypto()
val (stk,pbk) = Key.Signing.pair()
val (stk,pbk) = SigningSecretKey.generatePair()
val x = mapOf( stk to "STK!", pbk to "PBK!")
assertEquals("STK!", x[stk])
val s1 = Key.Signing(stk.packed)
val s1 = SigningSecretKey(stk.keyBytes)
assertEquals(stk, s1)
assertEquals("STK!", x[s1])
assertEquals("PBK!", x[pbk])
val data = "8 rays dev!".encodeToUByteArray()
val data1 = "8 rays dev!".encodeToUByteArray()
val s = SignedBox.Seal.create(stk, data)
assertTrue(s.verify(data))
val s = stk.seal(data)
assertTrue(s.isValid(data))
data1[0] = 0x01u
assertFalse(s.verify(data1))
val p2 = Key.Signing.pair()
val p3 = Key.Signing.pair()
assertFalse(s.isValid(data1))
val p2 = SigningSecretKey.generatePair()
val p3 = SigningSecretKey.generatePair()
val ms = SignedBox(data, s1) + p2.signing
val ms = SealedBox(data, s1) + p2.secretKey
// non tampered:
val ms1 = unpack<SignedBox>(pack(ms))
val ms1 = unpack<SealedBox>(pack(ms))
assertContentEquals(data, ms1.message)
assertTrue(pbk in ms1)
assertTrue(p2.verifying in ms1)
assertTrue(p3.verifying !in ms1)
assertTrue(p2.publicKey in ms1)
assertTrue(p3.publicKey !in ms1)
assertThrows<IllegalSignatureException> {
unpack<SignedBox>(pack(ms).also { it[3] = 1u })
}
}
@Test
fun secretEncryptTest() = runTest {
initCrypto()
val key = SecretBox.keygen()
val key1 = SecretBox.keygen()
assertEquals("hello", decrypt(key, encrypt(key, "hello".encodeToUByteArray())).decodeFromUByteArray())
assertThrows<DecryptionFailedException> {
decrypt(key, encrypt(key1, "hello".encodeToUByteArray())).decodeFromUByteArray()
unpack<SealedBox>(pack(ms).also { it[3] = 1u })
}
}
+10 -1
View File
@@ -1,11 +1,14 @@
import kotlinx.coroutines.test.runTest
import kotlinx.datetime.Instant
import net.sergeych.crypto.initCrypto
import net.sergeych.bipack.BipackEncoder
import net.sergeych.crypto2.initCrypto
import net.sergeych.kiloparsec.KiloParams
import net.sergeych.kiloparsec.Transport
import net.sergeych.utools.nowToSeconds
import net.sergeych.utools.pack
import net.sergeych.utools.unpack
import kotlin.test.Test
import kotlin.test.assertContentEquals
import kotlin.test.assertEquals
import kotlin.time.Duration.Companion.microseconds
@@ -40,4 +43,10 @@ class PackTest {
val b2 = unpack<Transport.Block>(p1)
assertEquals(b1,b2)
}
@Test
fun packPackage() = runTest {
val x = BipackEncoder.encode(KiloParams.Package(1u, ubyteArrayOf()))
assertContentEquals(byteArrayOf(4, 0), x)
}
}
+5 -18
View File
@@ -1,20 +1,7 @@
import kotlinx.coroutines.test.runTest
import net.sergeych.crypto.createContrail
import net.sergeych.crypto.initCrypto
import net.sergeych.crypto.isValidContrail
import kotlin.test.Test
import kotlin.test.assertEquals
import kotlin.test.assertFalse
import kotlin.test.assertTrue
class ToolsTest {
@Test
fun testContrails() = runTest {
initCrypto()
val c = createContrail(ubyteArrayOf(1u, 2u, 3u, 4u, 5u))
assertEquals(134u, c[0])
assertTrue { isValidContrail(c) }
c[2] = 11u
assertFalse { isValidContrail(c) }
}
// @Test
// fun testRemoceCmd() {
// assertEquals("lalala", removeCmd("lalala"))
// assertEquals("lalala", removeCmd("cmdlalala"))
// }
}
+70 -35
View File
@@ -1,15 +1,17 @@
import com.ionspin.kotlin.crypto.keyexchange.KeyExchange
import kotlinx.coroutines.*
import kotlinx.coroutines.channels.Channel
import kotlinx.coroutines.channels.ReceiveChannel
import kotlinx.coroutines.channels.SendChannel
import kotlinx.coroutines.test.runTest
import net.sergeych.crypto.Key
import net.sergeych.crypto.initCrypto
import net.sergeych.kiloparsec.command
import net.sergeych.crypto2.SigningSecretKey
import net.sergeych.crypto2.VerifyingPublicKey
import net.sergeych.crypto2.initCrypto
import net.sergeych.kiloparsec.*
import net.sergeych.mp_logger.Log
import kotlin.test.*
import kotlin.test.Test
import kotlin.test.assertContentEquals
import kotlin.test.assertEquals
import kotlin.test.fail
private var dcnt = 0
fun createTestDevice(): Pair<Transport.Device, Transport.Device> {
@@ -17,8 +19,9 @@ fun createTestDevice(): Pair<Transport.Device, Transport.Device> {
val p2 = Channel<UByteArray>(256)
val id = ++dcnt
val d1 = object : Transport.Device {
override val input: ReceiveChannel<UByteArray?> = p1
override val input: ReceiveChannel<UByteArray> = p1
override val output: SendChannel<UByteArray> = p2
override suspend fun close() {
p2.close()
}
@@ -28,7 +31,7 @@ fun createTestDevice(): Pair<Transport.Device, Transport.Device> {
}
}
val d2 = object : Transport.Device {
override val input: ReceiveChannel<UByteArray?> = p2
override val input: ReceiveChannel<UByteArray> = p2
override val output: SendChannel<UByteArray> = p1
override suspend fun close() {
p1.close()
@@ -70,29 +73,29 @@ class TransportTest {
val t1 = Transport(d1, l1, Unit)
val t2 = Transport(d2, l2, Unit)
val clip = KeyExchange.keypair()
val serp = KeyExchange.keypair()
val clisk = KeyExchange.clientSessionKeys(clip.publicKey, clip.secretKey, serp.publicKey)
val sersk = KeyExchange.serverSessionKeys(serp.publicKey, serp.secretKey, clip.publicKey)
val pser = KiloParams(true, t1, sersk, Unit, null, t1)
val pcli = KiloParams(false, t2, clisk, Unit, null, t2)
assertContentEquals(pcli.token, pser.token)
assertEquals(pser.decryptString(pcli.encrypt("hello!")), "hello!")
assertEquals(pser.decryptString(pcli.encrypt("hello!")), "hello!")
assertEquals(pser.decryptString(pcli.encrypt("hello2!")), "hello2!")
assertEquals(pser.decryptString(pcli.encrypt("hello3!")), "hello3!")
assertEquals(pser.decryptString(pcli.encrypt("hello!")), "hello!")
// val clip = KeyExchange.keypair()
// val serp = KeyExchange.keypair()
// val clisk = KeyExchange.clientSessionKeys(clip.publicKey, clip.secretKey, serp.publicKey)
// val sersk = KeyExchange.serverSessionKeys(serp.publicKey, serp.secretKey, clip.publicKey)
// val pser = KiloParams(true, t1, sersk, Unit, null, t1)
// val pcli = KiloParams(false, t2, clisk, Unit, null, t2)
// assertContentEquals(pcli.token, pser.token)
// assertEquals(pser.decryptString(pcli.encrypt("hello!")), "hello!")
// assertEquals(pser.decryptString(pcli.encrypt("hello!")), "hello!")
// assertEquals(pser.decryptString(pcli.encrypt("hello2!")), "hello2!")
// assertEquals(pser.decryptString(pcli.encrypt("hello3!")), "hello3!")
// assertEquals(pser.decryptString(pcli.encrypt("hello!")), "hello!")
//
// test nonce increment
assertFalse { pcli.encrypt("once") contentEquals pcli.encrypt("once") }
// assertFalse { pcli.encrypt("once") contentEquals pcli.encrypt("once") }
assertEquals(pcli.decryptString(pser.encrypt("hello!")), "hello!")
assertEquals(pcli.decryptString(pser.encrypt("hello!")), "hello!")
assertEquals(pcli.decryptString(pser.encrypt("hello!")), "hello!")
assertEquals(pcli.decryptString(pser.encrypt("hello!")), "hello!")
assertEquals(pcli.decryptString(pser.encrypt("hello!")), "hello!")
assertEquals(pcli.decryptString(pser.encrypt("hello!")), "hello!")
// assertEquals(pcli.decryptString(pser.encrypt("hello!")), "hello!")
// assertEquals(pcli.decryptString(pser.encrypt("hello!")), "hello!")
// assertEquals(pcli.decryptString(pser.encrypt("hello!")), "hello!")
// assertEquals(pcli.decryptString(pser.encrypt("hello!")), "hello!")
// assertEquals(pcli.decryptString(pser.encrypt("hello!")), "hello!")
// assertEquals(pcli.decryptString(pser.encrypt("hello!")), "hello!")
coroutineScope {
@@ -156,6 +159,8 @@ class TransportTest {
d2.close()
}
class TestException(text: String) : Exception(text)
@Test
fun testClient() = runTest {
initCrypto()
@@ -163,7 +168,7 @@ class TransportTest {
val cmdPing by command<String, String>()
val cmdPush by command<String, String>()
val cmdGetToken by command<Unit, UByteArray>()
val cmdGetClientId by command<Unit, Key.Verifying?>()
val cmdGetClientId by command<Unit, VerifyingPublicKey?>()
val cmdChainCallServer1 by command<String, String>()
val cmdChainCallClient1 by command<String, String>()
val cmdChainCallServer2 by command<String, String>()
@@ -172,8 +177,12 @@ class TransportTest {
// Log.defaultLevel = Log.Level.DEBUG
val (d1, d2) = createTestDevice()
val serverId = Key.Signing.pair()
val clientId = Key.Signing.pair()
val serverId = SigningSecretKey.generatePair()
val clientId = SigningSecretKey.generatePair()
val cmdException by command<Unit, Unit>()
val cmdRemoteExceptionTest by command<Unit, String>()
val cmdBreak by command<Unit, Unit>()
val serverInterface = KiloInterface<String>().apply {
on(cmdPing) {
@@ -191,16 +200,28 @@ class TransportTest {
on(cmdChainCallServer2) {
remote.call(cmdChainCallClient2, "$it-s2")
}
registerError { IllegalStateException() }
registerError { IllegalArgumentException(it) }
on(cmdException) { throw TestException("te1") }
on(cmdRemoteExceptionTest) {
try {
remote.call(cmdException)
"error!"
} catch (e: TestException) {
"ok: ${e.message}"
}
}
on(cmdBreak) { throw LocalInterface.BreakConnectionException() }
registerError { TestException(it) }
}
val kiloServerConnection = KiloServerConnection(serverInterface, d1, "server session", serverId.signing)
val kiloServerConnection = KiloServerConnection(
serverInterface, d1, "server session", serverId.secretKey
)
launch { kiloServerConnection.run() }
var cnt = 0
val client = KiloClient {
addErrors(serverInterface)
session { "client session!" }
secretIdKey = clientId.signing
secretIdKey = clientId.secretKey
local {
on(cmdPush) {
"server push: $it"
@@ -209,11 +230,13 @@ class TransportTest {
"client pong: $it"
}
on(cmdChainCallClient1) {
remote.call(cmdChainCallServer2,"$it-c1")
remote.call(cmdChainCallServer2, "$it-c1")
}
on(cmdChainCallClient2) { "$it-c2" }
on(cmdException) { throw TestException("te-local") }
}
connect {
println("Called connect: $cnt")
if (cnt++ > 0) {
cancel()
fail("connect called once again")
@@ -227,6 +250,18 @@ class TransportTest {
assertEquals("server push: bar", kiloServerConnection.call(cmdPush, "bar"))
assertEquals("**-s1-c1-s2-c2", client.call(cmdChainCallServer1, "**"))
assertThrows<TestException> { client.call(cmdException) }
assertEquals("ok: te-local", client.call(cmdRemoteExceptionTest))
assertThrows<RemoteInterface.ClosedException> {
client.call(cmdBreak)
}
// Note that current transport test is too simple,
// therefore, we can't test reconnecting, also we need server and client instances
// not connections, so that's all
d1.close()
d2.close()
client.close()
+2 -1
View File
@@ -8,6 +8,7 @@ inline fun <reified T: Throwable>assertThrows(f: ()->Unit): T {
}
catch(x: Throwable) {
if( x is T ) return x
fail("expected to throw $name but instead threw ${x::class.simpleName}: $x")
println("expected to throw $name but instead threw ${x::class.simpleName}: $x\b\n${x.stackTraceToString()}")
fail("expected to throw $name but instead threw ${x::class.simpleName}: $x\b\n${x.stackTraceToString()}")
}
}
@@ -1,5 +1,2 @@
package net.sergeych.kiloparsec.adapter
actual fun networkAddressOf(address: String): NetworkAddress {
TODO("Not yet implemented")
}
@@ -1,5 +1,15 @@
package net.sergeych.kiloparsec.adapter
import kotlinx.coroutines.flow.Flow
actual fun NetworkAddress(host: String, port: Int): NetworkAddress {
TODO("Not yet implemented")
}
actual fun acceptTcpDevice(port: Int): Flow<InetTransportDevice> {
TODO("Not yet implemented")
}
actual suspend fun connectTcpDevice(address: NetworkAddress): InetTransportDevice {
TODO("Not yet implemented")
}
@@ -1,6 +0,0 @@
package net.sergeych.tools
actual fun ProtectedOp(): ProtectedOpImplementation = object : ProtectedOpImplementation {
// JS targets are inherently single-threaded, so we do noting:
override fun <T> invoke(f: () -> T): T = f()
}
@@ -0,0 +1,20 @@
package net.sergeych.kiloparsec.adapter
import java.nio.channels.CompletionHandler
import kotlin.coroutines.Continuation
import kotlin.coroutines.resume
import kotlin.coroutines.resumeWithException
open class ContinuationHandler<T> : CompletionHandler<T, Continuation<T>> {
override fun completed(result: T, attachment: Continuation<T>) {
attachment.resume(result)
}
override fun failed(exc: Throwable, attachment: Continuation<T>) {
attachment.resumeWithException(exc)
}
}
object VoidCompletionHandler: ContinuationHandler<Void>()
object IntCompletionHandler: ContinuationHandler<Int>()
@@ -1,8 +0,0 @@
package net.sergeych.kiloparsec.adapter
import java.net.InetAddress
actual fun networkAddressOf(address: String): NetworkAddress {
val (host,port) = address.split(":")
return JvmNetworkAddress(InetAddress.getByName(host), port.toInt())
}
@@ -1,6 +1,49 @@
package net.sergeych.kiloparsec.adapter
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.flow
import kotlinx.coroutines.suspendCancellableCoroutine
import kotlinx.coroutines.withContext
import java.net.InetAddress
import java.net.InetSocketAddress
import java.nio.channels.AsynchronousServerSocketChannel
import java.nio.channels.AsynchronousSocketChannel
import kotlin.coroutines.suspendCoroutine
actual fun NetworkAddress(host: String, port: Int): NetworkAddress =
JvmNetworkAddress(InetAddress.getByName(host), port)
actual fun acceptTcpDevice(port: Int): Flow<InetTransportDevice> {
return flow {
val socket = withContext(Dispatchers.IO) {
AsynchronousServerSocketChannel.open().also {
it.bind(InetSocketAddress(port))
}
}
while (true) {
println("0 --- $port")
val connectedSocket = suspendCancellableCoroutine { continuation ->
continuation.invokeOnCancellation {
socket.close()
}
socket.accept(continuation, ContinuationHandler())
}
println("1 ---")
emit(asyncSocketToDevice(connectedSocket))
}
}
}
@Suppress("unused")
suspend fun connectTcpDevice(host: String, port: Int) = connectTcpDevice(NetworkAddress(host,port))
actual suspend fun connectTcpDevice(address: NetworkAddress): InetTransportDevice {
address as JvmNetworkAddress
val socket = withContext(Dispatchers.IO) {
AsynchronousSocketChannel.open()
}
suspendCoroutine { cont ->
socket.connect(address.socketAddress, cont, VoidCompletionHandler)
}
return asyncSocketToDevice(socket)
}
@@ -7,15 +7,13 @@ import net.sergeych.mp_logger.LogTag
import net.sergeych.mp_logger.exception
import net.sergeych.mp_logger.info
import net.sergeych.mp_logger.warning
import java.net.DatagramPacket
import java.net.DatagramSocket
import java.net.InetAddress
import java.net.*
import java.util.concurrent.atomic.AtomicInteger
private val counter = AtomicInteger(0)
class JvmNetworkAddress(val inetAddress: InetAddress, override val port: Int) : NetworkAddress {
override val host: String by lazy { inetAddress.hostName }
override val host: String by lazy { inetAddress.canonicalHostName }
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (other !is JvmNetworkAddress) return false
@@ -26,12 +24,15 @@ class JvmNetworkAddress(val inetAddress: InetAddress, override val port: Int) :
return true
}
val socketAddress: SocketAddress by lazy { InetSocketAddress(inetAddress,port) }
override fun hashCode(): Int {
var result = inetAddress.hashCode()
result = 31 * result + port
return result
}
override fun toString(): String = "$host:$port"
}
class UdpDatagram(override val message: UByteArray, val inetAddress: InetAddress, val port: Int) : Datagram {
@@ -40,22 +41,6 @@ class UdpDatagram(override val message: UByteArray, val inetAddress: InetAddress
JvmNetworkAddress(inetAddress, port)
}
override suspend fun respondWith(message: UByteArray) {
withContext(Dispatchers.IO) {
// We expect datagram is most often respond once or not responded at all, and
// we create and close a socket to send
DatagramSocket().use { socket ->
val packet = DatagramPacket(
message.toByteArray(),
message.size,
inetAddress,
port
)
socket.send(packet)
}
}
}
}
@@ -0,0 +1,92 @@
package net.sergeych.kiloparsec.adapter
import io.ktor.server.application.*
import io.ktor.server.routing.*
import io.ktor.server.websocket.*
import io.ktor.websocket.*
import kotlinx.coroutines.cancel
import kotlinx.coroutines.channels.Channel
import kotlinx.coroutines.channels.ClosedReceiveChannelException
import kotlinx.coroutines.isActive
import kotlinx.coroutines.launch
import net.sergeych.crypto2.SigningKey
import net.sergeych.crypto2.toDump
import net.sergeych.kiloparsec.KiloInterface
import net.sergeych.kiloparsec.KiloServerConnection
import net.sergeych.kiloparsec.RemoteInterface
import net.sergeych.mp_logger.*
import net.sergeych.tools.AtomicCounter
import java.time.Duration
fun <S> Application.setupWebsocketServer(
localInterface: KiloInterface<S>,
path: String = "/kp",
serverKey: SigningKey? = null,
createSession: () -> S,
) {
install(Routing)
install(WebSockets) {
pingPeriod = Duration.ofSeconds(15)
timeout = Duration.ofSeconds(15)
maxFrameSize = Long.MAX_VALUE
masking = false
}
val counter = AtomicCounter()
routing {
webSocket(path) {
val log = LogTag("KWS:${counter.incrementAndGet()}")
log.debug { "opening the connection" }
val input = Channel<UByteArray>(256)
val output = Channel<UByteArray>(256)
launch {
log.debug { "starting output pump" }
while (isActive) {
try {
send(output.receive().toByteArray())
}
catch(_: ClosedReceiveChannelException) {
log.debug { "closing output pump as output channel is closed" }
break
}
}
log.debug { "closing output pump" }
}
val server = KiloServerConnection(
localInterface,
ProxyDevice(input, output),
createSession(),
serverKey
)
launch {
server.run()
close()
}
log.debug { "KSC started, looking for incoming frames" }
for (f in incoming) {
log.debug { "incoming frame: ${f.frameType}" }
if (f is Frame.Binary)
try {
input.send(f.readBytes().toUByteArray().also {
log.debug { "in frame\n${it.toDump()}" }
})
} catch (_: RemoteInterface.ClosedException) {
log.warning { "caught local closed exception (strange!), closing" }
break
} catch (_: ClosedReceiveChannelException) {
log.info { "receive channel is closed, closing connection" }
break
} catch (t: Throwable) {
log.exception { "unexpected exception, server connection will close" to t }
break
}
else
log.warning { "unknown frame type ${f.frameType}, ignoring" }
}
log.debug { "closing the server" }
close()
cancel()
log.debug { "server wbesock processing done" }
}
}
}
@@ -0,0 +1,155 @@
package net.sergeych.kiloparsec.adapter
import kotlinx.coroutines.*
import kotlinx.coroutines.channels.Channel
import kotlinx.coroutines.channels.ClosedReceiveChannelException
import kotlinx.coroutines.flow.MutableStateFlow
import net.sergeych.crypto2.Contrail
import net.sergeych.crypto2.encodeVarUnsigned
import net.sergeych.crypto2.readVarUnsigned
import net.sergeych.kiloparsec.RemoteInterface
import net.sergeych.kiloparsec.Transport
import net.sergeych.mp_logger.LogTag
import net.sergeych.mp_logger.warning
import net.sergeych.mp_tools.globalLaunch
import net.sergeych.tools.waitFor
import java.net.InetSocketAddress
import java.net.StandardSocketOptions.TCP_NODELAY
import java.nio.ByteBuffer
import java.nio.channels.AsynchronousSocketChannel
import kotlin.coroutines.cancellation.CancellationException
import kotlin.coroutines.suspendCoroutine
private val log = LogTag("ASTD")
/**
* Prepend block with its size, varint-encoded
*/
private fun encode(block: UByteArray): ByteArray {
val c = Contrail.create(block)
return (encodeVarUnsigned(c.size.toUInt()) + c).toByteArray()
}
/**
* Convert asynchronous socket to a [Transport.Device] using non-blocking nio,
* in a coroutine-effective manner. Note that it runs coroutines to read/write
* to the socket in a global scope.These are closed when transport is closed
* or the socket is closed, for example, by network failure.
*/
suspend fun asyncSocketToDevice(socket: AsynchronousSocketChannel): InetTransportDevice {
val deferredDevice = CompletableDeferred<InetTransportDevice>()
globalLaunch {
coroutineScope {
val sendQueueEmpty = MutableStateFlow(true)
val receiving = MutableStateFlow(false)
// We're in block mode, every block we send worth immediate sending, we do not
// send partial blocks, so:
socket.setOption(TCP_NODELAY, true)
// socket input is to be parsed for blocks, so we receive bytes
// and decode them to blocks
val input = Channel<UByte>(1024)
val inputBlocks = Channel<UByteArray>()
// output is blocks, so we sent transformed, framed blocks:
val outputBlocks = Channel<UByteArray>()
fun stop() {
kotlin.runCatching { inputBlocks.close(RemoteInterface.ClosedException()) }
kotlin.runCatching { outputBlocks.close() }
socket.close()
cancel()
}
// copy incoming data from the socket to input channel:
launch {
val data = ByteArray(1024)
val inb = ByteBuffer.wrap(data)
kotlin.runCatching {
while (isActive) {
inb.position(0)
val size: Int = suspendCoroutine { continuation ->
socket.read(inb, continuation, IntCompletionHandler)
}
if (size < 0) stop()
else {
// println("recvd:\n${data.sliceArray(0..<size).toDump()}\n------------------")
for (i in 0..<size) input.send(data[i].toUByte())
}
}
}
}
// copy from output to socket:
launch {
try {
while (isActive) {
// wait for the first block to send
sendQueueEmpty.value = outputBlocks.isEmpty
var data = encode(outputBlocks.receive())
// now we're sending, so queue state is sending:
sendQueueEmpty.value = false
// if there are more, take them all (NO_DELAY optimization)
while (!outputBlocks.isEmpty)
data += encode(outputBlocks.receive())
// now send it all together:
val outBuff = ByteBuffer.wrap(data)
val cnt = suspendCoroutine { continuation ->
socket.write(outBuff, continuation, IntCompletionHandler)
}
// be sure it was all sent
if (outBuff.position() != data.size || cnt != data.size) {
throw RuntimeException("unexpected partial write")
}
}
// in the case of just breaking out of the loop:
sendQueueEmpty.value = true
} catch (_: ClosedReceiveChannelException) {
stop()
}
}
// transport device copes with blocks:
// decode blocks from a byte channel read from the socket:
launch {
try {
while (isActive) {
receiving.value = !input.isEmpty
val size = readVarUnsigned(input)
receiving.value = true
if (size == 0u) log.warning { "zero size block is ignored!" }
else {
val block = UByteArray(size.toInt())
for (i in 0..<size.toInt()) {
block[i] = input.receive()
}
Contrail.unpack(block)?.let { inputBlocks.send(it) }
?: log.warning { "skipping bad block ${block.size} bytes" }
}
}
} catch (_: CancellationException) {
} catch (_: ClosedReceiveChannelException) {
stop()
}
receiving.value = false
}
val addr = socket.remoteAddress as InetSocketAddress
deferredDevice.complete(
InetTransportDevice(inputBlocks, outputBlocks, JvmNetworkAddress(addr.address, addr.port), {
yield()
// wait until all received data are parsed, but not too long
withTimeoutOrNull(500) {
receiving.waitFor { !it }
}
// then stop it
stop()
})
)
}
globalLaunch { socket.close() }
}
return deferredDevice.await()
}
@@ -1,8 +0,0 @@
package net.sergeych.tools
actual fun ProtectedOp(): ProtectedOpImplementation = object : ProtectedOpImplementation {
private val lock = Object()
override fun <T> invoke(f: () -> T): T {
synchronized(lock) { return f() }
}
}
@@ -1,10 +1,144 @@
package net.sergeych.kiloparsec
import kotlin.test.Test
import assertThrows
import io.ktor.server.engine.*
import io.ktor.server.netty.*
import kotlinx.coroutines.launch
import kotlinx.coroutines.test.runTest
import net.sergeych.crypto2.initCrypto
import net.sergeych.kiloparsec.adapter.acceptTcpDevice
import net.sergeych.kiloparsec.adapter.connectTcpDevice
import net.sergeych.kiloparsec.adapter.setupWebsocketServer
import net.sergeych.kiloparsec.adapter.websocketClient
import net.sergeych.mp_logger.Log
import java.net.InetAddress
import kotlin.test.*
class ClientTest {
class TestException : Exception("test1")
@Test
fun testClient() {
// Todo
fun testAddresses() {
println(InetAddress.getLocalHost())
println(InetAddress.getByName("localhost"))
println(InetAddress.getByName("127.0.0.1"))
println(InetAddress.getByName("mail.ru"))
}
@Test
fun testClient() = runTest {
initCrypto()
Log.connectConsole(Log.Level.DEBUG)
data class Session(
var data: String
)
val cmdSave by command<String,Unit>()
val cmdLoad by command<Unit,String>()
val cmdDrop by command<Unit,Unit>()
val cmdException by command<Unit,Unit>()
val cli = KiloInterface<Session>().apply {
registerError { TestException() }
onConnected { session.data = "start" }
on(cmdSave) { session.data = it }
on(cmdLoad) {
session.data
}
on(cmdException) {
throw TestException()
}
on(cmdDrop) {
throw LocalInterface.BreakConnectionException()
}
}
val server = KiloServer(cli, acceptTcpDevice(17101)) {
Session("unknown")
}
val client = KiloClient<Unit>() {
addErrors(cli)
connect { connectTcpDevice("localhost:17101") }
}
println(client.call(cmdLoad))
assertEquals("start", client.call(cmdLoad))
client.call(cmdSave, "foobar")
assertEquals("foobar", client.call(cmdLoad))
val res = kotlin.runCatching { client.call(cmdException) }
println(res.exceptionOrNull())
assertIs<TestException>(res.exceptionOrNull())
assertEquals("foobar", client.call(cmdLoad))
assertThrows<RemoteInterface.ClosedException> { client.call(cmdDrop) }
// reconnect?
assertEquals("start", client.call(cmdLoad))
server.close()
}
@Test
fun webSocketTest() = runTest {
initCrypto()
// fun Application.
val cmdClose by command<Unit,Unit>()
val cmdGetFoo by command<Unit,String>()
val cmdSetFoo by command<String,Unit>()
val cmdCheckConnected by command<Unit,Boolean>()
Log.connectConsole(Log.Level.DEBUG)
data class Session(var foo: String="not set")
var closeCounter = 0
val serverInterface = KiloInterface<Session>().apply {
var connectedCalled = false
onConnected { connectedCalled = true }
on(cmdGetFoo) { session.foo }
on(cmdSetFoo) { session.foo = it }
on(cmdCheckConnected) { connectedCalled }
on(cmdClose) {
throw LocalInterface.BreakConnectionException()
}
}
val ns: NettyApplicationEngine = embeddedServer(Netty, port = 8080, host = "0.0.0.0", module = {
setupWebsocketServer(serverInterface) { Session() }
}).start(wait = false)
val client = websocketClient<Unit>("ws://localhost:8080/kp")
val states = mutableListOf<Boolean>()
val collector = launch {
client.connectedStateFlow.collect {
println("got: $closeCounter/$it")
states += it
if( !it) { closeCounter++ }
}
}
assertEquals(true, client.call(cmdCheckConnected))
assertTrue { client.connectedStateFlow.value }
assertEquals("not set", client.call(cmdGetFoo))
client.call(cmdSetFoo, "foo")
assertEquals("foo", client.call(cmdGetFoo))
assertTrue { client.connectedStateFlow.value }
assertThrows<RemoteInterface.ClosedException> {
client.call(cmdClose)
}
// connection should now be closed
assertFalse { client.connectedStateFlow.value }
// this should be run on automatically reopen connection
client.call(cmdSetFoo,"superbar")
assertTrue { client.connectedStateFlow.value }
assertEquals("superbar", client.call(cmdGetFoo))
client.close()
ns.stop()
collector.cancel()
// println("----= states: $states")
// println("stopped server")
// println("closed client")
}
}
@@ -0,0 +1,100 @@
package net.sergeych.kiloparsec.adapters
import kotlinx.coroutines.*
import kotlinx.coroutines.test.runTest
import net.sergeych.crypto2.initCrypto
import net.sergeych.kiloparsec.adapter.UdpServer
import net.sergeych.kiloparsec.adapter.acceptTcpDevice
import net.sergeych.kiloparsec.adapter.connectTcpDevice
import net.sergeych.kiloparsec.adapter.toNetworkAddress
import net.sergeych.kiloparsec.decodeFromUByteArray
import net.sergeych.kiloparsec.encodeToUByteArray
import net.sergeych.mp_logger.Log
import net.sergeych.mp_logger.LogTag
import net.sergeych.synctools.ProtectedOp
import net.sergeych.synctools.invoke
import kotlin.test.Test
import kotlin.test.assertContains
import kotlin.test.assertEquals
class NetworkTest {
@Test
fun udpProviderTest() = runTest {
Log.connectConsole(Log.Level.DEBUG)
val s1 = UdpServer(17120)
val s2 = UdpServer(17121)
s1.send("Hello".encodeToUByteArray(), "localhost", 17121)
val d1 = s2.incoming.receive()
assertEquals(d1.address.port, 17120)
assertEquals("Hello", d1.message.toByteArray().decodeToString())
s1.send("world".encodeToUByteArray(), d1.address)
assertEquals("world", s1.incoming.receive().message.toByteArray().decodeToString())
// println("s1: ${s1.bindAddress()}")
}
@Test
fun tcpAsyncConnectionTest() = runTest {
initCrypto()
Log.connectConsole(Log.Level.DEBUG)
coroutineScope {
val serverFlow = acceptTcpDevice(17171)
val op = ProtectedOp()
var pills = setOf<String>()
val j = launch {
println("serf")
serverFlow.collect { device ->
println("serf 0")
launch {
println("serf 1")
device.output.send("Hello, world!".encodeToUByteArray())
device.output.send("Great".encodeToUByteArray())
while (true) {
val x = device.input.receive().decodeFromUByteArray()
if (x.startsWith("die")) {
op.invoke {
pills += x
}
cancel()
}
else
println("ignoring unexpected input: $x")
}
}
}
}
yield()
run {
try {
println("pre-con")
val s = connectTcpDevice("127.0.1.1:17171".toNetworkAddress())
println("2")
assertEquals("Hello, world!", s.input.receive().decodeFromUByteArray())
assertEquals("Great", s.input.receive().decodeFromUByteArray())
s.output.send("Goodbye".encodeToUByteArray())
s.output.send("die1".encodeToUByteArray())
s.close()
}
catch(t: Throwable) {
t.printStackTrace()
throw t
}
}
println("pre-con2")
val s1 = connectTcpDevice("127.0.1.1:17171".toNetworkAddress())
assertEquals("Hello, world!", s1.input.receive().decodeFromUByteArray())
assertEquals("Great", s1.input.receive().decodeFromUByteArray())
s1.output.send("die2".encodeToUByteArray())
s1.close()
// check that channels were flushed prior to closed:
assertContains(pills, "die1")
assertContains(pills, "die2")
// Check that server jobs are closed
j.cancelAndJoin()
}
}
}
@@ -1,26 +0,0 @@
package net.sergeych.kiloparsec.adapters
import com.ionspin.kotlin.crypto.util.encodeToUByteArray
import kotlinx.coroutines.test.runTest
import net.sergeych.kiloparsec.adapter.UdpServer
import net.sergeych.mp_logger.Log
import org.junit.jupiter.api.Assertions.assertEquals
import kotlin.test.Test
class UServerTest {
@Test
fun udpProvider() = runTest {
Log.connectConsole(Log.Level.DEBUG)
val s1 = UdpServer(17120)
val s2 = UdpServer(17121)
s1.send("Hello".encodeToUByteArray(), "localhost",17121)
val d1 = s2.incoming.receive()
assertEquals(d1.address.port, 17120)
assertEquals("Hello", d1.message.toByteArray().decodeToString())
d1.respondWith("world".encodeToUByteArray())
assertEquals("world", s1.incoming.receive().message.toByteArray().decodeToString())
// println("s1: ${s1.bindAddress()}")
}
}
@@ -0,0 +1,43 @@
package net.sergeych.kiloparsec.adapter
import io.ktor.network.selector.*
import io.ktor.network.sockets.*
import io.ktor.util.network.*
import io.ktor.utils.io.*
import io.ktor.utils.io.core.*
import kotlinx.coroutines.*
import kotlinx.coroutines.channels.Channel
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.flow
import net.sergeych.kiloparsec.AsyncVarint
import net.sergeych.mp_logger.LogTag
import kotlin.io.use
//class SocketNetworkAddress(override val host: String, override val port: Int) : NetworkAddress
//
//actual fun NetworkAddress(host: String, port: Int): NetworkAddress = SocketNetworkAddress(host, port)
//
//fun acceptTcpSocketDevice(port: Int): Flow<InetTransportDevice> {
// val selectorManager = SelectorManager(Dispatchers.IO)
// val serverSocket = aSocket(selectorManager).tcp().bind("127.0.0.1", port)
// val log = LogTag("TCPS${}")
// return flow {
// serverSocket.accept().use { sock ->
// val closed = CompletableDeferred<Boolean>()
// val scope = coroutineScope {
// val networkAddress = sock.remoteAddress.toJavaAddress().let { NetworkAddress(it.hostname, it.port) }
// val inputBlocks = Channel<UByteArray>(4096)
// sock.launch {
// val sockInput = sock.openReadChannel()
// while (isActive && sock.isActive) {
// try {
// val size = AsyncVarint.decodeUnsigned(sockInput)
// } catch (e: Exception) {
//
// }
// }
// }
// }
// }
// }
//}
@@ -1,5 +1,2 @@
package net.sergeych.kiloparsec.adapter
actual fun networkAddressOf(address: String): NetworkAddress {
TODO("Not yet implemented")
}
@@ -1,5 +1,15 @@
package net.sergeych.kiloparsec.adapter
import kotlinx.coroutines.flow.Flow
actual fun NetworkAddress(host: String, port: Int): NetworkAddress {
TODO("Not yet implemented")
}
actual fun acceptTcpDevice(port: Int): Flow<InetTransportDevice> {
TODO("Not yet implemented")
}
actual suspend fun connectTcpDevice(address: NetworkAddress): InetTransportDevice {
TODO("Not yet implemented")
}
@@ -1,13 +0,0 @@
package net.sergeych.tools
import kotlinx.atomicfu.locks.SynchronizedObject
import kotlinx.atomicfu.locks.synchronized
actual fun ProtectedOp(): ProtectedOpImplementation = object : ProtectedOpImplementation {
private val lock = SynchronizedObject()
override fun <T> invoke(f: () -> T): T {
synchronized(lock) {
return f()
}
}
}