Compare commits
25 Commits
Author | SHA1 | Date | |
---|---|---|---|
2b35112566 | |||
a80820b9a8 | |||
c0bc0e3dfe | |||
2d593e4107 | |||
c1bd6f09a9 | |||
0ff27e6de9 | |||
59906bbd2f | |||
7871dc2d3d | |||
a0dce8e604 | |||
04ffde421d | |||
9545ca28cf | |||
4098358233 | |||
f2d8330ccc | |||
1032eebbbe | |||
93ab8ddf91 | |||
6ce1b576ee | |||
99e98827f7 | |||
26564b6081 | |||
9ddb1209c9 | |||
b68232653a | |||
2e4f551e8e | |||
40b8723132 | |||
4d178d951f | |||
f6fbf8e58e | |||
3c915a8f58 |
2
.gitignore
vendored
2
.gitignore
vendored
@ -5,6 +5,7 @@ build/
|
||||
!**/src/test/**/build/
|
||||
|
||||
### IntelliJ IDEA ###
|
||||
.idea
|
||||
.idea/modules.xml
|
||||
.idea/jarRepositories.xml
|
||||
.idea/compiler.xml
|
||||
@ -43,3 +44,4 @@ out/
|
||||
# More
|
||||
.kotlin
|
||||
/.idea/workspace.xml
|
||||
/.gigaide/gigaide.properties
|
||||
|
8
.idea/artifacts/kiloparsec_js_0_3_1.xml
generated
Normal file
8
.idea/artifacts/kiloparsec_js_0_3_1.xml
generated
Normal file
@ -0,0 +1,8 @@
|
||||
<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_2.xml
generated
Normal file
8
.idea/artifacts/kiloparsec_js_0_3_2.xml
generated
Normal file
@ -0,0 +1,8 @@
|
||||
<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
Normal file
8
.idea/artifacts/kiloparsec_js_0_3_3.xml
generated
Normal file
@ -0,0 +1,8 @@
|
||||
<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_jvm_0_3_1.xml
generated
Normal file
8
.idea/artifacts/kiloparsec_jvm_0_3_1.xml
generated
Normal file
@ -0,0 +1,8 @@
|
||||
<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>
|
8
.idea/artifacts/kiloparsec_jvm_0_3_2.xml
generated
Normal file
8
.idea/artifacts/kiloparsec_jvm_0_3_2.xml
generated
Normal file
@ -0,0 +1,8 @@
|
||||
<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
Normal file
8
.idea/artifacts/kiloparsec_jvm_0_3_3.xml
generated
Normal file
@ -0,0 +1,8 @@
|
||||
<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>
|
6
.idea/markdown.xml
generated
Normal file
6
.idea/markdown.xml
generated
Normal file
@ -0,0 +1,6 @@
|
||||
<?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
@ -3,7 +3,7 @@
|
||||
<component name="FrameworkDetectionExcludesConfiguration">
|
||||
<file type="web" url="file://$PROJECT_DIR$" />
|
||||
</component>
|
||||
<component name="ProjectRootManager" version="2" languageLevel="JDK_17" default="true" project-jdk-name="17 (5)" project-jdk-type="JavaSDK">
|
||||
<component name="ProjectRootManager" version="2" languageLevel="JDK_17" default="true" project-jdk-name="corretto-17" project-jdk-type="JavaSDK">
|
||||
<output url="file://$PROJECT_DIR$/out" />
|
||||
</component>
|
||||
</project>
|
124
.idea/uiDesigner.xml
generated
Normal file
124
.idea/uiDesigner.xml
generated
Normal file
@ -0,0 +1,124 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="Palette2">
|
||||
<group name="Swing">
|
||||
<item class="com.intellij.uiDesigner.HSpacer" tooltip-text="Horizontal Spacer" icon="/com/intellij/uiDesigner/icons/hspacer.svg" removable="false" auto-create-binding="false" can-attach-label="false">
|
||||
<default-constraints vsize-policy="1" hsize-policy="6" anchor="0" fill="1" />
|
||||
</item>
|
||||
<item class="com.intellij.uiDesigner.VSpacer" tooltip-text="Vertical Spacer" icon="/com/intellij/uiDesigner/icons/vspacer.svg" removable="false" auto-create-binding="false" can-attach-label="false">
|
||||
<default-constraints vsize-policy="6" hsize-policy="1" anchor="0" fill="2" />
|
||||
</item>
|
||||
<item class="javax.swing.JPanel" icon="/com/intellij/uiDesigner/icons/panel.svg" removable="false" auto-create-binding="false" can-attach-label="false">
|
||||
<default-constraints vsize-policy="3" hsize-policy="3" anchor="0" fill="3" />
|
||||
</item>
|
||||
<item class="javax.swing.JScrollPane" icon="/com/intellij/uiDesigner/icons/scrollPane.svg" removable="false" auto-create-binding="false" can-attach-label="true">
|
||||
<default-constraints vsize-policy="7" hsize-policy="7" anchor="0" fill="3" />
|
||||
</item>
|
||||
<item class="javax.swing.JButton" icon="/com/intellij/uiDesigner/icons/button.svg" removable="false" auto-create-binding="true" can-attach-label="false">
|
||||
<default-constraints vsize-policy="0" hsize-policy="3" anchor="0" fill="1" />
|
||||
<initial-values>
|
||||
<property name="text" value="Button" />
|
||||
</initial-values>
|
||||
</item>
|
||||
<item class="javax.swing.JRadioButton" icon="/com/intellij/uiDesigner/icons/radioButton.svg" removable="false" auto-create-binding="true" can-attach-label="false">
|
||||
<default-constraints vsize-policy="0" hsize-policy="3" anchor="8" fill="0" />
|
||||
<initial-values>
|
||||
<property name="text" value="RadioButton" />
|
||||
</initial-values>
|
||||
</item>
|
||||
<item class="javax.swing.JCheckBox" icon="/com/intellij/uiDesigner/icons/checkBox.svg" removable="false" auto-create-binding="true" can-attach-label="false">
|
||||
<default-constraints vsize-policy="0" hsize-policy="3" anchor="8" fill="0" />
|
||||
<initial-values>
|
||||
<property name="text" value="CheckBox" />
|
||||
</initial-values>
|
||||
</item>
|
||||
<item class="javax.swing.JLabel" icon="/com/intellij/uiDesigner/icons/label.svg" removable="false" auto-create-binding="false" can-attach-label="false">
|
||||
<default-constraints vsize-policy="0" hsize-policy="0" anchor="8" fill="0" />
|
||||
<initial-values>
|
||||
<property name="text" value="Label" />
|
||||
</initial-values>
|
||||
</item>
|
||||
<item class="javax.swing.JTextField" icon="/com/intellij/uiDesigner/icons/textField.svg" removable="false" auto-create-binding="true" can-attach-label="true">
|
||||
<default-constraints vsize-policy="0" hsize-policy="6" anchor="8" fill="1">
|
||||
<preferred-size width="150" height="-1" />
|
||||
</default-constraints>
|
||||
</item>
|
||||
<item class="javax.swing.JPasswordField" icon="/com/intellij/uiDesigner/icons/passwordField.svg" removable="false" auto-create-binding="true" can-attach-label="true">
|
||||
<default-constraints vsize-policy="0" hsize-policy="6" anchor="8" fill="1">
|
||||
<preferred-size width="150" height="-1" />
|
||||
</default-constraints>
|
||||
</item>
|
||||
<item class="javax.swing.JFormattedTextField" icon="/com/intellij/uiDesigner/icons/formattedTextField.svg" removable="false" auto-create-binding="true" can-attach-label="true">
|
||||
<default-constraints vsize-policy="0" hsize-policy="6" anchor="8" fill="1">
|
||||
<preferred-size width="150" height="-1" />
|
||||
</default-constraints>
|
||||
</item>
|
||||
<item class="javax.swing.JTextArea" icon="/com/intellij/uiDesigner/icons/textArea.svg" removable="false" auto-create-binding="true" can-attach-label="true">
|
||||
<default-constraints vsize-policy="6" hsize-policy="6" anchor="0" fill="3">
|
||||
<preferred-size width="150" height="50" />
|
||||
</default-constraints>
|
||||
</item>
|
||||
<item class="javax.swing.JTextPane" icon="/com/intellij/uiDesigner/icons/textPane.svg" removable="false" auto-create-binding="true" can-attach-label="true">
|
||||
<default-constraints vsize-policy="6" hsize-policy="6" anchor="0" fill="3">
|
||||
<preferred-size width="150" height="50" />
|
||||
</default-constraints>
|
||||
</item>
|
||||
<item class="javax.swing.JEditorPane" icon="/com/intellij/uiDesigner/icons/editorPane.svg" removable="false" auto-create-binding="true" can-attach-label="true">
|
||||
<default-constraints vsize-policy="6" hsize-policy="6" anchor="0" fill="3">
|
||||
<preferred-size width="150" height="50" />
|
||||
</default-constraints>
|
||||
</item>
|
||||
<item class="javax.swing.JComboBox" icon="/com/intellij/uiDesigner/icons/comboBox.svg" removable="false" auto-create-binding="true" can-attach-label="true">
|
||||
<default-constraints vsize-policy="0" hsize-policy="2" anchor="8" fill="1" />
|
||||
</item>
|
||||
<item class="javax.swing.JTable" icon="/com/intellij/uiDesigner/icons/table.svg" removable="false" auto-create-binding="true" can-attach-label="false">
|
||||
<default-constraints vsize-policy="6" hsize-policy="6" anchor="0" fill="3">
|
||||
<preferred-size width="150" height="50" />
|
||||
</default-constraints>
|
||||
</item>
|
||||
<item class="javax.swing.JList" icon="/com/intellij/uiDesigner/icons/list.svg" removable="false" auto-create-binding="true" can-attach-label="false">
|
||||
<default-constraints vsize-policy="6" hsize-policy="2" anchor="0" fill="3">
|
||||
<preferred-size width="150" height="50" />
|
||||
</default-constraints>
|
||||
</item>
|
||||
<item class="javax.swing.JTree" icon="/com/intellij/uiDesigner/icons/tree.svg" removable="false" auto-create-binding="true" can-attach-label="false">
|
||||
<default-constraints vsize-policy="6" hsize-policy="6" anchor="0" fill="3">
|
||||
<preferred-size width="150" height="50" />
|
||||
</default-constraints>
|
||||
</item>
|
||||
<item class="javax.swing.JTabbedPane" icon="/com/intellij/uiDesigner/icons/tabbedPane.svg" removable="false" auto-create-binding="true" can-attach-label="false">
|
||||
<default-constraints vsize-policy="3" hsize-policy="3" anchor="0" fill="3">
|
||||
<preferred-size width="200" height="200" />
|
||||
</default-constraints>
|
||||
</item>
|
||||
<item class="javax.swing.JSplitPane" icon="/com/intellij/uiDesigner/icons/splitPane.svg" removable="false" auto-create-binding="false" can-attach-label="false">
|
||||
<default-constraints vsize-policy="3" hsize-policy="3" anchor="0" fill="3">
|
||||
<preferred-size width="200" height="200" />
|
||||
</default-constraints>
|
||||
</item>
|
||||
<item class="javax.swing.JSpinner" icon="/com/intellij/uiDesigner/icons/spinner.svg" removable="false" auto-create-binding="true" can-attach-label="true">
|
||||
<default-constraints vsize-policy="0" hsize-policy="6" anchor="8" fill="1" />
|
||||
</item>
|
||||
<item class="javax.swing.JSlider" icon="/com/intellij/uiDesigner/icons/slider.svg" removable="false" auto-create-binding="true" can-attach-label="false">
|
||||
<default-constraints vsize-policy="0" hsize-policy="6" anchor="8" fill="1" />
|
||||
</item>
|
||||
<item class="javax.swing.JSeparator" icon="/com/intellij/uiDesigner/icons/separator.svg" removable="false" auto-create-binding="false" can-attach-label="false">
|
||||
<default-constraints vsize-policy="6" hsize-policy="6" anchor="0" fill="3" />
|
||||
</item>
|
||||
<item class="javax.swing.JProgressBar" icon="/com/intellij/uiDesigner/icons/progressbar.svg" removable="false" auto-create-binding="true" can-attach-label="false">
|
||||
<default-constraints vsize-policy="0" hsize-policy="6" anchor="0" fill="1" />
|
||||
</item>
|
||||
<item class="javax.swing.JToolBar" icon="/com/intellij/uiDesigner/icons/toolbar.svg" removable="false" auto-create-binding="false" can-attach-label="false">
|
||||
<default-constraints vsize-policy="0" hsize-policy="6" anchor="0" fill="1">
|
||||
<preferred-size width="-1" height="20" />
|
||||
</default-constraints>
|
||||
</item>
|
||||
<item class="javax.swing.JToolBar$Separator" icon="/com/intellij/uiDesigner/icons/toolbarSeparator.svg" removable="false" auto-create-binding="false" can-attach-label="false">
|
||||
<default-constraints vsize-policy="0" hsize-policy="0" anchor="0" fill="1" />
|
||||
</item>
|
||||
<item class="javax.swing.JScrollBar" icon="/com/intellij/uiDesigner/icons/scrollbar.svg" removable="false" auto-create-binding="true" can-attach-label="false">
|
||||
<default-constraints vsize-policy="6" hsize-policy="0" anchor="0" fill="2" />
|
||||
</item>
|
||||
</group>
|
||||
</component>
|
||||
</project>
|
102
README.md
102
README.md
@ -1,49 +1,64 @@
|
||||
# Kiloparsec
|
||||
|
||||
__Recommended version is `0.3.1`: to keep the code compatible with current and further versions we
|
||||
ask to upgrade to `0.3.1` at least.__
|
||||
__Recommended version is `0.4.1`: 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 | native |
|
||||
|----------------|-----|----|----------|
|
||||
| TCP/IP server | ✓ | | >= 0.2.6 |
|
||||
| TCP/IP client | ✓ | | >= 0.2.6 |
|
||||
| Websock server | ✓ | | |
|
||||
| Websock client | ✓ | ✓ | ✓ |
|
||||
| name | JVM | JS | native |
|
||||
|-------------------|--------|----|--------|
|
||||
| TCP/IP server | ✓ | | 0.2.6+ |
|
||||
| TCP/IP client | ✓ | | 0.2.6+ |
|
||||
| UDP server | 0.3.2+ | | 0.3.2+ |
|
||||
| UDP client | 0.3.2+ | | 0.3.2+ |
|
||||
| Websockets server | ✓ | | |
|
||||
| Websockets client | ✓ | ✓ | ✓ |
|
||||
|
||||
### Note on version compatibility
|
||||
|
||||
Version 0.5.1 could be backward incompatible due to upgrade of the crypto2.
|
||||
|
||||
Protocols >= 0.3.0 are not binary compatible with previous version due to 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
|
||||
- linxArm64, linuxX64
|
||||
- linuxArm64, linuxX64
|
||||
|
||||
### Non-native targets
|
||||
|
||||
- JS (browser and nodeJS)
|
||||
- JVM (android, macos, windows, linx, everywhere where JRE is installed)
|
||||
- JS (browser and Node.js)
|
||||
- JVM (android, macOS, windows, linux, everywhere where JRE is installed)
|
||||
|
||||
## TCP/IP transport
|
||||
## TCP/IP and UDP transports
|
||||
|
||||
It is the fastest based on async socket implementation of ktor client. It works everywhere but JS target as
|
||||
These are the fastest based on async socket implementation of ktor client. They works everywhere but JS target as
|
||||
there is currently no widely adopted sockets for browser javascript.
|
||||
|
||||
## Websock server
|
||||
While UDP is faster than TCP/IP, it is less reliable, especially with commands and return values that serializes 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.
|
||||
|
||||
While it is much slower than pure TCP, it is still faster than any http-based transport. It uses binary frames based on
|
||||
## 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 websock and tcp servers in real
|
||||
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 maven dependency, not as source.
|
||||
@ -67,7 +82,7 @@ It could be, depending on your project structure, something like:
|
||||
```kotlin
|
||||
val commonMain by getting {
|
||||
dependencies {
|
||||
api("net.sergeych:kiloparsec:0.3.1")
|
||||
api("net.sergeych:kiloparsec:0.4.1")
|
||||
}
|
||||
}
|
||||
```
|
||||
@ -96,7 +111,7 @@ val cmdPushClient by command<String, Unit>()
|
||||
## Call it from the client:
|
||||
|
||||
Remember, we need to implement client interface `cmdPushClient` in our example, so we need to provide
|
||||
local interace too:
|
||||
local interface too:
|
||||
|
||||
```kotlin
|
||||
// Unit: no session on the client:
|
||||
@ -126,7 +141,7 @@ assertEquals(FooArgs("bar", 117), client.call(cmdGetFoo))
|
||||
## Create ktor-based server
|
||||
|
||||
Normally server side needs some session. It is convenient and avoid sending repeating data on each request speeding up
|
||||
the protocol. With KILOPARSEC it is rather basic operation:
|
||||
the protocol. With KILOPARSEC, it is rather basic operation:
|
||||
|
||||
~~~kotlin
|
||||
// Our session just keeps Foo for cmd{Get|Set}Foo:
|
||||
@ -165,12 +180,38 @@ Documentation is available in samples here:
|
||||
|
||||
- [TCP/IP client](https://code.sergeych.net/docs/kiloparsec/kiloparsec/net.sergeych.kiloparsec/-kilo-client/index.html)
|
||||
|
||||
In short, there are two functions that implements aysnchronous TCP/IP transport on all platforms buy JS:
|
||||
In short, there are two functions that implements 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, 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.
|
||||
@ -181,17 +222,16 @@ This is a common proactive to create a business logic in a `KiloInterface`, then
|
||||
|
||||
We do not recommend to rely 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 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?query=class%20KiloServer%3CS%3E(clientInterface:%20KiloInterface%3CS%3E,%20connections:%20Flow%3CInetTransportDevice%3E,%20serverSecretKey:%20SigningKey?%20=%20null,%20sessionBuilder:%20()%20-%3E%20S) when creating a server.
|
||||
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 protetcs against certificate chain spoofing in the case of the application installed from the trusted source.
|
||||
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. In the case of we applications we strongly recommend not to use CDN except your own where you can control actual traffic rules.
|
||||
__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 to use 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/sergeych/kiloparsec/wiki)
|
||||
- [Project's WIKI](https://gitea.sergeych.net/SergeychWorks/kiloparsec/wiki)
|
||||
|
||||
# Details
|
||||
|
||||
@ -213,13 +253,13 @@ All RPC is performed over the encrypted connection.
|
||||
|
||||
# Technical description
|
||||
|
||||
Kiloparsec is a dull-duplex fully async (coroutine based) Remote Procedure Call protocol with typed parameters
|
||||
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)
|
||||
independently on the ends and is never transferred with the network. Comparing it somehow (visually, with QR code, etc.)
|
||||
could add a very robust guarantee of the connection safety and ingenuity.
|
||||
|
||||
Kiloparsec has built-in completely asynchronous (coroutine based top-down) transport layer based on TCP (JVM only as for
|
||||
@ -228,4 +268,10 @@ JVM only insofar.
|
||||
|
||||
# Licensing
|
||||
|
||||
Currently, you need to obtain a license from https://8-rays.dev or Sergey Chernov.
|
||||
This is work in progress, not yet moved to 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 will be 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
|
10
bin/pubdocs
10
bin/pubdocs
@ -1,4 +1,14 @@
|
||||
#!/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
|
||||
|
@ -1,36 +1,52 @@
|
||||
/*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
plugins {
|
||||
kotlin("multiplatform") version "2.0.0"
|
||||
id("org.jetbrains.kotlin.plugin.serialization") version "2.0.0"
|
||||
kotlin("multiplatform") version "2.1.0"
|
||||
id("org.jetbrains.kotlin.plugin.serialization") version "2.1.0"
|
||||
`maven-publish`
|
||||
id("org.jetbrains.dokka") version "1.9.20"
|
||||
}
|
||||
|
||||
group = "net.sergeych"
|
||||
version = "0.3.1"
|
||||
version = "0.6.1-SNAPSHOT"
|
||||
|
||||
repositories {
|
||||
mavenCentral()
|
||||
mavenLocal()
|
||||
maven("https://maven.universablockchain.com/")
|
||||
maven("https://gitea.sergeych.net/api/packages/SergeychWorks/maven")
|
||||
maven("https://gitea.sergeych.net/api/packages/YoungBlood/maven")
|
||||
}
|
||||
|
||||
kotlin {
|
||||
jvmToolchain(17)
|
||||
jvm()
|
||||
js {
|
||||
browser {
|
||||
}
|
||||
nodejs()
|
||||
}
|
||||
macosArm64()
|
||||
iosX64()
|
||||
iosArm64()
|
||||
iosSimulatorArm64()
|
||||
// macosArm64()
|
||||
// iosX64()
|
||||
// iosArm64()
|
||||
// iosSimulatorArm64()
|
||||
linuxX64()
|
||||
linuxArm64()
|
||||
macosX64()
|
||||
// macosX64()
|
||||
// macosX64()
|
||||
mingwX64()
|
||||
// @OptIn(ExperimentalWasmDsl::class)
|
||||
// wasmJs()
|
||||
|
||||
val ktor_version = "2.3.12"
|
||||
val ktor_version = "3.1.0"
|
||||
|
||||
sourceSets {
|
||||
all {
|
||||
@ -41,10 +57,10 @@ kotlin {
|
||||
|
||||
val commonMain by getting {
|
||||
dependencies {
|
||||
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.7.3")
|
||||
implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.7.1")
|
||||
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.10.1")
|
||||
implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.7.2")
|
||||
api("io.ktor:ktor-client-core:$ktor_version")
|
||||
api("net.sergeych:crypto2:0.4.2")
|
||||
api("net.sergeych:crypto2:0.7.2-SNAPSHOT")
|
||||
}
|
||||
}
|
||||
val ktorSocketMain by creating {
|
||||
@ -83,27 +99,27 @@ kotlin {
|
||||
}
|
||||
}
|
||||
val jsTest by getting
|
||||
val macosArm64Main by getting {
|
||||
dependsOn(ktorSocketMain)
|
||||
}
|
||||
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 macosArm64Main by getting {
|
||||
// dependsOn(ktorSocketMain)
|
||||
// }
|
||||
// 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)
|
||||
}
|
||||
|
@ -1,2 +1,12 @@
|
||||
#
|
||||
# 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.mpp.applyDefaultHierarchyTemplate=false
|
10
gradle/wrapper/gradle-wrapper.properties
vendored
10
gradle/wrapper/gradle-wrapper.properties
vendored
@ -1,3 +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.
|
||||
#
|
||||
|
||||
distributionBase=GRADLE_USER_HOME
|
||||
distributionPath=wrapper/dists
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-8.2-bin.zip
|
||||
|
16
gradlew
vendored
16
gradlew
vendored
@ -1,19 +1,13 @@
|
||||
#!/bin/sh
|
||||
|
||||
#
|
||||
# Copyright © 2015-2021 the original authors.
|
||||
# Copyright (c) 2025. Sergey S. Chernov - All Rights Reserved
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
# You may use, distribute and modify this code under the
|
||||
# terms of the private license, which you must obtain from the author
|
||||
#
|
||||
# https://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# 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.
|
||||
# To obtain the license, contact the author: https://t.me/real_sergeych or email to
|
||||
# real dot sergeych at gmail.
|
||||
#
|
||||
|
||||
##############################################################################
|
||||
|
@ -1,3 +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.
|
||||
*/
|
||||
|
||||
pluginManagement {
|
||||
repositories {
|
||||
mavenCentral()
|
||||
|
@ -1,3 +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.
|
||||
*/
|
||||
|
||||
package net.sergeych.kiloparsec
|
||||
|
||||
import io.ktor.utils.io.*
|
||||
|
@ -0,0 +1,52 @@
|
||||
/*
|
||||
* 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,3 +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.
|
||||
*/
|
||||
|
||||
package net.sergeych.kiloparsec
|
||||
|
||||
import kotlinx.serialization.KSerializer
|
||||
@ -5,6 +15,8 @@ import kotlinx.serialization.Serializable
|
||||
import net.sergeych.bintools.toDataSource
|
||||
import net.sergeych.bipack.BipackDecoder
|
||||
import net.sergeych.bipack.BipackEncoder
|
||||
import net.sergeych.kiloparsec.Command.Call
|
||||
import net.sergeych.kiloparsec.Command.Companion.unpackCall
|
||||
import net.sergeych.utools.unpack
|
||||
|
||||
/**
|
||||
@ -12,8 +24,20 @@ import net.sergeych.utools.unpack
|
||||
* 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
|
||||
* 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].
|
||||
*
|
||||
*/
|
||||
class Command<A, R>(
|
||||
open class Command<A, R>(
|
||||
val name: String,
|
||||
val argsSerializer: KSerializer<A>,
|
||||
val resultSerializer: KSerializer<R>
|
||||
@ -21,13 +45,29 @@ class Command<A, R>(
|
||||
@Serializable
|
||||
data class Call(val name: String,val serializedArgs: UByteArray)
|
||||
|
||||
fun packCall(args: A): UByteArray = BipackEncoder.encode(
|
||||
Call(name, BipackEncoder.encode(argsSerializer, args).toUByteArray())
|
||||
).toUByteArray()
|
||||
/**
|
||||
* Pack command invocation with specified arguments.
|
||||
*/
|
||||
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 =
|
||||
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 =
|
||||
BipackEncoder.encode(
|
||||
resultSerializer,
|
||||
@ -36,6 +76,10 @@ class Command<A, R>(
|
||||
).toUByteArray()
|
||||
|
||||
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())
|
||||
}
|
||||
}
|
@ -1,3 +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.
|
||||
*/
|
||||
|
||||
package net.sergeych.kiloparsec
|
||||
|
||||
import kotlinx.serialization.KSerializer
|
||||
|
@ -1,3 +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.
|
||||
*/
|
||||
|
||||
package net.sergeych.kiloparsec
|
||||
|
||||
/**
|
||||
|
@ -0,0 +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
|
||||
|
||||
interface ExceptionWithCode {
|
||||
val code: String
|
||||
val message: String?
|
||||
}
|
@ -1,3 +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.
|
||||
*/
|
||||
|
||||
package net.sergeych.kiloparsec
|
||||
|
||||
import kotlinx.coroutines.CancellationException
|
||||
@ -9,6 +19,7 @@ import kotlinx.coroutines.isActive
|
||||
import kotlinx.coroutines.sync.Mutex
|
||||
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.Loggable
|
||||
@ -60,6 +71,14 @@ class KiloClient<S>(
|
||||
@Suppress("unused")
|
||||
val connectedStateFlow = _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 val job =
|
||||
@ -89,6 +108,7 @@ class KiloClient<S>(
|
||||
}
|
||||
_state.value = false
|
||||
resetDeferredClient()
|
||||
// reconnection timeout
|
||||
delay(100)
|
||||
}
|
||||
}
|
||||
@ -140,7 +160,7 @@ class KiloClient<S>(
|
||||
* a key. Connection is established either with a properly authenticated key or no key at all.
|
||||
*/
|
||||
@Suppress("unused")
|
||||
suspend fun remoteId(): VerifyingPublicKey? = deferredClient.await().remoteId()
|
||||
suspend fun remoteIdentity(): VerifyingPublicKey? = deferredClient.await().remoteId()
|
||||
|
||||
companion object {
|
||||
class Builder<S>() {
|
||||
@ -155,8 +175,19 @@ class KiloClient<S>(
|
||||
var secretIdKey: SigningKey? = null
|
||||
|
||||
/**
|
||||
* Build local command implementations (remotely callable ones), exception
|
||||
* class handlers, etc.
|
||||
* Build local command implementations, those callable from the server, exception
|
||||
* class handlers, and anything else [KiloInterface] allows. Usage sample:
|
||||
*
|
||||
* ```kotlin
|
||||
* val client = KiloClient {
|
||||
* connect { connectTcpDevice("localhost:$port") }
|
||||
* local {
|
||||
* on(cmdPing) {
|
||||
* "pong! $it"
|
||||
* }
|
||||
* }
|
||||
* }
|
||||
* ```
|
||||
*/
|
||||
fun local(f: KiloInterface<S>.() -> Unit) {
|
||||
interfaceBuilder = f
|
||||
|
@ -1,3 +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.
|
||||
*/
|
||||
|
||||
package net.sergeych.kiloparsec
|
||||
|
||||
import kotlinx.coroutines.*
|
||||
@ -80,7 +90,7 @@ class KiloClientConnection<S>(
|
||||
kiloRemoteInterface.complete(
|
||||
KiloRemoteInterface(deferredParams, clientInterface)
|
||||
)
|
||||
clientInterface.onConnectHandler?.invoke(params.scope)
|
||||
clientInterface.onConnectHandlers.invokeAll(params.scope)
|
||||
onConnectedStateChanged?.invoke(true)
|
||||
job.join()
|
||||
|
||||
@ -104,4 +114,7 @@ class KiloClientConnection<S>(
|
||||
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,5 +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
|
||||
|
||||
typealias KiloHandler<S> = KiloScope<S>.()->Unit
|
||||
/**
|
||||
* The local interface to provide functions, register errors for Kiloparsec users. Use it
|
||||
* with [KiloClient], [KiloClientConnection], [KiloServerConnection], etc.
|
||||
@ -14,12 +25,19 @@ package net.sergeych.kiloparsec
|
||||
*/
|
||||
open class KiloInterface<S> : LocalInterface<KiloScope<S>>() {
|
||||
|
||||
internal var onConnectHandler: (KiloScope<S>.()->Unit) ? = null
|
||||
internal val onConnectHandlers = mutableListOf<KiloHandler<S>>()
|
||||
|
||||
fun onConnected(f: KiloScope<S>.()->Unit) { onConnectHandler = f }
|
||||
/**
|
||||
* 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 {
|
||||
registerError { RemoteInterface.UnknownCommand() }
|
||||
registerError { RemoteInterface.UnknownCommand(it) }
|
||||
registerError { RemoteInterface.InternalError(it) }
|
||||
registerError { RemoteInterface.ClosedException(it) }
|
||||
// registerError { RemoteInterface.SecurityException(it) }
|
||||
|
@ -1,3 +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.
|
||||
*/
|
||||
|
||||
package net.sergeych.kiloparsec
|
||||
|
||||
import kotlinx.coroutines.CompletableDeferred
|
||||
|
@ -1,3 +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.
|
||||
*/
|
||||
|
||||
package net.sergeych.kiloparsec
|
||||
|
||||
import kotlinx.serialization.Serializable
|
||||
|
@ -1,3 +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.
|
||||
*/
|
||||
|
||||
package net.sergeych.kiloparsec
|
||||
|
||||
import kotlinx.coroutines.CompletableDeferred
|
||||
|
@ -1,3 +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.
|
||||
*/
|
||||
|
||||
package net.sergeych.kiloparsec
|
||||
|
||||
import net.sergeych.crypto2.SigningKey
|
||||
|
@ -1,3 +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.
|
||||
*/
|
||||
|
||||
package net.sergeych.kiloparsec
|
||||
|
||||
import kotlinx.coroutines.CancellationException
|
||||
|
@ -1,3 +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.
|
||||
*/
|
||||
|
||||
package net.sergeych.kiloparsec
|
||||
|
||||
import kotlinx.coroutines.CompletableDeferred
|
||||
@ -75,7 +85,7 @@ class KiloServerConnection<S>(
|
||||
kiloRemoteInterface.complete(
|
||||
KiloRemoteInterface(deferredParams, clientInterface)
|
||||
)
|
||||
clientInterface.onConnectHandler?.invoke(p.scope)
|
||||
clientInterface.onConnectHandlers.invokeAll(p.scope)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1,3 +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.
|
||||
*/
|
||||
|
||||
package net.sergeych.kiloparsec
|
||||
|
||||
import net.sergeych.mp_logger.LogTag
|
||||
@ -67,14 +77,18 @@ open class LocalInterface<S> : Loggable by LogTag("LocalInterface${idCounter.inc
|
||||
name: String,
|
||||
packedArgs: UByteArray,
|
||||
): UByteArray =
|
||||
(commands[name] ?: throw RemoteInterface.UnknownCommand())
|
||||
(commands[name] ?: throw RemoteInterface.UnknownCommand(name))
|
||||
.invoke(scope, packedArgs)
|
||||
|
||||
|
||||
private val errorByClass = mutableMapOf<KClass<*>, String>()
|
||||
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!!,
|
||||
exceptionBuilder: (String, UByteArray?) -> T,
|
||||
) {
|
||||
@ -82,10 +96,17 @@ open class LocalInterface<S> : Loggable by LogTag("LocalInterface${idCounter.inc
|
||||
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(
|
||||
noinline exceptionBuilder: (String) -> T,
|
||||
) {
|
||||
registerError(T::class) { msg, _ -> exceptionBuilder(msg) }
|
||||
registerErrorClass(T::class) { msg, _ -> exceptionBuilder(msg) }
|
||||
}
|
||||
|
||||
val errorProviders = mutableListOf<LocalInterface<*>>()
|
||||
@ -95,7 +116,8 @@ open class LocalInterface<S> : Loggable by LogTag("LocalInterface${idCounter.inc
|
||||
}
|
||||
|
||||
fun getErrorCode(t: Throwable): String? =
|
||||
errorByClass[t::class] ?: errorProviders.firstNonNull { it.getErrorCode(t) }
|
||||
(t as? ExceptionWithCode)?.code
|
||||
?: errorByClass[t::class] ?: errorProviders.firstNonNull { it.getErrorCode(t) }
|
||||
|
||||
fun encodeError(forId: UInt, t: Throwable): Transport.Block.Error =
|
||||
if (t is RemoteInterface.ClosedException) {
|
||||
|
@ -1,3 +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.
|
||||
*/
|
||||
|
||||
package net.sergeych.kiloparsec
|
||||
|
||||
/**
|
||||
@ -40,14 +50,21 @@ interface RemoteInterface {
|
||||
/**
|
||||
* Command is not supported by the remote party
|
||||
*/
|
||||
class UnknownCommand : RemoteException("UnknownCommand")
|
||||
class UnknownCommand(commandName: String) : RemoteException("UnknownCommand: $commandName")
|
||||
|
||||
open class InternalError(code: String="0"): RemoteException("Internal error: $code")
|
||||
|
||||
suspend fun <R> call(cmd: Command<Unit, R>): R = call(cmd, Unit)
|
||||
|
||||
/**
|
||||
* Call the remote procedure with specified args and return its result
|
||||
* Call the remote procedure with specified [args] and return its result of type [R]. The calling coroutine
|
||||
* 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
|
||||
|
||||
@ -55,6 +72,8 @@ interface RemoteInterface {
|
||||
* 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)
|
||||
|
||||
|
@ -1,3 +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.
|
||||
*/
|
||||
|
||||
package net.sergeych.kiloparsec
|
||||
|
||||
import kotlinx.coroutines.*
|
||||
@ -124,7 +134,7 @@ class Transport<S>(
|
||||
}
|
||||
|
||||
// now we have mutex freed so we can call:
|
||||
val r = runCatching { device.output.send(pack(b).also { debug { ">>>\n${it.toDump()}" } }) }
|
||||
val r = runCatching { device.output.send(pack(b)) }
|
||||
if (!r.isSuccess) {
|
||||
r.exceptionOrNull()?.let {
|
||||
exception { "failed to send output block" to it }
|
||||
@ -184,10 +194,7 @@ class Transport<S>(
|
||||
while (isActive && !isClosed) {
|
||||
try {
|
||||
device.input.receive().let { packed ->
|
||||
debug { "<<<\n${packed.toDump()}" }
|
||||
val b = unpack<Block>(packed)
|
||||
debug { "<<$ $b" }
|
||||
debug { "access state: ${access.isLocked}" }
|
||||
when (b) {
|
||||
is Block.Error -> access.withLock {
|
||||
val error = localInterface.decodeError(b)
|
||||
@ -198,10 +205,7 @@ class Transport<S>(
|
||||
}
|
||||
|
||||
is Block.Response -> access.withLock {
|
||||
calls.remove(b.forId)?.let {
|
||||
debug { "activating wait handle for ${b.forId}" }
|
||||
it.complete(b.packedResult)
|
||||
}
|
||||
calls.remove(b.forId)?.complete(b.packedResult)
|
||||
?: warning { "wait handle not found for ${b.forId}" }
|
||||
}
|
||||
|
||||
@ -228,12 +232,10 @@ class Transport<S>(
|
||||
} catch (t: Throwable) {
|
||||
send(Block.Error(b.id, "UnknownError", t.message))
|
||||
}
|
||||
.also { debug { "command executed: ${b.name}" } }
|
||||
}
|
||||
}
|
||||
debug { "=---------------------------------------------" }
|
||||
}
|
||||
debug { "input step performed closed=$isClosed active=$isActive" }
|
||||
// debug { "input step performed closed=$isClosed active=$isActive" }
|
||||
} catch (_: ClosedSendChannelException) {
|
||||
info { "closed send channel" }
|
||||
isClosed = true
|
||||
|
@ -1,3 +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.
|
||||
*/
|
||||
|
||||
package net.sergeych.kiloparsec.adapter
|
||||
|
||||
import kotlinx.coroutines.channels.Channel
|
||||
|
@ -1,3 +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.
|
||||
*/
|
||||
|
||||
package net.sergeych.kiloparsec.adapter
|
||||
|
||||
/**
|
||||
|
@ -1,3 +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.
|
||||
*/
|
||||
|
||||
package net.sergeych.kiloparsec.adapter
|
||||
|
||||
import kotlinx.coroutines.channels.Channel
|
||||
|
@ -1,3 +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.
|
||||
*/
|
||||
|
||||
package net.sergeych.kiloparsec.adapter
|
||||
|
||||
import io.ktor.client.*
|
||||
@ -11,10 +21,7 @@ import kotlinx.coroutines.channels.ClosedReceiveChannelException
|
||||
import kotlinx.coroutines.channels.ClosedSendChannelException
|
||||
import kotlinx.coroutines.launch
|
||||
import net.sergeych.crypto2.SigningKey
|
||||
import net.sergeych.kiloparsec.KiloClient
|
||||
import net.sergeych.kiloparsec.KiloConnectionData
|
||||
import net.sergeych.kiloparsec.KiloInterface
|
||||
import net.sergeych.kiloparsec.RemoteInterface
|
||||
import net.sergeych.kiloparsec.*
|
||||
import net.sergeych.mp_logger.LogTag
|
||||
import net.sergeych.mp_logger.exception
|
||||
import net.sergeych.mp_logger.info
|
||||
@ -24,91 +31,106 @@ 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(),
|
||||
client: HttpClient = HttpClient { install(WebSockets) },
|
||||
secretKey: SigningKey? = null,
|
||||
sessionMaker: () -> S = {
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
Unit as S
|
||||
},
|
||||
): KiloClient<S> {
|
||||
return KiloClient(clientInterface, secretKey) {
|
||||
KiloConnectionData(websocketTransportDevice(path), 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,
|
||||
client: HttpClient = HttpClient { install(WebSockets) },
|
||||
): Transport.Device {
|
||||
var u = Url(path)
|
||||
if (u.encodedPath.length <= 1)
|
||||
u = URLBuilder(u).apply {
|
||||
encodedPath = "/kp"
|
||||
}.build()
|
||||
|
||||
return KiloClient(clientInterface, secretKey) {
|
||||
val input = Channel<UByteArray>()
|
||||
val output = Channel<UByteArray>()
|
||||
val closeHandle = CompletableDeferred<Boolean>()
|
||||
globalLaunch {
|
||||
val log = LogTag("KC:${counter.incrementAndGet()}")
|
||||
client.webSocket({
|
||||
url.protocol = u.protocol
|
||||
url.host = u.host
|
||||
url.port = u.port
|
||||
url.encodedPath = u.encodedPath
|
||||
url.parameters.appendAll(u.parameters)
|
||||
log.info { "kiloparsec server URL: $url" }
|
||||
}) {
|
||||
log.info { "connected to the server" }
|
||||
val input = Channel<UByteArray>()
|
||||
val output = Channel<UByteArray>()
|
||||
val closeHandle = CompletableDeferred<Boolean>()
|
||||
globalLaunch {
|
||||
val log = LogTag("KC:${counter.incrementAndGet()}")
|
||||
client.webSocket({
|
||||
url.protocol = u.protocol
|
||||
url.host = u.host
|
||||
url.port = u.port
|
||||
url.encodedPath = u.encodedPath
|
||||
url.parameters.appendAll(u.parameters)
|
||||
log.info { "kiloparsec server URL: $url" }
|
||||
}) {
|
||||
log.info { "connected to the server" }
|
||||
// println("SENDING!!!")
|
||||
// send("Helluva")
|
||||
launch {
|
||||
try {
|
||||
for (block in output) {
|
||||
send(block.toByteArray())
|
||||
launch {
|
||||
try {
|
||||
for (block in output) {
|
||||
send(block.toByteArray())
|
||||
}
|
||||
log.info { "input is closed, closing the websocket" }
|
||||
if (closeHandle.isActive) closeHandle.complete(true)
|
||||
} catch (_: ClosedSendChannelException) {
|
||||
log.info { "send channel closed" }
|
||||
} catch (_: CancellationException) {
|
||||
} catch (t: Throwable) {
|
||||
log.info { "unexpected exception in websock sender: ${t.stackTraceToString()}" }
|
||||
closeHandle.completeExceptionally(t)
|
||||
}
|
||||
if (closeHandle.isActive) closeHandle.complete(false)
|
||||
}
|
||||
launch {
|
||||
try {
|
||||
for (f in incoming) {
|
||||
if (f is Frame.Binary) {
|
||||
input.send(f.readBytes().toUByteArray())
|
||||
} else {
|
||||
log.warning { "ignoring unexpected frame of type ${f.frameType}" }
|
||||
}
|
||||
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(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)
|
||||
}
|
||||
launch {
|
||||
try {
|
||||
for (f in incoming) {
|
||||
if (f is Frame.Binary) {
|
||||
input.send(f.readBytes().toUByteArray())
|
||||
} else {
|
||||
log.warning { "ignoring unexpected frame of type ${f.frameType}" }
|
||||
}
|
||||
}
|
||||
if (closeHandle.isActive) closeHandle.complete(true)
|
||||
} catch (_: CancellationException) {
|
||||
if (closeHandle.isActive) closeHandle.complete(false)
|
||||
} catch (_: ClosedReceiveChannelException) {
|
||||
log.warning { "receive channel closed unexpectedly" }
|
||||
if (closeHandle.isActive) closeHandle.complete(false)
|
||||
} catch (t: Throwable) {
|
||||
log.exception { "unexpected error" to t }
|
||||
if (closeHandle.isActive) closeHandle.complete(false)
|
||||
}
|
||||
}
|
||||
if(!closeHandle.await()) {
|
||||
log.warning { "Client is closing with error" }
|
||||
throw RemoteInterface.ClosedException()
|
||||
}
|
||||
output.close()
|
||||
input.close()
|
||||
}
|
||||
log.info { "closing connection" }
|
||||
if (!closeHandle.await()) {
|
||||
log.warning { "Client is closing with error" }
|
||||
throw RemoteInterface.ClosedException()
|
||||
}
|
||||
output.close()
|
||||
input.close()
|
||||
}
|
||||
val device = ProxyDevice(input, output) {
|
||||
// we need to explicitly close the coroutine job, or it can hang for a long time
|
||||
// leaking resources.
|
||||
closeHandle.complete(true)
|
||||
// job.cancel()
|
||||
}
|
||||
KiloConnectionData(device, sessionMaker())
|
||||
log.info { "closing connection" }
|
||||
}
|
||||
val device = ProxyDevice(input, output) {
|
||||
// we need to explicitly close the coroutine job, or it can hang for a long time
|
||||
// leaking resources.
|
||||
closeHandle.complete(true)
|
||||
// job.cancel()
|
||||
}
|
||||
return device
|
||||
}
|
||||
|
||||
|
@ -1,3 +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.
|
||||
*/
|
||||
|
||||
package net.sergeych.kiloparsec
|
||||
|
||||
import kotlinx.serialization.Serializable
|
||||
|
@ -1,6 +1,34 @@
|
||||
/*
|
||||
* 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()
|
||||
|
||||
fun UByteArray.decodeFromUByteArray(): String = toByteArray().decodeToString()
|
||||
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
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -1,3 +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 net.sergeych.crypto2.IllegalSignatureException
|
||||
import net.sergeych.crypto2.SealedBox
|
||||
|
@ -1,3 +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.datetime.Instant
|
||||
import net.sergeych.bipack.BipackEncoder
|
||||
|
@ -1,3 +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.
|
||||
*/
|
||||
|
||||
class ToolsTest {
|
||||
// @Test
|
||||
// fun testRemoceCmd() {
|
||||
|
@ -1,3 +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.*
|
||||
import kotlinx.coroutines.channels.Channel
|
||||
import kotlinx.coroutines.channels.ReceiveChannel
|
||||
|
@ -1,3 +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 kotlin.test.fail
|
||||
|
||||
inline fun <reified T: Throwable>assertThrows(f: ()->Unit): T {
|
||||
|
@ -1,3 +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.
|
||||
*/
|
||||
|
||||
package net.sergeych.kiloparsec.adapter
|
||||
|
||||
import io.ktor.server.application.*
|
||||
@ -10,25 +20,42 @@ import kotlinx.coroutines.channels.ClosedReceiveChannelException
|
||||
import kotlinx.coroutines.isActive
|
||||
import kotlinx.coroutines.launch
|
||||
import net.sergeych.crypto2.SigningKey
|
||||
import net.sergeych.crypto2.toDump
|
||||
import net.sergeych.kiloparsec.KiloInterface
|
||||
import net.sergeych.kiloparsec.KiloServerConnection
|
||||
import net.sergeych.kiloparsec.RemoteInterface
|
||||
import net.sergeych.mp_logger.*
|
||||
import net.sergeych.tools.AtomicCounter
|
||||
import java.time.Duration
|
||||
import kotlin.time.Duration.Companion.seconds
|
||||
|
||||
/**
|
||||
* Create a ktor-based websocket server.
|
||||
* This call install Routing and WebSockets with proper configuration.
|
||||
*
|
||||
* The course of action is:
|
||||
*
|
||||
* - create LocalInterface and populate it with functionality
|
||||
* - call this method with localInterface
|
||||
* - optionally, connect the same interface with TCP or UDP providers on supported platforms,
|
||||
* in which case it might be useful to hae session creating function [createSession] separate.
|
||||
*
|
||||
* _Note_: [KiloInterface] as for now does not contain session creation in it as we suggest
|
||||
* session could be transport specific.
|
||||
*
|
||||
* @param localInterface where the actual work is performed.
|
||||
* @param path default http path to the websocket.
|
||||
* @param serverKey optional key to authenticate the connection. If the client specify expected
|
||||
* server key it should match of connection will not be established.
|
||||
* @param createSession function to create a server session.
|
||||
*/
|
||||
fun <S> Application.setupWebsocketServer(
|
||||
localInterface: KiloInterface<S>,
|
||||
path: String = "/kp",
|
||||
serverKey: SigningKey? = null,
|
||||
createSession: () -> S,
|
||||
) {
|
||||
|
||||
install(Routing)
|
||||
install(WebSockets) {
|
||||
pingPeriod = Duration.ofSeconds(15)
|
||||
timeout = Duration.ofSeconds(15)
|
||||
pingPeriod = 60.seconds //Duration.ofSeconds(15)
|
||||
timeout = 45.seconds
|
||||
maxFrameSize = Long.MAX_VALUE
|
||||
masking = false
|
||||
}
|
||||
@ -64,12 +91,9 @@ fun <S> Application.setupWebsocketServer(
|
||||
}
|
||||
log.debug { "KSC started, looking for incoming frames" }
|
||||
for (f in incoming) {
|
||||
log.debug { "incoming frame: ${f.frameType}" }
|
||||
if (f is Frame.Binary)
|
||||
try {
|
||||
input.send(f.readBytes().toUByteArray().also {
|
||||
log.debug { "in frame\n${it.toDump()}" }
|
||||
})
|
||||
input.send(f.readBytes().toUByteArray())
|
||||
} catch (_: RemoteInterface.ClosedException) {
|
||||
log.warning { "caught local closed exception (strange!), closing" }
|
||||
break
|
||||
|
@ -1,8 +1,19 @@
|
||||
/*
|
||||
* 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 assertThrows
|
||||
import io.ktor.server.engine.*
|
||||
import io.ktor.server.netty.*
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import net.sergeych.crypto2.initCrypto
|
||||
@ -10,6 +21,7 @@ import net.sergeych.kiloparsec.adapter.setupWebsocketServer
|
||||
import net.sergeych.kiloparsec.adapter.websocketClient
|
||||
import net.sergeych.mp_logger.Log
|
||||
import java.net.InetAddress
|
||||
import kotlin.random.Random
|
||||
import kotlin.test.Test
|
||||
import kotlin.test.assertEquals
|
||||
import kotlin.test.assertFalse
|
||||
@ -50,11 +62,12 @@ class ClientTest {
|
||||
}
|
||||
}
|
||||
|
||||
val ns: NettyApplicationEngine = embeddedServer(Netty, port = 8080, host = "0.0.0.0", module = {
|
||||
val port = Random.nextInt(8080,9090)
|
||||
val ns = embeddedServer(Netty, port = port, host = "0.0.0.0", module = {
|
||||
setupWebsocketServer(serverInterface) { Session() }
|
||||
}).start(wait = false)
|
||||
|
||||
val client = websocketClient<Unit>("ws://localhost:8080/kp")
|
||||
val client = websocketClient<Unit>("ws://localhost:$port/kp")
|
||||
val states = mutableListOf<Boolean>()
|
||||
val collector = launch {
|
||||
client.connectedStateFlow.collect {
|
||||
@ -75,6 +88,9 @@ class ClientTest {
|
||||
}
|
||||
|
||||
// connection should now be closed
|
||||
// the problem is: it needs some unspecified time to close
|
||||
// as it is async process.
|
||||
delay(100)
|
||||
assertFalse { client.connectedStateFlow.value }
|
||||
|
||||
// this should be run on automatically reopen connection
|
||||
|
@ -1,8 +1,19 @@
|
||||
/*
|
||||
* 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.network.selector.*
|
||||
import io.ktor.network.sockets.*
|
||||
import io.ktor.utils.io.*
|
||||
import io.ktor.utils.io.writeByte
|
||||
import kotlinx.coroutines.*
|
||||
import kotlinx.coroutines.CancellationException
|
||||
import kotlinx.coroutines.channels.Channel
|
||||
@ -12,14 +23,10 @@ import kotlinx.coroutines.flow.flow
|
||||
import kotlinx.coroutines.sync.Mutex
|
||||
import kotlinx.coroutines.sync.withLock
|
||||
import kotlinx.datetime.Clock
|
||||
import net.sergeych.kiloparsec.AsyncVarint
|
||||
import net.sergeych.kiloparsec.KiloClient
|
||||
import net.sergeych.kiloparsec.KiloServer
|
||||
import net.sergeych.kiloparsec.LocalInterface
|
||||
import net.sergeych.kiloparsec.*
|
||||
import net.sergeych.mp_logger.*
|
||||
import net.sergeych.mp_tools.globalLaunch
|
||||
import net.sergeych.tools.AtomicCounter
|
||||
import net.sergeych.tools.AtomicValue
|
||||
import kotlin.time.Duration.Companion.seconds
|
||||
|
||||
private val logCounter = AtomicCounter(0)
|
||||
@ -27,16 +34,16 @@ private val logCounter = AtomicCounter(0)
|
||||
class ProtocolException(text: String, cause: Throwable? = null) : RuntimeException(text, cause)
|
||||
|
||||
const val MAX_TCP_BLOCK_SIZE = 16776216
|
||||
val PING_INACTIVITY_TIME = 30.seconds
|
||||
internal val PING_INACTIVITY_TIME = 30.seconds
|
||||
|
||||
/**
|
||||
* Listen for incoming TCP/IP connections on all local interfaces and the specified [port]
|
||||
* anc create flow of [InetTransportDevice] suitable for [KiloClient].
|
||||
*/
|
||||
fun acceptTcpDevice(port: Int): Flow<InetTransportDevice> {
|
||||
fun acceptTcpDevice(port: Int, localInterface: String = "0.0.0.0"): Flow<InetTransportDevice> {
|
||||
val selectorManager = SelectorManager(Dispatchers.IO)
|
||||
val serverSocket = aSocket(selectorManager).tcp().bind("127.0.0.1", port)
|
||||
return flow {
|
||||
val serverSocket = aSocket(selectorManager).tcp().bind(localInterface, port)
|
||||
while (true) {
|
||||
serverSocket.accept().let { sock ->
|
||||
emit(inetTransportDevice(sock, "srv"))
|
||||
@ -74,7 +81,7 @@ private fun inetTransportDevice(
|
||||
val outputBlocks = Channel<UByteArray>(4096)
|
||||
|
||||
val log = LogTag("TCPT${logCounter.incrementAndGet()}:$suffix:$networkAddress")
|
||||
val job = AtomicValue<Job?>(null)
|
||||
val job = AtomicAsyncValue<Job?>(null)
|
||||
|
||||
val sockOutput = sock.openWriteChannel()
|
||||
val sockInput = runCatching { sock.openReadChannel() }.getOrElse {
|
||||
@ -82,16 +89,16 @@ private fun inetTransportDevice(
|
||||
throw IllegalStateException("failed to open read channel")
|
||||
}
|
||||
|
||||
fun stop() {
|
||||
suspend fun stop() {
|
||||
job.mutate {
|
||||
if ( it != null ) {
|
||||
if (it != null) {
|
||||
log.debug { "stopping" }
|
||||
runCatching { inputBlocks.close() }
|
||||
runCatching { outputBlocks.close() }
|
||||
// The problem: on mac platofrms closing the socket does not close its input
|
||||
// and output channels!
|
||||
runCatching { sockInput.cancel() }
|
||||
runCatching { sockOutput.close() }
|
||||
runCatching { sockOutput.flushAndClose() }
|
||||
if (!sock.isClosed)
|
||||
runCatching {
|
||||
log.debug { "closing socket by stop" }
|
||||
@ -108,46 +115,47 @@ private fun inetTransportDevice(
|
||||
}
|
||||
|
||||
var lastActiveAt = Clock.System.now()
|
||||
job.value = globalLaunch {
|
||||
launch {
|
||||
globalLaunch {
|
||||
job.reset(globalLaunch {
|
||||
launch {
|
||||
|
||||
log.debug { "opening read channel" }
|
||||
log.debug { "opening read channel" }
|
||||
|
||||
while (isActive && sock.isActive) {
|
||||
try {
|
||||
val size = AsyncVarint.decodeUnsigned(sockInput).toInt()
|
||||
if (size > MAX_TCP_BLOCK_SIZE) // 16M is a max command block
|
||||
throw ProtocolException("Illegal block size: $size should be < $MAX_TCP_BLOCK_SIZE")
|
||||
val data = ByteArray(size)
|
||||
if (size == 0) {
|
||||
log.debug { "ping received" }
|
||||
lastActiveAt = Clock.System.now()
|
||||
} else {
|
||||
sockInput.readFully(data, 0, size)
|
||||
inputBlocks.send(data.toUByteArray())
|
||||
while (isActive && sock.isActive) {
|
||||
try {
|
||||
val size = AsyncVarint.decodeUnsigned(sockInput).toInt()
|
||||
if (size > MAX_TCP_BLOCK_SIZE) // 16M is a max command block
|
||||
throw ProtocolException("Illegal block size: $size should be < $MAX_TCP_BLOCK_SIZE")
|
||||
val data = ByteArray(size)
|
||||
if (size == 0) {
|
||||
log.debug { "ping received" }
|
||||
lastActiveAt = Clock.System.now()
|
||||
} else {
|
||||
sockInput.readFully(data, 0, size)
|
||||
inputBlocks.send(data.toUByteArray())
|
||||
}
|
||||
} catch (e: ClosedReceiveChannelException) {
|
||||
log.error { "closed receive channel " }
|
||||
stop()
|
||||
break
|
||||
} catch (_: CancellationException) {
|
||||
log.error { "cancellation exception " }
|
||||
break
|
||||
} catch (e: Exception) {
|
||||
log.exception { "unexpected exception in TCP socket read" to e }
|
||||
stop()
|
||||
break
|
||||
}
|
||||
} catch (e: ClosedReceiveChannelException) {
|
||||
log.error { "closed receive channel " }
|
||||
stop()
|
||||
break
|
||||
} catch (_: CancellationException) {
|
||||
log.error { "cancellation exception " }
|
||||
break
|
||||
} catch (e: Exception) {
|
||||
log.exception { "unexpected exception in TCP socket read" to e }
|
||||
stop()
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
})
|
||||
launch {
|
||||
val outAccess = Mutex()
|
||||
var lastSentAt = Clock.System.now()
|
||||
launch {
|
||||
while (isActive && sock.isActive) {
|
||||
delay(500)
|
||||
val activityTime = if(lastSentAt > lastActiveAt) lastSentAt else lastActiveAt
|
||||
val activityTime = if (lastSentAt > lastActiveAt) lastSentAt else lastActiveAt
|
||||
if (Clock.System.now() - activityTime > PING_INACTIVITY_TIME) {
|
||||
log.debug { "pinging for inactivity" }
|
||||
val repeat = outAccess.withLock {
|
@ -0,0 +1,113 @@
|
||||
/*
|
||||
* 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.network.sockets.*
|
||||
import io.ktor.utils.io.core.*
|
||||
import kotlinx.io.readByteArray
|
||||
import net.sergeych.crypto2.toDump
|
||||
import net.sergeych.kiloparsec.adapter.UdpBlock.Companion.CANCEL_BLOCK
|
||||
import net.sergeych.kiloparsec.adapter.UdpBlock.Companion.ESCAPE_BYTE
|
||||
import net.sergeych.kiloparsec.adapter.UdpBlock.Companion.PING_BLOCK
|
||||
import net.sergeych.kiloparsec.adapter.UdpBlock.Companion.decode
|
||||
|
||||
/**
|
||||
* Encoded block for UDP datagram space-savvy. Minimum dara size is two bytes, which is fine
|
||||
* for Kiloparsec blocks.
|
||||
*
|
||||
* First byte is encoded using [ESCAPE_BYTE] depending on the second byte:
|
||||
*
|
||||
* | 0 | 1 | meaning |
|
||||
* |---|---|---------|
|
||||
* | [ESCAPE_BYTE] | [ESCAPE_BYTE] | Data block, dropping first byte |
|
||||
* | [ESCAPE_BYTE] | [PING_BLOCK] | Ping block, reset timers |
|
||||
* | [ESCAPE_BYTE] | [CANCEL_BLOCK] | close connection |
|
||||
* | any other | * | data block, all bytes |
|
||||
*
|
||||
* Use [encoded] and [toDatagram] to create binary or the datagram from a block, and [decode] to restore.
|
||||
*
|
||||
* We do not use serialization to speed up the transport layer.
|
||||
*/
|
||||
sealed class UdpBlock {
|
||||
/**
|
||||
* Block to show that the connection is closed and should also be closed on the other side
|
||||
*/
|
||||
object Cancel : UdpBlock()
|
||||
|
||||
/**
|
||||
* Parties show pings if there is no activity to keep it alive, detect connection loss and in some
|
||||
* cases revive NAT/Proxy state in routers.
|
||||
*/
|
||||
object Ping : UdpBlock()
|
||||
|
||||
/**
|
||||
* Parsec data block. Could not be smaller than two bytes.
|
||||
*/
|
||||
class Data(val data: UByteArray) : UdpBlock() {
|
||||
override fun toString(): String {
|
||||
return "UDP Data (${data.size}):\n${data.toDump()}"
|
||||
}
|
||||
init {
|
||||
if( data.size < 2) throw IllegalArgumentException("data must be at least 2 bytes")
|
||||
}
|
||||
}
|
||||
|
||||
val encoded: UByteArray by lazy {
|
||||
when(this) {
|
||||
is Data -> {
|
||||
// Do we need escaping?
|
||||
if( data[0] == ESCAPE_BYTE )
|
||||
escapeAsArray + data
|
||||
else
|
||||
data
|
||||
}
|
||||
is Cancel -> cancelAsArray
|
||||
is Ping -> pingAsArray
|
||||
}
|
||||
}
|
||||
|
||||
fun toDatagram(address: SocketAddress): Datagram {
|
||||
val encoded = encoded.toByteArray()
|
||||
return Datagram(ByteReadPacket(encoded, 0, encoded.size), address)
|
||||
}
|
||||
|
||||
companion object {
|
||||
val ESCAPE_BYTE = 255.toUByte()
|
||||
val PING_BLOCK = 0.toUByte()
|
||||
val CANCEL_BLOCK = 1.toUByte()
|
||||
|
||||
private val escapeAsArray = ubyteArrayOf(ESCAPE_BYTE)
|
||||
private val pingAsArray = ubyteArrayOf(ESCAPE_BYTE, PING_BLOCK)
|
||||
private val cancelAsArray = ubyteArrayOf(ESCAPE_BYTE, CANCEL_BLOCK)
|
||||
|
||||
fun decode(data: UByteArray): UdpBlock {
|
||||
if (data.size < 2)
|
||||
throw UdpTransportException("block too short: ${data.size}")
|
||||
return if( data[0] != ESCAPE_BYTE )
|
||||
// plain data
|
||||
Data(data)
|
||||
else {
|
||||
when(val b2 = data[1]) {
|
||||
ESCAPE_BYTE -> {
|
||||
// Escaped first byte, then plain data
|
||||
Data(data.sliceArray(1 ..< data.size))
|
||||
}
|
||||
PING_BLOCK -> Ping
|
||||
CANCEL_BLOCK -> Cancel
|
||||
else -> throw UdpTransportException("invalid block type: $b2")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun decode(datagram: Datagram) =
|
||||
decode(datagram.packet.readByteArray().toUByteArray())
|
||||
}
|
||||
}
|
@ -0,0 +1,28 @@
|
||||
/*
|
||||
* 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.network.sockets.*
|
||||
|
||||
/**
|
||||
* The interface for common UDP connector shared by UDP components
|
||||
*/
|
||||
internal interface UdpConnector {
|
||||
/**
|
||||
* Called when client connection is done so the provider could free resources
|
||||
*/
|
||||
suspend fun disconnectClient(address: SocketAddress)
|
||||
|
||||
/**
|
||||
* Send a block from a proper UDP socket
|
||||
*/
|
||||
suspend fun sendBlock(block: UdpBlock, toAddress: SocketAddress)
|
||||
}
|
@ -0,0 +1,158 @@
|
||||
/*
|
||||
* 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.network.selector.*
|
||||
import io.ktor.network.sockets.*
|
||||
import io.ktor.utils.io.CancellationException
|
||||
import kotlinx.coroutines.*
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import net.sergeych.kiloparsec.KiloClient
|
||||
import net.sergeych.kiloparsec.KiloServer
|
||||
import net.sergeych.kiloparsec.RemoteInterface
|
||||
import net.sergeych.mp_tools.globalLaunch
|
||||
import net.sergeych.tools.AtomicCounter
|
||||
import kotlin.time.Duration
|
||||
import kotlin.time.Duration.Companion.minutes
|
||||
|
||||
internal val udpCounter = AtomicCounter(0)
|
||||
|
||||
class UdpTransportException(override val message: String) : RemoteInterface.InvalidDataException(message)
|
||||
|
||||
/**
|
||||
* Listen for incoming UDP connections and provide transport flow for it. See also [UdpServer.transportFlow]
|
||||
* for another way to create a server. Use it with [KiloServer]:
|
||||
* ```kotlin
|
||||
* // Whatever server session data we might need:
|
||||
* data class Session(
|
||||
* var data: String,
|
||||
* )
|
||||
*
|
||||
* // declare some commands (normally in a shared module):
|
||||
* val cmdSave by command<String, Unit>()
|
||||
* val cmdLoad by command<Unit, String>()
|
||||
* val cmdDrop by command<Unit, Unit>()
|
||||
* val cmdException by command<Unit, Unit>()
|
||||
*
|
||||
* // Interface using the session above, can be shared between many
|
||||
* // server types and instances (different ports and protocols):
|
||||
* val cli = KiloInterface<Session>().apply {
|
||||
* onConnected { session.data = "start" }
|
||||
* on(cmdSave) { session.data = it }
|
||||
* on(cmdLoad) {
|
||||
* session.data
|
||||
* }
|
||||
* on(cmdException) {
|
||||
* throw TestException()
|
||||
* }
|
||||
* on(cmdDrop) {
|
||||
* throw LocalInterface.BreakConnectionException()
|
||||
* }
|
||||
* }
|
||||
* // Now create a server to accept incoming UDPs on our port:
|
||||
* val server = KiloServer(cli, acceptUdpDevice(port)) {
|
||||
* // This initializes new session for each incoming command
|
||||
* Session("unknown")
|
||||
* }
|
||||
* ```
|
||||
*
|
||||
* See [connectUdpDevice] for the client sample.
|
||||
*
|
||||
* When it is necessary to stop listening to some port, use [UdpServer] instead.
|
||||
*
|
||||
* @param port port to listen
|
||||
* @param localInterface string form local interface to listen
|
||||
* @param maxInactivityTimeout maximum silence time after which the connection is supposed to be lost.
|
||||
* the module automatically issues pings on inactivity when there is no data often enough
|
||||
* to maintain the connection open.
|
||||
*/
|
||||
fun acceptUdpDevice(
|
||||
port: Int,
|
||||
localInterface: String = "0.0.0.0",
|
||||
maxInactivityTimeout: Duration = 2.minutes,
|
||||
): Flow<InetTransportDevice> =
|
||||
UdpServer(port, localInterface,maxInactivityTimeout).transportFlow
|
||||
|
||||
/**
|
||||
* Connect to UDP server (see [acceptUdpDevice] or [UdpServer]) and return a [InetTransportDevice] for it. It
|
||||
* should be used with [KiloClient] as connection provider:
|
||||
* ```kotlin
|
||||
* val client = KiloClient<Unit>() {
|
||||
* connect { connectUdpDevice("localhost:$port") }
|
||||
* }
|
||||
* // now we can execute remote commands:
|
||||
* assertEquals("start", client.call(cmdLoad))
|
||||
* ```
|
||||
*
|
||||
* @param hostPort "host:port" string address of the remote UDP port to connect to
|
||||
* @param maxInactivityTimeout maximum silence time after which the connection is supposed to be lost.
|
||||
* the module automatically issues pings on inactivity when there is no data often enough
|
||||
* to maintain the connection open.
|
||||
*/
|
||||
suspend fun connectUdpDevice(
|
||||
hostPort: String,
|
||||
maxInactivityTimeout: Duration = 2.minutes,
|
||||
) = connectUdpDevice(hostPort.toNetworkAddress(), maxInactivityTimeout)
|
||||
|
||||
/**
|
||||
* Connect to UDP server (see [acceptUdpDevice]) and return a [InetTransportDevice] for it. It
|
||||
* should be used with [KiloClient] as connection provider:
|
||||
* ```kotlin
|
||||
* val client = KiloClient<Unit>() {
|
||||
* connect { connectUdpDevice("localhost:$port") }
|
||||
* }
|
||||
* // now we can execute remote commands:
|
||||
* assertEquals("start", client.call(cmdLoad))
|
||||
* ```
|
||||
* @param addr the network address where to connect to
|
||||
* @param maxInactivityTimeout maximum silence time after which the connection is supposed to be lost.
|
||||
* the module automatically issues pings on inactivity when there is no data often enough
|
||||
* to maintain the connection open.
|
||||
*/
|
||||
suspend fun connectUdpDevice(
|
||||
addr: NetworkAddress,
|
||||
maxInactivityTimeout: Duration = 2.minutes,
|
||||
): InetTransportDevice {
|
||||
val selectorManager = SelectorManager(Dispatchers.IO)
|
||||
val remoteAddress = InetSocketAddress(addr.host, addr.port)
|
||||
|
||||
val done = CompletableDeferred<Unit>()
|
||||
|
||||
val socket = aSocket(selectorManager).udp().connect(remoteAddress)
|
||||
val transport = UdpSocketTransport(object : UdpConnector {
|
||||
override suspend fun sendBlock(block: UdpBlock, toAddress: SocketAddress) {
|
||||
socket.send(block.toDatagram(remoteAddress))
|
||||
}
|
||||
|
||||
override suspend fun disconnectClient(address: SocketAddress) {
|
||||
done.complete(Unit)
|
||||
}
|
||||
|
||||
}, remoteAddress, false, maxInactivityTimeout)
|
||||
|
||||
globalLaunch {
|
||||
launch {
|
||||
while (isActive) {
|
||||
try {
|
||||
transport.processIncoming(UdpBlock.decode(socket.receive()))
|
||||
} catch (_: CancellationException) {
|
||||
break
|
||||
} catch (e: Exception) {
|
||||
transport.close()
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
done.await()
|
||||
}
|
||||
|
||||
return transport.transportDevice
|
||||
}
|
@ -0,0 +1,140 @@
|
||||
/*
|
||||
* 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.network.selector.*
|
||||
import io.ktor.network.sockets.*
|
||||
import io.ktor.utils.io.*
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.IO
|
||||
import kotlinx.coroutines.channels.ClosedReceiveChannelException
|
||||
import kotlinx.coroutines.flow.flow
|
||||
import kotlinx.coroutines.sync.Mutex
|
||||
import kotlinx.coroutines.sync.withLock
|
||||
import net.sergeych.kiloparsec.KiloServer
|
||||
import net.sergeych.mp_logger.LogTag
|
||||
import net.sergeych.mp_logger.Loggable
|
||||
import net.sergeych.mp_logger.debug
|
||||
import net.sergeych.mp_logger.exception
|
||||
import net.sergeych.mp_tools.globalDefer
|
||||
import kotlin.time.Duration
|
||||
import kotlin.time.Duration.Companion.minutes
|
||||
|
||||
/**
|
||||
* UDP server for kiloparsec. Unlike [acceptUdpDevice], it allow stopping listening
|
||||
* to the port when need with [close]. Use [transportFlow] with [KiloServer], here is the
|
||||
* basic sample:
|
||||
*
|
||||
* ```kotlin
|
||||
* val uServer = UdpServer(port)
|
||||
* KiloServer(cli, uServer.transportFlow()) {
|
||||
* Session("unknown")
|
||||
* }
|
||||
*
|
||||
* // server is now active and accepts connections
|
||||
* // ...
|
||||
*
|
||||
* // close and stop listening to the port:
|
||||
* uServer.close()
|
||||
* ```
|
||||
*
|
||||
* See [acceptUdpDevice] for more information.
|
||||
*
|
||||
* @param port port to listen to
|
||||
* @param localInterface string form of local interface to listen to
|
||||
* @param maxInactivityTimeout maximum silence time after which the connection is supposed to be lost.
|
||||
* the module automatically issues pings on inactivity when there is no data often enough
|
||||
* to maintain the connection open.
|
||||
|
||||
*/
|
||||
class UdpServer(val port: Int, localInterface: String = "0.0.0.0", maxInactivityTimeout: Duration = 2.minutes) :
|
||||
Loggable by LogTag("UDPS${udpCounter.incrementAndGet()}"), UdpConnector {
|
||||
|
||||
private val sessions = mutableMapOf<SocketAddress, UdpSocketTransport>()
|
||||
private val access = Mutex()
|
||||
|
||||
private val selectorManager = SelectorManager(Dispatchers.IO)
|
||||
private val serverSocket = globalDefer {
|
||||
aSocket(selectorManager).udp().bind(InetSocketAddress(localInterface, port))
|
||||
}
|
||||
|
||||
override suspend fun disconnectClient(address: SocketAddress) {
|
||||
access.withLock { sessions.remove(address) }
|
||||
}
|
||||
|
||||
/**
|
||||
* a transport flow of [InetTransportDevice] suitable to be used with [KiloServer], see [UdpServer] for the
|
||||
* usage sample.
|
||||
*/
|
||||
val transportFlow by lazy {
|
||||
flow {
|
||||
while (true) {
|
||||
try {
|
||||
val datagram = serverSocket.await().receive()
|
||||
val block = UdpBlock.decode(datagram)
|
||||
val remoteAddress = datagram.address
|
||||
|
||||
access.withLock {
|
||||
if (block == UdpBlock.Cancel) {
|
||||
// if the cancel comes to already closed transport, do nothing
|
||||
sessions.remove(remoteAddress)?.processIncoming(block)
|
||||
} else {
|
||||
sessions.getOrPut(remoteAddress) {
|
||||
// new connection: create transport
|
||||
debug { "Creating new connection to $remoteAddress" }
|
||||
UdpSocketTransport(this@UdpServer, remoteAddress, true, maxInactivityTimeout)
|
||||
// and emit it:
|
||||
.also { emit(it.transportDevice) }
|
||||
}.processIncoming(block)
|
||||
}
|
||||
}
|
||||
} catch (_: CancellationException) {
|
||||
break
|
||||
} catch (_: ClosedReceiveChannelException) {
|
||||
break
|
||||
} catch (e: Exception) {
|
||||
exception { "unexpected exception in incoming datagram processing" to e }
|
||||
close()
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun sendBlock(block: UdpBlock, toAddress: SocketAddress) {
|
||||
serverSocket.await().send(block.toDatagram(toAddress))
|
||||
}
|
||||
|
||||
suspend fun isClosed(): Boolean = serverSocket.await().isClosed
|
||||
|
||||
/**
|
||||
* Close the UDP server. Calling it will cause:
|
||||
*
|
||||
* - Closing nound UDP socket on [port]
|
||||
* - Closing all pending connections
|
||||
* - cancelling the [transportFlow], which will cause Kiloparsec server to also stop
|
||||
*
|
||||
* Call suspends until socket and all sessions are closed. Later calls do nothing.
|
||||
*/
|
||||
suspend fun close() {
|
||||
access.withLock {
|
||||
if (!isClosed()) {
|
||||
runCatching { serverSocket.await().close() }
|
||||
}
|
||||
}
|
||||
while (sessions.isNotEmpty()) {
|
||||
runCatching {
|
||||
access.withLock { sessions.values.firstOrNull() }
|
||||
?.close()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,189 @@
|
||||
/*
|
||||
* 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.network.sockets.*
|
||||
import io.ktor.utils.io.*
|
||||
import kotlinx.coroutines.channels.BufferOverflow
|
||||
import kotlinx.coroutines.channels.Channel
|
||||
import kotlinx.coroutines.channels.ClosedReceiveChannelException
|
||||
import kotlinx.coroutines.channels.ClosedSendChannelException
|
||||
import kotlinx.coroutines.coroutineScope
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.datetime.Clock
|
||||
import net.sergeych.kiloparsec.SyncValue
|
||||
import net.sergeych.mp_logger.Log
|
||||
import net.sergeych.mp_logger.Loggable
|
||||
import net.sergeych.mp_logger.debug
|
||||
import net.sergeych.mp_logger.exception
|
||||
import net.sergeych.mp_tools.globalLaunch
|
||||
import kotlin.time.Duration
|
||||
|
||||
/**
|
||||
* This is a common part of UDP transport shared between client and server connections.
|
||||
* It should not be used directly but bu the [UdpServer], [acceptUdpDevice] and [connectUdpDevice]
|
||||
* respectively.
|
||||
*/
|
||||
internal class UdpSocketTransport(
|
||||
private val server: UdpConnector,
|
||||
val socketAddress: SocketAddress,
|
||||
val isServer: Boolean,
|
||||
val maxInactivityTimeout: Duration
|
||||
) :
|
||||
Loggable {
|
||||
|
||||
// IMPORTANT! Log stuff must be the first (or you shot your leg):
|
||||
val address = (socketAddress as InetSocketAddress).let { NetworkAddress(it.hostname, it.port) }
|
||||
override var logTag: String = "UDPT:$address${if (isServer) ":server" else ":client"}"
|
||||
override var logLevel: Log.Level? = Log.Level.DEBUG
|
||||
|
||||
// Pinger params: keep them first!
|
||||
private var lastSendAt = Clock.System.now()
|
||||
private var lastReceived = Clock.System.now()
|
||||
private val pingTimeout = maxInactivityTimeout / 3
|
||||
private val pingSleep = pingTimeout / 3
|
||||
private val pingMinTimeout = pingTimeout * 2 / 3
|
||||
|
||||
val inputDataBlocks = Channel<UByteArray>(256, onBufferOverflow = BufferOverflow.DROP_OLDEST)
|
||||
val outputDataBlocks = Channel<UByteArray>(256, onBufferOverflow = BufferOverflow.DROP_OLDEST)
|
||||
|
||||
val inputUdpBlocks = Channel<UdpBlock>(256, onBufferOverflow = BufferOverflow.DROP_OLDEST)
|
||||
|
||||
private val job = globalLaunch {
|
||||
coroutineScope {
|
||||
launch { convertOutput() }
|
||||
launch { convertInput() }
|
||||
launch { pinger() }
|
||||
}
|
||||
}
|
||||
|
||||
init {
|
||||
// This is iverly important: it requires that
|
||||
// all members are initialized before use. Otherwise kotlin
|
||||
// may execute class members pr
|
||||
debug { "initialization done" }
|
||||
}
|
||||
|
||||
|
||||
val transportDevice: InetTransportDevice by lazy {
|
||||
InetTransportDevice(inputDataBlocks, outputDataBlocks, address, { close() }, {})
|
||||
}
|
||||
|
||||
private val closedFlag = SyncValue(false)
|
||||
|
||||
val isClosed: Boolean = closedFlag.value
|
||||
|
||||
suspend fun close() {
|
||||
closedFlag.mutate {
|
||||
if (!it) {
|
||||
runCatching { server.sendBlock(UdpBlock.Cancel, socketAddress) }
|
||||
server.disconnectClient(socketAddress)
|
||||
runCatching { inputDataBlocks.close() }
|
||||
runCatching { outputDataBlocks.close() }
|
||||
job.cancel()
|
||||
}
|
||||
true
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private suspend fun send(block: UdpBlock) {
|
||||
server.sendBlock(block, socketAddress)
|
||||
lastSendAt = Clock.System.now()
|
||||
}
|
||||
|
||||
/**
|
||||
* Process the block recoded by the server. Note that it should properly process all
|
||||
* block types, e.g. close on [UdpBlock.Cancel], etc. Server will not close us!
|
||||
*
|
||||
* __Important: it should not block, instead, server expects it to return ASAP__, so it
|
||||
* executes in a local coroutine context.
|
||||
*
|
||||
* Also it should not throw exceptions.
|
||||
*/
|
||||
fun processIncoming(block: UdpBlock) {
|
||||
inputUdpBlocks.trySend(block)
|
||||
}
|
||||
|
||||
suspend fun convertInput() {
|
||||
while(!isClosed) {
|
||||
when (val block = inputUdpBlocks.receiveCatching().getOrNull()) {
|
||||
|
||||
null -> break
|
||||
|
||||
is UdpBlock.Cancel -> globalLaunch {
|
||||
debug { "received cancel block, requesting close" }
|
||||
kotlin.runCatching { close() }
|
||||
}
|
||||
|
||||
is UdpBlock.Data -> {
|
||||
// input does not block, it uses DROP_OLDEST policy
|
||||
lastReceived = Clock.System.now()
|
||||
val result = kotlin.runCatching { inputDataBlocks.send(block.data) }
|
||||
when (val e = result.exceptionOrNull()) {
|
||||
null -> {}
|
||||
is ClosedSendChannelException -> {
|
||||
debug { "received close channel" }
|
||||
close()
|
||||
}
|
||||
|
||||
is CancellationException -> {}
|
||||
else -> {
|
||||
exception { "unexpected exception" to e }
|
||||
close()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
UdpBlock.Ping -> {
|
||||
lastReceived = Clock.System.now()
|
||||
if (lastSendAt - lastReceived > pingMinTimeout) send(UdpBlock.Ping)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun pinger() {
|
||||
while (!isClosed) {
|
||||
delay(pingSleep)
|
||||
val inactivity = Clock.System.now() - lastSendAt
|
||||
if( inactivity > maxInactivityTimeout) {
|
||||
debug { "inactivity timout: closing the connection" }
|
||||
close()
|
||||
}
|
||||
if (inactivity >= pingTimeout) {
|
||||
debug { "pinger sends a ping on timeout" }
|
||||
send(UdpBlock.Ping)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun convertOutput() {
|
||||
while (!isClosed) {
|
||||
try {
|
||||
server.sendBlock(UdpBlock.Data(outputDataBlocks.receive()), socketAddress)
|
||||
} catch (e: CancellationException) {
|
||||
// this is ok
|
||||
break
|
||||
} catch (e: ClosedReceiveChannelException) {
|
||||
debug { "input channel is closed, closing" }
|
||||
close()
|
||||
break
|
||||
} catch (e: Exception) {
|
||||
exception { "unexpected exception in convertOutput" to e }
|
||||
close()
|
||||
break
|
||||
}
|
||||
}
|
||||
debug { "exiting convertOutput" }
|
||||
}
|
||||
}
|
@ -1,69 +0,0 @@
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import net.sergeych.crypto2.initCrypto
|
||||
import net.sergeych.kiloparsec.*
|
||||
import net.sergeych.kiloparsec.adapter.acceptTcpDevice
|
||||
import net.sergeych.kiloparsec.adapter.connectTcpDevice
|
||||
import kotlin.random.Random
|
||||
import kotlin.test.Test
|
||||
import kotlin.test.assertEquals
|
||||
import kotlin.test.assertIs
|
||||
|
||||
class TcpTest {
|
||||
class TestException : Exception("test1")
|
||||
|
||||
@Test
|
||||
fun tcpTest() = runTest {
|
||||
initCrypto()
|
||||
// Log.connectConsole(Log.Level.DEBUG)
|
||||
data class Session(
|
||||
var data: String,
|
||||
)
|
||||
|
||||
val port = 27170 + Random.nextInt(1, 200)
|
||||
|
||||
val cmdSave by command<String, Unit>()
|
||||
val cmdLoad by command<Unit, String>()
|
||||
val cmdDrop by command<Unit, Unit>()
|
||||
val cmdException by command<Unit, Unit>()
|
||||
|
||||
val cli = KiloInterface<Session>().apply {
|
||||
registerError { TestException() }
|
||||
onConnected { session.data = "start" }
|
||||
on(cmdSave) { session.data = it }
|
||||
on(cmdLoad) {
|
||||
session.data
|
||||
}
|
||||
on(cmdException) {
|
||||
throw TestException()
|
||||
}
|
||||
on(cmdDrop) {
|
||||
throw LocalInterface.BreakConnectionException()
|
||||
}
|
||||
}
|
||||
val server = KiloServer(cli, acceptTcpDevice(port)) {
|
||||
Session("unknown")
|
||||
}
|
||||
|
||||
val client = KiloClient<Unit>() {
|
||||
addErrors(cli)
|
||||
// TODO: add register error variant
|
||||
connect { connectTcpDevice("localhost:$port") }
|
||||
}
|
||||
|
||||
assertEquals("start", client.call(cmdLoad))
|
||||
|
||||
client.call(cmdSave, "foobar")
|
||||
assertEquals("foobar", client.call(cmdLoad))
|
||||
|
||||
val res = kotlin.runCatching { client.call(cmdException) }
|
||||
assertIs<TestException>(res.exceptionOrNull())
|
||||
assertEquals("foobar", client.call(cmdLoad))
|
||||
|
||||
assertThrows<RemoteInterface.ClosedException> { client.call(cmdDrop) }
|
||||
|
||||
// reconnect?
|
||||
assertEquals("start", client.call(cmdLoad))
|
||||
|
||||
server.close()
|
||||
}
|
||||
}
|
@ -0,0 +1,215 @@
|
||||
/*
|
||||
* 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 assertThrows
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import net.sergeych.crypto2.initCrypto
|
||||
import net.sergeych.kiloparsec.*
|
||||
import net.sergeych.mp_logger.Log
|
||||
import kotlin.random.Random
|
||||
import kotlin.test.Test
|
||||
import kotlin.test.assertEquals
|
||||
import kotlin.test.assertIs
|
||||
|
||||
class InternetTest {
|
||||
class TestException : Exception("test1")
|
||||
|
||||
@Test
|
||||
fun tcpTest() = runTest {
|
||||
initCrypto()
|
||||
Log.connectConsole(Log.Level.DEBUG)
|
||||
data class Session(
|
||||
var data: String,
|
||||
)
|
||||
|
||||
val port = 27170 + Random.nextInt(1, 200)
|
||||
|
||||
val cmdSave by command<String, Unit>()
|
||||
val cmdLoad by command<Unit, String>()
|
||||
val cmdDrop by command<Unit, Unit>()
|
||||
val cmdPing by command<String, String>()
|
||||
val cmdException by command<Unit, Unit>()
|
||||
val cmdCallClient by command<String, String>()
|
||||
|
||||
val serverInterface = KiloInterface<Session>().apply {
|
||||
registerError { TestException() }
|
||||
onConnected { session.data = "start" }
|
||||
on(cmdSave) { session.data = it }
|
||||
on(cmdLoad) {
|
||||
session.data
|
||||
}
|
||||
on(cmdException) {
|
||||
throw TestException()
|
||||
}
|
||||
on(cmdDrop) {
|
||||
throw LocalInterface.BreakConnectionException()
|
||||
}
|
||||
on(cmdCallClient) {
|
||||
remote.call(cmdPing, it)
|
||||
}
|
||||
}
|
||||
val server = KiloServer(serverInterface, acceptTcpDevice(port)) {
|
||||
Session("unknown")
|
||||
}
|
||||
|
||||
data class LocalSession(val localFoo: String)
|
||||
|
||||
val client = KiloClient {
|
||||
addErrors(serverInterface)
|
||||
session { LocalSession("unknown") }
|
||||
// TODO: add register error variant
|
||||
connect { connectTcpDevice("localhost:$port") }
|
||||
local {
|
||||
on(cmdPing) {
|
||||
"pong! $it"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
assertEquals("start", client.call(cmdLoad))
|
||||
|
||||
client.call(cmdSave, "foobar")
|
||||
assertEquals("foobar", client.call(cmdLoad))
|
||||
|
||||
val res = kotlin.runCatching { client.call(cmdException) }
|
||||
assertIs<TestException>(res.exceptionOrNull())
|
||||
assertEquals("foobar", client.call(cmdLoad))
|
||||
|
||||
assertThrows<RemoteInterface.ClosedException> { client.call(cmdDrop) }
|
||||
|
||||
// reconnect?
|
||||
assertEquals("start", client.call(cmdLoad))
|
||||
assertEquals("pong! 42", client.call(cmdCallClient, "42"))
|
||||
server.close()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun udpTest() = runTest {
|
||||
initCrypto()
|
||||
Log.connectConsole(Log.Level.DEBUG)
|
||||
data class Session(
|
||||
var data: String,
|
||||
)
|
||||
|
||||
val port = 27170 + Random.nextInt(1, 200)
|
||||
|
||||
val cmdSave by command<String, Unit>()
|
||||
val cmdLoad by command<Unit, String>()
|
||||
val cmdDrop by command<Unit, Unit>()
|
||||
val cmdException by command<Unit, Unit>()
|
||||
|
||||
val cli = KiloInterface<Session>().apply {
|
||||
registerError { TestException() }
|
||||
onConnected { session.data = "start" }
|
||||
on(cmdSave) { session.data = it }
|
||||
on(cmdLoad) {
|
||||
session.data
|
||||
}
|
||||
on(cmdException) {
|
||||
throw TestException()
|
||||
}
|
||||
on(cmdDrop) {
|
||||
throw LocalInterface.BreakConnectionException()
|
||||
}
|
||||
}
|
||||
val server = KiloServer(cli, acceptUdpDevice(port)) {
|
||||
Session("unknown")
|
||||
}
|
||||
|
||||
val client = KiloClient<Unit>() {
|
||||
addErrors(cli)
|
||||
connect { connectUdpDevice("127.0.0.1:$port") }
|
||||
}
|
||||
|
||||
assertEquals("start", client.call(cmdLoad))
|
||||
|
||||
client.call(cmdSave, "foobar")
|
||||
assertEquals("foobar", client.call(cmdLoad))
|
||||
|
||||
val res = kotlin.runCatching { client.call(cmdException) }
|
||||
assertIs<TestException>(res.exceptionOrNull())
|
||||
assertEquals("foobar", client.call(cmdLoad))
|
||||
|
||||
assertThrows<RemoteInterface.ClosedException> { client.call(cmdDrop) }
|
||||
|
||||
// reconnect?
|
||||
assertEquals("start", client.call(cmdLoad))
|
||||
|
||||
server.close()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun udpServerTest() = runTest {
|
||||
initCrypto()
|
||||
Log.connectConsole(Log.Level.DEBUG)
|
||||
data class Session(
|
||||
var data: String,
|
||||
)
|
||||
|
||||
val port = 27170 + Random.nextInt(1, 900)
|
||||
|
||||
val cmdSave by command<String, Unit>()
|
||||
val cmdLoad by command<Unit, String>()
|
||||
val cmdDrop by command<Unit, Unit>()
|
||||
val cmdException by command<Unit, Unit>()
|
||||
|
||||
val cli = KiloInterface<Session>().apply {
|
||||
registerError { TestException() }
|
||||
onConnected { session.data = "start" }
|
||||
on(cmdSave) { session.data = it }
|
||||
on(cmdLoad) {
|
||||
session.data
|
||||
}
|
||||
on(cmdException) {
|
||||
throw TestException()
|
||||
}
|
||||
on(cmdDrop) {
|
||||
throw LocalInterface.BreakConnectionException()
|
||||
}
|
||||
}
|
||||
val uServer = UdpServer(port)
|
||||
|
||||
KiloServer(cli, uServer.transportFlow) {
|
||||
Session("unknown")
|
||||
}
|
||||
|
||||
// second server
|
||||
KiloServer(cli,acceptUdpDevice(uServer.port+1)) { Session("unknown2") }
|
||||
|
||||
|
||||
val client = KiloClient<Unit>() {
|
||||
addErrors(cli)
|
||||
connect { connectUdpDevice("127.0.0.1:$port") }
|
||||
}
|
||||
val client2 = KiloClient<Unit>() { connect { connectUdpDevice("127.0.0.1:${port+1}") } }
|
||||
|
||||
assertEquals("start", client.call(cmdLoad))
|
||||
assertEquals("start", client2.call(cmdLoad))
|
||||
|
||||
client.call(cmdSave, "foobar")
|
||||
client2.call(cmdSave, "buzz")
|
||||
assertEquals("foobar", client.call(cmdLoad))
|
||||
assertEquals("buzz", client2.call(cmdLoad))
|
||||
|
||||
val res = kotlin.runCatching { client.call(cmdException) }
|
||||
assertIs<TestException>(res.exceptionOrNull())
|
||||
assertEquals("foobar", client.call(cmdLoad))
|
||||
|
||||
assertThrows<RemoteInterface.ClosedException> { client.call(cmdDrop) }
|
||||
|
||||
// reconnect?
|
||||
assertEquals("start", client.call(cmdLoad))
|
||||
|
||||
uServer.close()
|
||||
// server.close()
|
||||
}
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user