Compare commits
1 Commits
master
...
experiment
| Author | SHA1 | Date | |
|---|---|---|---|
| d8f73e21b4 |
12
.gitignore
vendored
12
.gitignore
vendored
@ -5,7 +5,6 @@ build/
|
|||||||
!**/src/test/**/build/
|
!**/src/test/**/build/
|
||||||
|
|
||||||
### IntelliJ IDEA ###
|
### IntelliJ IDEA ###
|
||||||
.idea
|
|
||||||
.idea/modules.xml
|
.idea/modules.xml
|
||||||
.idea/jarRepositories.xml
|
.idea/jarRepositories.xml
|
||||||
.idea/compiler.xml
|
.idea/compiler.xml
|
||||||
@ -24,7 +23,8 @@ out/
|
|||||||
.project
|
.project
|
||||||
.settings
|
.settings
|
||||||
.springBeans
|
.springBeans
|
||||||
.sts4-caches
|
.sts4-cache
|
||||||
|
bin/
|
||||||
!**/src/main/**/bin/
|
!**/src/main/**/bin/
|
||||||
!**/src/test/**/bin/
|
!**/src/test/**/bin/
|
||||||
|
|
||||||
@ -39,10 +39,4 @@ out/
|
|||||||
.vscode/
|
.vscode/
|
||||||
|
|
||||||
### Mac OS ###
|
### Mac OS ###
|
||||||
.DS_Store
|
.DS_Store
|
||||||
|
|
||||||
# More
|
|
||||||
.kotlin
|
|
||||||
/.idea/workspace.xml
|
|
||||||
/.gigaide/gigaide.properties
|
|
||||||
local.properties
|
|
||||||
8
.idea/.gitignore
generated
vendored
Normal file
8
.idea/.gitignore
generated
vendored
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
# Default ignored files
|
||||||
|
/shelf/
|
||||||
|
/workspace.xml
|
||||||
|
# Editor-based HTTP Client requests
|
||||||
|
/httpRequests/
|
||||||
|
# Datasource local storage ignored files
|
||||||
|
/dataSources/
|
||||||
|
/dataSources.local.xml
|
||||||
@ -1,7 +1,7 @@
|
|||||||
<component name="ArtifactManager">
|
<component name="ArtifactManager">
|
||||||
<artifact type="jar" name="kiloparsec-js-0.2.4">
|
<artifact type="jar" name="kiloparsec-js-0.1.0-SNAPSHOT">
|
||||||
<output-path>$PROJECT_DIR$/build/libs</output-path>
|
<output-path>$PROJECT_DIR$/build/libs</output-path>
|
||||||
<root id="archive" name="kiloparsec-js-0.2.4.jar">
|
<root id="archive" name="kiloparsec-js-0.1.0-SNAPSHOT.jar">
|
||||||
<element id="module-output" name="kiloparsec.jsMain" />
|
<element id="module-output" name="kiloparsec.jsMain" />
|
||||||
</root>
|
</root>
|
||||||
</artifact>
|
</artifact>
|
||||||
6
.idea/artifacts/kiloparsec_js_0_1_2_SNAPSHOT.xml
generated
6
.idea/artifacts/kiloparsec_js_0_1_2_SNAPSHOT.xml
generated
@ -1,6 +0,0 @@
|
|||||||
<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>
|
|
||||||
8
.idea/artifacts/kiloparsec_js_0_2_1_SNAPSHOT.xml
generated
8
.idea/artifacts/kiloparsec_js_0_2_1_SNAPSHOT.xml
generated
@ -1,8 +0,0 @@
|
|||||||
<component name="ArtifactManager">
|
|
||||||
<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.2.1-SNAPSHOT.jar">
|
|
||||||
<element id="module-output" name="kiloparsec.jsMain" />
|
|
||||||
</root>
|
|
||||||
</artifact>
|
|
||||||
</component>
|
|
||||||
8
.idea/artifacts/kiloparsec_js_0_2_2_SNAPSHOT.xml
generated
8
.idea/artifacts/kiloparsec_js_0_2_2_SNAPSHOT.xml
generated
@ -1,8 +0,0 @@
|
|||||||
<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
.idea/artifacts/kiloparsec_js_0_2_3.xml
generated
8
.idea/artifacts/kiloparsec_js_0_2_3.xml
generated
@ -1,8 +0,0 @@
|
|||||||
<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>
|
|
||||||
8
.idea/artifacts/kiloparsec_js_0_2_5.xml
generated
8
.idea/artifacts/kiloparsec_js_0_2_5.xml
generated
@ -1,8 +0,0 @@
|
|||||||
<component name="ArtifactManager">
|
|
||||||
<artifact type="jar" name="kiloparsec-js-0.2.5">
|
|
||||||
<output-path>$PROJECT_DIR$/build/libs</output-path>
|
|
||||||
<root id="archive" name="kiloparsec-js-0.2.5.jar">
|
|
||||||
<element id="module-output" name="kiloparsec.jsMain" />
|
|
||||||
</root>
|
|
||||||
</artifact>
|
|
||||||
</component>
|
|
||||||
8
.idea/artifacts/kiloparsec_js_0_2_5_SNAPSHOT.xml
generated
8
.idea/artifacts/kiloparsec_js_0_2_5_SNAPSHOT.xml
generated
@ -1,8 +0,0 @@
|
|||||||
<component name="ArtifactManager">
|
|
||||||
<artifact type="jar" name="kiloparsec-js-0.2.5-SNAPSHOT">
|
|
||||||
<output-path>$PROJECT_DIR$/build/libs</output-path>
|
|
||||||
<root id="archive" name="kiloparsec-js-0.2.5-SNAPSHOT.jar">
|
|
||||||
<element id="module-output" name="kiloparsec.jsMain" />
|
|
||||||
</root>
|
|
||||||
</artifact>
|
|
||||||
</component>
|
|
||||||
6
.idea/artifacts/kiloparsec_js_0_2_6.xml
generated
6
.idea/artifacts/kiloparsec_js_0_2_6.xml
generated
@ -1,6 +0,0 @@
|
|||||||
<component name="ArtifactManager">
|
|
||||||
<artifact type="jar" name="kiloparsec-js-0.2.6">
|
|
||||||
<output-path>$PROJECT_DIR$/build/libs</output-path>
|
|
||||||
<root id="archive" name="kiloparsec-js-0.2.6.jar" />
|
|
||||||
</artifact>
|
|
||||||
</component>
|
|
||||||
8
.idea/artifacts/kiloparsec_js_0_3_1.xml
generated
8
.idea/artifacts/kiloparsec_js_0_3_1.xml
generated
@ -1,8 +0,0 @@
|
|||||||
<component name="ArtifactManager">
|
|
||||||
<artifact type="jar" name="kiloparsec-js-0.3.1">
|
|
||||||
<output-path>$PROJECT_DIR$/build/libs</output-path>
|
|
||||||
<root id="archive" name="kiloparsec-js-0.3.1.jar">
|
|
||||||
<element id="module-output" name="kiloparsec.jsMain" />
|
|
||||||
</root>
|
|
||||||
</artifact>
|
|
||||||
</component>
|
|
||||||
8
.idea/artifacts/kiloparsec_js_0_3_1_SNAPSHOT.xml
generated
8
.idea/artifacts/kiloparsec_js_0_3_1_SNAPSHOT.xml
generated
@ -1,8 +0,0 @@
|
|||||||
<component name="ArtifactManager">
|
|
||||||
<artifact type="jar" name="kiloparsec-js-0.3.1-SNAPSHOT">
|
|
||||||
<output-path>$PROJECT_DIR$/build/libs</output-path>
|
|
||||||
<root id="archive" name="kiloparsec-js-0.3.1-SNAPSHOT.jar">
|
|
||||||
<element id="module-output" name="kiloparsec.jsMain" />
|
|
||||||
</root>
|
|
||||||
</artifact>
|
|
||||||
</component>
|
|
||||||
8
.idea/artifacts/kiloparsec_js_0_3_2.xml
generated
8
.idea/artifacts/kiloparsec_js_0_3_2.xml
generated
@ -1,8 +0,0 @@
|
|||||||
<component name="ArtifactManager">
|
|
||||||
<artifact type="jar" name="kiloparsec-js-0.3.2">
|
|
||||||
<output-path>$PROJECT_DIR$/build/libs</output-path>
|
|
||||||
<root id="archive" name="kiloparsec-js-0.3.2.jar">
|
|
||||||
<element id="module-output" name="kiloparsec.jsMain" />
|
|
||||||
</root>
|
|
||||||
</artifact>
|
|
||||||
</component>
|
|
||||||
8
.idea/artifacts/kiloparsec_js_0_3_3.xml
generated
8
.idea/artifacts/kiloparsec_js_0_3_3.xml
generated
@ -1,8 +0,0 @@
|
|||||||
<component name="ArtifactManager">
|
|
||||||
<artifact type="jar" name="kiloparsec-js-0.3.3">
|
|
||||||
<output-path>$PROJECT_DIR$/build/libs</output-path>
|
|
||||||
<root id="archive" name="kiloparsec-js-0.3.3.jar">
|
|
||||||
<element id="module-output" name="kiloparsec.jsMain" />
|
|
||||||
</root>
|
|
||||||
</artifact>
|
|
||||||
</component>
|
|
||||||
8
.idea/artifacts/kiloparsec_js_0_4_1.xml
generated
8
.idea/artifacts/kiloparsec_js_0_4_1.xml
generated
@ -1,8 +0,0 @@
|
|||||||
<component name="ArtifactManager">
|
|
||||||
<artifact type="jar" name="kiloparsec-js-0.4.1">
|
|
||||||
<output-path>$PROJECT_DIR$/build/libs</output-path>
|
|
||||||
<root id="archive" name="kiloparsec-js-0.4.1.jar">
|
|
||||||
<element id="module-output" name="kiloparsec.jsMain" />
|
|
||||||
</root>
|
|
||||||
</artifact>
|
|
||||||
</component>
|
|
||||||
8
.idea/artifacts/kiloparsec_js_0_4_3.xml
generated
8
.idea/artifacts/kiloparsec_js_0_4_3.xml
generated
@ -1,8 +0,0 @@
|
|||||||
<component name="ArtifactManager">
|
|
||||||
<artifact type="jar" name="kiloparsec-js-0.4.3">
|
|
||||||
<output-path>$PROJECT_DIR$/build/libs</output-path>
|
|
||||||
<root id="archive" name="kiloparsec-js-0.4.3.jar">
|
|
||||||
<element id="module-output" name="kiloparsec.jsMain" />
|
|
||||||
</root>
|
|
||||||
</artifact>
|
|
||||||
</component>
|
|
||||||
@ -1,7 +1,7 @@
|
|||||||
<component name="ArtifactManager">
|
<component name="ArtifactManager">
|
||||||
<artifact type="jar" name="kiloparsec-jvm-0.2.1-SNAPSHOT">
|
<artifact type="jar" name="kiloparsec-jvm-0.1.0-SNAPSHOT">
|
||||||
<output-path>$PROJECT_DIR$/build/libs</output-path>
|
<output-path>$PROJECT_DIR$/build/libs</output-path>
|
||||||
<root id="archive" name="kiloparsec-jvm-0.2.1-SNAPSHOT.jar">
|
<root id="archive" name="kiloparsec-jvm-0.1.0-SNAPSHOT.jar">
|
||||||
<element id="module-output" name="kiloparsec.jvmMain" />
|
<element id="module-output" name="kiloparsec.jvmMain" />
|
||||||
</root>
|
</root>
|
||||||
</artifact>
|
</artifact>
|
||||||
@ -1,6 +0,0 @@
|
|||||||
<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,8 +0,0 @@
|
|||||||
<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
.idea/artifacts/kiloparsec_jvm_0_2_3.xml
generated
8
.idea/artifacts/kiloparsec_jvm_0_2_3.xml
generated
@ -1,8 +0,0 @@
|
|||||||
<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>
|
|
||||||
8
.idea/artifacts/kiloparsec_jvm_0_2_4.xml
generated
8
.idea/artifacts/kiloparsec_jvm_0_2_4.xml
generated
@ -1,8 +0,0 @@
|
|||||||
<component name="ArtifactManager">
|
|
||||||
<artifact type="jar" name="kiloparsec-jvm-0.2.4">
|
|
||||||
<output-path>$PROJECT_DIR$/build/libs</output-path>
|
|
||||||
<root id="archive" name="kiloparsec-jvm-0.2.4.jar">
|
|
||||||
<element id="module-output" name="kiloparsec.jvmMain" />
|
|
||||||
</root>
|
|
||||||
</artifact>
|
|
||||||
</component>
|
|
||||||
8
.idea/artifacts/kiloparsec_jvm_0_2_5.xml
generated
8
.idea/artifacts/kiloparsec_jvm_0_2_5.xml
generated
@ -1,8 +0,0 @@
|
|||||||
<component name="ArtifactManager">
|
|
||||||
<artifact type="jar" name="kiloparsec-jvm-0.2.5">
|
|
||||||
<output-path>$PROJECT_DIR$/build/libs</output-path>
|
|
||||||
<root id="archive" name="kiloparsec-jvm-0.2.5.jar">
|
|
||||||
<element id="module-output" name="kiloparsec.jvmMain" />
|
|
||||||
</root>
|
|
||||||
</artifact>
|
|
||||||
</component>
|
|
||||||
@ -1,8 +0,0 @@
|
|||||||
<component name="ArtifactManager">
|
|
||||||
<artifact type="jar" name="kiloparsec-jvm-0.2.5-SNAPSHOT">
|
|
||||||
<output-path>$PROJECT_DIR$/build/libs</output-path>
|
|
||||||
<root id="archive" name="kiloparsec-jvm-0.2.5-SNAPSHOT.jar">
|
|
||||||
<element id="module-output" name="kiloparsec.jvmMain" />
|
|
||||||
</root>
|
|
||||||
</artifact>
|
|
||||||
</component>
|
|
||||||
6
.idea/artifacts/kiloparsec_jvm_0_2_6.xml
generated
6
.idea/artifacts/kiloparsec_jvm_0_2_6.xml
generated
@ -1,6 +0,0 @@
|
|||||||
<component name="ArtifactManager">
|
|
||||||
<artifact type="jar" name="kiloparsec-jvm-0.2.6">
|
|
||||||
<output-path>$PROJECT_DIR$/build/libs</output-path>
|
|
||||||
<root id="archive" name="kiloparsec-jvm-0.2.6.jar" />
|
|
||||||
</artifact>
|
|
||||||
</component>
|
|
||||||
8
.idea/artifacts/kiloparsec_jvm_0_3_1.xml
generated
8
.idea/artifacts/kiloparsec_jvm_0_3_1.xml
generated
@ -1,8 +0,0 @@
|
|||||||
<component name="ArtifactManager">
|
|
||||||
<artifact type="jar" name="kiloparsec-jvm-0.3.1">
|
|
||||||
<output-path>$PROJECT_DIR$/build/libs</output-path>
|
|
||||||
<root id="archive" name="kiloparsec-jvm-0.3.1.jar">
|
|
||||||
<element id="module-output" name="kiloparsec.jvmMain" />
|
|
||||||
</root>
|
|
||||||
</artifact>
|
|
||||||
</component>
|
|
||||||
@ -1,8 +0,0 @@
|
|||||||
<component name="ArtifactManager">
|
|
||||||
<artifact type="jar" name="kiloparsec-jvm-0.3.1-SNAPSHOT">
|
|
||||||
<output-path>$PROJECT_DIR$/build/libs</output-path>
|
|
||||||
<root id="archive" name="kiloparsec-jvm-0.3.1-SNAPSHOT.jar">
|
|
||||||
<element id="module-output" name="kiloparsec.jvmMain" />
|
|
||||||
</root>
|
|
||||||
</artifact>
|
|
||||||
</component>
|
|
||||||
8
.idea/artifacts/kiloparsec_jvm_0_3_2.xml
generated
8
.idea/artifacts/kiloparsec_jvm_0_3_2.xml
generated
@ -1,8 +0,0 @@
|
|||||||
<component name="ArtifactManager">
|
|
||||||
<artifact type="jar" name="kiloparsec-jvm-0.3.2">
|
|
||||||
<output-path>$PROJECT_DIR$/build/libs</output-path>
|
|
||||||
<root id="archive" name="kiloparsec-jvm-0.3.2.jar">
|
|
||||||
<element id="module-output" name="kiloparsec.jvmMain" />
|
|
||||||
</root>
|
|
||||||
</artifact>
|
|
||||||
</component>
|
|
||||||
8
.idea/artifacts/kiloparsec_jvm_0_3_3.xml
generated
8
.idea/artifacts/kiloparsec_jvm_0_3_3.xml
generated
@ -1,8 +0,0 @@
|
|||||||
<component name="ArtifactManager">
|
|
||||||
<artifact type="jar" name="kiloparsec-jvm-0.3.3">
|
|
||||||
<output-path>$PROJECT_DIR$/build/libs</output-path>
|
|
||||||
<root id="archive" name="kiloparsec-jvm-0.3.3.jar">
|
|
||||||
<element id="module-output" name="kiloparsec.jvmMain" />
|
|
||||||
</root>
|
|
||||||
</artifact>
|
|
||||||
</component>
|
|
||||||
8
.idea/artifacts/kiloparsec_jvm_0_4_1.xml
generated
8
.idea/artifacts/kiloparsec_jvm_0_4_1.xml
generated
@ -1,8 +0,0 @@
|
|||||||
<component name="ArtifactManager">
|
|
||||||
<artifact type="jar" name="kiloparsec-jvm-0.4.1">
|
|
||||||
<output-path>$PROJECT_DIR$/build/libs</output-path>
|
|
||||||
<root id="archive" name="kiloparsec-jvm-0.4.1.jar">
|
|
||||||
<element id="module-output" name="kiloparsec.jvmMain" />
|
|
||||||
</root>
|
|
||||||
</artifact>
|
|
||||||
</component>
|
|
||||||
8
.idea/artifacts/kiloparsec_jvm_0_4_3.xml
generated
8
.idea/artifacts/kiloparsec_jvm_0_4_3.xml
generated
@ -1,8 +0,0 @@
|
|||||||
<component name="ArtifactManager">
|
|
||||||
<artifact type="jar" name="kiloparsec-jvm-0.4.3">
|
|
||||||
<output-path>$PROJECT_DIR$/build/libs</output-path>
|
|
||||||
<root id="archive" name="kiloparsec-jvm-0.4.3.jar">
|
|
||||||
<element id="module-output" name="kiloparsec.jvmMain" />
|
|
||||||
</root>
|
|
||||||
</artifact>
|
|
||||||
</component>
|
|
||||||
24
.idea/codeStyles/Project.xml
generated
24
.idea/codeStyles/Project.xml
generated
@ -1,29 +1,5 @@
|
|||||||
<component name="ProjectCodeStyleConfiguration">
|
<component name="ProjectCodeStyleConfiguration">
|
||||||
<code_scheme name="Project" version="173">
|
<code_scheme name="Project" version="173">
|
||||||
<DBN-PSQL>
|
|
||||||
<case-options enabled="true">
|
|
||||||
<option name="KEYWORD_CASE" value="lower" />
|
|
||||||
<option name="FUNCTION_CASE" value="lower" />
|
|
||||||
<option name="PARAMETER_CASE" value="lower" />
|
|
||||||
<option name="DATATYPE_CASE" value="lower" />
|
|
||||||
<option name="OBJECT_CASE" value="preserve" />
|
|
||||||
</case-options>
|
|
||||||
<formatting-settings enabled="false" />
|
|
||||||
</DBN-PSQL>
|
|
||||||
<DBN-SQL>
|
|
||||||
<case-options enabled="true">
|
|
||||||
<option name="KEYWORD_CASE" value="lower" />
|
|
||||||
<option name="FUNCTION_CASE" value="lower" />
|
|
||||||
<option name="PARAMETER_CASE" value="lower" />
|
|
||||||
<option name="DATATYPE_CASE" value="lower" />
|
|
||||||
<option name="OBJECT_CASE" value="preserve" />
|
|
||||||
</case-options>
|
|
||||||
<formatting-settings enabled="false">
|
|
||||||
<option name="STATEMENT_SPACING" value="one_line" />
|
|
||||||
<option name="CLAUSE_CHOP_DOWN" value="chop_down_if_statement_long" />
|
|
||||||
<option name="ITERATION_ELEMENTS_WRAPPING" value="chop_down_if_not_single" />
|
|
||||||
</formatting-settings>
|
|
||||||
</DBN-SQL>
|
|
||||||
<ScalaCodeStyleSettings>
|
<ScalaCodeStyleSettings>
|
||||||
<option name="MULTILINE_STRING_CLOSING_QUOTES_ON_NEW_LINE" value="true" />
|
<option name="MULTILINE_STRING_CLOSING_QUOTES_ON_NEW_LINE" value="true" />
|
||||||
</ScalaCodeStyleSettings>
|
</ScalaCodeStyleSettings>
|
||||||
|
|||||||
6
.idea/inspectionProfiles/Project_Default.xml
generated
Normal file
6
.idea/inspectionProfiles/Project_Default.xml
generated
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
<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
.idea/kotlinc.xml
generated
Normal file
6
.idea/kotlinc.xml
generated
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<project version="4">
|
||||||
|
<component name="KotlinJpsPluginSettings">
|
||||||
|
<option name="version" value="1.9.20" />
|
||||||
|
</component>
|
||||||
|
</project>
|
||||||
6
.idea/markdown.xml
generated
6
.idea/markdown.xml
generated
@ -1,6 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8"?>
|
|
||||||
<project version="4">
|
|
||||||
<component name="MarkdownSettings">
|
|
||||||
<option name="showProblemsInCodeBlocks" value="false" />
|
|
||||||
</component>
|
|
||||||
</project>
|
|
||||||
2
.idea/misc.xml
generated
2
.idea/misc.xml
generated
@ -4,7 +4,7 @@
|
|||||||
<component name="FrameworkDetectionExcludesConfiguration">
|
<component name="FrameworkDetectionExcludesConfiguration">
|
||||||
<file type="web" url="file://$PROJECT_DIR$" />
|
<file type="web" url="file://$PROJECT_DIR$" />
|
||||||
</component>
|
</component>
|
||||||
<component name="ProjectRootManager" version="2" languageLevel="JDK_21" project-jdk-name="corretto-17" project-jdk-type="JavaSDK">
|
<component name="ProjectRootManager" version="2" languageLevel="JDK_1_8" project-jdk-name="17 (5)" project-jdk-type="JavaSDK">
|
||||||
<output url="file://$PROJECT_DIR$/out" />
|
<output url="file://$PROJECT_DIR$/out" />
|
||||||
</component>
|
</component>
|
||||||
</project>
|
</project>
|
||||||
6
.idea/scala_compiler.xml
generated
6
.idea/scala_compiler.xml
generated
@ -1,6 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8"?>
|
|
||||||
<project version="4">
|
|
||||||
<component name="ScalaCompilerConfiguration">
|
|
||||||
<option name="separateProdTestSources" value="false" />
|
|
||||||
</component>
|
|
||||||
</project>
|
|
||||||
2
.idea/vcs.xml
generated
2
.idea/vcs.xml
generated
@ -1,6 +1,6 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8"?>
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
<project version="4">
|
<project version="4">
|
||||||
<component name="VcsDirectoryMappings">
|
<component name="VcsDirectoryMappings">
|
||||||
<mapping directory="" vcs="Git" />
|
<mapping directory="$PROJECT_DIR$" vcs="Git" />
|
||||||
</component>
|
</component>
|
||||||
</project>
|
</project>
|
||||||
312
README.md
312
README.md
@ -1,312 +0,0 @@
|
|||||||
# Kiloparsec
|
|
||||||
|
|
||||||
> 0.7.0-SNAPSHOT is recommended for newer builds; due to extremely poor Kotlin time migration architecture, on Apple targets it is the only working version.
|
|
||||||
|
|
||||||
|
|
||||||
__Recommended version is `0.6.8`: to keep the code compatible with current and further versions we
|
|
||||||
ask to upgrade to `0.4.2` at least.__ Starting from this version some package names are changed for
|
|
||||||
better clarity and fast UDP endpoints are added.
|
|
||||||
|
|
||||||
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 | wasmJS | native |
|
|
||||||
|-------------------|:----:|:---:|:-------:|:------:|
|
|
||||||
| TCP/IP server | ✓ | | | ✓ |
|
|
||||||
| TCP/IP client | ✓ | | | ✓ |
|
|
||||||
| UDP server | ✓ | | | ✓ |
|
|
||||||
| UDP client | ✓ | | | ✓ |
|
|
||||||
| Websockets server | ✓ | | | |
|
|
||||||
| Websockets client | ✓ | ✓ | ✓ | ✓ |
|
|
||||||
|
|
||||||
### Note on version compatibility
|
|
||||||
|
|
||||||
We recommend using `0.6.12`
|
|
||||||
|
|
||||||
Since version 0.6.9 websocket protocol supports both text and binary frames; old clients are backward compatible with
|
|
||||||
mew servers, but new clients only can work with older servers only in default binary frame mode. Upgrade also your
|
|
||||||
servers to get better websocket compatibility[^1].
|
|
||||||
|
|
||||||
Version 0.5.1 could be backward incompatible due to the upgrade of the crypto2.
|
|
||||||
|
|
||||||
Protocols >= 0.3.0 are not binary compatible with the previous version due to a more compact binary
|
|
||||||
format. The format from 0.3.0 onwards is supposed to keep compatible.
|
|
||||||
|
|
||||||
#### ID calculation algorithm is changed since 0.4.1
|
|
||||||
|
|
||||||
We recommend to upgrade to 0.4+ ASAP as public/shared key id derivation method was changed for even higher security.
|
|
||||||
|
|
||||||
### Supported native targets
|
|
||||||
|
|
||||||
- iosArm64, iosX64
|
|
||||||
- macosArm64, macosArm64
|
|
||||||
- linuxArm64, linuxX64
|
|
||||||
|
|
||||||
### Non-native targets
|
|
||||||
|
|
||||||
- JS (browser and Node.js)
|
|
||||||
- JVM (android, macOS, windows, linux, everywhere where JRE is installed)
|
|
||||||
|
|
||||||
## TCP/IP and UDP transports
|
|
||||||
|
|
||||||
These are the fastest based on async socket implementation of a ktor client. They work everywhere but JS target as
|
|
||||||
there are currently no widely adopted sockets for browser JavaScript.
|
|
||||||
|
|
||||||
While UDP is faster than TCP/IP, it is less reliable, especially with commands and return values that serialize to more
|
|
||||||
than 240 bytes approx, and has no retransmission facilities (use TCP!). UDP, though, shines when all you need is
|
|
||||||
to [push](https://code.sergeych.net/docs/kiloparsec/kiloparsec/net.sergeych.kiloparsec/-remote-interface/push.html) with
|
|
||||||
little or no data in it.
|
|
||||||
|
|
||||||
## Websockets server
|
|
||||||
|
|
||||||
While it is much slower than TCP or UDP, it is still faster than any http-based API; 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 websockets and tcp servers in real
|
|
||||||
applications to get easy access from anywhere.
|
|
||||||
|
|
||||||
## Websocket client
|
|
||||||
|
|
||||||
It is slower than TCP or UDP, but it works on literally all platforms. See the sample below.
|
|
||||||
|
|
||||||
# Usage
|
|
||||||
|
|
||||||
The library should be used as a 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.6.8")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
## Create a 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 interface 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 a ktor-based server
|
|
||||||
|
|
||||||
Normally the server side needs some session. It is convenient and avoids sending repeating data on each request speeding
|
|
||||||
up
|
|
||||||
the protocol. With KILOPARSEC, it is a 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)
|
|
||||||
|
|
||||||
~~~
|
|
||||||
|
|
||||||
## Create a TCP / IP client and server
|
|
||||||
|
|
||||||
Using plain TCP/IP is even simpler, and it works way faster than websocket one, and is _the same
|
|
||||||
protected as `wss://` (and `ws://`) the variant above due to same kiloparsec encryption in both cases. Still, a TCP/IP
|
|
||||||
client is not available in JavaScript browser targets, and custom TCP ports could often be blocked by firewalls.
|
|
||||||
|
|
||||||
Documentation is available in samples here:
|
|
||||||
|
|
||||||
- [TCP/IP server creation](https://code.sergeych.net/docs/kiloparsec/kiloparsec/net.sergeych.kiloparsec/-kilo-server/index.html)
|
|
||||||
|
|
||||||
- [TCP/IP client](https://code.sergeych.net/docs/kiloparsec/kiloparsec/net.sergeych.kiloparsec/-kilo-client/index.html)
|
|
||||||
|
|
||||||
In short, there are two functions that implement asynchronous TCP/IP transport on all platforms buy JS:
|
|
||||||
|
|
||||||
- [acceptTcpDevice](https://code.sergeych.net/docs/kiloparsec/kiloparsec/net.sergeych.kiloparsec.adapter/accept-tcp-device.html?query=fun%20acceptTcpDevice(port:%20Int):%20Flow%3CInetTransportDevice%3E)
|
|
||||||
to create a server
|
|
||||||
|
|
||||||
- [connectTcpDevice](https://code.sergeych.net/docs/kiloparsec/kiloparsec/net.sergeych.kiloparsec.adapter/connect-tcp-device.html)
|
|
||||||
to connect to the server
|
|
||||||
|
|
||||||
## UDP client and server
|
|
||||||
|
|
||||||
Is very much straightforward, same as with TCP/IP:
|
|
||||||
|
|
||||||
- [UDP server creation](https://code.sergeych.net/docs/kiloparsec/kiloparsec/net.sergeych.kiloparsec.adapter/accept-udp-device.html)
|
|
||||||
- [Connect UDP client](https://code.sergeych.net/docs/kiloparsec/kiloparsec/net.sergeych.kiloparsec.adapter/connect-udp-device.html)
|
|
||||||
|
|
||||||
### UDP specifics
|
|
||||||
|
|
||||||
#### Command size
|
|
||||||
|
|
||||||
Each command invocation and result are packed in a separate UDP diagram using effective binary packing.
|
|
||||||
Thus, for the best results commands and results should be relatively short, best to fit into 240 bytes. While bigger
|
|
||||||
datagrams are often transmitted successfully, the probability of the effective transmission drops with the size.
|
|
||||||
|
|
||||||
Kiloparsec UDP transport does not retransmit not delivered packets. Use TCP/IP or websocket if it is a concern.
|
|
||||||
|
|
||||||
For the best results, we recommend
|
|
||||||
using [push](https://code.sergeych.net/docs/kiloparsec/kiloparsec/net.sergeych.kiloparsec/-remote-interface/index.html#1558240250%2FFunctions%2F788909594)
|
|
||||||
for remote interfaces with UDP.
|
|
||||||
|
|
||||||
#### Timeouts
|
|
||||||
|
|
||||||
As Datagrams do not form protocol itself, kiloparsec issues pings when no data is circulated between parties.
|
|
||||||
When no pings are received long enough, the kiloparsec connection is closed. There are `maxInactivityTimeout` in all
|
|
||||||
relevant functions and constructors.
|
|
||||||
|
|
||||||
Client should not issue pings manually.
|
|
||||||
|
|
||||||
## Reusing code between servers
|
|
||||||
|
|
||||||
The same instance of
|
|
||||||
the [KiloInterface](https://code.sergeych.net/docs/kiloparsec/kiloparsec/net.sergeych.kiloparsec/-kilo-interface/index.html?query=open%20class%20KiloInterface%3CS%3E%20:%20LocalInterface%3CKiloScope%3CS%3E%3E)
|
|
||||||
could easily be reused with all instances of servers with different protocols.
|
|
||||||
|
|
||||||
This is a common practice to create a business logic in a `KiloInterface`, then create a TCP/IP and Websocket servers
|
|
||||||
passing the same instance of the logic to both.
|
|
||||||
|
|
||||||
## Note on the server identification
|
|
||||||
|
|
||||||
We do not recommend relying on TLS (HTTPS://, WSS://) host identification solely; in the modern world there is
|
|
||||||
a high probability of attacks on unfriendly (in respect to at least some of your users) states to the SSL certificates
|
|
||||||
chain, in which case the [MITM attack] and spoofing will be undetected. Check
|
|
||||||
the [remoteId](https://code.sergeych.net/docs/kiloparsec/kiloparsec/net.sergeych.kiloparsec/-kilo-client/remote-id.html?query=suspend%20fun%20remoteId():%20VerifyingPublicKey?)
|
|
||||||
in your client on each connection and provide the
|
|
||||||
safe [serverSecretKey](https://code.sergeych.net/docs/kiloparsec/kiloparsec/net.sergeych.kiloparsec/-kilo-server/index.html)
|
|
||||||
when creating a server.
|
|
||||||
|
|
||||||
This will effectively protect against certificate chain spoofing in the case of the application installed from the
|
|
||||||
trusted source.
|
|
||||||
|
|
||||||
__Important note__. The web application could not be completely secured this way unless is loaded from the IP-address,
|
|
||||||
as the DNS could be spoofed the same, especially when used with `Cloudflare` or other CDN that can transparently
|
|
||||||
substitute the whole site. For applications, we strongly recommend not using CDN except your own, controlled ones. You
|
|
||||||
generally can't neither detect nor repel [MITM attack] performed from _any single cloudflare 'ray'_.
|
|
||||||
|
|
||||||
## See also:
|
|
||||||
|
|
||||||
- [Source documentation](https://code.sergeych.net/docs/kiloparsec/)
|
|
||||||
- [Project's WIKI](https://gitea.sergeych.net/SergeychWorks/kiloparsec/wiki)
|
|
||||||
|
|
||||||
# Details
|
|
||||||
|
|
||||||
It is not compatible with Parsec family and no more based on the 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 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 the 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 full-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 a 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
|
|
||||||
|
|
||||||
This is a work in progress, not yet moved to the public domain;
|
|
||||||
you need to obtain a license from https://8-rays.dev or [Sergey Chernov]. For open source projects it will most be free
|
|
||||||
on some special terms.
|
|
||||||
|
|
||||||
It will be moved to open source; we also guarantee that it will be moved to open source immediately if the software
|
|
||||||
export restrictions are lifted. We do not support such practices here at 8-rays.dev.
|
|
||||||
|
|
||||||
[MITM]: https://en.wikipedia.org/wiki/Man-in-the-middle_attack
|
|
||||||
|
|
||||||
[Sergey Chernov]: https://t.me/real_sergeych
|
|
||||||
[^1]: On some new Xiaomi phones we found problems with websocket binary frames, probably in ktor; use text frames
|
|
||||||
otherwise.
|
|
||||||
14
bin/pubdocs
14
bin/pubdocs
@ -1,14 +0,0 @@
|
|||||||
#!/bin/bash
|
|
||||||
#
|
|
||||||
# Copyright (c) 2025. Sergey S. Chernov - All Rights Reserved
|
|
||||||
#
|
|
||||||
# You may use, distribute and modify this code under the
|
|
||||||
# terms of the private license, which you must obtain from the author
|
|
||||||
#
|
|
||||||
# To obtain the license, contact the author: https://t.me/real_sergeych or email to
|
|
||||||
# real dot sergeych at gmail.
|
|
||||||
#
|
|
||||||
|
|
||||||
set -e
|
|
||||||
./gradlew dokkaHtml
|
|
||||||
rsync -avz ./build/dokka/* code.sergeych.net:/bigstore/sergeych_pub/code/docs/kiloparsec
|
|
||||||
199
build.gradle.kts
199
build.gradle.kts
@ -1,190 +1,83 @@
|
|||||||
/*
|
import org.jetbrains.kotlin.gradle.plugin.KotlinJsCompilerType
|
||||||
* Copyright (c) 2025. Sergey S. Chernov - All Rights Reserved
|
|
||||||
*
|
|
||||||
* You may use, distribute and modify this code under the
|
|
||||||
* terms of the private license, which you must obtain from the author
|
|
||||||
*
|
|
||||||
* To obtain the license, contact the author: https://t.me/real_sergeych or email to
|
|
||||||
* real dot sergeych at gmail.
|
|
||||||
*/
|
|
||||||
|
|
||||||
import org.jetbrains.kotlin.gradle.ExperimentalWasmDsl
|
|
||||||
|
|
||||||
plugins {
|
plugins {
|
||||||
kotlin("multiplatform") version "2.2.21"
|
kotlin("multiplatform") version "1.9.20"
|
||||||
id("org.jetbrains.kotlin.plugin.serialization") version "2.2.21"
|
id("org.jetbrains.kotlin.plugin.serialization") version "1.9.20"
|
||||||
id("com.android.library") version "8.7.2" apply true
|
|
||||||
`maven-publish`
|
`maven-publish`
|
||||||
id("org.jetbrains.dokka") version "1.9.20"
|
|
||||||
}
|
}
|
||||||
|
|
||||||
group = "net.sergeych"
|
group = "net.sergeych"
|
||||||
version = "0.7.0-SNAPSHOT"
|
version = "0.1.0-SNAPSHOT"
|
||||||
|
|
||||||
repositories {
|
repositories {
|
||||||
mavenCentral()
|
mavenCentral()
|
||||||
mavenLocal()
|
mavenLocal()
|
||||||
maven("https://maven.universablockchain.com/")
|
maven("https://maven.universablockchain.com/")
|
||||||
maven("https://gitea.sergeych.net/api/packages/SergeychWorks/maven")
|
|
||||||
maven("https://gitea.sergeych.net/api/packages/YoungBlood/maven")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
kotlin {
|
kotlin {
|
||||||
jvmToolchain(21)
|
jvm {
|
||||||
jvm()
|
jvmToolchain(8)
|
||||||
js {
|
withJava()
|
||||||
browser()
|
testRuns.named("test") {
|
||||||
nodejs()
|
executionTask.configure {
|
||||||
|
useJUnitPlatform()
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
linuxX64()
|
js(KotlinJsCompilerType.IR) {
|
||||||
linuxArm64()
|
browser {
|
||||||
|
// commonWebpackConfig {
|
||||||
macosArm64()
|
// cssSupport {
|
||||||
macosX64()
|
// enabled.set(true)
|
||||||
iosX64()
|
// }
|
||||||
iosArm64()
|
// }
|
||||||
iosSimulatorArm64()
|
}
|
||||||
|
}
|
||||||
mingwX64()
|
val hostOs = System.getProperty("os.name")
|
||||||
|
val isArm64 = System.getProperty("os.arch") == "aarch64"
|
||||||
androidTarget()
|
val isMingwX64 = hostOs.startsWith("Windows")
|
||||||
@OptIn(ExperimentalWasmDsl::class)
|
val nativeTarget = when {
|
||||||
wasmJs {
|
hostOs == "Mac OS X" && isArm64 -> macosArm64("native")
|
||||||
browser()
|
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 ktor_version = "3.1.1"
|
|
||||||
|
|
||||||
sourceSets {
|
sourceSets {
|
||||||
all {
|
all {
|
||||||
languageSettings.optIn("kotlinx.serialization.ExperimentalSerializationApi")
|
languageSettings.optIn("kotlinx.serialization.ExperimentalSerializationApi")
|
||||||
languageSettings.optIn("kotlinx.coroutines.ExperimentalCoroutinesApi")
|
languageSettings.optIn("kotlinx.coroutines.ExperimentalCoroutinesApi")
|
||||||
languageSettings.optIn("kotlin.ExperimentalUnsignedTypes")
|
languageSettings.optIn("kotlin.ExperimentalUnsignedTypes")
|
||||||
languageSettings.optIn("kotlin.time.ExperimentalTime")
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
val commonMain by getting {
|
val commonMain by getting {
|
||||||
dependencies {
|
dependencies {
|
||||||
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.10.2")
|
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.7.3")
|
||||||
implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.9.0")
|
implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.5.1")
|
||||||
api("io.ktor:ktor-client-core:$ktor_version")
|
|
||||||
api("net.sergeych:crypto2:0.9.0")
|
implementation("com.ionspin.kotlin:multiplatform-crypto-libsodium-bindings:0.9.0")
|
||||||
}
|
api("com.ionspin.kotlin:bignum:0.3.8")
|
||||||
}
|
|
||||||
val androidMain by getting {
|
api("net.sergeych:mp_bintools:0.0.6-SNAPSHOT")
|
||||||
dependencies {
|
api("net.sergeych:mp_stools:1.4.1")
|
||||||
implementation("io.ktor:ktor-client-okhttp:$ktor_version")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
val ktorSocketMain by creating {
|
|
||||||
dependsOn(commonMain)
|
|
||||||
dependencies {
|
|
||||||
implementation("io.ktor:ktor-network:$ktor_version")
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
val commonTest by getting {
|
val commonTest by getting {
|
||||||
dependencies {
|
dependencies {
|
||||||
implementation(kotlin("test"))
|
implementation(kotlin("test"))
|
||||||
implementation("org.slf4j:slf4j-simple:2.0.17")
|
implementation("org.slf4j:slf4j-simple:2.0.9")
|
||||||
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-test:1.10.2")
|
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-test:1.7.3")
|
||||||
}
|
|
||||||
}
|
|
||||||
val ktorSocketTest by creating {
|
|
||||||
dependsOn(commonTest)
|
|
||||||
}
|
|
||||||
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 {
|
|
||||||
dependsOn(ktorSocketTest)
|
|
||||||
}
|
|
||||||
val jsMain by getting {
|
|
||||||
dependencies {
|
|
||||||
implementation("io.ktor:ktor-client-js:$ktor_version")
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
val jvmMain by getting
|
||||||
|
val jvmTest by getting
|
||||||
|
val jsMain by getting
|
||||||
val jsTest by getting
|
val jsTest by getting
|
||||||
val macosArm64Main by getting {
|
val nativeMain by getting
|
||||||
dependsOn(ktorSocketMain)
|
val nativeTest by getting
|
||||||
}
|
|
||||||
val macosArm64Test by getting {
|
|
||||||
dependsOn(ktorSocketTest)
|
|
||||||
}
|
|
||||||
val macosX64Main by getting {
|
|
||||||
dependsOn(ktorSocketMain)
|
|
||||||
}
|
|
||||||
val iosX64Main by getting {
|
|
||||||
dependsOn(ktorSocketMain)
|
|
||||||
}
|
|
||||||
val iosX64Test by getting {
|
|
||||||
dependsOn(ktorSocketTest)
|
|
||||||
}
|
|
||||||
val iosArm64Main by getting {
|
|
||||||
dependsOn(ktorSocketMain)
|
|
||||||
}
|
|
||||||
val iosArm64Test by getting {
|
|
||||||
dependsOn(ktorSocketTest)
|
|
||||||
}
|
|
||||||
val linuxArm64Main by getting {
|
|
||||||
dependsOn(ktorSocketMain)
|
|
||||||
}
|
|
||||||
val linuxArm64Test by getting {
|
|
||||||
dependsOn(ktorSocketTest)
|
|
||||||
}
|
|
||||||
val linuxX64Main by getting {
|
|
||||||
dependsOn(ktorSocketMain)
|
|
||||||
}
|
|
||||||
val linuxX64Test by getting {
|
|
||||||
dependsOn(ktorSocketTest)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
tasks.dokkaHtml.configure {
|
|
||||||
outputDirectory.set(buildDir.resolve("dokka"))
|
|
||||||
dokkaSourceSets {
|
|
||||||
configureEach {
|
|
||||||
// includes.from("docs/bipack.md")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
android {
|
|
||||||
namespace = "net.sergeych.kiloparsec"
|
|
||||||
compileSdk = 34
|
|
||||||
defaultConfig {
|
|
||||||
minSdk = 24
|
|
||||||
}
|
|
||||||
compileOptions {
|
|
||||||
sourceCompatibility = JavaVersion.VERSION_17
|
|
||||||
targetCompatibility = JavaVersion.VERSION_17
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|||||||
@ -1,20 +1 @@
|
|||||||
#
|
|
||||||
# Copyright (c) 2025. Sergey S. Chernov - All Rights Reserved
|
|
||||||
#
|
|
||||||
# You may use, distribute and modify this code under the
|
|
||||||
# terms of the private license, which you must obtain from the author
|
|
||||||
#
|
|
||||||
# To obtain the license, contact the author: https://t.me/real_sergeych or email to
|
|
||||||
# real dot sergeych at gmail.
|
|
||||||
#
|
|
||||||
|
|
||||||
kotlin.code.style=official
|
kotlin.code.style=official
|
||||||
kotlin.mpp.applyDefaultHierarchyTemplate=false
|
|
||||||
|
|
||||||
kotlin.daemon.jvmargs=-Xmx2048m -Dfile.encoding=UTF-8
|
|
||||||
kotlin.native.ignoreDisabledTargets=true
|
|
||||||
|
|
||||||
org.gradle.parallel=true
|
|
||||||
org.gradle.configuration-cache=true
|
|
||||||
org.gradle.caching=true
|
|
||||||
|
|
||||||
|
|||||||
12
gradle/wrapper/gradle-wrapper.properties
vendored
12
gradle/wrapper/gradle-wrapper.properties
vendored
@ -1,15 +1,5 @@
|
|||||||
#
|
|
||||||
# Copyright (c) 2025. Sergey S. Chernov - All Rights Reserved
|
|
||||||
#
|
|
||||||
# You may use, distribute and modify this code under the
|
|
||||||
# terms of the private license, which you must obtain from the author
|
|
||||||
#
|
|
||||||
# To obtain the license, contact the author: https://t.me/real_sergeych or email to
|
|
||||||
# real dot sergeych at gmail.
|
|
||||||
#
|
|
||||||
|
|
||||||
distributionBase=GRADLE_USER_HOME
|
distributionBase=GRADLE_USER_HOME
|
||||||
distributionPath=wrapper/dists
|
distributionPath=wrapper/dists
|
||||||
distributionUrl=https\://services.gradle.org/distributions/gradle-8.14.3-bin.zip
|
distributionUrl=https\://services.gradle.org/distributions/gradle-8.2-bin.zip
|
||||||
zipStoreBase=GRADLE_USER_HOME
|
zipStoreBase=GRADLE_USER_HOME
|
||||||
zipStorePath=wrapper/dists
|
zipStorePath=wrapper/dists
|
||||||
16
gradlew
vendored
16
gradlew
vendored
@ -1,13 +1,19 @@
|
|||||||
#!/bin/sh
|
#!/bin/sh
|
||||||
|
|
||||||
#
|
#
|
||||||
# Copyright (c) 2025. Sergey S. Chernov - All Rights Reserved
|
# Copyright © 2015-2021 the original authors.
|
||||||
#
|
#
|
||||||
# You may use, distribute and modify this code under the
|
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
# terms of the private license, which you must obtain from the author
|
# you may not use this file except in compliance with the License.
|
||||||
|
# You may obtain a copy of the License at
|
||||||
#
|
#
|
||||||
# To obtain the license, contact the author: https://t.me/real_sergeych or email to
|
# https://www.apache.org/licenses/LICENSE-2.0
|
||||||
# real dot sergeych at gmail.
|
#
|
||||||
|
# Unless required by applicable law or agreed to in writing, software
|
||||||
|
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
# See the License for the specific language governing permissions and
|
||||||
|
# limitations under the License.
|
||||||
#
|
#
|
||||||
|
|
||||||
##############################################################################
|
##############################################################################
|
||||||
|
|||||||
@ -1,16 +1,5 @@
|
|||||||
/*
|
|
||||||
* Copyright (c) 2025. Sergey S. Chernov - All Rights Reserved
|
|
||||||
*
|
|
||||||
* You may use, distribute and modify this code under the
|
|
||||||
* terms of the private license, which you must obtain from the author
|
|
||||||
*
|
|
||||||
* To obtain the license, contact the author: https://t.me/real_sergeych or email to
|
|
||||||
* real dot sergeych at gmail.
|
|
||||||
*/
|
|
||||||
|
|
||||||
pluginManagement {
|
pluginManagement {
|
||||||
repositories {
|
repositories {
|
||||||
google()
|
|
||||||
mavenCentral()
|
mavenCentral()
|
||||||
gradlePluginPortal()
|
gradlePluginPortal()
|
||||||
}
|
}
|
||||||
|
|||||||
27
src/commonMain/kotlin/net/sergeych/crypto/InitCrypto.kt
Normal file
27
src/commonMain/kotlin/net/sergeych/crypto/InitCrypto.kt
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
81
src/commonMain/kotlin/net/sergeych/crypto/SignedBox.kt
Normal file
81
src/commonMain/kotlin/net/sergeych/crypto/SignedBox.kt
Normal file
@ -0,0 +1,81 @@
|
|||||||
|
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)
|
||||||
|
}
|
||||||
|
}
|
||||||
7
src/commonMain/kotlin/net/sergeych/crypto/contrail.kt
Normal file
7
src/commonMain/kotlin/net/sergeych/crypto/contrail.kt
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
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
|
||||||
75
src/commonMain/kotlin/net/sergeych/crypto/keys.kt
Normal file
75
src/commonMain/kotlin/net/sergeych/crypto/keys.kt
Normal file
@ -0,0 +1,75 @@
|
|||||||
|
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")
|
||||||
85
src/commonMain/kotlin/net/sergeych/crypto/tools.kt
Normal file
85
src/commonMain/kotlin/net/sergeych/crypto/tools.kt
Normal file
@ -0,0 +1,85 @@
|
|||||||
|
@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()
|
||||||
|
}
|
||||||
|
}
|
||||||
8
src/commonMain/kotlin/net/sergeych/crypto/utools.kt
Normal file
8
src/commonMain/kotlin/net/sergeych/crypto/utools.kt
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
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()
|
||||||
@ -1,47 +0,0 @@
|
|||||||
/*
|
|
||||||
* Copyright (c) 2025. Sergey S. Chernov - All Rights Reserved
|
|
||||||
*
|
|
||||||
* You may use, distribute and modify this code under the
|
|
||||||
* terms of the private license, which you must obtain from the author
|
|
||||||
*
|
|
||||||
* To obtain the license, contact the author: https://t.me/real_sergeych or email to
|
|
||||||
* real dot sergeych at gmail.
|
|
||||||
*/
|
|
||||||
|
|
||||||
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())
|
|
||||||
}
|
|
||||||
@ -1,52 +0,0 @@
|
|||||||
/*
|
|
||||||
* Copyright (c) 2025. Sergey S. Chernov - All Rights Reserved
|
|
||||||
*
|
|
||||||
* You may use, distribute and modify this code under the
|
|
||||||
* terms of the private license, which you must obtain from the author
|
|
||||||
*
|
|
||||||
* To obtain the license, contact the author: https://t.me/real_sergeych or email to
|
|
||||||
* real dot sergeych at gmail.
|
|
||||||
*/
|
|
||||||
|
|
||||||
package net.sergeych.kiloparsec
|
|
||||||
|
|
||||||
import kotlinx.coroutines.sync.Mutex
|
|
||||||
import kotlinx.coroutines.sync.withLock
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Multiplatform atomically mutable value to be used in [kotlinx.coroutines],
|
|
||||||
* with suspending mutating operations, see [mutate].
|
|
||||||
*
|
|
||||||
* Actual value can be either changed in a block of [mutate] when
|
|
||||||
* new value _depends on the current value_ or with [reset].
|
|
||||||
*
|
|
||||||
* [value] getter is suspended because it waits until the mutation finishes
|
|
||||||
*/
|
|
||||||
open class AtomicAsyncValue<T>(initialValue: T) {
|
|
||||||
private var actualValue = initialValue
|
|
||||||
private val access = Mutex()
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Change the value: get the current and set to the returned, all in the
|
|
||||||
* atomic suspend operation. All other mutating requests including assigning to [value]
|
|
||||||
* will be blocked and queued.
|
|
||||||
* @return result of the mutation. Note that immediate call to property [value]
|
|
||||||
* could already return modified bu some other thread value!
|
|
||||||
*/
|
|
||||||
suspend fun mutate(mutator: suspend (T) -> T): T = access.withLock {
|
|
||||||
actualValue = mutator(actualValue)
|
|
||||||
actualValue
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Atomic get or set the value. Atomic get means if there is a [mutate] in progress
|
|
||||||
* it will wait until the mutation finishes and then return the correct result.
|
|
||||||
*/
|
|
||||||
suspend fun value() = access.withLock { actualValue }
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Set the new value without checking it. Shortcut to
|
|
||||||
* ```mutate { value = newValue }```
|
|
||||||
*/
|
|
||||||
suspend fun reset(value: T) = mutate { value }
|
|
||||||
}
|
|
||||||
@ -1,13 +1,3 @@
|
|||||||
/*
|
|
||||||
* Copyright (c) 2025. Sergey S. Chernov - All Rights Reserved
|
|
||||||
*
|
|
||||||
* You may use, distribute and modify this code under the
|
|
||||||
* terms of the private license, which you must obtain from the author
|
|
||||||
*
|
|
||||||
* To obtain the license, contact the author: https://t.me/real_sergeych or email to
|
|
||||||
* real dot sergeych at gmail.
|
|
||||||
*/
|
|
||||||
|
|
||||||
package net.sergeych.kiloparsec
|
package net.sergeych.kiloparsec
|
||||||
|
|
||||||
import kotlinx.serialization.KSerializer
|
import kotlinx.serialization.KSerializer
|
||||||
@ -15,28 +5,15 @@ import kotlinx.serialization.Serializable
|
|||||||
import net.sergeych.bintools.toDataSource
|
import net.sergeych.bintools.toDataSource
|
||||||
import net.sergeych.bipack.BipackDecoder
|
import net.sergeych.bipack.BipackDecoder
|
||||||
import net.sergeych.bipack.BipackEncoder
|
import net.sergeych.bipack.BipackEncoder
|
||||||
import net.sergeych.kiloparsec.Command.Companion.unpackCall
|
|
||||||
import net.sergeych.utools.unpack
|
import net.sergeych.utools.unpack
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Typesafe command definition. Command is a universal entity in kiloparsec (and also diwan): it is used
|
* Typesafe command definition. Command is a universal entity in Divan: it is used
|
||||||
* in node-2-node protocols and client API, and most importantly in calling smart contract
|
* in node-2-node protocols and client API, and most importantly in calling smart contract
|
||||||
* methods. This is essentially a Kotlin binding to typesafe serialize command calls and
|
* methods. This is essentially a Kotlin binding to typesafe serialize command calls and
|
||||||
* deserialize results.
|
* deserialize results.
|
||||||
*
|
|
||||||
* To create command instances, it is recommended to use [command] that returns [CommandDelegate].
|
|
||||||
*
|
|
||||||
* Use [packCall] to serialize the command call with some arguments.
|
|
||||||
*
|
|
||||||
* Note that `Command` instances themselves are not serialized, instead, the call is serialized,
|
|
||||||
* in the form of [Call], containing name and properly serialized arguments.
|
|
||||||
*
|
|
||||||
* [unpackCall] deserialized result of the [packCall] so the proper handler for the command could
|
|
||||||
* be used. Then the result of the execution could be packed with [exec] and then unpacked with
|
|
||||||
* [unpackResult].
|
|
||||||
*
|
|
||||||
*/
|
*/
|
||||||
open class Command<A, R>(
|
class Command<A, R>(
|
||||||
val name: String,
|
val name: String,
|
||||||
val argsSerializer: KSerializer<A>,
|
val argsSerializer: KSerializer<A>,
|
||||||
val resultSerializer: KSerializer<R>
|
val resultSerializer: KSerializer<R>
|
||||||
@ -44,41 +21,20 @@ open class Command<A, R>(
|
|||||||
@Serializable
|
@Serializable
|
||||||
data class Call(val name: String,val serializedArgs: UByteArray)
|
data class Call(val name: String,val serializedArgs: UByteArray)
|
||||||
|
|
||||||
/**
|
fun packCall(args: A): UByteArray = BipackEncoder.encode(
|
||||||
* Pack command invocation with specified arguments.
|
Call(name, BipackEncoder.encode(argsSerializer, args).toUByteArray())
|
||||||
*/
|
).toUByteArray()
|
||||||
fun packCall(args: A): UByteArray = BipackEncoder.encode(createCall(args)).toUByteArray()
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Create [Call] instance for specified args vy serializing it properly
|
|
||||||
*/
|
|
||||||
fun createCall(args: A): Call = Call(name, BipackEncoder.encode(argsSerializer, args).toUByteArray())
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Unpack result, obtained by [exec].
|
|
||||||
*/
|
|
||||||
fun unpackResult(packedResult: UByteArray): R =
|
fun unpackResult(packedResult: UByteArray): R =
|
||||||
unpack(resultSerializer, packedResult)
|
unpack(resultSerializer, packedResult)
|
||||||
|
|
||||||
/**
|
|
||||||
* Execute a command unpacking args.
|
|
||||||
*
|
|
||||||
* @param packedArgs arguments, as provided by [packCall] in the [Call] instance
|
|
||||||
* @param handler actual code to execute the command
|
|
||||||
* @return properly serialized result to be unpacked with [unpackResult].
|
|
||||||
*/
|
|
||||||
suspend fun exec(packedArgs: UByteArray, handler: suspend (A) -> R): UByteArray =
|
suspend fun exec(packedArgs: UByteArray, handler: suspend (A) -> R): UByteArray =
|
||||||
BipackEncoder.encode(
|
BipackEncoder.encode(
|
||||||
resultSerializer,
|
resultSerializer,
|
||||||
handler(
|
handler(BipackDecoder.decode(packedArgs.toDataSource(), argsSerializer))
|
||||||
BipackDecoder.decode(packedArgs.toDataSource(), argsSerializer))
|
|
||||||
).toUByteArray()
|
).toUByteArray()
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
/**
|
|
||||||
* Unpack command invocation instance from [packCall]. Use [exec] to deserialize arguments and
|
|
||||||
* perform command.
|
|
||||||
*/
|
|
||||||
fun unpackCall(packedCall: UByteArray): Call = BipackDecoder.decode(packedCall.toDataSource())
|
fun unpackCall(packedCall: UByteArray): Call = BipackDecoder.decode(packedCall.toDataSource())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -1,13 +1,3 @@
|
|||||||
/*
|
|
||||||
* Copyright (c) 2025. Sergey S. Chernov - All Rights Reserved
|
|
||||||
*
|
|
||||||
* You may use, distribute and modify this code under the
|
|
||||||
* terms of the private license, which you must obtain from the author
|
|
||||||
*
|
|
||||||
* To obtain the license, contact the author: https://t.me/real_sergeych or email to
|
|
||||||
* real dot sergeych at gmail.
|
|
||||||
*/
|
|
||||||
|
|
||||||
package net.sergeych.kiloparsec
|
package net.sergeych.kiloparsec
|
||||||
|
|
||||||
import kotlinx.serialization.KSerializer
|
import kotlinx.serialization.KSerializer
|
||||||
@ -17,9 +7,6 @@ import kotlin.reflect.KProperty
|
|||||||
/**
|
/**
|
||||||
* delegate returning function that creates a [Command] in the current context which by default has the name of
|
* delegate returning function that creates a [Command] in the current context which by default has the name of
|
||||||
* the property.
|
* 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> {
|
inline fun <reified A, reified R> command(overrideName: String? = null): CommandDelegate<A, R> {
|
||||||
return CommandDelegate(
|
return CommandDelegate(
|
||||||
@ -29,33 +16,19 @@ inline fun <reified A, reified R> command(overrideName: String? = null): Command
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Declare a Push: Unit-returning command usually used with [RemoteInterface.push]
|
|
||||||
*/
|
|
||||||
inline fun <reified A> push(overrideName: String? = null): CommandDelegate<A, Unit> = command(overrideName)
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Delegate to create [Command] via property
|
* Delegate to create [Command] via property
|
||||||
*/
|
*/
|
||||||
class CommandDelegate<A, R>(
|
class CommandDelegate<A, R>(
|
||||||
private val argsSerializer: KSerializer<A>,
|
private val argsSerializer: KSerializer<A>,
|
||||||
private val resultSerializer: KSerializer<R>,
|
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> {
|
operator fun getValue(nothing: Nothing?, property: KProperty<*>): Command<A, R> {
|
||||||
if (name.isEmpty()) {
|
|
||||||
name = overrideName ?: removeCmd(property.name)
|
|
||||||
}
|
|
||||||
return Command(
|
return Command(
|
||||||
name,
|
overrideName ?: property.name,
|
||||||
argsSerializer,
|
argsSerializer,
|
||||||
resultSerializer
|
resultSerializer
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun removeCmd(name: String) =
|
|
||||||
if (name.startsWith("cmd"))
|
|
||||||
name.substring(3)
|
|
||||||
else name
|
|
||||||
@ -1,13 +1,3 @@
|
|||||||
/*
|
|
||||||
* Copyright (c) 2025. Sergey S. Chernov - All Rights Reserved
|
|
||||||
*
|
|
||||||
* You may use, distribute and modify this code under the
|
|
||||||
* terms of the private license, which you must obtain from the author
|
|
||||||
*
|
|
||||||
* To obtain the license, contact the author: https://t.me/real_sergeych or email to
|
|
||||||
* real dot sergeych at gmail.
|
|
||||||
*/
|
|
||||||
|
|
||||||
package net.sergeych.kiloparsec
|
package net.sergeych.kiloparsec
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@ -1,20 +0,0 @@
|
|||||||
/*
|
|
||||||
* Copyright (c) 2025. Sergey S. Chernov - All Rights Reserved
|
|
||||||
*
|
|
||||||
* You may use, distribute and modify this code under the
|
|
||||||
* terms of the private license, which you must obtain from the author
|
|
||||||
*
|
|
||||||
* To obtain the license, contact the author: https://t.me/real_sergeych or email to
|
|
||||||
* real dot sergeych at gmail.
|
|
||||||
*/
|
|
||||||
|
|
||||||
package net.sergeych.kiloparsec
|
|
||||||
|
|
||||||
/**
|
|
||||||
* External errors wich needs to be encoded with a specific code
|
|
||||||
* should implement this interface so it will be serialized properly.
|
|
||||||
*/
|
|
||||||
interface ExceptionWithCode {
|
|
||||||
val code: String
|
|
||||||
val message: String?
|
|
||||||
}
|
|
||||||
@ -1,26 +1,11 @@
|
|||||||
/*
|
|
||||||
* Copyright (c) 2025. Sergey S. Chernov - All Rights Reserved
|
|
||||||
*
|
|
||||||
* You may use, distribute and modify this code under the
|
|
||||||
* terms of the private license, which you must obtain from the author
|
|
||||||
*
|
|
||||||
* To obtain the license, contact the author: https://t.me/real_sergeych or email to
|
|
||||||
* real dot sergeych at gmail.
|
|
||||||
*/
|
|
||||||
|
|
||||||
package net.sergeych.kiloparsec
|
package net.sergeych.kiloparsec
|
||||||
|
|
||||||
import kotlinx.coroutines.CancellationException
|
import kotlinx.coroutines.CancellationException
|
||||||
import kotlinx.coroutines.CompletableDeferred
|
import kotlinx.coroutines.CompletableDeferred
|
||||||
import kotlinx.coroutines.delay
|
|
||||||
import kotlinx.coroutines.flow.MutableStateFlow
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
import kotlinx.coroutines.flow.asStateFlow
|
import kotlinx.coroutines.flow.asStateFlow
|
||||||
import kotlinx.coroutines.isActive
|
import kotlinx.coroutines.isActive
|
||||||
import kotlinx.coroutines.sync.Mutex
|
import net.sergeych.crypto.Key
|
||||||
import kotlinx.coroutines.sync.withLock
|
|
||||||
import net.sergeych.crypto2.SigningKey
|
|
||||||
import net.sergeych.crypto2.VerifyingKey
|
|
||||||
import net.sergeych.crypto2.VerifyingPublicKey
|
|
||||||
import net.sergeych.mp_logger.LogTag
|
import net.sergeych.mp_logger.LogTag
|
||||||
import net.sergeych.mp_logger.Loggable
|
import net.sergeych.mp_logger.Loggable
|
||||||
import net.sergeych.mp_logger.debug
|
import net.sergeych.mp_logger.debug
|
||||||
@ -28,40 +13,18 @@ import net.sergeych.mp_logger.exception
|
|||||||
import net.sergeych.mp_tools.globalLaunch
|
import net.sergeych.mp_tools.globalLaunch
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The auto-connecting client that reconnects to the kiloparsec server,
|
* The auto-connecting client that reconnects to the kiloparsec server
|
||||||
* [KiloServer],
|
|
||||||
* and maintain connection state flow. Client factory launches a disconnected
|
* and maintain connection state flow. Client factory launches a disconnected
|
||||||
* set of coroutines to support automatic reconnection, so you _must_ [close]
|
* set of coroutines to support automatic reconnection, so you _must_ [close]
|
||||||
* it manually when it is unnecessary, otherwise it will continue to reconnect.
|
* it manually when it is not needed, otherwise it will continue to reconnect.
|
||||||
*
|
|
||||||
* ## Usage
|
|
||||||
*
|
|
||||||
* Suppose we have TCP/IP server as in the [KiloServer] usage sample. Then we can connect
|
|
||||||
* to it providing TCP/IP connector like:
|
|
||||||
*
|
|
||||||
* ```kotlin
|
|
||||||
* val client = KiloClient<Unit>() {
|
|
||||||
* connect { connectTcpDevice("localhost:$port") }
|
|
||||||
* }
|
|
||||||
*
|
|
||||||
* // now we can invoke remote commands:
|
|
||||||
* assertEquals("unknown", client.call(cmdLoad))
|
|
||||||
*
|
|
||||||
* client.call(cmdSave, "foobar")
|
|
||||||
* assertEquals("foobar", client.call(cmdLoad))
|
|
||||||
* ```
|
|
||||||
*
|
|
||||||
* ## See also
|
|
||||||
*
|
|
||||||
* [KiloServer]
|
|
||||||
*
|
|
||||||
*/
|
*/
|
||||||
class KiloClient<S>(
|
class KiloClient<S>(
|
||||||
val localInterface: KiloInterface<S>,
|
localInterface: KiloInterface<S>,
|
||||||
secretKey: SigningKey? = null,
|
secretKey: Key.Signing? = null,
|
||||||
connectionDataFactory: ConnectionDataFactory<S>,
|
connectionDataFactory: ConnectionDataFactory<S>,
|
||||||
) : RemoteInterface,
|
) : RemoteInterface,
|
||||||
Loggable by LogTag("CLIF") {
|
Loggable by LogTag("CLIF") {
|
||||||
|
|
||||||
val _state = MutableStateFlow(false)
|
val _state = MutableStateFlow(false)
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -69,15 +32,7 @@ class KiloClient<S>(
|
|||||||
* to authenticate a client on connection restore, for example.
|
* to authenticate a client on connection restore, for example.
|
||||||
*/
|
*/
|
||||||
@Suppress("unused")
|
@Suppress("unused")
|
||||||
val connectedStateFlow = _state.asStateFlow()
|
val state = _state.asStateFlow()
|
||||||
|
|
||||||
/**
|
|
||||||
* The verifying, or public, key identifying client sessions. It could be used to
|
|
||||||
* restore environment on reconnection. This is what remote side, e.g. server, sees as
|
|
||||||
* [KiloScope.remoteIdentity].
|
|
||||||
*/
|
|
||||||
@Suppress("unused")
|
|
||||||
val localIdentity: VerifyingKey? = secretKey?.verifyingKey
|
|
||||||
|
|
||||||
private var deferredClient = CompletableDeferred<KiloClientConnection<S>>()
|
private var deferredClient = CompletableDeferred<KiloClientConnection<S>>()
|
||||||
|
|
||||||
@ -89,63 +44,30 @@ class KiloClient<S>(
|
|||||||
debug { "getting connection" }
|
debug { "getting connection" }
|
||||||
val kc = connectionDataFactory()
|
val kc = connectionDataFactory()
|
||||||
debug { "get device and session" }
|
debug { "get device and session" }
|
||||||
val client = KiloClientConnection(localInterface, kc, secretKey)
|
val client = KiloClientConnection(localInterface, kc,secretKey)
|
||||||
deferredClient.complete(client)
|
deferredClient.complete(client)
|
||||||
debug { "starting client run"}
|
client.run {
|
||||||
val r = runCatching { client.run {
|
_state.value = false
|
||||||
_state.value = it
|
}
|
||||||
} }
|
|
||||||
debug { "----------- client run finished: $r" }
|
|
||||||
resetDeferredClient()
|
|
||||||
debug { "client run finished" }
|
debug { "client run finished" }
|
||||||
} catch (_: RemoteInterface.ClosedException) {
|
} catch (_: RemoteInterface.ClosedException) {
|
||||||
debug { "remote closed" }
|
debug { "remote closed" }
|
||||||
delay(1000)
|
|
||||||
} catch (_: CancellationException) {
|
} catch (_: CancellationException) {
|
||||||
debug { "cancelled" }
|
debug { "cancelled" }
|
||||||
break
|
|
||||||
} catch (t: Throwable) {
|
} catch (t: Throwable) {
|
||||||
exception { "unexpected exception" to t }
|
exception { "unexpected exception" to t }
|
||||||
delay(1000)
|
|
||||||
}
|
}
|
||||||
_state.value = false
|
_state.value = false
|
||||||
resetDeferredClient()
|
if (deferredClient.isActive)
|
||||||
// reconnection timeout
|
deferredClient = CompletableDeferred()
|
||||||
delay(700)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun close() {
|
fun close() {
|
||||||
job.cancel()
|
job.cancel()
|
||||||
debug { "client is closed" }
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private val defMutex = Mutex()
|
override suspend fun <A, R> call(cmd: Command<A, R>, args: A): R = deferredClient.await().call(cmd, args)
|
||||||
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
|
|
||||||
}
|
|
||||||
|
|
||||||
override suspend fun <A> push(cmd: Command<A, Unit>, args: A) {
|
|
||||||
try {
|
|
||||||
deferredClient.await().push(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
|
* Current session token. This is a per-connection unique random value same on the client and server part so
|
||||||
@ -155,14 +77,14 @@ class KiloClient<S>(
|
|||||||
suspend fun token() = deferredClient.await().token()
|
suspend fun token() = deferredClient.await().token()
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Remote party shared key ([VerifyingPublicKey]), could be used ti ensure server is what we expected and
|
* Remote party shared key ([Key.Verifying]]), could be used ti ensure server is what we expected and
|
||||||
* there is no active MITM attack.
|
* there is no active MITM attack.
|
||||||
*
|
*
|
||||||
* Non-null value means the key was successfully authenticated, null means remote party did not provide
|
* 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.
|
* a key. Connection is established either with a properly authenticated key or no key at all.
|
||||||
*/
|
*/
|
||||||
@Suppress("unused")
|
@Suppress("unused")
|
||||||
suspend fun remoteIdentity(): VerifyingPublicKey? = deferredClient.await().remoteId()
|
suspend fun remoteId() = deferredClient.await().remoteId()
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
class Builder<S>() {
|
class Builder<S>() {
|
||||||
@ -174,42 +96,16 @@ class KiloClient<S>(
|
|||||||
}
|
}
|
||||||
private var connectionBuilder: (suspend () -> Transport.Device)? = null
|
private var connectionBuilder: (suspend () -> Transport.Device)? = null
|
||||||
|
|
||||||
var secretIdKey: SigningKey? = null
|
var secretIdKey: Key.Signing? = null
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Build local command implementations, those callable from the server, exception
|
* Build local command implementations (remotely callable ones), exception
|
||||||
* class handlers, and anything else [KiloInterface] allows. Usage sample:
|
* class handlers, etc.
|
||||||
*
|
|
||||||
* ```kotlin
|
|
||||||
* val client = KiloClient {
|
|
||||||
* connect { connectTcpDevice("localhost:$port") }
|
|
||||||
* local {
|
|
||||||
* on(cmdPing) {
|
|
||||||
* "pong! $it"
|
|
||||||
* }
|
|
||||||
* }
|
|
||||||
* }
|
|
||||||
* ```
|
|
||||||
*/
|
*/
|
||||||
fun local(f: KiloInterface<S>.() -> Unit) {
|
fun local(f: KiloInterface<S>.() -> Unit) {
|
||||||
interfaceBuilder = f
|
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
|
* Create a new session object, otherwise Unit session will be used
|
||||||
*/
|
*/
|
||||||
@ -221,15 +117,12 @@ class KiloClient<S>(
|
|||||||
connectionBuilder = f
|
connectionBuilder = f
|
||||||
}
|
}
|
||||||
|
|
||||||
val errorProviders = mutableListOf<LocalInterface<*>>()
|
|
||||||
|
|
||||||
internal fun build(): KiloClient<S> {
|
internal fun build(): KiloClient<S> {
|
||||||
val i = KiloInterface<S>()
|
val i = KiloInterface<S>()
|
||||||
for (ep in errorProviders) i.addErrorProvider(ep)
|
|
||||||
interfaceBuilder?.let { i.it() }
|
interfaceBuilder?.let { i.it() }
|
||||||
val connector = connectionBuilder ?: throw IllegalArgumentException("connect handler was not set")
|
val connector = connectionBuilder ?: throw IllegalArgumentException("connect handler was not set")
|
||||||
return KiloClient(i, secretIdKey) {
|
return KiloClient(i,secretIdKey) {
|
||||||
KiloConnectionData(connector(), sessionBuilder())
|
KiloConnectionData(connector(),sessionBuilder())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,19 +1,8 @@
|
|||||||
/*
|
|
||||||
* Copyright (c) 2025. Sergey S. Chernov - All Rights Reserved
|
|
||||||
*
|
|
||||||
* You may use, distribute and modify this code under the
|
|
||||||
* terms of the private license, which you must obtain from the author
|
|
||||||
*
|
|
||||||
* To obtain the license, contact the author: https://t.me/real_sergeych or email to
|
|
||||||
* real dot sergeych at gmail.
|
|
||||||
*/
|
|
||||||
|
|
||||||
package net.sergeych.kiloparsec
|
package net.sergeych.kiloparsec
|
||||||
|
|
||||||
|
import com.ionspin.kotlin.crypto.keyexchange.KeyExchange
|
||||||
import kotlinx.coroutines.*
|
import kotlinx.coroutines.*
|
||||||
import net.sergeych.crypto2.SafeKeyExchange
|
import net.sergeych.crypto.Key
|
||||||
import net.sergeych.crypto2.SigningKey
|
|
||||||
import net.sergeych.crypto2.VerifyingPublicKey
|
|
||||||
import net.sergeych.mp_logger.LogTag
|
import net.sergeych.mp_logger.LogTag
|
||||||
import net.sergeych.mp_logger.Loggable
|
import net.sergeych.mp_logger.Loggable
|
||||||
import net.sergeych.mp_logger.debug
|
import net.sergeych.mp_logger.debug
|
||||||
@ -23,20 +12,20 @@ import net.sergeych.utools.pack
|
|||||||
private var clientIds = 0
|
private var clientIds = 0
|
||||||
|
|
||||||
class KiloClientConnection<S>(
|
class KiloClientConnection<S>(
|
||||||
private val clientInterface: KiloInterface<S>,
|
private val clientInterface: LocalInterface<KiloScope<S>>,
|
||||||
private val device: Transport.Device,
|
private val device: Transport.Device,
|
||||||
private val session: S,
|
private val session: S,
|
||||||
private val secretIdKey: SigningKey? = null,
|
private val secretIdKey: Key.Signing? = null,
|
||||||
) : RemoteInterface, Loggable by LogTag("KPC:${++clientIds}") {
|
) : RemoteInterface, Loggable by LogTag("KPC:${++clientIds}") {
|
||||||
|
|
||||||
constructor(localInterface: KiloInterface<S>, connection: KiloConnectionData<S>, secretIdKey: SigningKey? = null)
|
constructor(localInterface: KiloInterface<S>, connection: KiloConnectionData<S>, secretIdKey: Key.Signing? = null)
|
||||||
: this(localInterface, connection.device, connection.session, secretIdKey)
|
: this(localInterface, connection.device, connection.session, secretIdKey)
|
||||||
|
|
||||||
private val kiloRemoteInterface = CompletableDeferred<KiloRemoteInterface<S>>()
|
private val kiloRemoteInterface = CompletableDeferred<KiloRemoteInterface<S>>()
|
||||||
|
|
||||||
private val deferredParams = CompletableDeferred<KiloParams<S>>()
|
private val deferredParams = CompletableDeferred<KiloParams<S>>()
|
||||||
|
|
||||||
suspend fun remoteId(): VerifyingPublicKey? = deferredParams.await().remoteIdentity
|
suspend fun remoteId(): Key.Verifying? = deferredParams.await().remoteIdentity
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Run the client, blocking until the device is closed, or some critical exception
|
* Run the client, blocking until the device is closed, or some critical exception
|
||||||
@ -48,7 +37,8 @@ class KiloClientConnection<S>(
|
|||||||
var job: Job? = null
|
var job: Job? = null
|
||||||
try {
|
try {
|
||||||
// in parallel: keys and connection
|
// in parallel: keys and connection
|
||||||
val deferredKeyPair = async { SafeKeyExchange() }
|
val deferredKeyPair = async { KeyExchange.keypair() }
|
||||||
|
debug { "opening device" }
|
||||||
debug { "got a transport device $device" }
|
debug { "got a transport device $device" }
|
||||||
|
|
||||||
|
|
||||||
@ -61,26 +51,27 @@ class KiloClientConnection<S>(
|
|||||||
debug { "transport started" }
|
debug { "transport started" }
|
||||||
|
|
||||||
val pair = deferredKeyPair.await()
|
val pair = deferredKeyPair.await()
|
||||||
debug { "keypair ready (1)" }
|
debug { "keypair ready" }
|
||||||
|
|
||||||
val serverHe = transport.call(L0Request, Handshake(1u, pair.publicKey))
|
val serverHe = transport.call(L0Request, Handshake(1u, pair.publicKey))
|
||||||
|
|
||||||
debug { "got server HE (2)" }
|
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)
|
var params = KiloParams(false, transport, sk, session, null, this@KiloClientConnection)
|
||||||
|
|
||||||
// Check ID if any
|
// Check ID if any
|
||||||
serverHe.signature?.let { s ->
|
serverHe.serverSharedKey?.let { k ->
|
||||||
if (!s.isValid(params.token))
|
if (serverHe.signature == null)
|
||||||
|
throw RemoteInterface.SecurityException("missing signature")
|
||||||
|
if (!k.verify(serverHe.signature, params.token))
|
||||||
throw RemoteInterface.SecurityException("wrong signature")
|
throw RemoteInterface.SecurityException("wrong signature")
|
||||||
params = params.copy(remoteIdentity = s.publicKey)
|
params = params.copy(remoteIdentity = k)
|
||||||
}
|
}
|
||||||
|
|
||||||
transport.call(
|
transport.call(
|
||||||
L0ClientId, params.encrypt(
|
L0ClientId, params.encrypt(
|
||||||
pack(
|
pack(
|
||||||
ClientIdentity(
|
ClientIdentity(
|
||||||
secretIdKey?.verifyingKey,
|
secretIdKey?.verifying,
|
||||||
secretIdKey?.sign(params.token)
|
secretIdKey?.sign(params.token)
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
@ -90,14 +81,13 @@ class KiloClientConnection<S>(
|
|||||||
kiloRemoteInterface.complete(
|
kiloRemoteInterface.complete(
|
||||||
KiloRemoteInterface(deferredParams, clientInterface)
|
KiloRemoteInterface(deferredParams, clientInterface)
|
||||||
)
|
)
|
||||||
clientInterface.onConnectHandlers.invokeAll(params.scope)
|
|
||||||
onConnectedStateChanged?.invoke(true)
|
onConnectedStateChanged?.invoke(true)
|
||||||
job.join()
|
job.join()
|
||||||
|
|
||||||
} catch (x: CancellationException) {
|
} catch (x: CancellationException) {
|
||||||
info { "client is cancelled" }
|
info { "client is cancelled" }
|
||||||
} catch (x: RemoteInterface.ClosedException) {
|
} catch (x: RemoteInterface.ClosedException) {
|
||||||
debug { "connection closed/refused by remote" }
|
info { "connection closed by remote" }
|
||||||
} finally {
|
} finally {
|
||||||
onConnectedStateChanged?.invoke(false)
|
onConnectedStateChanged?.invoke(false)
|
||||||
job?.cancel()
|
job?.cancel()
|
||||||
@ -109,11 +99,4 @@ class KiloClientConnection<S>(
|
|||||||
suspend fun token() = deferredParams.await().token
|
suspend fun token() = deferredParams.await().token
|
||||||
override suspend fun <A, R> call(cmd: Command<A, R>, args: A): R =
|
override suspend fun <A, R> call(cmd: Command<A, R>, args: A): R =
|
||||||
kiloRemoteInterface.await().call(cmd, args)
|
kiloRemoteInterface.await().call(cmd, args)
|
||||||
|
}
|
||||||
override suspend fun <A> push(cmd: Command<A, Unit>, args: A) {
|
|
||||||
kiloRemoteInterface.await().push(cmd, args)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
internal fun <S>Collection<KiloHandler<S>>.invokeAll(scope: KiloScope<S>) =
|
|
||||||
forEach { runCatching { scope.it() } }
|
|
||||||
@ -1,44 +1,14 @@
|
|||||||
/*
|
|
||||||
* Copyright (c) 2025. Sergey S. Chernov - All Rights Reserved
|
|
||||||
*
|
|
||||||
* You may use, distribute and modify this code under the
|
|
||||||
* terms of the private license, which you must obtain from the author
|
|
||||||
*
|
|
||||||
* To obtain the license, contact the author: https://t.me/real_sergeych or email to
|
|
||||||
* real dot sergeych at gmail.
|
|
||||||
*/
|
|
||||||
|
|
||||||
package net.sergeych.kiloparsec
|
package net.sergeych.kiloparsec
|
||||||
|
|
||||||
typealias KiloHandler<S> = KiloScope<S>.()->Unit
|
|
||||||
/**
|
/**
|
||||||
* The local interface to provide functions, register errors for Kiloparsec users. Use it
|
* The local interface to provice functions, register errors for Kiloparsec users. Use it
|
||||||
* with [KiloClient], [KiloClientConnection], [KiloServerConnection], etc.
|
* with [KiloClient], [KiloClientConnection], [KiloServerConnection], etc.
|
||||||
*
|
*
|
||||||
* Base class implementation does the following:
|
* BAse implementation registers relevant exceptions.
|
||||||
*
|
|
||||||
* - It registers common exceptions from [RemoteInterface] and kotlin/java `IllegalArgumentException` and
|
|
||||||
* `IllegalStateException`
|
|
||||||
* - It provides [onConnected] handler
|
|
||||||
*
|
|
||||||
* See [KiloServer] for usage sample.
|
|
||||||
*/
|
*/
|
||||||
open class KiloInterface<S> : LocalInterface<KiloScope<S>>() {
|
class KiloInterface<S> : LocalInterface<KiloScope<S>>() {
|
||||||
|
|
||||||
internal val onConnectHandlers = mutableListOf<KiloHandler<S>>()
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Registers handler [f] for [onConnected] event, to the head or the end of handler list.
|
|
||||||
*
|
|
||||||
* @param addFirst if true, [f] will be added to the beginning of the list of handlers
|
|
||||||
*/
|
|
||||||
fun onConnected(addFirst: Boolean = false, f: KiloScope<S>.()->Unit) {
|
|
||||||
if( addFirst ) onConnectHandlers.add(0, f) else onConnectHandlers += f
|
|
||||||
}
|
|
||||||
|
|
||||||
init {
|
init {
|
||||||
registerError { RemoteInterface.UnknownCommand(it) }
|
registerError { RemoteInterface.UnknownCommand() }
|
||||||
registerError { RemoteInterface.InternalError(it) }
|
|
||||||
registerError { RemoteInterface.ClosedException(it) }
|
registerError { RemoteInterface.ClosedException(it) }
|
||||||
registerError { RemoteInterface.SecurityException(it) }
|
registerError { RemoteInterface.SecurityException(it) }
|
||||||
registerError { RemoteInterface.InvalidDataException(it) }
|
registerError { RemoteInterface.InvalidDataException(it) }
|
||||||
|
|||||||
@ -1,21 +1,8 @@
|
|||||||
/*
|
|
||||||
* Copyright (c) 2025. Sergey S. Chernov - All Rights Reserved
|
|
||||||
*
|
|
||||||
* You may use, distribute and modify this code under the
|
|
||||||
* terms of the private license, which you must obtain from the author
|
|
||||||
*
|
|
||||||
* To obtain the license, contact the author: https://t.me/real_sergeych or email to
|
|
||||||
* real dot sergeych at gmail.
|
|
||||||
*/
|
|
||||||
|
|
||||||
package net.sergeych.kiloparsec
|
package net.sergeych.kiloparsec
|
||||||
|
|
||||||
import kotlinx.coroutines.CompletableDeferred
|
import kotlinx.coroutines.CompletableDeferred
|
||||||
import net.sergeych.tools.AtomicCounter
|
|
||||||
import net.sergeych.utools.pack
|
import net.sergeych.utools.pack
|
||||||
|
|
||||||
private val idCounter = AtomicCounter(0)
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* This class is not normally used directly. This is a local interface that supports
|
* 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].
|
* secure transport command layer (encrypted calls/results) to work with [KiloRemoteInterface].
|
||||||
@ -25,10 +12,7 @@ private val idCounter = AtomicCounter(0)
|
|||||||
internal class KiloL0Interface<T>(
|
internal class KiloL0Interface<T>(
|
||||||
private val clientInterface: LocalInterface<KiloScope<T>>,
|
private val clientInterface: LocalInterface<KiloScope<T>>,
|
||||||
private val deferredParams: CompletableDeferred<KiloParams<T>>,
|
private val deferredParams: CompletableDeferred<KiloParams<T>>,
|
||||||
) : LocalInterface<Unit>() {
|
): LocalInterface<Unit>() {
|
||||||
|
|
||||||
override var logTag: String = "KL0:${idCounter.incrementAndGet()}"
|
|
||||||
|
|
||||||
init {
|
init {
|
||||||
// local interface uses the same session as a client:
|
// local interface uses the same session as a client:
|
||||||
addErrorProvider(clientInterface)
|
addErrorProvider(clientInterface)
|
||||||
@ -43,10 +27,7 @@ internal class KiloL0Interface<T>(
|
|||||||
0u,
|
0u,
|
||||||
clientInterface.execute(params.scope, call.name, call.serializedArgs)
|
clientInterface.execute(params.scope, call.name, call.serializedArgs)
|
||||||
)
|
)
|
||||||
} catch(t: BreakConnectionException) {
|
} catch (t: Throwable) {
|
||||||
throw t
|
|
||||||
}
|
|
||||||
catch (t: Throwable) {
|
|
||||||
clientInterface.encodeError(0u, t)
|
clientInterface.encodeError(0u, t)
|
||||||
}
|
}
|
||||||
params.encrypt(pack(result))
|
params.encrypt(pack(result))
|
||||||
|
|||||||
@ -1,23 +1,23 @@
|
|||||||
/*
|
|
||||||
* Copyright (c) 2025. Sergey S. Chernov - All Rights Reserved
|
|
||||||
*
|
|
||||||
* You may use, distribute and modify this code under the
|
|
||||||
* terms of the private license, which you must obtain from the author
|
|
||||||
*
|
|
||||||
* To obtain the license, contact the author: https://t.me/real_sergeych or email to
|
|
||||||
* real dot sergeych at gmail.
|
|
||||||
*/
|
|
||||||
|
|
||||||
package net.sergeych.kiloparsec
|
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 kotlinx.serialization.Serializable
|
||||||
import net.sergeych.bintools.toDataSource
|
import net.sergeych.bintools.toDataSource
|
||||||
import net.sergeych.bipack.BipackDecoder
|
import net.sergeych.bipack.BipackDecoder
|
||||||
import net.sergeych.bipack.Unsigned
|
import net.sergeych.bipack.BipackEncoder
|
||||||
import net.sergeych.crypto2.*
|
import net.sergeych.crypto.DecryptionFailedException
|
||||||
import net.sergeych.synctools.ProtectedOp
|
import net.sergeych.crypto.Key
|
||||||
import net.sergeych.synctools.invoke
|
import net.sergeych.crypto.randomBytes
|
||||||
|
import net.sergeych.crypto.randomUInt
|
||||||
|
import net.sergeych.tools.ProtectedOp
|
||||||
import net.sergeych.utools.pack
|
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
|
import kotlin.math.roundToInt
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -32,18 +32,23 @@ import kotlin.math.roundToInt
|
|||||||
data class KiloParams<S>(
|
data class KiloParams<S>(
|
||||||
val isServer: Boolean,
|
val isServer: Boolean,
|
||||||
val transport: RemoteInterface,
|
val transport: RemoteInterface,
|
||||||
val sessionKey: SafeKeyExchange.SessionKey,
|
val sessionKeyPair: KeyExchangeSessionKeyPair,
|
||||||
val scopeSession: S,
|
val scopeSession: S,
|
||||||
val remoteIdentity: VerifyingPublicKey?,
|
val remoteIdentity: Key.Verifying?,
|
||||||
val remoteTransport: RemoteInterface,
|
val remoteTransport: RemoteInterface
|
||||||
) {
|
) {
|
||||||
@Serializable
|
@Serializable
|
||||||
data class Package(
|
data class Package(
|
||||||
@Unsigned
|
|
||||||
val nonce: ULong,
|
val nonce: ULong,
|
||||||
val encryptedMessage: UByteArray,
|
val encryptedMessage: UByteArray,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
data class FilledData(
|
||||||
|
val message: UByteArray,
|
||||||
|
val fill: UByteArray,
|
||||||
|
)
|
||||||
|
|
||||||
private var nonce = 0UL
|
private var nonce = 0UL
|
||||||
|
|
||||||
val scope: KiloScope<S> by lazy {
|
val scope: KiloScope<S> by lazy {
|
||||||
@ -51,39 +56,81 @@ data class KiloParams<S>(
|
|||||||
override val session = scopeSession
|
override val session = scopeSession
|
||||||
override val remote: RemoteInterface = remoteTransport
|
override val remote: RemoteInterface = remoteTransport
|
||||||
override val sessionToken: UByteArray = token
|
override val sessionToken: UByteArray = token
|
||||||
override val remoteIdentity: VerifyingPublicKey? = this@KiloParams.remoteIdentity
|
override val remoteIdentity: Key.Verifying? = this@KiloParams.remoteIdentity
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
val token: UByteArray by lazy {
|
val token: UByteArray by lazy {
|
||||||
blake2b("token_".encodeToUByteArray() + sessionKey.sessionTag).sliceArray(0..<SymmetricKey.nonceLength)
|
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)
|
||||||
}
|
}
|
||||||
|
|
||||||
private val numericNonce = NumericNonce(token)
|
private val sendBase by lazy {
|
||||||
|
Keccak.digest(
|
||||||
|
sessionKeyPair.sendKey.toByteArray(), KeccakParameter.KECCAK_256
|
||||||
|
).toUByteArray().sliceArray(0..<crypto_secretbox_NONCEBYTES)
|
||||||
|
}
|
||||||
|
|
||||||
private val protectedOp = ProtectedOp()
|
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()
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Encrypt using send keys and proper nonce
|
* Encrypt using send keys and proper nonce
|
||||||
*/
|
*/
|
||||||
fun encrypt(message: UByteArray, fillFactor: Float = 0f): UByteArray {
|
fun encrypt(message: UByteArray, fillFactor: Float = 0f): UByteArray {
|
||||||
val fill = if (fillFactor > 0f)
|
val fill: UByteArray = if (fillFactor > 0f)
|
||||||
0..(message.size * fillFactor).roundToInt()
|
randomBytes(randomUInt((message.size * fillFactor).roundToInt()))
|
||||||
else
|
else
|
||||||
null
|
ubyteArrayOf()
|
||||||
|
|
||||||
val n = protectedOp.invoke { nonce++ }
|
val withFill = BipackEncoder.encode(FilledData(message, fill)).toUByteArray()
|
||||||
|
|
||||||
|
val n = proptectedOp { nonce++ }
|
||||||
|
|
||||||
return pack(
|
return pack(
|
||||||
Package(n, sessionKey.encryptWithNonce(message, numericNonce.withULong(n), fill))
|
Package(n, SecretBox.easy(withFill, encodeSendNonce(n), sessionKeyPair.sendKey))
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
// fun decryptString(cipherText: UByteArray): String = decrypt(cipherText).decodeFromUByteArray()
|
fun decryptString(cipherText: UByteArray): String = decrypt(cipherText).decodeFromUByteArray()
|
||||||
fun decrypt(encryptedMessage: UByteArray): UByteArray =
|
fun decrypt(encryptedMessage: UByteArray): UByteArray {
|
||||||
protectDecryption {
|
val p: Package = BipackDecoder.decode(encryptedMessage.toDataSource())
|
||||||
val p: Package = BipackDecoder.decode(encryptedMessage.toDataSource())
|
try {
|
||||||
sessionKey.decryptWithNonce(p.encryptedMessage, numericNonce.withULong(p.nonce))
|
return unpack<FilledData>(
|
||||||
|
SecretBox.openEasy(
|
||||||
|
p.encryptedMessage,
|
||||||
|
encodeReceiveNonce(p.nonce),
|
||||||
|
sessionKeyPair.receiveKey
|
||||||
|
)
|
||||||
|
).message
|
||||||
|
} catch (_: com.ionspin.kotlin.crypto.secretbox.SecretBoxCorruptedOrTamperedDataExceptionOrInvalidKey) {
|
||||||
|
throw DecryptionFailedException()
|
||||||
}
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@ -1,13 +1,3 @@
|
|||||||
/*
|
|
||||||
* Copyright (c) 2025. Sergey S. Chernov - All Rights Reserved
|
|
||||||
*
|
|
||||||
* You may use, distribute and modify this code under the
|
|
||||||
* terms of the private license, which you must obtain from the author
|
|
||||||
*
|
|
||||||
* To obtain the license, contact the author: https://t.me/real_sergeych or email to
|
|
||||||
* real dot sergeych at gmail.
|
|
||||||
*/
|
|
||||||
|
|
||||||
package net.sergeych.kiloparsec
|
package net.sergeych.kiloparsec
|
||||||
|
|
||||||
import kotlinx.coroutines.CompletableDeferred
|
import kotlinx.coroutines.CompletableDeferred
|
||||||
@ -17,13 +7,7 @@ import net.sergeych.utools.unpack
|
|||||||
|
|
||||||
private var L1IdCounter = 0
|
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>(
|
class KiloRemoteInterface<S>(
|
||||||
private val deferredParams: CompletableDeferred<KiloParams<S>>,
|
private val deferredParams: CompletableDeferred<KiloParams<S>>,
|
||||||
private val clientInterface: LocalInterface<KiloScope<S>>,
|
private val clientInterface: LocalInterface<KiloScope<S>>,
|
||||||
@ -45,10 +29,5 @@ class KiloRemoteInterface<S>(
|
|||||||
else -> throw RemoteInterface.Exception("unexpected block type: $block")
|
else -> throw RemoteInterface.Exception("unexpected block type: $block")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun <A> push(cmd: Command<A, Unit>, args: A) {
|
|
||||||
val params = deferredParams.await()
|
|
||||||
params.transport.call(L0Call, params.encrypt(cmd.packCall(args)))
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -1,17 +1,6 @@
|
|||||||
/*
|
|
||||||
* Copyright (c) 2025. Sergey S. Chernov - All Rights Reserved
|
|
||||||
*
|
|
||||||
* You may use, distribute and modify this code under the
|
|
||||||
* terms of the private license, which you must obtain from the author
|
|
||||||
*
|
|
||||||
* To obtain the license, contact the author: https://t.me/real_sergeych or email to
|
|
||||||
* real dot sergeych at gmail.
|
|
||||||
*/
|
|
||||||
|
|
||||||
package net.sergeych.kiloparsec
|
package net.sergeych.kiloparsec
|
||||||
|
|
||||||
import net.sergeych.crypto2.SigningKey
|
import net.sergeych.crypto.Key
|
||||||
import net.sergeych.crypto2.VerifyingPublicKey
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Scope for Kiloparsec client/server commands execution, contain per-connection specific data. The scope
|
* Scope for Kiloparsec client/server commands execution, contain per-connection specific data. The scope
|
||||||
@ -37,8 +26,8 @@ interface KiloScope<S> {
|
|||||||
val sessionToken: UByteArray
|
val sessionToken: UByteArray
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* If the remote part has provided a secret key, e.g., gave non-null [SigningKey] on construction,
|
* 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 [VerifyingPublicKey] shared key here.
|
* the kiloparsec checks it in the MITM-safe way and provides its [Key.Verifying] shared key here.
|
||||||
* Knowing a remote party shared key, it is possible to be sure that the connection is made directly
|
* 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.
|
* to this party with no middle point intruders.
|
||||||
*
|
*
|
||||||
@ -48,6 +37,6 @@ interface KiloScope<S> {
|
|||||||
* In spite of the above said, which means, non-null value in this field means the key is authorized, but
|
* 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.
|
* It is up to the caller to ensure it is expected key of the remote party.
|
||||||
*/
|
*/
|
||||||
val remoteIdentity: VerifyingPublicKey?
|
val remoteIdentity: Key.Verifying?
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -1,153 +0,0 @@
|
|||||||
/*
|
|
||||||
* Copyright (c) 2025. Sergey S. Chernov - All Rights Reserved
|
|
||||||
*
|
|
||||||
* You may use, distribute and modify this code under the
|
|
||||||
* terms of the private license, which you must obtain from the author
|
|
||||||
*
|
|
||||||
* To obtain the license, contact the author: https://t.me/real_sergeych or email to
|
|
||||||
* real dot sergeych at gmail.
|
|
||||||
*/
|
|
||||||
|
|
||||||
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()
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The Kiloparsec server.
|
|
||||||
* Server accepts incoming connections and serves them using the same [clientInterface].
|
|
||||||
*
|
|
||||||
* ## Incoming connections
|
|
||||||
*
|
|
||||||
* Server collecting incoming connections provided by [connections] `Flow`. For each incoming connection
|
|
||||||
* the Kiloparsec handshake is performed, then the session object is created, see below, and connection is
|
|
||||||
* served with [clientInterface] until closed.
|
|
||||||
*
|
|
||||||
* ## Session param [S]
|
|
||||||
*
|
|
||||||
* After the successful handshake server creates new session for each connection calling the [sessionBuilder].
|
|
||||||
* Then it creates a [KiloScope] so [KiloScope.session] holds this connection-specific instance. Then
|
|
||||||
* [KiloInterface.onConnected] is called with this scope. Session can be used to hold connection state. Session
|
|
||||||
* objects are not persistent, but could be initialized in [KiloInterface.onConnected] where the remote side
|
|
||||||
* [KiloScope.remoteIdentity] is already verified and set.
|
|
||||||
*
|
|
||||||
* ## Usage:
|
|
||||||
*
|
|
||||||
* Create a shared library between you server and clients, to specify the interface (otherwise you can
|
|
||||||
* share sources).
|
|
||||||
*
|
|
||||||
* Suppose we have session with a state:
|
|
||||||
* ```kotlin
|
|
||||||
* data class Session(
|
|
||||||
* var data: String,
|
|
||||||
* )
|
|
||||||
*```
|
|
||||||
*
|
|
||||||
* And some commands to access and change it, in the shared library too:
|
|
||||||
*
|
|
||||||
* ```kotlin
|
|
||||||
* val cmdSave by command<String, Unit>()
|
|
||||||
* val cmdLoad by command<Unit, String>()
|
|
||||||
* val cmdDrop by command<Unit, Unit>()
|
|
||||||
* val cmdException by command<Unit, Unit>()
|
|
||||||
* ```
|
|
||||||
* Then the server code (TCP/IP variant) could look like:
|
|
||||||
*
|
|
||||||
* ```kotlin
|
|
||||||
* // The server implementation (could be shared between server instances connected
|
|
||||||
* // to different protocol adapters):
|
|
||||||
*
|
|
||||||
* val cli = KiloInterface<Session>().apply {
|
|
||||||
* // Suppose we want to throw this exception at the caller site, so we need to register it:
|
|
||||||
* registerError { SomeException() }
|
|
||||||
*
|
|
||||||
* // Session initialization. If you need a sessino to depend initially on the client's identity.
|
|
||||||
* // you can do it here:
|
|
||||||
* onConnected {
|
|
||||||
* // check the remoteIdentity
|
|
||||||
* session.data = if( remoteIdentity == somePublicVerifyingKey )
|
|
||||||
* "known"
|
|
||||||
* else
|
|
||||||
* "unknown"
|
|
||||||
* }
|
|
||||||
* on(cmdSave) { session.data = it }
|
|
||||||
* on(cmdLoad) {
|
|
||||||
* session.data
|
|
||||||
* }
|
|
||||||
* on(cmdException) {
|
|
||||||
* throw TestException()
|
|
||||||
* }
|
|
||||||
* on(cmdDrop) {
|
|
||||||
* throw LocalInterface.BreakConnectionException()
|
|
||||||
* }
|
|
||||||
* }
|
|
||||||
*
|
|
||||||
* // Create the server instance that accepts incoming TCP/IP connections on all local interfaces using the
|
|
||||||
* // specified port:
|
|
||||||
*
|
|
||||||
* val server = KiloServer(cli, acceptTcpDevice(port)) {
|
|
||||||
* // This creates a new session
|
|
||||||
* Session("unknown")
|
|
||||||
* }
|
|
||||||
* ```
|
|
||||||
*
|
|
||||||
* See [KiloClient] to connect to the server.
|
|
||||||
*
|
|
||||||
* @param S the type of the server session object, returned by [sessionBuilder]. See above
|
|
||||||
* @param clientInterface the interface available for remote calls
|
|
||||||
* @param connections flow of incoming connections. Server stops when the flow is fully collected (normally
|
|
||||||
* it shouldn't)
|
|
||||||
* @param serverSecretKey the [SigningKey] used to identify this server during Kiloparsec handshake.
|
|
||||||
* @param sessionBuilder callback that creates session objects for successful incoming connections
|
|
||||||
*/
|
|
||||||
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 }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Stop the server and cancel all pending sessions. Unlike finishing the flow passed
|
|
||||||
* for [KiloServer.connections], it will cancel all currently active sessions.
|
|
||||||
*/
|
|
||||||
fun close() {
|
|
||||||
job.cancel()
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Server is closed either if [close] was called or all [KiloServer.connections] were collected and flow was
|
|
||||||
* closed.
|
|
||||||
*/
|
|
||||||
@Suppress("unused")
|
|
||||||
val isClosed: Boolean get() = job.isCompleted
|
|
||||||
}
|
|
||||||
@ -1,18 +1,8 @@
|
|||||||
/*
|
|
||||||
* Copyright (c) 2025. Sergey S. Chernov - All Rights Reserved
|
|
||||||
*
|
|
||||||
* You may use, distribute and modify this code under the
|
|
||||||
* terms of the private license, which you must obtain from the author
|
|
||||||
*
|
|
||||||
* To obtain the license, contact the author: https://t.me/real_sergeych or email to
|
|
||||||
* real dot sergeych at gmail.
|
|
||||||
*/
|
|
||||||
|
|
||||||
package net.sergeych.kiloparsec
|
package net.sergeych.kiloparsec
|
||||||
|
|
||||||
|
import com.ionspin.kotlin.crypto.keyexchange.KeyExchange
|
||||||
import kotlinx.coroutines.CompletableDeferred
|
import kotlinx.coroutines.CompletableDeferred
|
||||||
import net.sergeych.crypto2.SafeKeyExchange
|
import net.sergeych.crypto.Key
|
||||||
import net.sergeych.crypto2.SigningKey
|
|
||||||
import net.sergeych.mp_logger.LogTag
|
import net.sergeych.mp_logger.LogTag
|
||||||
import net.sergeych.mp_logger.Loggable
|
import net.sergeych.mp_logger.Loggable
|
||||||
import net.sergeych.mp_logger.debug
|
import net.sergeych.mp_logger.debug
|
||||||
@ -33,15 +23,15 @@ class KiloServerConnection<S>(
|
|||||||
private val clientInterface: KiloInterface<S>,
|
private val clientInterface: KiloInterface<S>,
|
||||||
private val device: Transport.Device,
|
private val device: Transport.Device,
|
||||||
private val session: S,
|
private val session: S,
|
||||||
private val serverSigningKey: SigningKey? = null
|
private val serverSigningKey: Key.Signing? = null
|
||||||
) : RemoteInterface, Loggable by LogTag("SRV${++serverIds}") {
|
) : RemoteInterface, Loggable by LogTag("SRV${++serverIds}") {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Shortcut to construct with [KiloConnectionData] intance
|
* Shortcut to construct with [KiloConnectionData] intance
|
||||||
*/
|
*/
|
||||||
@Suppress("unused")
|
@Suppress("unused")
|
||||||
constructor(localInterface: KiloInterface<S>, connection: KiloConnectionData<S>, serverSecretKey: SigningKey? = null)
|
constructor(localInterface: KiloInterface<S>, connection: KiloConnectionData<S>, serverSigningKey: Key.Signing? = null)
|
||||||
: this(localInterface, connection.device, connection.session, serverSecretKey)
|
: this(localInterface, connection.device, connection.session, serverSigningKey)
|
||||||
|
|
||||||
private val kiloRemoteInterface = CompletableDeferred<KiloRemoteInterface<S>>()
|
private val kiloRemoteInterface = CompletableDeferred<KiloRemoteInterface<S>>()
|
||||||
|
|
||||||
@ -57,7 +47,9 @@ class KiloServerConnection<S>(
|
|||||||
val l0Interface = KiloL0Interface(clientInterface, deferredParams).apply {
|
val l0Interface = KiloL0Interface(clientInterface, deferredParams).apply {
|
||||||
var params: KiloParams<S>? = null
|
var params: KiloParams<S>? = null
|
||||||
on(L0Request) {
|
on(L0Request) {
|
||||||
val sk = pair.serverSessionKey(it.publicKey)
|
val sk = KeyExchange.serverSessionKeys(
|
||||||
|
pair.publicKey, pair.secretKey, it.publicKey
|
||||||
|
)
|
||||||
|
|
||||||
params = KiloParams(
|
params = KiloParams(
|
||||||
true,
|
true,
|
||||||
@ -67,9 +59,15 @@ class KiloServerConnection<S>(
|
|||||||
null,
|
null,
|
||||||
this@KiloServerConnection
|
this@KiloServerConnection
|
||||||
)
|
)
|
||||||
Handshake(1u, pair.publicKey, serverSigningKey?.seal(params!!.token))
|
|
||||||
}
|
|
||||||
|
|
||||||
|
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)
|
||||||
|
}
|
||||||
on(L0ClientId) {
|
on(L0ClientId) {
|
||||||
var p = params ?: throw RemoteInterface.ClosedException("wrong handshake sequence")
|
var p = params ?: throw RemoteInterface.ClosedException("wrong handshake sequence")
|
||||||
val ci = unpack<ClientIdentity>(p.decrypt(it))
|
val ci = unpack<ClientIdentity>(p.decrypt(it))
|
||||||
@ -85,27 +83,22 @@ class KiloServerConnection<S>(
|
|||||||
kiloRemoteInterface.complete(
|
kiloRemoteInterface.complete(
|
||||||
KiloRemoteInterface(deferredParams, clientInterface)
|
KiloRemoteInterface(deferredParams, clientInterface)
|
||||||
)
|
)
|
||||||
clientInterface.onConnectHandlers.invokeAll(p.scope)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
val transport = Transport(device, l0Interface, Unit)
|
val transport = Transport(device, l0Interface, Unit)
|
||||||
deferredTransport.complete(transport)
|
deferredTransport.complete(transport)
|
||||||
kiloRemoteInterface.complete(KiloRemoteInterface(deferredParams,clientInterface))
|
kiloRemoteInterface.complete(KiloRemoteInterface(deferredParams,clientInterface))
|
||||||
debug { "starting the transport"}
|
debug { "starintg the transport"}
|
||||||
transport.run()
|
transport.run()
|
||||||
debug { "server transport finished" }
|
debug { "server transport finished" }
|
||||||
}
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
val pair = SafeKeyExchange()
|
val pair = KeyExchange.keypair()
|
||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun <A, R> call(cmd: Command<A, R>, args: A): R {
|
override suspend fun <A, R> call(cmd: Command<A, R>, args: A): R {
|
||||||
return kiloRemoteInterface.await().call(cmd, args)
|
return kiloRemoteInterface.await().call(cmd, args)
|
||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun <A> push(cmd: Command<A, Unit>, args: A) {
|
|
||||||
kiloRemoteInterface.await().push(cmd, args)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
@ -1,51 +1,14 @@
|
|||||||
/*
|
|
||||||
* Copyright (c) 2025. Sergey S. Chernov - All Rights Reserved
|
|
||||||
*
|
|
||||||
* You may use, distribute and modify this code under the
|
|
||||||
* terms of the private license, which you must obtain from the author
|
|
||||||
*
|
|
||||||
* To obtain the license, contact the author: https://t.me/real_sergeych or email to
|
|
||||||
* real dot sergeych at gmail.
|
|
||||||
*/
|
|
||||||
|
|
||||||
package net.sergeych.kiloparsec
|
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 net.sergeych.utools.firstNonNull
|
||||||
import kotlin.reflect.KClass
|
import kotlin.reflect.KClass
|
||||||
|
|
||||||
private typealias RawCommandHandler<C> = suspend (C, UByteArray) -> UByteArray
|
private typealias RawCommandHandler<C> = suspend (C, UByteArray) -> UByteArray
|
||||||
|
|
||||||
private val idCounter = AtomicCounter()
|
open class LocalInterface<S> {
|
||||||
|
|
||||||
open class LocalInterface<S> : Loggable by LogTag("LocalInterface${idCounter.incrementAndGet()}") {
|
|
||||||
|
|
||||||
private val commands = mutableMapOf<String, RawCommandHandler<S>>()
|
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
|
* New session creator. Rarely needed directlym it can be used for delegation
|
||||||
* of local interfaces.
|
* of local interfaces.
|
||||||
@ -77,18 +40,14 @@ open class LocalInterface<S> : Loggable by LogTag("LocalInterface${idCounter.inc
|
|||||||
name: String,
|
name: String,
|
||||||
packedArgs: UByteArray,
|
packedArgs: UByteArray,
|
||||||
): UByteArray =
|
): UByteArray =
|
||||||
(commands[name] ?: throw RemoteInterface.UnknownCommand(name))
|
(commands[name] ?: throw RemoteInterface.UnknownCommand())
|
||||||
.invoke(scope, packedArgs)
|
.invoke(scope, packedArgs)
|
||||||
|
|
||||||
|
|
||||||
private val errorByClass = mutableMapOf<KClass<*>, String>()
|
private val errorByClass = mutableMapOf<KClass<*>, String>()
|
||||||
private val errorBuilder = mutableMapOf<String, (String, UByteArray?) -> Throwable>()
|
private val errorBuilder = mutableMapOf<String, (String, UByteArray?) -> Throwable>()
|
||||||
|
|
||||||
/**
|
fun <T : Throwable> registerError(
|
||||||
* Register exception for automatic transmission over the kiloparsec connection using its [klass]
|
|
||||||
* and possibly override code. In most cases it is simpler to use [registerError].
|
|
||||||
*/
|
|
||||||
fun <T : Throwable> registerErrorClass(
|
|
||||||
klass: KClass<T>, code: String = klass.simpleName!!,
|
klass: KClass<T>, code: String = klass.simpleName!!,
|
||||||
exceptionBuilder: (String, UByteArray?) -> T,
|
exceptionBuilder: (String, UByteArray?) -> T,
|
||||||
) {
|
) {
|
||||||
@ -96,17 +55,10 @@ open class LocalInterface<S> : Loggable by LogTag("LocalInterface${idCounter.inc
|
|||||||
errorBuilder[code] = exceptionBuilder
|
errorBuilder[code] = exceptionBuilder
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Register an exception to be transmitted automatically over the kiloparsec connection.
|
|
||||||
* Example:
|
|
||||||
* ```kotlin
|
|
||||||
* localInterface.registerError { IllegalArgumentException(it) }
|
|
||||||
* ```
|
|
||||||
*/
|
|
||||||
inline fun <reified T : Throwable> registerError(
|
inline fun <reified T : Throwable> registerError(
|
||||||
noinline exceptionBuilder: (String) -> T,
|
noinline exceptionBuilder: (String) -> T,
|
||||||
) {
|
) {
|
||||||
registerErrorClass(T::class) { msg, _ -> exceptionBuilder(msg) }
|
registerError(T::class) { msg, _ -> exceptionBuilder(msg) }
|
||||||
}
|
}
|
||||||
|
|
||||||
val errorProviders = mutableListOf<LocalInterface<*>>()
|
val errorProviders = mutableListOf<LocalInterface<*>>()
|
||||||
@ -116,26 +68,18 @@ open class LocalInterface<S> : Loggable by LogTag("LocalInterface${idCounter.inc
|
|||||||
}
|
}
|
||||||
|
|
||||||
fun getErrorCode(t: Throwable): String? =
|
fun getErrorCode(t: Throwable): String? =
|
||||||
(t as? ExceptionWithCode)?.code
|
errorByClass[t::class] ?: errorProviders.firstNonNull { it.getErrorCode(t) }
|
||||||
?: errorByClass[t::class] ?: errorProviders.firstNonNull { it.getErrorCode(t) }
|
|
||||||
|
|
||||||
fun encodeError(forId: UInt, t: Throwable): Transport.Block.Error =
|
fun encodeError(forId: UInt, t: Throwable): Transport.Block.Error =
|
||||||
if (t is RemoteInterface.ClosedException) {
|
getErrorCode(t)?.let { Transport.Block.Error(forId, it, t.message) }
|
||||||
exception { "Illegal attempt to send ClosedException" to t }
|
?: Transport.Block.Error(forId, "UnknownError", t.message)
|
||||||
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)? =
|
open fun getErrorBuilder(code: String): ((String, UByteArray?) -> Throwable)? =
|
||||||
errorBuilder[code] ?: errorProviders.firstNonNull { it.getErrorBuilder(code) }
|
errorBuilder[code] ?: errorProviders.firstNonNull { it.getErrorBuilder(code) }
|
||||||
|
|
||||||
fun decodeError(tbe: Transport.Block.Error): Throwable =
|
fun decodeError(tbe: Transport.Block.Error): Throwable =
|
||||||
getErrorBuilder(tbe.code)?.invoke(tbe.message, tbe.extra)
|
getErrorBuilder(tbe.code)?.invoke(tbe.message, tbe.extra)
|
||||||
?: RemoteInterface.RemoteException(tbe).also {
|
?: RemoteInterface.RemoteException(tbe)
|
||||||
info { "can't decode error ${tbe.code}: ${tbe.message}" }
|
|
||||||
}
|
|
||||||
|
|
||||||
fun decodeAndThrow(tbe: Transport.Block.Error): Nothing {
|
fun decodeAndThrow(tbe: Transport.Block.Error): Nothing {
|
||||||
throw decodeError(tbe)
|
throw decodeError(tbe)
|
||||||
|
|||||||
12
src/commonMain/kotlin/net/sergeych/kiloparsec/ProxyDevice.kt
Normal file
12
src/commonMain/kotlin/net/sergeych/kiloparsec/ProxyDevice.kt
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
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() }
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -1,13 +1,3 @@
|
|||||||
/*
|
|
||||||
* Copyright (c) 2025. Sergey S. Chernov - All Rights Reserved
|
|
||||||
*
|
|
||||||
* You may use, distribute and modify this code under the
|
|
||||||
* terms of the private license, which you must obtain from the author
|
|
||||||
*
|
|
||||||
* To obtain the license, contact the author: https://t.me/real_sergeych or email to
|
|
||||||
* real dot sergeych at gmail.
|
|
||||||
*/
|
|
||||||
|
|
||||||
package net.sergeych.kiloparsec
|
package net.sergeych.kiloparsec
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -25,12 +15,11 @@ interface RemoteInterface {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Is thrown when the channel is closed, in an attempt to execute a command, also to all pending
|
* Is thrown when the channel is closed, in an attempt to execute a command, also to all pending
|
||||||
* calls (see [call]). Client code should never throw it. If command handler needs to break connection
|
* calls (see [call]).
|
||||||
* it should throw [LocalInterface.BreakConnectionException]
|
|
||||||
*/
|
*/
|
||||||
open class ClosedException(t: String = "connection is closed") : Exception(t)
|
open class ClosedException(t: String = "connection is closed") : Exception(t)
|
||||||
|
|
||||||
open class SecurityException(t: String = "invalid remote id and signature") : LocalInterface.BreakConnectionException(t)
|
open class SecurityException(t: String = "invalid remote id and signature") : ClosedException(t)
|
||||||
|
|
||||||
|
|
||||||
open class InvalidDataException(msg: String="invalid data, can't unpack") : Exception(msg)
|
open class InvalidDataException(msg: String="invalid data, can't unpack") : Exception(msg)
|
||||||
@ -50,37 +39,12 @@ interface RemoteInterface {
|
|||||||
/**
|
/**
|
||||||
* Command is not supported by the remote party
|
* Command is not supported by the remote party
|
||||||
*/
|
*/
|
||||||
class UnknownCommand(commandName: String) : RemoteException("UnknownCommand: $commandName")
|
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)
|
suspend fun <R> call(cmd: Command<Unit, R>): R = call(cmd, Unit)
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Call the remote procedure with specified [args] and return its result of type [R]. The calling coroutine
|
* Call the remote procedure with specified args and return its result
|
||||||
* suspends until the request is performed on the remote side and the result value (also `Unit`) will be received.
|
|
||||||
*
|
|
||||||
* When it is not necessary to wait for the return value and/or command execution, it is recommended to
|
|
||||||
* use [push] instead.
|
|
||||||
*
|
|
||||||
* @throws RemoteException if the execution caused exception on the remote size
|
|
||||||
* @throws Exception for registered exceptions, see [LocalInterface.registerError], etc.
|
|
||||||
*/
|
*/
|
||||||
suspend fun <A, R> call(cmd: Command<A, R>, args: A): R
|
suspend fun <A, R> call(cmd: Command<A, R>, args: A): R
|
||||||
|
|
||||||
/**
|
|
||||||
* Push the notification without waiting for reception or processing.
|
|
||||||
* It returns immediately after sending data to the transport (e.g., to the network).
|
|
||||||
* Use [call] if it is necessary to wait until the command will be received and processed by the remote.
|
|
||||||
*
|
|
||||||
* Push is onlu available for commands without returned value.
|
|
||||||
*/
|
|
||||||
suspend fun <A> push(cmd: Command<A, Unit>, args: A)
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Push the command with no args.
|
|
||||||
* It returns immediately after sending data to the transport (e.g., to the network).
|
|
||||||
* Use [call] if it is necessary to wait until the command will be received and processed by the remote.
|
|
||||||
*/
|
|
||||||
suspend fun push(cmd: Command<Unit,Unit>) = push(cmd,Unit)
|
|
||||||
}
|
}
|
||||||
@ -1,18 +1,6 @@
|
|||||||
/*
|
|
||||||
* Copyright (c) 2025. Sergey S. Chernov - All Rights Reserved
|
|
||||||
*
|
|
||||||
* You may use, distribute and modify this code under the
|
|
||||||
* terms of the private license, which you must obtain from the author
|
|
||||||
*
|
|
||||||
* To obtain the license, contact the author: https://t.me/real_sergeych or email to
|
|
||||||
* real dot sergeych at gmail.
|
|
||||||
*/
|
|
||||||
|
|
||||||
package net.sergeych.kiloparsec
|
package net.sergeych.kiloparsec
|
||||||
|
|
||||||
import kotlinx.coroutines.*
|
import kotlinx.coroutines.*
|
||||||
import kotlinx.coroutines.channels.ClosedReceiveChannelException
|
|
||||||
import kotlinx.coroutines.channels.ClosedSendChannelException
|
|
||||||
import kotlinx.coroutines.channels.ReceiveChannel
|
import kotlinx.coroutines.channels.ReceiveChannel
|
||||||
import kotlinx.coroutines.channels.SendChannel
|
import kotlinx.coroutines.channels.SendChannel
|
||||||
import kotlinx.coroutines.sync.Mutex
|
import kotlinx.coroutines.sync.Mutex
|
||||||
@ -25,14 +13,14 @@ import kotlinx.serialization.descriptors.SerialDescriptor
|
|||||||
import kotlinx.serialization.encoding.Decoder
|
import kotlinx.serialization.encoding.Decoder
|
||||||
import kotlinx.serialization.encoding.Encoder
|
import kotlinx.serialization.encoding.Encoder
|
||||||
import kotlinx.serialization.serializer
|
import kotlinx.serialization.serializer
|
||||||
import net.sergeych.bipack.Unsigned
|
import net.sergeych.crypto.toDump
|
||||||
import net.sergeych.crypto2.toDump
|
import net.sergeych.kiloparsec.Transport.Device
|
||||||
import net.sergeych.mp_logger.*
|
import net.sergeych.mp_logger.*
|
||||||
import net.sergeych.utools.pack
|
import net.sergeych.utools.pack
|
||||||
import net.sergeych.utools.unpack
|
import net.sergeych.utools.unpack
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Kiloparsec channel that operates some block [Device] exporting a given [localInterface]
|
* Divan channel that operates some block [Device] exporting a given [localInterface]
|
||||||
* to remote callers. [LocalInterface] allows session managing, transmitting exceptions
|
* to remote callers. [LocalInterface] allows session managing, transmitting exceptions
|
||||||
* in a scure and multiplatform way and provide local command execution (typed RPC)
|
* in a scure and multiplatform way and provide local command execution (typed RPC)
|
||||||
*/
|
*/
|
||||||
@ -53,7 +41,7 @@ class Transport<S>(
|
|||||||
* Input blocks. When the device is disconnected, it should send one null to this channel
|
* 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.
|
* 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]
|
* Send a binary block to a remote party where it should be received and put into [input]
|
||||||
@ -66,19 +54,12 @@ class Transport<S>(
|
|||||||
* possible. This method must not throw exceptions.
|
* possible. This method must not throw exceptions.
|
||||||
*/
|
*/
|
||||||
suspend fun close()
|
suspend fun close()
|
||||||
|
|
||||||
suspend fun flush() {}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Serializable(TransportBlockSerializer::class)
|
@Serializable(TransportBlockSerializer::class)
|
||||||
sealed class Block {
|
sealed class Block {
|
||||||
@Serializable
|
@Serializable
|
||||||
data class Call(
|
data class Call(val id: UInt, val name: String, val packedArgs: UByteArray) : Block() {
|
||||||
@Unsigned
|
|
||||||
val id: UInt,
|
|
||||||
val name: String,
|
|
||||||
val packedArgs: UByteArray
|
|
||||||
) : Block() {
|
|
||||||
override fun equals(other: Any?): Boolean {
|
override fun equals(other: Any?): Boolean {
|
||||||
if (this === other) return true
|
if (this === other) return true
|
||||||
if (other !is Call) return false
|
if (other !is Call) return false
|
||||||
@ -98,10 +79,10 @@ class Transport<S>(
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Serializable
|
@Serializable
|
||||||
data class Response(@Unsigned val forId: UInt, val packedResult: UByteArray) : Block()
|
data class Response(val forId: UInt, val packedResult: UByteArray) : Block()
|
||||||
|
|
||||||
@Serializable
|
@Serializable
|
||||||
data class Error(@Unsigned val forId: UInt, val code: String, val text: String? = null, val extra: UByteArray? = null) :
|
data class Error(val forId: UInt, val code: String, val text: String? = null, val extra: UByteArray? = null) :
|
||||||
Block() {
|
Block() {
|
||||||
val message by lazy { text ?: "remote exception: $code" }
|
val message by lazy { text ?: "remote exception: $code" }
|
||||||
}
|
}
|
||||||
@ -113,8 +94,7 @@ class Transport<S>(
|
|||||||
var isClosed: Boolean = false
|
var isClosed: Boolean = false
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Send a call block for a command and packed args and return packed result if it is not an error. It suspends
|
* Send a call block for a command and packed args and return packed result if it is not an error
|
||||||
* until receiving answer from the remote side, even if returns `Unit`.
|
|
||||||
* @throws RemoteInterface.RemoteException if the remote call caused an exception. Normally use [call] instead.
|
* @throws RemoteInterface.RemoteException if the remote call caused an exception. Normally use [call] instead.
|
||||||
* @throws RemoteInterface.ClosedException
|
* @throws RemoteInterface.ClosedException
|
||||||
*/
|
*/
|
||||||
@ -127,52 +107,18 @@ class Transport<S>(
|
|||||||
// We need to shield calls and lastID with mutex, but nothing more:
|
// We need to shield calls and lastID with mutex, but nothing more:
|
||||||
access.withLock {
|
access.withLock {
|
||||||
if (isClosed) throw RemoteInterface.ClosedException()
|
if (isClosed) throw RemoteInterface.ClosedException()
|
||||||
// the order is important: first id in use MUST BE >= 1, not zero:
|
|
||||||
b = Block.Call(++lastId, name, packedArgs)
|
b = Block.Call(++lastId, name, packedArgs)
|
||||||
calls[b.id] = deferred
|
calls[b.id] = deferred
|
||||||
}
|
}
|
||||||
|
|
||||||
// now we have mutex freed so we can call:
|
// now we have mutex freed so we can call:
|
||||||
val r = runCatching {
|
val r = device.output.trySend(pack(b).also { debug { ">>>\n${it.toDump()}" } })
|
||||||
do {
|
if (!r.isSuccess) deferred.completeExceptionally(RemoteInterface.ClosedException())
|
||||||
val cr = device.output.trySend(pack(b))
|
|
||||||
if( cr.isClosed ) throw ClosedSendChannelException("can't send block: channel is closed")
|
|
||||||
delay(100)
|
|
||||||
} while(!cr.isSuccess)
|
|
||||||
}
|
|
||||||
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:
|
// it returns packed result or throws a proper error:
|
||||||
return deferred.await()
|
return deferred.await()
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Send a call block for a command and packed args and return packed result if it is not an error. It suspends
|
|
||||||
* until receiving answer from the remote side, even if returns `Unit`.
|
|
||||||
* @throws RemoteInterface.RemoteException if the remote call caused an exception. Normally use [call] instead.
|
|
||||||
* @throws RemoteInterface.ClosedException
|
|
||||||
*/
|
|
||||||
private suspend fun sendPushBlock(name: String, packedArgs: UByteArray) {
|
|
||||||
if (isClosed) throw RemoteInterface.ClosedException()
|
|
||||||
|
|
||||||
// All push blocks have the same id == 0:
|
|
||||||
val b = Block.Call(0u, name, packedArgs)
|
|
||||||
val r = runCatching { device.output.send(pack(b).also { debug { ">>$\n${it.toDump()}" } }) }
|
|
||||||
when(val e = r.exceptionOrNull()) {
|
|
||||||
is RemoteInterface.ClosedException, is CancellationException, is RemoteInterface.RemoteException
|
|
||||||
-> throw e
|
|
||||||
else -> throw RemoteInterface.ClosedException()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Call the remote procedure with specified args and return its result
|
* Call the remote procedure with specified args and return its result
|
||||||
*/
|
*/
|
||||||
@ -181,10 +127,6 @@ class Transport<S>(
|
|||||||
return unpack(cmd.resultSerializer, result)
|
return unpack(cmd.resultSerializer, result)
|
||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun <A>push(cmd: Command<A,Unit>,args: A) {
|
|
||||||
sendPushBlock(cmd.name, pack(cmd.argsSerializer, args))
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Start running the transport. This function suspends until the transport is closed
|
* Start running the transport. This function suspends until the transport is closed
|
||||||
* normally or by error. If you need to cancel it prematurely, cancel the coroutine
|
* normally or by error. If you need to cancel it prematurely, cancel the coroutine
|
||||||
@ -195,96 +137,76 @@ class Transport<S>(
|
|||||||
suspend fun run() {
|
suspend fun run() {
|
||||||
coroutineScope {
|
coroutineScope {
|
||||||
debug { "awaiting incoming blocks" }
|
debug { "awaiting incoming blocks" }
|
||||||
// todo: rewrite it to close the job with no exceptions at all, always
|
|
||||||
while (isActive && !isClosed) {
|
while (isActive && !isClosed) {
|
||||||
try {
|
try {
|
||||||
device.input.receive().let { packed ->
|
device.input.receive()?.let { packed ->
|
||||||
|
debug { "<<<\n${packed.toDump()}" }
|
||||||
val b = unpack<Block>(packed)
|
val b = unpack<Block>(packed)
|
||||||
|
debug { "<<$ $b" }
|
||||||
|
debug { "access state: ${access.isLocked}" }
|
||||||
when (b) {
|
when (b) {
|
||||||
is Block.Error -> access.withLock {
|
is Block.Error -> access.withLock {
|
||||||
val error = localInterface.decodeError(b)
|
val error = localInterface.decodeError(b)
|
||||||
warning { "decoded error: ${error::class.simpleName}: $error" }
|
warning { "decoded error: ${error::class.simpleName}: $error" }
|
||||||
calls.remove(b.forId)?.completeExceptionally(localInterface.decodeError(b))
|
calls.remove(b.forId)?.completeExceptionally(localInterface.decodeError(b))
|
||||||
?: warning { "error handler not found for ${b.forId}" }
|
?: warning { "error handler not found for ${b.forId}" }
|
||||||
info { "error processed" }
|
info { "error processed"}
|
||||||
}
|
}
|
||||||
|
|
||||||
is Block.Response -> access.withLock {
|
is Block.Response -> access.withLock {
|
||||||
calls.remove(b.forId)?.complete(b.packedResult)
|
calls.remove(b.forId)?.let {
|
||||||
|
debug { "activating wait handle for ${b.forId}" }
|
||||||
|
it.complete(b.packedResult)
|
||||||
|
}
|
||||||
?: warning { "wait handle not found for ${b.forId}" }
|
?: warning { "wait handle not found for ${b.forId}" }
|
||||||
}
|
}
|
||||||
|
|
||||||
is Block.Call -> launch {
|
is Block.Call -> launch {
|
||||||
try {
|
try {
|
||||||
if (b.id == 0u)
|
send(
|
||||||
// Command does not waits return
|
Block.Response(
|
||||||
localInterface.execute(commandContext, b.name, b.packedArgs)
|
b.id,
|
||||||
else
|
localInterface.execute(commandContext, b.name, b.packedArgs)
|
||||||
send(
|
|
||||||
Block.Response(
|
|
||||||
b.id,
|
|
||||||
localInterface.execute(commandContext, b.name, b.packedArgs)
|
|
||||||
)
|
|
||||||
)
|
)
|
||||||
} catch (x: LocalInterface.BreakConnectionException) {
|
)
|
||||||
// handler forced close
|
} catch (x: RemoteInterface.ClosedException) {
|
||||||
warning { "handler requested closing of the connection (${x.flushSendQueue}" }
|
// strange case: handler throws closed?
|
||||||
isClosed = true
|
error { "not supported: command handler for $b has thrown ClosedException" }
|
||||||
if (x.flushSendQueue) device.flush()
|
send(Block.Error(b.id, "UnexpectedException", x.message))
|
||||||
device.close()
|
|
||||||
} catch (x: RemoteInterface.RemoteException) {
|
} catch (x: RemoteInterface.RemoteException) {
|
||||||
send(Block.Error(b.id, x.code, x.text, x.extra))
|
send(Block.Error(b.id, x.code, x.text, x.extra))
|
||||||
} catch (t: Throwable) {
|
} catch (t: Throwable) {
|
||||||
send(Block.Error(b.id, "UnknownError", t.message))
|
send(Block.Error(b.id, "UnknownError", t.message))
|
||||||
}
|
}
|
||||||
|
.also { debug { "command executed: ${b.name}" } }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
} ?: run {
|
||||||
|
debug { "remote channel close received" }
|
||||||
|
isClosed = true
|
||||||
}
|
}
|
||||||
// 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 (_: RemoteInterface.ClosedException) {
|
|
||||||
// it is ok: we just exit the coroutine normally
|
|
||||||
// and mark we're closing
|
|
||||||
isClosed = true
|
|
||||||
} catch (_: CancellationException) {
|
} catch (_: CancellationException) {
|
||||||
info { "loop is cancelled with CancellationException" }
|
info { "loop is cancelled" }
|
||||||
isClosed = true
|
isClosed = true
|
||||||
} catch (t: Throwable) {
|
} catch (t: Throwable) {
|
||||||
exception { "channel closed on error" to t }
|
exception { "channel closed on error" to t }
|
||||||
info { "isa? $isActive / $isClosed" }
|
info { "isa? $isActive / $isClosed" }
|
||||||
|
runCatching { device.close() }
|
||||||
isClosed = true
|
isClosed = true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
debug { "leaving transport loop" }
|
|
||||||
access.withLock {
|
access.withLock {
|
||||||
debug { "access lock obtained" }
|
|
||||||
isClosed = true
|
isClosed = true
|
||||||
debug { "closing device $device, calls in queue ${calls.size}" }
|
for (c in calls.values) c.completeExceptionally(RemoteInterface.ClosedException())
|
||||||
runCatching { device.close() }
|
|
||||||
for (c in calls.values)
|
|
||||||
c.completeExceptionally(RemoteInterface.ClosedException())
|
|
||||||
calls.clear()
|
calls.clear()
|
||||||
debug { "calls clear has been called" }
|
|
||||||
}
|
}
|
||||||
debug { "no more active: $isActive / ${calls.size}" }
|
debug { "no more active: $isActive / ${calls.size}" }
|
||||||
}
|
}
|
||||||
debug { "exiting transport loop" }
|
info { "exiting transport loop" }
|
||||||
}
|
}
|
||||||
|
|
||||||
private suspend fun send(block: Block) {
|
private suspend fun send(block: Block) {
|
||||||
try {
|
device.output.send(pack(block))
|
||||||
device.output.send(pack(block))
|
|
||||||
} catch (_: ClosedSendChannelException) {
|
|
||||||
throw RemoteInterface.ClosedException()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
@ -313,7 +235,7 @@ object TransportBlockSerializer : KSerializer<Transport.Block> {
|
|||||||
|
|
||||||
|
|
||||||
override fun deserialize(decoder: Decoder): Transport.Block =
|
override fun deserialize(decoder: Decoder): Transport.Block =
|
||||||
when (val id = decoder.decodeByte().toInt()) {
|
when( val id = decoder.decodeByte().toInt()) {
|
||||||
0 -> decoder.decodeSerializableValue(serializer<Transport.Block.Call>())
|
0 -> decoder.decodeSerializableValue(serializer<Transport.Block.Call>())
|
||||||
1 -> decoder.decodeSerializableValue(serializer<Transport.Block.Error>())
|
1 -> decoder.decodeSerializableValue(serializer<Transport.Block.Error>())
|
||||||
2 -> decoder.decodeSerializableValue(serializer<Transport.Block.Response>())
|
2 -> decoder.decodeSerializableValue(serializer<Transport.Block.Response>())
|
||||||
|
|||||||
@ -0,0 +1,6 @@
|
|||||||
|
package net.sergeych.kiloparsec.adapter
|
||||||
|
|
||||||
|
internal data class ConnectionId(
|
||||||
|
val address: NetworkAddress,
|
||||||
|
val id: UInt,
|
||||||
|
)
|
||||||
@ -0,0 +1,117 @@
|
|||||||
|
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()
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,7 @@
|
|||||||
|
package net.sergeych.kiloparsec.adapter
|
||||||
|
|
||||||
|
enum class DatagramError {
|
||||||
|
Closed,
|
||||||
|
NotExists,
|
||||||
|
Busy
|
||||||
|
}
|
||||||
@ -0,0 +1,15 @@
|
|||||||
|
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()
|
||||||
|
}
|
||||||
@ -0,0 +1,136 @@
|
|||||||
|
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)
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -1,27 +0,0 @@
|
|||||||
/*
|
|
||||||
* Copyright (c) 2025. Sergey S. Chernov - All Rights Reserved
|
|
||||||
*
|
|
||||||
* You may use, distribute and modify this code under the
|
|
||||||
* terms of the private license, which you must obtain from the author
|
|
||||||
*
|
|
||||||
* To obtain the license, contact the author: https://t.me/real_sergeych or email to
|
|
||||||
* real dot sergeych at gmail.
|
|
||||||
*/
|
|
||||||
|
|
||||||
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,23 +1,51 @@
|
|||||||
/*
|
|
||||||
* Copyright (c) 2025. Sergey S. Chernov - All Rights Reserved
|
|
||||||
*
|
|
||||||
* You may use, distribute and modify this code under the
|
|
||||||
* terms of the private license, which you must obtain from the author
|
|
||||||
*
|
|
||||||
* To obtain the license, contact the author: https://t.me/real_sergeych or email to
|
|
||||||
* real dot sergeych at gmail.
|
|
||||||
*/
|
|
||||||
|
|
||||||
package net.sergeych.kiloparsec.adapter
|
package net.sergeych.kiloparsec.adapter
|
||||||
|
|
||||||
|
import kotlinx.coroutines.channels.ReceiveChannel
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Multiplatform internet address.
|
* Multiplatform implementation of an internet address.
|
||||||
|
* Notice to implementors. It must provide correct and effective [equals] and [hashCode].
|
||||||
*/
|
*/
|
||||||
data class NetworkAddress(
|
interface NetworkAddress {
|
||||||
val host: String,
|
val host: String
|
||||||
val port: Int
|
val port: Int
|
||||||
) {
|
|
||||||
override fun toString(): String {
|
|
||||||
return "$host:$port"
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Multiplatform datagram abstraction
|
||||||
|
*/
|
||||||
|
interface Datagram {
|
||||||
|
/**
|
||||||
|
* Received message
|
||||||
|
*/
|
||||||
|
val message: UByteArray
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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)
|
||||||
|
interface DatagramConnector: AutoCloseable {
|
||||||
|
|
||||||
|
val incoming: ReceiveChannel<Datagram>
|
||||||
|
suspend fun send(message: UByteArray, networkAddress: NetworkAddress)
|
||||||
|
@Suppress("unused")
|
||||||
|
suspend fun send(message: UByteArray, datagramAddress: String) {
|
||||||
|
send(message, networkAddressOf(datagramAddress))
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun send(message: UByteArray,host: String,port: Int) =
|
||||||
|
send(message, NetworkAddress(host,port))
|
||||||
|
override fun close()
|
||||||
|
}
|
||||||
|
|
||||||
|
expect fun networkAddressOf(address: String): NetworkAddress
|
||||||
|
expect fun NetworkAddress(host: String,port: Int): NetworkAddress
|
||||||
|
|||||||
@ -0,0 +1,9 @@
|
|||||||
|
package net.sergeych.kiloparsec.adapter
|
||||||
|
|
||||||
|
import kotlinx.serialization.Serializable
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
internal data class OuterPacket(
|
||||||
|
val id: UInt,
|
||||||
|
val payload: UByteArray,
|
||||||
|
)
|
||||||
@ -1,50 +0,0 @@
|
|||||||
/*
|
|
||||||
* Copyright (c) 2025. Sergey S. Chernov - All Rights Reserved
|
|
||||||
*
|
|
||||||
* You may use, distribute and modify this code under the
|
|
||||||
* terms of the private license, which you must obtain from the author
|
|
||||||
*
|
|
||||||
* To obtain the license, contact the author: https://t.me/real_sergeych or email to
|
|
||||||
* real dot sergeych at gmail.
|
|
||||||
*/
|
|
||||||
|
|
||||||
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"
|
|
||||||
}
|
|
||||||
@ -1,159 +0,0 @@
|
|||||||
/*
|
|
||||||
* Copyright (c) 2025. Sergey S. Chernov - All Rights Reserved
|
|
||||||
*
|
|
||||||
* You may use, distribute and modify this code under the
|
|
||||||
* terms of the private license, which you must obtain from the author
|
|
||||||
*
|
|
||||||
* To obtain the license, contact the author: https://t.me/real_sergeych or email to
|
|
||||||
* real dot sergeych at gmail.
|
|
||||||
*/
|
|
||||||
|
|
||||||
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 kotlinx.io.IOException
|
|
||||||
import net.sergeych.crypto2.SigningKey
|
|
||||||
import net.sergeych.kiloparsec.*
|
|
||||||
import net.sergeych.mp_logger.*
|
|
||||||
import net.sergeych.mp_tools.decodeBase64Compact
|
|
||||||
import net.sergeych.mp_tools.encodeToBase64Compact
|
|
||||||
import net.sergeych.mp_tools.globalLaunch
|
|
||||||
import net.sergeych.tools.AtomicCounter
|
|
||||||
|
|
||||||
private val counter = AtomicCounter()
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Shortcut to create websocket client. Use [websocketTransportDevice] with [KiloClient]
|
|
||||||
* for fine-grained control.
|
|
||||||
*/
|
|
||||||
fun <S> websocketClient(
|
|
||||||
path: String,
|
|
||||||
clientInterface: KiloInterface<S> = KiloInterface(),
|
|
||||||
secretKey: SigningKey? = null,
|
|
||||||
useTextFrames: Boolean = false,
|
|
||||||
sessionMaker: () -> S = {
|
|
||||||
@Suppress("UNCHECKED_CAST")
|
|
||||||
Unit as S
|
|
||||||
},
|
|
||||||
): KiloClient<S> {
|
|
||||||
return KiloClient(clientInterface, secretKey) {
|
|
||||||
KiloConnectionData(websocketTransportDevice(path, useTextFrames), sessionMaker())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Create kilopaarsec transport over websocket (ws or wss).
|
|
||||||
* @param path websocket path (must start with ws:// or wss:// and contain a path part)
|
|
||||||
* @client use default [HttpClient], it installs [WebSockets] plugin
|
|
||||||
*/
|
|
||||||
fun websocketTransportDevice(
|
|
||||||
path: String,
|
|
||||||
useTextFrames: Boolean = false,
|
|
||||||
client: HttpClient = HttpClient {
|
|
||||||
install(WebSockets)
|
|
||||||
},
|
|
||||||
): Transport.Device {
|
|
||||||
var u = Url(path)
|
|
||||||
if (u.encodedPath.length <= 1)
|
|
||||||
u = URLBuilder(u).apply {
|
|
||||||
encodedPath = "/kp"
|
|
||||||
}.build()
|
|
||||||
|
|
||||||
val input = Channel<UByteArray>()
|
|
||||||
val output = Channel<UByteArray>()
|
|
||||||
val closeHandle = CompletableDeferred<Boolean>()
|
|
||||||
val readyHandle = CompletableDeferred<Unit>()
|
|
||||||
|
|
||||||
globalLaunch {
|
|
||||||
val log = LogTag("KC:${counter.incrementAndGet()}")
|
|
||||||
try {
|
|
||||||
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")
|
|
||||||
readyHandle.complete(Unit)
|
|
||||||
launch {
|
|
||||||
try {
|
|
||||||
for (block in output) {
|
|
||||||
if (useTextFrames)
|
|
||||||
send(
|
|
||||||
Frame.Text(block.asByteArray().encodeToBase64Compact())
|
|
||||||
)
|
|
||||||
else
|
|
||||||
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) {
|
|
||||||
when (f) {
|
|
||||||
is Frame.Binary -> input.send(f.readBytes().toUByteArray())
|
|
||||||
is Frame.Text -> input.send(f.readText().decodeBase64Compact().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()
|
|
||||||
}
|
|
||||||
runCatching { output.close() }
|
|
||||||
runCatching { input.close() }
|
|
||||||
runCatching { close() }
|
|
||||||
}
|
|
||||||
} catch (x: IOException) {
|
|
||||||
if ("refused" in x.toString()) log.debug { "connection refused" }
|
|
||||||
else log.warning { "unexpected IO error $x" }
|
|
||||||
runCatching { output.close() }
|
|
||||||
runCatching { input.close() }
|
|
||||||
}
|
|
||||||
log.info { "closing connection" }
|
|
||||||
}
|
|
||||||
// Wait for connection be established or failed
|
|
||||||
val device = ProxyDevice(input, output, doClose = {
|
|
||||||
// we need to explicitly close the coroutine job, or it can hang for a long time
|
|
||||||
// leaking resources.
|
|
||||||
runCatching { output.close() }
|
|
||||||
runCatching { input.close() }
|
|
||||||
closeHandle.complete(true)
|
|
||||||
// job.cancel()
|
|
||||||
})
|
|
||||||
return device
|
|
||||||
}
|
|
||||||
|
|
||||||
@ -1,27 +1,16 @@
|
|||||||
/*
|
|
||||||
* Copyright (c) 2025. Sergey S. Chernov - All Rights Reserved
|
|
||||||
*
|
|
||||||
* You may use, distribute and modify this code under the
|
|
||||||
* terms of the private license, which you must obtain from the author
|
|
||||||
*
|
|
||||||
* To obtain the license, contact the author: https://t.me/real_sergeych or email to
|
|
||||||
* real dot sergeych at gmail.
|
|
||||||
*/
|
|
||||||
|
|
||||||
package net.sergeych.kiloparsec
|
package net.sergeych.kiloparsec
|
||||||
|
|
||||||
import kotlinx.serialization.Serializable
|
import kotlinx.serialization.Serializable
|
||||||
import net.sergeych.crypto2.SafeKeyExchange
|
import net.sergeych.crypto.Key
|
||||||
import net.sergeych.crypto2.Seal
|
|
||||||
import net.sergeych.crypto2.VerifyingPublicKey
|
|
||||||
|
|
||||||
// L0 commands - key exchange and check:
|
// L0 commands - key exchange and check:
|
||||||
@Serializable
|
@Serializable
|
||||||
data class Handshake(val version: UInt, val publicKey: SafeKeyExchange.PublicKey,
|
data class Handshake(val version: UInt, val publicKey: UByteArray,
|
||||||
val signature: Seal? = null)
|
val serverSharedKey: Key.Verifying? = null,
|
||||||
|
val signature: UByteArray? = null)
|
||||||
|
|
||||||
@Serializable
|
@Serializable
|
||||||
data class ClientIdentity(val clientIdKey: VerifyingPublicKey?, val signature: UByteArray?)
|
data class ClientIdentity(val clientIdKey: Key.Verifying?, val signature: UByteArray?)
|
||||||
|
|
||||||
// Level 0 command: request key exchange
|
// Level 0 command: request key exchange
|
||||||
internal val L0Request by command<Handshake, Handshake>()
|
internal val L0Request by command<Handshake, Handshake>()
|
||||||
|
|||||||
@ -1,34 +0,0 @@
|
|||||||
/*
|
|
||||||
* Copyright (c) 2025. Sergey S. Chernov - All Rights Reserved
|
|
||||||
*
|
|
||||||
* You may use, distribute and modify this code under the
|
|
||||||
* terms of the private license, which you must obtain from the author
|
|
||||||
*
|
|
||||||
* To obtain the license, contact the author: https://t.me/real_sergeych or email to
|
|
||||||
* real dot sergeych at gmail.
|
|
||||||
*/
|
|
||||||
|
|
||||||
package net.sergeych.kiloparsec
|
|
||||||
|
|
||||||
import kotlinx.coroutines.sync.Mutex
|
|
||||||
import kotlinx.coroutines.sync.withLock
|
|
||||||
|
|
||||||
fun String.encodeToUByteArray() =
|
|
||||||
encodeToByteArray().toUByteArray()
|
|
||||||
|
|
||||||
class SyncValue<T>(initialValue: T) {
|
|
||||||
private val access = Mutex()
|
|
||||||
|
|
||||||
var value = initialValue
|
|
||||||
private set
|
|
||||||
|
|
||||||
suspend fun mutate(f: suspend (T)->T): T = access.withLock { f(value).also { value = it } }
|
|
||||||
|
|
||||||
@Suppress("unused")
|
|
||||||
suspend fun getAndSet(newValue: T): T = mutate {
|
|
||||||
val old = value
|
|
||||||
value = newValue
|
|
||||||
old
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
@ -0,0 +1,8 @@
|
|||||||
|
package net.sergeych.tools
|
||||||
|
|
||||||
|
class AtomicCounter(startWith: Long=0) {
|
||||||
|
private var op = ProtectedOp()
|
||||||
|
private var counter = startWith
|
||||||
|
|
||||||
|
fun next(): Long = op { counter++ }
|
||||||
|
}
|
||||||
21
src/commonMain/kotlin/net/sergeych/tools/ProtectedOp.kt
Normal file
21
src/commonMain/kotlin/net/sergeych/tools/ProtectedOp.kt
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
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
|
||||||
12
src/commonMain/kotlin/net/sergeych/utools/collections.kt
Normal file
12
src/commonMain/kotlin/net/sergeych/utools/collections.kt
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
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
|
||||||
|
}
|
||||||
46
src/commonMain/kotlin/net/sergeych/utools/packing.kt
Normal file
46
src/commonMain/kotlin/net/sergeych/utools/packing.kt
Normal file
@ -0,0 +1,46 @@
|
|||||||
|
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)
|
||||||
12
src/commonMain/kotlin/net/sergeych/utools/time.kt
Normal file
12
src/commonMain/kotlin/net/sergeych/utools/time.kt
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
@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)
|
||||||
183
src/commonMain/kotlin/org/komputing/khash/keccak/Keccak.kt
Normal file
183
src/commonMain/kotlin/org/komputing/khash/keccak/Keccak.kt
Normal file
@ -0,0 +1,183 @@
|
|||||||
|
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)
|
||||||
|
}
|
||||||
@ -0,0 +1,22 @@
|
|||||||
|
@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)
|
||||||
|
}
|
||||||
@ -0,0 +1,42 @@
|
|||||||
|
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 > toIndex</tt>
|
||||||
|
* @throws ArrayIndexOutOfBoundsException if <tt>fromIndex < 0</tt> or
|
||||||
|
* <tt>toIndex > 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")
|
||||||
@ -0,0 +1,19 @@
|
|||||||
|
@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)
|
||||||
|
}
|
||||||
@ -1,19 +1,8 @@
|
|||||||
/*
|
import com.ionspin.kotlin.crypto.secretbox.SecretBox
|
||||||
* Copyright (c) 2025. Sergey S. Chernov - All Rights Reserved
|
import com.ionspin.kotlin.crypto.util.decodeFromUByteArray
|
||||||
*
|
import com.ionspin.kotlin.crypto.util.encodeToUByteArray
|
||||||
* You may use, distribute and modify this code under the
|
|
||||||
* terms of the private license, which you must obtain from the author
|
|
||||||
*
|
|
||||||
* To obtain the license, contact the author: https://t.me/real_sergeych or email to
|
|
||||||
* real dot sergeych at gmail.
|
|
||||||
*/
|
|
||||||
|
|
||||||
import kotlinx.coroutines.test.runTest
|
import kotlinx.coroutines.test.runTest
|
||||||
import net.sergeych.crypto2.IllegalSignatureException
|
import net.sergeych.crypto.*
|
||||||
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.pack
|
||||||
import net.sergeych.utools.unpack
|
import net.sergeych.utools.unpack
|
||||||
import kotlin.test.*
|
import kotlin.test.*
|
||||||
@ -22,36 +11,47 @@ class KeysTest {
|
|||||||
@Test
|
@Test
|
||||||
fun testCreationAndMap() = runTest {
|
fun testCreationAndMap() = runTest {
|
||||||
initCrypto()
|
initCrypto()
|
||||||
val (stk,pbk) = SigningSecretKey.generatePair()
|
val (stk,pbk) = Key.Signing.pair()
|
||||||
|
|
||||||
val x = mapOf( stk to "STK!", pbk to "PBK!")
|
val x = mapOf( stk to "STK!", pbk to "PBK!")
|
||||||
assertEquals("STK!", x[stk])
|
assertEquals("STK!", x[stk])
|
||||||
val s1 = SigningSecretKey(stk.keyBytes)
|
val s1 = Key.Signing(stk.packed)
|
||||||
assertEquals(stk, s1)
|
assertEquals(stk, s1)
|
||||||
assertEquals("STK!", x[s1])
|
assertEquals("STK!", x[s1])
|
||||||
assertEquals("PBK!", x[pbk])
|
assertEquals("PBK!", x[pbk])
|
||||||
|
|
||||||
val data = "8 rays dev!".encodeToUByteArray()
|
val data = "8 rays dev!".encodeToUByteArray()
|
||||||
val data1 = "8 rays dev!".encodeToUByteArray()
|
val data1 = "8 rays dev!".encodeToUByteArray()
|
||||||
val s = stk.seal(data)
|
val s = SignedBox.Seal.create(stk, data)
|
||||||
assertTrue(s.isValid(data))
|
assertTrue(s.verify(data))
|
||||||
|
|
||||||
data1[0] = 0x01u
|
data1[0] = 0x01u
|
||||||
assertFalse(s.isValid(data1))
|
assertFalse(s.verify(data1))
|
||||||
val p2 = SigningSecretKey.generatePair()
|
val p2 = Key.Signing.pair()
|
||||||
val p3 = SigningSecretKey.generatePair()
|
val p3 = Key.Signing.pair()
|
||||||
|
|
||||||
val ms = SealedBox(data, s1) + p2.secretKey
|
val ms = SignedBox(data, s1) + p2.signing
|
||||||
|
|
||||||
// non tampered:
|
// non tampered:
|
||||||
val ms1 = unpack<SealedBox>(pack(ms))
|
val ms1 = unpack<SignedBox>(pack(ms))
|
||||||
assertContentEquals(data, ms1.message)
|
assertContentEquals(data, ms1.message)
|
||||||
assertTrue(pbk in ms1)
|
assertTrue(pbk in ms1)
|
||||||
assertTrue(p2.publicKey in ms1)
|
assertTrue(p2.verifying in ms1)
|
||||||
assertTrue(p3.publicKey !in ms1)
|
assertTrue(p3.verifying !in ms1)
|
||||||
|
|
||||||
assertThrows<IllegalSignatureException> {
|
assertThrows<IllegalSignatureException> {
|
||||||
unpack<SealedBox>(pack(ms).also { it[3] = 1u })
|
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()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -1,26 +1,13 @@
|
|||||||
/*
|
|
||||||
* Copyright (c) 2025. Sergey S. Chernov - All Rights Reserved
|
|
||||||
*
|
|
||||||
* You may use, distribute and modify this code under the
|
|
||||||
* terms of the private license, which you must obtain from the author
|
|
||||||
*
|
|
||||||
* To obtain the license, contact the author: https://t.me/real_sergeych or email to
|
|
||||||
* real dot sergeych at gmail.
|
|
||||||
*/
|
|
||||||
|
|
||||||
import kotlinx.coroutines.test.runTest
|
import kotlinx.coroutines.test.runTest
|
||||||
import net.sergeych.bipack.BipackEncoder
|
import kotlinx.datetime.Instant
|
||||||
import net.sergeych.crypto2.initCrypto
|
import net.sergeych.crypto.initCrypto
|
||||||
import net.sergeych.kiloparsec.KiloParams
|
|
||||||
import net.sergeych.kiloparsec.Transport
|
import net.sergeych.kiloparsec.Transport
|
||||||
import net.sergeych.utools.nowToSeconds
|
import net.sergeych.utools.nowToSeconds
|
||||||
import net.sergeych.utools.pack
|
import net.sergeych.utools.pack
|
||||||
import net.sergeych.utools.unpack
|
import net.sergeych.utools.unpack
|
||||||
import kotlin.test.Test
|
import kotlin.test.Test
|
||||||
import kotlin.test.assertContentEquals
|
|
||||||
import kotlin.test.assertEquals
|
import kotlin.test.assertEquals
|
||||||
import kotlin.time.Duration.Companion.microseconds
|
import kotlin.time.Duration.Companion.microseconds
|
||||||
import kotlin.time.Instant
|
|
||||||
|
|
||||||
class PackTest {
|
class PackTest {
|
||||||
inline fun <reified T>check(x: T?) {
|
inline fun <reified T>check(x: T?) {
|
||||||
@ -53,10 +40,4 @@ class PackTest {
|
|||||||
val b2 = unpack<Transport.Block>(p1)
|
val b2 = unpack<Transport.Block>(p1)
|
||||||
assertEquals(b1,b2)
|
assertEquals(b1,b2)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
|
||||||
fun packPackage() = runTest {
|
|
||||||
val x = BipackEncoder.encode(KiloParams.Package(1u, ubyteArrayOf()))
|
|
||||||
assertContentEquals(byteArrayOf(4, 0), x)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
@ -1,17 +1,20 @@
|
|||||||
/*
|
import kotlinx.coroutines.test.runTest
|
||||||
* Copyright (c) 2025. Sergey S. Chernov - All Rights Reserved
|
import net.sergeych.crypto.createContrail
|
||||||
*
|
import net.sergeych.crypto.initCrypto
|
||||||
* You may use, distribute and modify this code under the
|
import net.sergeych.crypto.isValidContrail
|
||||||
* terms of the private license, which you must obtain from the author
|
import kotlin.test.Test
|
||||||
*
|
import kotlin.test.assertEquals
|
||||||
* To obtain the license, contact the author: https://t.me/real_sergeych or email to
|
import kotlin.test.assertFalse
|
||||||
* real dot sergeych at gmail.
|
import kotlin.test.assertTrue
|
||||||
*/
|
|
||||||
|
|
||||||
class ToolsTest {
|
class ToolsTest {
|
||||||
// @Test
|
@Test
|
||||||
// fun testRemoceCmd() {
|
fun testContrails() = runTest {
|
||||||
// assertEquals("lalala", removeCmd("lalala"))
|
initCrypto()
|
||||||
// assertEquals("lalala", removeCmd("cmdlalala"))
|
val c = createContrail(ubyteArrayOf(1u, 2u, 3u, 4u, 5u))
|
||||||
// }
|
assertEquals(134u, c[0])
|
||||||
|
assertTrue { isValidContrail(c) }
|
||||||
|
c[2] = 11u
|
||||||
|
assertFalse { isValidContrail(c) }
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@ -1,27 +1,15 @@
|
|||||||
/*
|
import com.ionspin.kotlin.crypto.keyexchange.KeyExchange
|
||||||
* Copyright (c) 2025. Sergey S. Chernov - All Rights Reserved
|
|
||||||
*
|
|
||||||
* You may use, distribute and modify this code under the
|
|
||||||
* terms of the private license, which you must obtain from the author
|
|
||||||
*
|
|
||||||
* To obtain the license, contact the author: https://t.me/real_sergeych or email to
|
|
||||||
* real dot sergeych at gmail.
|
|
||||||
*/
|
|
||||||
|
|
||||||
import kotlinx.coroutines.*
|
import kotlinx.coroutines.*
|
||||||
import kotlinx.coroutines.channels.Channel
|
import kotlinx.coroutines.channels.Channel
|
||||||
import kotlinx.coroutines.channels.ReceiveChannel
|
import kotlinx.coroutines.channels.ReceiveChannel
|
||||||
import kotlinx.coroutines.channels.SendChannel
|
import kotlinx.coroutines.channels.SendChannel
|
||||||
import kotlinx.coroutines.test.runTest
|
import kotlinx.coroutines.test.runTest
|
||||||
import net.sergeych.crypto2.SigningSecretKey
|
import net.sergeych.crypto.Key
|
||||||
import net.sergeych.crypto2.VerifyingPublicKey
|
import net.sergeych.crypto.initCrypto
|
||||||
import net.sergeych.crypto2.initCrypto
|
import net.sergeych.kiloparsec.command
|
||||||
import net.sergeych.kiloparsec.*
|
import net.sergeych.kiloparsec.*
|
||||||
import net.sergeych.mp_logger.Log
|
import net.sergeych.mp_logger.Log
|
||||||
import kotlin.test.Test
|
import kotlin.test.*
|
||||||
import kotlin.test.assertContentEquals
|
|
||||||
import kotlin.test.assertEquals
|
|
||||||
import kotlin.test.fail
|
|
||||||
|
|
||||||
private var dcnt = 0
|
private var dcnt = 0
|
||||||
fun createTestDevice(): Pair<Transport.Device, Transport.Device> {
|
fun createTestDevice(): Pair<Transport.Device, Transport.Device> {
|
||||||
@ -29,9 +17,8 @@ fun createTestDevice(): Pair<Transport.Device, Transport.Device> {
|
|||||||
val p2 = Channel<UByteArray>(256)
|
val p2 = Channel<UByteArray>(256)
|
||||||
val id = ++dcnt
|
val id = ++dcnt
|
||||||
val d1 = object : Transport.Device {
|
val d1 = object : Transport.Device {
|
||||||
override val input: ReceiveChannel<UByteArray> = p1
|
override val input: ReceiveChannel<UByteArray?> = p1
|
||||||
override val output: SendChannel<UByteArray> = p2
|
override val output: SendChannel<UByteArray> = p2
|
||||||
|
|
||||||
override suspend fun close() {
|
override suspend fun close() {
|
||||||
p2.close()
|
p2.close()
|
||||||
}
|
}
|
||||||
@ -41,7 +28,7 @@ fun createTestDevice(): Pair<Transport.Device, Transport.Device> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
val d2 = object : 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 val output: SendChannel<UByteArray> = p1
|
||||||
override suspend fun close() {
|
override suspend fun close() {
|
||||||
p1.close()
|
p1.close()
|
||||||
@ -76,37 +63,36 @@ class TransportTest {
|
|||||||
"p2: $it"
|
"p2: $it"
|
||||||
}
|
}
|
||||||
on(cmdSlow) {
|
on(cmdSlow) {
|
||||||
// the problem: runTest() breaks delays so it is not enough
|
delay(100)
|
||||||
for( i in 0..10000) delay(1)
|
|
||||||
"done"
|
"done"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
val t1 = Transport(d1, l1, Unit)
|
val t1 = Transport(d1, l1, Unit)
|
||||||
val t2 = Transport(d2, l2, Unit)
|
val t2 = Transport(d2, l2, Unit)
|
||||||
|
|
||||||
// val clip = KeyExchange.keypair()
|
val clip = KeyExchange.keypair()
|
||||||
// val serp = KeyExchange.keypair()
|
val serp = KeyExchange.keypair()
|
||||||
// val clisk = KeyExchange.clientSessionKeys(clip.publicKey, clip.secretKey, serp.publicKey)
|
val clisk = KeyExchange.clientSessionKeys(clip.publicKey, clip.secretKey, serp.publicKey)
|
||||||
// val sersk = KeyExchange.serverSessionKeys(serp.publicKey, serp.secretKey, clip.publicKey)
|
val sersk = KeyExchange.serverSessionKeys(serp.publicKey, serp.secretKey, clip.publicKey)
|
||||||
// val pser = KiloParams(true, t1, sersk, Unit, null, t1)
|
val pser = KiloParams(true, t1, sersk, Unit, null, t1)
|
||||||
// val pcli = KiloParams(false, t2, clisk, Unit, null, t2)
|
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!")
|
||||||
|
|
||||||
// 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
|
// 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 {
|
coroutineScope {
|
||||||
@ -170,8 +156,6 @@ class TransportTest {
|
|||||||
d2.close()
|
d2.close()
|
||||||
}
|
}
|
||||||
|
|
||||||
class TestException(text: String) : Exception(text)
|
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun testClient() = runTest {
|
fun testClient() = runTest {
|
||||||
initCrypto()
|
initCrypto()
|
||||||
@ -179,7 +163,7 @@ class TransportTest {
|
|||||||
val cmdPing by command<String, String>()
|
val cmdPing by command<String, String>()
|
||||||
val cmdPush by command<String, String>()
|
val cmdPush by command<String, String>()
|
||||||
val cmdGetToken by command<Unit, UByteArray>()
|
val cmdGetToken by command<Unit, UByteArray>()
|
||||||
val cmdGetClientId by command<Unit, VerifyingPublicKey?>()
|
val cmdGetClientId by command<Unit, Key.Verifying?>()
|
||||||
val cmdChainCallServer1 by command<String, String>()
|
val cmdChainCallServer1 by command<String, String>()
|
||||||
val cmdChainCallClient1 by command<String, String>()
|
val cmdChainCallClient1 by command<String, String>()
|
||||||
val cmdChainCallServer2 by command<String, String>()
|
val cmdChainCallServer2 by command<String, String>()
|
||||||
@ -188,23 +172,13 @@ class TransportTest {
|
|||||||
// Log.defaultLevel = Log.Level.DEBUG
|
// Log.defaultLevel = Log.Level.DEBUG
|
||||||
val (d1, d2) = createTestDevice()
|
val (d1, d2) = createTestDevice()
|
||||||
|
|
||||||
val serverId = SigningSecretKey.generatePair()
|
val serverId = Key.Signing.pair()
|
||||||
val clientId = SigningSecretKey.generatePair()
|
val clientId = Key.Signing.pair()
|
||||||
|
|
||||||
val cmdException by command<Unit, Unit>()
|
|
||||||
val cmdRemoteExceptionTest by command<Unit, String>()
|
|
||||||
val cmdBreak by command<Unit, Unit>()
|
|
||||||
|
|
||||||
val cmdPushServer by push<String>()
|
|
||||||
val pushedFromServer = CompletableDeferred<String>()
|
|
||||||
|
|
||||||
val serverInterface = KiloInterface<String>().apply {
|
val serverInterface = KiloInterface<String>().apply {
|
||||||
on(cmdPing) {
|
on(cmdPing) {
|
||||||
"pong! [$it]"
|
"pong! [$it]"
|
||||||
}
|
}
|
||||||
on(cmdPushServer) {
|
|
||||||
pushedFromServer.complete(it)
|
|
||||||
}
|
|
||||||
on(cmdGetToken) {
|
on(cmdGetToken) {
|
||||||
sessionToken
|
sessionToken
|
||||||
}
|
}
|
||||||
@ -217,28 +191,16 @@ class TransportTest {
|
|||||||
on(cmdChainCallServer2) {
|
on(cmdChainCallServer2) {
|
||||||
remote.call(cmdChainCallClient2, "$it-s2")
|
remote.call(cmdChainCallClient2, "$it-s2")
|
||||||
}
|
}
|
||||||
on(cmdException) { throw TestException("te1") }
|
registerError { IllegalStateException() }
|
||||||
on(cmdRemoteExceptionTest) {
|
registerError { IllegalArgumentException(it) }
|
||||||
try {
|
|
||||||
remote.call(cmdException)
|
|
||||||
"error!"
|
|
||||||
} catch (e: TestException) {
|
|
||||||
"ok: ${e.message}"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
on(cmdBreak) { throw LocalInterface.BreakConnectionException() }
|
|
||||||
registerError { TestException(it) }
|
|
||||||
}
|
}
|
||||||
val kiloServerConnection = KiloServerConnection(
|
val kiloServerConnection = KiloServerConnection(serverInterface, d1, "server session", serverId.signing)
|
||||||
serverInterface, d1, "server session", serverId.secretKey
|
|
||||||
)
|
|
||||||
launch { kiloServerConnection.run() }
|
launch { kiloServerConnection.run() }
|
||||||
|
|
||||||
var cnt = 0
|
var cnt = 0
|
||||||
val client = KiloClient {
|
val client = KiloClient {
|
||||||
addErrors(serverInterface)
|
|
||||||
session { "client session!" }
|
session { "client session!" }
|
||||||
secretIdKey = clientId.secretKey
|
secretIdKey = clientId.signing
|
||||||
local {
|
local {
|
||||||
on(cmdPush) {
|
on(cmdPush) {
|
||||||
"server push: $it"
|
"server push: $it"
|
||||||
@ -247,13 +209,11 @@ class TransportTest {
|
|||||||
"client pong: $it"
|
"client pong: $it"
|
||||||
}
|
}
|
||||||
on(cmdChainCallClient1) {
|
on(cmdChainCallClient1) {
|
||||||
remote.call(cmdChainCallServer2, "$it-c1")
|
remote.call(cmdChainCallServer2,"$it-c1")
|
||||||
}
|
}
|
||||||
on(cmdChainCallClient2) { "$it-c2" }
|
on(cmdChainCallClient2) { "$it-c2" }
|
||||||
on(cmdException) { throw TestException("te-local") }
|
|
||||||
}
|
}
|
||||||
connect {
|
connect {
|
||||||
println("Called connect: $cnt")
|
|
||||||
if (cnt++ > 0) {
|
if (cnt++ > 0) {
|
||||||
cancel()
|
cancel()
|
||||||
fail("connect called once again")
|
fail("connect called once again")
|
||||||
@ -266,25 +226,7 @@ class TransportTest {
|
|||||||
assertEquals("client pong: foo", kiloServerConnection.call(cmdPing, "foo"))
|
assertEquals("client pong: foo", kiloServerConnection.call(cmdPing, "foo"))
|
||||||
assertEquals("server push: bar", kiloServerConnection.call(cmdPush, "bar"))
|
assertEquals("server push: bar", kiloServerConnection.call(cmdPush, "bar"))
|
||||||
|
|
||||||
client.push(cmdPushServer, "42")
|
|
||||||
|
|
||||||
assertEquals("**-s1-c1-s2-c2", client.call(cmdChainCallServer1, "**"))
|
assertEquals("**-s1-c1-s2-c2", client.call(cmdChainCallServer1, "**"))
|
||||||
|
|
||||||
|
|
||||||
assertThrows<TestException> { client.call(cmdException) }
|
|
||||||
assertEquals("ok: te-local", client.call(cmdRemoteExceptionTest))
|
|
||||||
|
|
||||||
// wait for push to be received and check
|
|
||||||
assertEquals("42", pushedFromServer.await())
|
|
||||||
|
|
||||||
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()
|
d1.close()
|
||||||
d2.close()
|
d2.close()
|
||||||
client.close()
|
client.close()
|
||||||
|
|||||||
@ -1,13 +1,3 @@
|
|||||||
/*
|
|
||||||
* Copyright (c) 2025. Sergey S. Chernov - All Rights Reserved
|
|
||||||
*
|
|
||||||
* You may use, distribute and modify this code under the
|
|
||||||
* terms of the private license, which you must obtain from the author
|
|
||||||
*
|
|
||||||
* To obtain the license, contact the author: https://t.me/real_sergeych or email to
|
|
||||||
* real dot sergeych at gmail.
|
|
||||||
*/
|
|
||||||
|
|
||||||
import kotlin.test.fail
|
import kotlin.test.fail
|
||||||
|
|
||||||
inline fun <reified T: Throwable>assertThrows(f: ()->Unit): T {
|
inline fun <reified T: Throwable>assertThrows(f: ()->Unit): T {
|
||||||
@ -18,7 +8,6 @@ inline fun <reified T: Throwable>assertThrows(f: ()->Unit): T {
|
|||||||
}
|
}
|
||||||
catch(x: Throwable) {
|
catch(x: Throwable) {
|
||||||
if( x is T ) return x
|
if( x is T ) return 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")
|
||||||
fail("expected to throw $name but instead threw ${x::class.simpleName}: $x\b\n${x.stackTraceToString()}")
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -0,0 +1,5 @@
|
|||||||
|
package net.sergeych.kiloparsec.adapter
|
||||||
|
|
||||||
|
actual fun networkAddressOf(address: String): NetworkAddress {
|
||||||
|
TODO("Not yet implemented")
|
||||||
|
}
|
||||||
@ -0,0 +1,5 @@
|
|||||||
|
package net.sergeych.kiloparsec.adapter
|
||||||
|
|
||||||
|
actual fun NetworkAddress(host: String, port: Int): NetworkAddress {
|
||||||
|
TODO("Not yet implemented")
|
||||||
|
}
|
||||||
6
src/jsMain/kotlin/net/sergeych/tools/ProtectedOp.js.kt
Normal file
6
src/jsMain/kotlin/net/sergeych/tools/ProtectedOp.js.kt
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
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,8 @@
|
|||||||
|
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())
|
||||||
|
}
|
||||||
@ -0,0 +1,6 @@
|
|||||||
|
package net.sergeych.kiloparsec.adapter
|
||||||
|
|
||||||
|
import java.net.InetAddress
|
||||||
|
|
||||||
|
actual fun NetworkAddress(host: String, port: Int): NetworkAddress =
|
||||||
|
JvmNetworkAddress(InetAddress.getByName(host), port)
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user