Compare commits
13 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| fe4cb2ca21 | |||
| 6dc246e68c | |||
| bc5b5b3b3a | |||
| bc46e11158 | |||
| 40f4352a31 | |||
| cb696f5c0f | |||
| fe96ac69d7 | |||
| 212f82dedd | |||
| 59b7310385 | |||
| 8837e49248 | |||
| 7e1f7ec4aa | |||
| 146878629e | |||
| d7fb26c03b |
1
.gitignore
vendored
1
.gitignore
vendored
@ -45,3 +45,4 @@ out/
|
|||||||
.kotlin
|
.kotlin
|
||||||
/.idea/workspace.xml
|
/.idea/workspace.xml
|
||||||
/.gigaide/gigaide.properties
|
/.gigaide/gigaide.properties
|
||||||
|
local.properties
|
||||||
|
|||||||
8
.idea/artifacts/kiloparsec_js_0_4_1.xml
generated
Normal file
8
.idea/artifacts/kiloparsec_js_0_4_1.xml
generated
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
<component name="ArtifactManager">
|
||||||
|
<artifact type="jar" name="kiloparsec-js-0.4.1">
|
||||||
|
<output-path>$PROJECT_DIR$/build/libs</output-path>
|
||||||
|
<root id="archive" name="kiloparsec-js-0.4.1.jar">
|
||||||
|
<element id="module-output" name="kiloparsec.jsMain" />
|
||||||
|
</root>
|
||||||
|
</artifact>
|
||||||
|
</component>
|
||||||
8
.idea/artifacts/kiloparsec_js_0_4_3.xml
generated
Normal file
8
.idea/artifacts/kiloparsec_js_0_4_3.xml
generated
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
<component name="ArtifactManager">
|
||||||
|
<artifact type="jar" name="kiloparsec-js-0.4.3">
|
||||||
|
<output-path>$PROJECT_DIR$/build/libs</output-path>
|
||||||
|
<root id="archive" name="kiloparsec-js-0.4.3.jar">
|
||||||
|
<element id="module-output" name="kiloparsec.jsMain" />
|
||||||
|
</root>
|
||||||
|
</artifact>
|
||||||
|
</component>
|
||||||
8
.idea/artifacts/kiloparsec_jvm_0_4_1.xml
generated
Normal file
8
.idea/artifacts/kiloparsec_jvm_0_4_1.xml
generated
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
<component name="ArtifactManager">
|
||||||
|
<artifact type="jar" name="kiloparsec-jvm-0.4.1">
|
||||||
|
<output-path>$PROJECT_DIR$/build/libs</output-path>
|
||||||
|
<root id="archive" name="kiloparsec-jvm-0.4.1.jar">
|
||||||
|
<element id="module-output" name="kiloparsec.jvmMain" />
|
||||||
|
</root>
|
||||||
|
</artifact>
|
||||||
|
</component>
|
||||||
8
.idea/artifacts/kiloparsec_jvm_0_4_3.xml
generated
Normal file
8
.idea/artifacts/kiloparsec_jvm_0_4_3.xml
generated
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
<component name="ArtifactManager">
|
||||||
|
<artifact type="jar" name="kiloparsec-jvm-0.4.3">
|
||||||
|
<output-path>$PROJECT_DIR$/build/libs</output-path>
|
||||||
|
<root id="archive" name="kiloparsec-jvm-0.4.3.jar">
|
||||||
|
<element id="module-output" name="kiloparsec.jvmMain" />
|
||||||
|
</root>
|
||||||
|
</artifact>
|
||||||
|
</component>
|
||||||
24
.idea/codeStyles/Project.xml
generated
24
.idea/codeStyles/Project.xml
generated
@ -1,5 +1,29 @@
|
|||||||
<component name="ProjectCodeStyleConfiguration">
|
<component name="ProjectCodeStyleConfiguration">
|
||||||
<code_scheme name="Project" version="173">
|
<code_scheme name="Project" version="173">
|
||||||
|
<DBN-PSQL>
|
||||||
|
<case-options enabled="true">
|
||||||
|
<option name="KEYWORD_CASE" value="lower" />
|
||||||
|
<option name="FUNCTION_CASE" value="lower" />
|
||||||
|
<option name="PARAMETER_CASE" value="lower" />
|
||||||
|
<option name="DATATYPE_CASE" value="lower" />
|
||||||
|
<option name="OBJECT_CASE" value="preserve" />
|
||||||
|
</case-options>
|
||||||
|
<formatting-settings enabled="false" />
|
||||||
|
</DBN-PSQL>
|
||||||
|
<DBN-SQL>
|
||||||
|
<case-options enabled="true">
|
||||||
|
<option name="KEYWORD_CASE" value="lower" />
|
||||||
|
<option name="FUNCTION_CASE" value="lower" />
|
||||||
|
<option name="PARAMETER_CASE" value="lower" />
|
||||||
|
<option name="DATATYPE_CASE" value="lower" />
|
||||||
|
<option name="OBJECT_CASE" value="preserve" />
|
||||||
|
</case-options>
|
||||||
|
<formatting-settings enabled="false">
|
||||||
|
<option name="STATEMENT_SPACING" value="one_line" />
|
||||||
|
<option name="CLAUSE_CHOP_DOWN" value="chop_down_if_statement_long" />
|
||||||
|
<option name="ITERATION_ELEMENTS_WRAPPING" value="chop_down_if_not_single" />
|
||||||
|
</formatting-settings>
|
||||||
|
</DBN-SQL>
|
||||||
<ScalaCodeStyleSettings>
|
<ScalaCodeStyleSettings>
|
||||||
<option name="MULTILINE_STRING_CLOSING_QUOTES_ON_NEW_LINE" value="true" />
|
<option name="MULTILINE_STRING_CLOSING_QUOTES_ON_NEW_LINE" value="true" />
|
||||||
</ScalaCodeStyleSettings>
|
</ScalaCodeStyleSettings>
|
||||||
|
|||||||
120
README.md
120
README.md
@ -1,6 +1,6 @@
|
|||||||
# Kiloparsec
|
# Kiloparsec
|
||||||
|
|
||||||
__Recommended version is `0.4.1`: to keep the code compatible with current and further versions we
|
__Recommended version is `0.6.8`: to keep the code compatible with current and further versions we
|
||||||
ask to upgrade to `0.4.2` at least.__ Starting from this version some package names are changed for
|
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.
|
better clarity and fast UDP endpoints are added.
|
||||||
|
|
||||||
@ -8,20 +8,26 @@ The new generation of __PARanoid SECurity__ protocol, advanced, faster, more sec
|
|||||||
block device" transport to the same local interface. Out if the box it
|
block device" transport to the same local interface. Out if the box it
|
||||||
provides the following transports:
|
provides the following transports:
|
||||||
|
|
||||||
| name | JVM | JS | native |
|
| name | JVM | JS | wasmJS | native |
|
||||||
|-------------------|--------|----|--------|
|
|-------------------|:----:|:---:|:-------:|:------:|
|
||||||
| TCP/IP server | ✓ | | 0.2.6+ |
|
| TCP/IP server | ✓ | | | ✓ |
|
||||||
| TCP/IP client | ✓ | | 0.2.6+ |
|
| TCP/IP client | ✓ | | | ✓ |
|
||||||
| UDP server | 0.3.2+ | | 0.3.2+ |
|
| UDP server | ✓ | | | ✓ |
|
||||||
| UDP client | 0.3.2+ | | 0.3.2+ |
|
| UDP client | ✓ | | | ✓ |
|
||||||
| Websockets server | ✓ | | |
|
| Websockets server | ✓ | | | |
|
||||||
| Websockets client | ✓ | ✓ | ✓ |
|
| Websockets client | ✓ | ✓ | ✓ | ✓ |
|
||||||
|
|
||||||
### Note on version compatibility
|
### Note on version compatibility
|
||||||
|
|
||||||
Version 0.5.1 could be backward incompatible due to upgrade of the crypto2.
|
We recommend using `0.6.12`
|
||||||
|
|
||||||
Protocols >= 0.3.0 are not binary compatible with previous version due to more compact binary
|
Since version 0.6.9 websocket protocol supports both text and binary frames; old clients are backward compatible with
|
||||||
|
mew servers, but new clients only can work with older servers only in default binary frame mode. Upgrade also your
|
||||||
|
servers to get better websocket compatibility[^1].
|
||||||
|
|
||||||
|
Version 0.5.1 could be backward incompatible due to the upgrade of the crypto2.
|
||||||
|
|
||||||
|
Protocols >= 0.3.0 are not binary compatible with the previous version due to a more compact binary
|
||||||
format. The format from 0.3.0 onwards is supposed to keep compatible.
|
format. The format from 0.3.0 onwards is supposed to keep compatible.
|
||||||
|
|
||||||
#### ID calculation algorithm is changed since 0.4.1
|
#### ID calculation algorithm is changed since 0.4.1
|
||||||
@ -41,10 +47,13 @@ We recommend to upgrade to 0.4+ ASAP as public/shared key id derivation method w
|
|||||||
|
|
||||||
## TCP/IP and UDP transports
|
## TCP/IP and UDP transports
|
||||||
|
|
||||||
These are the fastest based on async socket implementation of ktor client. They works everywhere but JS target as
|
These are the fastest based on async socket implementation of a ktor client. They work everywhere but JS target as
|
||||||
there is currently no widely adopted sockets for browser javascript.
|
there are currently no widely adopted sockets for browser JavaScript.
|
||||||
|
|
||||||
While UDP is faster than TCP/IP, it is less reliable, especially with commands and return values that 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 UDP is faster than TCP/IP, it is less reliable, especially with commands and return values that serialize to more
|
||||||
|
than 240 bytes approx, and has no retransmission facilities (use TCP!). UDP, though, shines when all you need is
|
||||||
|
to [push](https://code.sergeych.net/docs/kiloparsec/kiloparsec/net.sergeych.kiloparsec/-remote-interface/push.html) with
|
||||||
|
little or no data in it.
|
||||||
|
|
||||||
## Websockets server
|
## Websockets server
|
||||||
|
|
||||||
@ -61,7 +70,7 @@ It is slower than TCP or UDP, but it works on literally all platforms. See the s
|
|||||||
|
|
||||||
# Usage
|
# Usage
|
||||||
|
|
||||||
The library should be used as maven dependency, not as source.
|
The library should be used as a maven dependency, not as source.
|
||||||
|
|
||||||
## Adding dependency
|
## Adding dependency
|
||||||
|
|
||||||
@ -82,12 +91,12 @@ It could be, depending on your project structure, something like:
|
|||||||
```kotlin
|
```kotlin
|
||||||
val commonMain by getting {
|
val commonMain by getting {
|
||||||
dependencies {
|
dependencies {
|
||||||
api("net.sergeych:kiloparsec:0.4.1")
|
api("net.sergeych:kiloparsec:0.6.8")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
## Create shared interface for your server and the client
|
## Create a shared interface for your server and the client
|
||||||
|
|
||||||
It could be a multiplatform library that exports it or just a shared or copied source file declaring structures
|
It could be a multiplatform library that exports it or just a shared or copied source file declaring structures
|
||||||
and functions available, like:
|
and functions available, like:
|
||||||
@ -138,10 +147,11 @@ assertEquals(FooArgs("bar", 117), client.call(cmdGetFoo))
|
|||||||
|
|
||||||
```
|
```
|
||||||
|
|
||||||
## Create ktor-based server
|
## Create a ktor-based server
|
||||||
|
|
||||||
Normally server side needs some session. It is convenient and avoid sending repeating data on each request speeding up
|
Normally the server side needs some session. It is convenient and avoids sending repeating data on each request speeding
|
||||||
the protocol. With KILOPARSEC, it is rather basic operation:
|
up
|
||||||
|
the protocol. With KILOPARSEC, it is a rather basic operation:
|
||||||
|
|
||||||
~~~kotlin
|
~~~kotlin
|
||||||
// Our session just keeps Foo for cmd{Get|Set}Foo:
|
// Our session just keeps Foo for cmd{Get|Set}Foo:
|
||||||
@ -168,11 +178,11 @@ val ns: NettyApplicationEngine = embeddedServer(Netty, port = 8080, host = "0.0.
|
|||||||
|
|
||||||
~~~
|
~~~
|
||||||
|
|
||||||
## Create TCP/IP client and server
|
## Create a TCP / IP client and server
|
||||||
|
|
||||||
Using plain TCP/IP is even simpler, and it works way faster than websocket one, and is _the same
|
Using plain TCP/IP is even simpler, and it works way faster than websocket one, and is _the same
|
||||||
protected as `wss://` (and `ws://`) variant above due to same kiloparsec encryption in both cases. Still, a TCP/IP
|
protected as `wss://` (and `ws://`) the variant above due to same kiloparsec encryption in both cases. Still, a TCP/IP
|
||||||
client is not available in Javascript browser targets and custom TCP ports could often be blocked by firewalls.
|
client is not available in JavaScript browser targets, and custom TCP ports could often be blocked by firewalls.
|
||||||
|
|
||||||
Documentation is available in samples here:
|
Documentation is available in samples here:
|
||||||
|
|
||||||
@ -180,11 +190,13 @@ Documentation is available in samples here:
|
|||||||
|
|
||||||
- [TCP/IP client](https://code.sergeych.net/docs/kiloparsec/kiloparsec/net.sergeych.kiloparsec/-kilo-client/index.html)
|
- [TCP/IP client](https://code.sergeych.net/docs/kiloparsec/kiloparsec/net.sergeych.kiloparsec/-kilo-client/index.html)
|
||||||
|
|
||||||
In short, there are two functions that implements asynchronous TCP/IP transport on all platforms buy JS:
|
In short, there are two functions that implement asynchronous TCP/IP transport on all platforms buy JS:
|
||||||
|
|
||||||
- [acceptTcpDevice](https://code.sergeych.net/docs/kiloparsec/kiloparsec/net.sergeych.kiloparsec.adapter/accept-tcp-device.html?query=fun%20acceptTcpDevice(port:%20Int):%20Flow%3CInetTransportDevice%3E) to create a server
|
- [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
|
- [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
|
## UDP client and server
|
||||||
|
|
||||||
@ -198,35 +210,49 @@ Is very much straightforward, same as with TCP/IP:
|
|||||||
#### Command size
|
#### Command size
|
||||||
|
|
||||||
Each command invocation and result are packed in a separate UDP diagram using effective binary packing.
|
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.
|
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.
|
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.
|
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
|
#### Timeouts
|
||||||
|
|
||||||
As Datagrams do not form protocol itself, kiloparsec issues pings when no data is circulated between parties.
|
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
|
When no pings are received long enough, the kiloparsec connection is closed. There are `maxInactivityTimeout` in all
|
||||||
relevant functions and constructors.
|
relevant functions and constructors.
|
||||||
|
|
||||||
Client should not issue pings manually.
|
Client should not issue pings manually.
|
||||||
|
|
||||||
## Reusing code between servers
|
## 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.
|
The same instance of
|
||||||
|
the [KiloInterface](https://code.sergeych.net/docs/kiloparsec/kiloparsec/net.sergeych.kiloparsec/-kilo-interface/index.html?query=open%20class%20KiloInterface%3CS%3E%20:%20LocalInterface%3CKiloScope%3CS%3E%3E)
|
||||||
|
could easily be reused with all instances of servers with different protocols.
|
||||||
|
|
||||||
This is a common proactive to create a business logic in a `KiloInterface`, then create a TCP/IP and Websocket servers passing the same instance of the logic to both.
|
This is a common practice to create a business logic in a `KiloInterface`, then create a TCP/IP and Websocket servers
|
||||||
|
passing the same instance of the logic to both.
|
||||||
|
|
||||||
## Note on the server identification
|
## Note on the server identification
|
||||||
|
|
||||||
We do not recommend to rely on TLS (HTTPS://, WSS://) host identification solely, in the modern world there is
|
We do not recommend relying on TLS (HTTPS://, WSS://) host identification solely; in the modern world there is
|
||||||
a high probability of attacks on unfriendly (in respect to at least some of your users) states to the SSL certificates
|
a high probability of attacks on unfriendly (in respect to at least some of your users) states to the SSL certificates
|
||||||
chain, in which case the [MITM attack] and spoofing will be undetected. Check the [remoteId](https://code.sergeych.net/docs/kiloparsec/kiloparsec/net.sergeych.kiloparsec/-kilo-client/remote-id.html?query=suspend%20fun%20remoteId():%20VerifyingPublicKey?) in your client on each connection and provide the safe [serverSecretKey](https://code.sergeych.net/docs/kiloparsec/kiloparsec/net.sergeych.kiloparsec/-kilo-server/index.html) when creating a server.
|
chain, in which case the [MITM attack] and spoofing will be undetected. Check
|
||||||
|
the [remoteId](https://code.sergeych.net/docs/kiloparsec/kiloparsec/net.sergeych.kiloparsec/-kilo-client/remote-id.html?query=suspend%20fun%20remoteId():%20VerifyingPublicKey?)
|
||||||
|
in your client on each connection and provide the
|
||||||
|
safe [serverSecretKey](https://code.sergeych.net/docs/kiloparsec/kiloparsec/net.sergeych.kiloparsec/-kilo-server/index.html)
|
||||||
|
when creating a server.
|
||||||
|
|
||||||
This will effectively protect against certificate chain spoofing in the case of the application installed from the trusted source.
|
This will effectively protect against certificate chain spoofing in the case of the application installed from the
|
||||||
|
trusted source.
|
||||||
|
|
||||||
__Important note__. The web application could not be completely secured this way unless is loaded from the IP-address, as the DNS could be spoofed the same, especially when used with `Cloudflare` or other CDN that can transparently substitute the whole site. For applications, we strongly recommend not to use CDN except your own, controlled ones. You generally can't neither detect nor repel [MITM attack] performed from _any single cloudflare 'ray'_.
|
__Important note__. The web application could not be completely secured this way unless is loaded from the IP-address,
|
||||||
|
as the DNS could be spoofed the same, especially when used with `Cloudflare` or other CDN that can transparently
|
||||||
|
substitute the whole site. For applications, we strongly recommend not using CDN except your own, controlled ones. You
|
||||||
|
generally can't neither detect nor repel [MITM attack] performed from _any single cloudflare 'ray'_.
|
||||||
|
|
||||||
## See also:
|
## See also:
|
||||||
|
|
||||||
@ -235,25 +261,25 @@ __Important note__. The web application could not be completely secured this way
|
|||||||
|
|
||||||
# Details
|
# Details
|
||||||
|
|
||||||
It is not compatible with parsec family and no more based on an Universa crypto library. To better fit
|
It is not compatible with Parsec family and no more based on the Universa crypto library. To better fit
|
||||||
the modern state of threats and rate of cyber crimes, KiloParsec uses more encryption and random key exchange on each
|
the modern state of threats and rate of cyber crimes, KiloParsec uses more encryption and random key exchange on each
|
||||||
and every connection (while parsec caches session keys to avoid time-consuming keys exchange). For the same reason,
|
and every connection (while parsec caches session keys to avoid time-consuming keys exchange). For the same reason,
|
||||||
keys cryptography for session is shifted to use ed25519 curves which are supposed to provide agreeable strength with
|
keys cryptography for session is shifted to use ed25519 curves, which are supposed to provide agreeable strength with
|
||||||
enough speed to protect every connection with a unique new keys. Also, we completely get rid of SHA2.
|
enough speed to protect every connection with unique new keys. Also, we completely get rid of SHA2.
|
||||||
|
|
||||||
Kiloparsec also uses a denser binary format [bipack](https://gitea.sergeych.net/SergeychWorks/mp_bintools), no more
|
Kiloparsec also uses a denser binary format [bipack](https://gitea.sergeych.net/SergeychWorks/mp_bintools), no more
|
||||||
key-values,
|
key-values,
|
||||||
which reveals much less on the inner data structure, providing advanced
|
which reveals much less on the inner data structure, providing advanced
|
||||||
typed RPC interfaces with kotlinx.serialization. There is also Rust
|
typed RPC interfaces with kotlinx.serialization. There is also Rust
|
||||||
implementation [bipack_ru](https://gitea.sergeych.net/DiWAN/bipack_ru).
|
implementation [bipack_ru](https://gitea.sergeych.net/DiWAN/bipack_ru).
|
||||||
The architecture allows connecting same functional interfaces to several various type channels at once.
|
The architecture allows connecting the same functional interfaces to several various type channels at once.
|
||||||
|
|
||||||
Also, the difference from parsecs is that there are no more unencrypted layer commands available to users.
|
Also, the difference from parsecs is that there are no more unencrypted layer commands available to users.
|
||||||
All RPC is performed over the encrypted connection.
|
All RPC is performed over the encrypted connection.
|
||||||
|
|
||||||
# Technical description
|
# Technical description
|
||||||
|
|
||||||
Kiloparsec is a full-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
|
and support for serializing exceptions (e.g. exception thrown while executing remote command will be caught and
|
||||||
rethrown at the caller context).
|
rethrown at the caller context).
|
||||||
|
|
||||||
@ -262,16 +288,22 @@ Integrated tools to prevent MITM attacks include also non-transferred independen
|
|||||||
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.
|
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
|
Kiloparsec has a built-in completely asynchronous (coroutine-based top-down) transport layer based on TCP (JVM only as
|
||||||
|
for
|
||||||
now) and the same async Websocket-based transport based on KTOR. Websocket client is multiplatform, though the server is
|
now) and the same async Websocket-based transport based on KTOR. Websocket client is multiplatform, though the server is
|
||||||
JVM only insofar.
|
JVM only insofar.
|
||||||
|
|
||||||
# Licensing
|
# Licensing
|
||||||
|
|
||||||
This is work in progress, not yet moved to public domain;
|
This is a work in progress, not yet moved to the public domain;
|
||||||
you need to obtain a license from https://8-rays.dev or [Sergey Chernov]. For open source projects it will most be free on some special terms.
|
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.
|
It will be moved to open source; we also guarantee that it will be moved to open source immediately if the software
|
||||||
|
export restrictions are lifted. We do not support such practices here at 8-rays.dev.
|
||||||
|
|
||||||
[MITM]: https://en.wikipedia.org/wiki/Man-in-the-middle_attack
|
[MITM]: https://en.wikipedia.org/wiki/Man-in-the-middle_attack
|
||||||
|
|
||||||
[Sergey Chernov]: https://t.me/real_sergeych
|
[Sergey Chernov]: https://t.me/real_sergeych
|
||||||
|
[^1]: On some new Xiaomi phones we found problems with websocket binary frames, probably in ktor; use text frames
|
||||||
|
otherwise.
|
||||||
103
build.gradle.kts
103
build.gradle.kts
@ -8,15 +8,18 @@
|
|||||||
* real dot sergeych at gmail.
|
* real dot sergeych at gmail.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
import org.jetbrains.kotlin.gradle.ExperimentalWasmDsl
|
||||||
|
|
||||||
plugins {
|
plugins {
|
||||||
kotlin("multiplatform") version "2.1.0"
|
kotlin("multiplatform") version "2.2.20"
|
||||||
id("org.jetbrains.kotlin.plugin.serialization") version "2.1.0"
|
id("org.jetbrains.kotlin.plugin.serialization") version "2.2.20"
|
||||||
|
id("com.android.library") version "8.5.2" apply true
|
||||||
`maven-publish`
|
`maven-publish`
|
||||||
id("org.jetbrains.dokka") version "1.9.20"
|
id("org.jetbrains.dokka") version "1.9.20"
|
||||||
}
|
}
|
||||||
|
|
||||||
group = "net.sergeych"
|
group = "net.sergeych"
|
||||||
version = "0.6.3"
|
version = "0.6.12"
|
||||||
|
|
||||||
repositories {
|
repositories {
|
||||||
mavenCentral()
|
mavenCentral()
|
||||||
@ -34,19 +37,24 @@ kotlin {
|
|||||||
}
|
}
|
||||||
nodejs()
|
nodejs()
|
||||||
}
|
}
|
||||||
// macosArm64()
|
|
||||||
// iosX64()
|
|
||||||
// iosArm64()
|
|
||||||
// iosSimulatorArm64()
|
|
||||||
linuxX64()
|
linuxX64()
|
||||||
linuxArm64()
|
linuxArm64()
|
||||||
// macosX64()
|
|
||||||
// macosX64()
|
|
||||||
mingwX64()
|
|
||||||
// @OptIn(ExperimentalWasmDsl::class)
|
|
||||||
// wasmJs()
|
|
||||||
|
|
||||||
val ktor_version = "3.1.0"
|
macosArm64()
|
||||||
|
macosX64()
|
||||||
|
iosX64()
|
||||||
|
iosArm64()
|
||||||
|
iosSimulatorArm64()
|
||||||
|
|
||||||
|
mingwX64()
|
||||||
|
|
||||||
|
androidTarget()
|
||||||
|
@OptIn(ExperimentalWasmDsl::class)
|
||||||
|
wasmJs {
|
||||||
|
browser()
|
||||||
|
}
|
||||||
|
|
||||||
|
val ktor_version = "3.1.1"
|
||||||
|
|
||||||
sourceSets {
|
sourceSets {
|
||||||
all {
|
all {
|
||||||
@ -58,9 +66,14 @@ kotlin {
|
|||||||
val commonMain by getting {
|
val commonMain by getting {
|
||||||
dependencies {
|
dependencies {
|
||||||
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.10.1")
|
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.10.1")
|
||||||
implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.7.2")
|
implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.8.1")
|
||||||
api("io.ktor:ktor-client-core:$ktor_version")
|
api("io.ktor:ktor-client-core:$ktor_version")
|
||||||
api("net.sergeych:crypto2:0.7.4-SNAPSHOT")
|
api("net.sergeych:crypto2:0.8.5")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
val androidMain by getting {
|
||||||
|
dependencies {
|
||||||
|
implementation("io.ktor:ktor-client-okhttp:$ktor_version")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
val ktorSocketMain by creating {
|
val ktorSocketMain by creating {
|
||||||
@ -73,7 +86,7 @@ kotlin {
|
|||||||
dependencies {
|
dependencies {
|
||||||
implementation(kotlin("test"))
|
implementation(kotlin("test"))
|
||||||
implementation("org.slf4j:slf4j-simple:2.0.9")
|
implementation("org.slf4j:slf4j-simple:2.0.9")
|
||||||
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-test:1.8.1")
|
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-test:1.10.1")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
val ktorSocketTest by creating {
|
val ktorSocketTest by creating {
|
||||||
@ -92,34 +105,33 @@ kotlin {
|
|||||||
val jvmTest by getting {
|
val jvmTest by getting {
|
||||||
dependsOn(ktorSocketTest)
|
dependsOn(ktorSocketTest)
|
||||||
}
|
}
|
||||||
|
|
||||||
val jsMain by getting {
|
val jsMain by getting {
|
||||||
dependencies {
|
dependencies {
|
||||||
implementation("io.ktor:ktor-client-js:$ktor_version")
|
implementation("io.ktor:ktor-client-js:$ktor_version")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
val jsTest by getting
|
val jsTest by getting
|
||||||
// val macosArm64Main by getting {
|
val macosArm64Main by getting {
|
||||||
// dependsOn(ktorSocketMain)
|
dependsOn(ktorSocketMain)
|
||||||
// }
|
}
|
||||||
// val macosArm64Test by getting {
|
val macosArm64Test by getting {
|
||||||
// dependsOn(ktorSocketTest)
|
dependsOn(ktorSocketTest)
|
||||||
// }
|
}
|
||||||
// val macosX64Main by getting {
|
val macosX64Main by getting {
|
||||||
// dependsOn(ktorSocketMain)
|
dependsOn(ktorSocketMain)
|
||||||
// }
|
}
|
||||||
// val iosX64Main by getting {
|
val iosX64Main by getting {
|
||||||
// dependsOn(ktorSocketMain)
|
dependsOn(ktorSocketMain)
|
||||||
// }
|
}
|
||||||
// val iosX64Test by getting {
|
val iosX64Test by getting {
|
||||||
// dependsOn(ktorSocketTest)
|
dependsOn(ktorSocketTest)
|
||||||
// }
|
}
|
||||||
// val iosArm64Main by getting {
|
val iosArm64Main by getting {
|
||||||
// dependsOn(ktorSocketMain)
|
dependsOn(ktorSocketMain)
|
||||||
// }
|
}
|
||||||
// val iosArm64Test by getting {
|
val iosArm64Test by getting {
|
||||||
// dependsOn(ktorSocketTest)
|
dependsOn(ktorSocketTest)
|
||||||
// }
|
}
|
||||||
val linuxArm64Main by getting {
|
val linuxArm64Main by getting {
|
||||||
dependsOn(ktorSocketMain)
|
dependsOn(ktorSocketMain)
|
||||||
}
|
}
|
||||||
@ -133,6 +145,7 @@ kotlin {
|
|||||||
dependsOn(ktorSocketTest)
|
dependsOn(ktorSocketTest)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
publishing {
|
publishing {
|
||||||
val mavenToken by lazy {
|
val mavenToken by lazy {
|
||||||
@ -151,7 +164,7 @@ kotlin {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
tasks.dokkaHtml.configure {
|
tasks.dokkaHtml.configure {
|
||||||
outputDirectory.set(buildDir.resolve("dokka"))
|
outputDirectory.set(buildDir.resolve("dokka"))
|
||||||
@ -162,3 +175,15 @@ tasks.dokkaHtml.configure {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
android {
|
||||||
|
namespace = "net.sergeych.kiloparsec"
|
||||||
|
compileSdk = 34
|
||||||
|
defaultConfig {
|
||||||
|
minSdk = 24
|
||||||
|
}
|
||||||
|
compileOptions {
|
||||||
|
sourceCompatibility = JavaVersion.VERSION_17
|
||||||
|
targetCompatibility = JavaVersion.VERSION_17
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|||||||
@ -10,3 +10,6 @@
|
|||||||
|
|
||||||
kotlin.code.style=official
|
kotlin.code.style=official
|
||||||
kotlin.mpp.applyDefaultHierarchyTemplate=false
|
kotlin.mpp.applyDefaultHierarchyTemplate=false
|
||||||
|
|
||||||
|
kotlin.daemon.jvmargs=-Xmx2048m
|
||||||
|
kotlin.native.ignoreDisabledTargets=true
|
||||||
2
gradle/wrapper/gradle-wrapper.properties
vendored
2
gradle/wrapper/gradle-wrapper.properties
vendored
@ -10,6 +10,6 @@
|
|||||||
|
|
||||||
distributionBase=GRADLE_USER_HOME
|
distributionBase=GRADLE_USER_HOME
|
||||||
distributionPath=wrapper/dists
|
distributionPath=wrapper/dists
|
||||||
distributionUrl=https\://services.gradle.org/distributions/gradle-8.2-bin.zip
|
distributionUrl=https\://services.gradle.org/distributions/gradle-8.7-bin.zip
|
||||||
zipStoreBase=GRADLE_USER_HOME
|
zipStoreBase=GRADLE_USER_HOME
|
||||||
zipStorePath=wrapper/dists
|
zipStorePath=wrapper/dists
|
||||||
@ -10,6 +10,7 @@
|
|||||||
|
|
||||||
pluginManagement {
|
pluginManagement {
|
||||||
repositories {
|
repositories {
|
||||||
|
google()
|
||||||
mavenCentral()
|
mavenCentral()
|
||||||
gradlePluginPortal()
|
gradlePluginPortal()
|
||||||
}
|
}
|
||||||
|
|||||||
@ -91,9 +91,11 @@ class KiloClient<S>(
|
|||||||
debug { "get device and session" }
|
debug { "get device and session" }
|
||||||
val client = KiloClientConnection(localInterface, kc, secretKey)
|
val client = KiloClientConnection(localInterface, kc, secretKey)
|
||||||
deferredClient.complete(client)
|
deferredClient.complete(client)
|
||||||
client.run {
|
debug { "starting client run"}
|
||||||
|
val r = runCatching { client.run {
|
||||||
_state.value = it
|
_state.value = it
|
||||||
}
|
} }
|
||||||
|
debug { "----------- client run finished: $r" }
|
||||||
resetDeferredClient()
|
resetDeferredClient()
|
||||||
debug { "client run finished" }
|
debug { "client run finished" }
|
||||||
} catch (_: RemoteInterface.ClosedException) {
|
} catch (_: RemoteInterface.ClosedException) {
|
||||||
@ -109,7 +111,7 @@ class KiloClient<S>(
|
|||||||
_state.value = false
|
_state.value = false
|
||||||
resetDeferredClient()
|
resetDeferredClient()
|
||||||
// reconnection timeout
|
// reconnection timeout
|
||||||
delay(100)
|
delay(700)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -49,7 +49,6 @@ class KiloClientConnection<S>(
|
|||||||
try {
|
try {
|
||||||
// in parallel: keys and connection
|
// in parallel: keys and connection
|
||||||
val deferredKeyPair = async { SafeKeyExchange() }
|
val deferredKeyPair = async { SafeKeyExchange() }
|
||||||
debug { "opening device" }
|
|
||||||
debug { "got a transport device $device" }
|
debug { "got a transport device $device" }
|
||||||
|
|
||||||
|
|
||||||
@ -62,10 +61,11 @@ class KiloClientConnection<S>(
|
|||||||
debug { "transport started" }
|
debug { "transport started" }
|
||||||
|
|
||||||
val pair = deferredKeyPair.await()
|
val pair = deferredKeyPair.await()
|
||||||
debug { "keypair ready" }
|
debug { "keypair ready (1)" }
|
||||||
|
|
||||||
val serverHe = transport.call(L0Request, Handshake(1u, pair.publicKey))
|
val serverHe = transport.call(L0Request, Handshake(1u, pair.publicKey))
|
||||||
|
|
||||||
|
debug { "got server HE (2)" }
|
||||||
val sk = pair.clientSessionKey(serverHe.publicKey)
|
val sk = pair.clientSessionKey(serverHe.publicKey)
|
||||||
var params = KiloParams(false, transport, sk, session, null, this@KiloClientConnection)
|
var params = KiloParams(false, transport, sk, session, null, this@KiloClientConnection)
|
||||||
|
|
||||||
@ -97,8 +97,7 @@ class KiloClientConnection<S>(
|
|||||||
} catch (x: CancellationException) {
|
} catch (x: CancellationException) {
|
||||||
info { "client is cancelled" }
|
info { "client is cancelled" }
|
||||||
} catch (x: RemoteInterface.ClosedException) {
|
} catch (x: RemoteInterface.ClosedException) {
|
||||||
x.printStackTrace()
|
debug { "connection closed/refused by remote" }
|
||||||
info { "connection closed by remote" }
|
|
||||||
} finally {
|
} finally {
|
||||||
onConnectedStateChanged?.invoke(false)
|
onConnectedStateChanged?.invoke(false)
|
||||||
job?.cancel()
|
job?.cancel()
|
||||||
|
|||||||
@ -134,7 +134,13 @@ class Transport<S>(
|
|||||||
}
|
}
|
||||||
|
|
||||||
// now we have mutex freed so we can call:
|
// now we have mutex freed so we can call:
|
||||||
val r = runCatching { device.output.send(pack(b)) }
|
val r = runCatching {
|
||||||
|
do {
|
||||||
|
val cr = device.output.trySend(pack(b))
|
||||||
|
if( cr.isClosed ) throw ClosedSendChannelException("can't send block: channel is closed")
|
||||||
|
delay(100)
|
||||||
|
} while(!cr.isSuccess)
|
||||||
|
}
|
||||||
if (!r.isSuccess) {
|
if (!r.isSuccess) {
|
||||||
r.exceptionOrNull()?.let {
|
r.exceptionOrNull()?.let {
|
||||||
exception { "failed to send output block" to it }
|
exception { "failed to send output block" to it }
|
||||||
@ -271,7 +277,7 @@ class Transport<S>(
|
|||||||
}
|
}
|
||||||
debug { "no more active: $isActive / ${calls.size}" }
|
debug { "no more active: $isActive / ${calls.size}" }
|
||||||
}
|
}
|
||||||
info { "exiting transport loop" }
|
debug { "exiting transport loop" }
|
||||||
}
|
}
|
||||||
|
|
||||||
private suspend fun send(block: Block) {
|
private suspend fun send(block: Block) {
|
||||||
|
|||||||
@ -20,12 +20,12 @@ import kotlinx.coroutines.channels.Channel
|
|||||||
import kotlinx.coroutines.channels.ClosedReceiveChannelException
|
import kotlinx.coroutines.channels.ClosedReceiveChannelException
|
||||||
import kotlinx.coroutines.channels.ClosedSendChannelException
|
import kotlinx.coroutines.channels.ClosedSendChannelException
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
|
import kotlinx.io.IOException
|
||||||
import net.sergeych.crypto2.SigningKey
|
import net.sergeych.crypto2.SigningKey
|
||||||
import net.sergeych.kiloparsec.*
|
import net.sergeych.kiloparsec.*
|
||||||
import net.sergeych.mp_logger.LogTag
|
import net.sergeych.mp_logger.*
|
||||||
import net.sergeych.mp_logger.exception
|
import net.sergeych.mp_tools.decodeBase64Compact
|
||||||
import net.sergeych.mp_logger.info
|
import net.sergeych.mp_tools.encodeToBase64Compact
|
||||||
import net.sergeych.mp_logger.warning
|
|
||||||
import net.sergeych.mp_tools.globalLaunch
|
import net.sergeych.mp_tools.globalLaunch
|
||||||
import net.sergeych.tools.AtomicCounter
|
import net.sergeych.tools.AtomicCounter
|
||||||
|
|
||||||
@ -39,13 +39,14 @@ fun <S> websocketClient(
|
|||||||
path: String,
|
path: String,
|
||||||
clientInterface: KiloInterface<S> = KiloInterface(),
|
clientInterface: KiloInterface<S> = KiloInterface(),
|
||||||
secretKey: SigningKey? = null,
|
secretKey: SigningKey? = null,
|
||||||
|
useTextFrames: Boolean = false,
|
||||||
sessionMaker: () -> S = {
|
sessionMaker: () -> S = {
|
||||||
@Suppress("UNCHECKED_CAST")
|
@Suppress("UNCHECKED_CAST")
|
||||||
Unit as S
|
Unit as S
|
||||||
},
|
},
|
||||||
): KiloClient<S> {
|
): KiloClient<S> {
|
||||||
return KiloClient(clientInterface, secretKey) {
|
return KiloClient(clientInterface, secretKey) {
|
||||||
KiloConnectionData(websocketTransportDevice(path), sessionMaker())
|
KiloConnectionData(websocketTransportDevice(path, useTextFrames), sessionMaker())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -56,7 +57,10 @@ fun <S> websocketClient(
|
|||||||
*/
|
*/
|
||||||
fun websocketTransportDevice(
|
fun websocketTransportDevice(
|
||||||
path: String,
|
path: String,
|
||||||
client: HttpClient = HttpClient { install(WebSockets) },
|
useTextFrames: Boolean = false,
|
||||||
|
client: HttpClient = HttpClient {
|
||||||
|
install(WebSockets)
|
||||||
|
},
|
||||||
): Transport.Device {
|
): Transport.Device {
|
||||||
var u = Url(path)
|
var u = Url(path)
|
||||||
if (u.encodedPath.length <= 1)
|
if (u.encodedPath.length <= 1)
|
||||||
@ -67,8 +71,11 @@ fun websocketTransportDevice(
|
|||||||
val input = Channel<UByteArray>()
|
val input = Channel<UByteArray>()
|
||||||
val output = Channel<UByteArray>()
|
val output = Channel<UByteArray>()
|
||||||
val closeHandle = CompletableDeferred<Boolean>()
|
val closeHandle = CompletableDeferred<Boolean>()
|
||||||
|
val readyHandle = CompletableDeferred<Unit>()
|
||||||
|
|
||||||
globalLaunch {
|
globalLaunch {
|
||||||
val log = LogTag("KC:${counter.incrementAndGet()}")
|
val log = LogTag("KC:${counter.incrementAndGet()}")
|
||||||
|
try {
|
||||||
client.webSocket({
|
client.webSocket({
|
||||||
url.protocol = u.protocol
|
url.protocol = u.protocol
|
||||||
url.host = u.host
|
url.host = u.host
|
||||||
@ -80,9 +87,15 @@ fun websocketTransportDevice(
|
|||||||
log.info { "connected to the server" }
|
log.info { "connected to the server" }
|
||||||
// println("SENDING!!!")
|
// println("SENDING!!!")
|
||||||
// send("Helluva")
|
// send("Helluva")
|
||||||
|
readyHandle.complete(Unit)
|
||||||
launch {
|
launch {
|
||||||
try {
|
try {
|
||||||
for (block in output) {
|
for (block in output) {
|
||||||
|
if (useTextFrames)
|
||||||
|
send(
|
||||||
|
Frame.Text(block.asByteArray().encodeToBase64Compact())
|
||||||
|
)
|
||||||
|
else
|
||||||
send(block.toByteArray())
|
send(block.toByteArray())
|
||||||
}
|
}
|
||||||
log.info { "input is closed, closing the websocket" }
|
log.info { "input is closed, closing the websocket" }
|
||||||
@ -99,10 +112,10 @@ fun websocketTransportDevice(
|
|||||||
launch {
|
launch {
|
||||||
try {
|
try {
|
||||||
for (f in incoming) {
|
for (f in incoming) {
|
||||||
if (f is Frame.Binary) {
|
when (f) {
|
||||||
input.send(f.readBytes().toUByteArray())
|
is Frame.Binary -> input.send(f.readBytes().toUByteArray())
|
||||||
} else {
|
is Frame.Text -> input.send(f.readText().decodeBase64Compact().toUByteArray())
|
||||||
log.warning { "ignoring unexpected frame of type ${f.frameType}" }
|
else -> log.warning { "ignoring unexpected frame of type ${f.frameType}" }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (closeHandle.isActive) closeHandle.complete(true)
|
if (closeHandle.isActive) closeHandle.complete(true)
|
||||||
@ -120,17 +133,27 @@ fun websocketTransportDevice(
|
|||||||
log.warning { "Client is closing with error" }
|
log.warning { "Client is closing with error" }
|
||||||
throw RemoteInterface.ClosedException()
|
throw RemoteInterface.ClosedException()
|
||||||
}
|
}
|
||||||
output.close()
|
runCatching { output.close() }
|
||||||
input.close()
|
runCatching { input.close() }
|
||||||
|
runCatching { close() }
|
||||||
|
}
|
||||||
|
} catch (x: IOException) {
|
||||||
|
if ("refused" in x.toString()) log.debug { "connection refused" }
|
||||||
|
else log.warning { "unexpected IO error $x" }
|
||||||
|
runCatching { output.close() }
|
||||||
|
runCatching { input.close() }
|
||||||
}
|
}
|
||||||
log.info { "closing connection" }
|
log.info { "closing connection" }
|
||||||
}
|
}
|
||||||
val device = ProxyDevice(input, output) {
|
// Wait for connection be established or failed
|
||||||
|
val device = ProxyDevice(input, output, doClose = {
|
||||||
// we need to explicitly close the coroutine job, or it can hang for a long time
|
// we need to explicitly close the coroutine job, or it can hang for a long time
|
||||||
// leaking resources.
|
// leaking resources.
|
||||||
|
runCatching { output.close() }
|
||||||
|
runCatching { input.close() }
|
||||||
closeHandle.complete(true)
|
closeHandle.complete(true)
|
||||||
// job.cancel()
|
// job.cancel()
|
||||||
}
|
})
|
||||||
return device
|
return device
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -24,6 +24,8 @@ import net.sergeych.kiloparsec.KiloInterface
|
|||||||
import net.sergeych.kiloparsec.KiloServerConnection
|
import net.sergeych.kiloparsec.KiloServerConnection
|
||||||
import net.sergeych.kiloparsec.RemoteInterface
|
import net.sergeych.kiloparsec.RemoteInterface
|
||||||
import net.sergeych.mp_logger.*
|
import net.sergeych.mp_logger.*
|
||||||
|
import net.sergeych.mp_tools.decodeBase64Compact
|
||||||
|
import net.sergeych.mp_tools.encodeToBase64Compact
|
||||||
import net.sergeych.tools.AtomicCounter
|
import net.sergeych.tools.AtomicCounter
|
||||||
import kotlin.time.Duration.Companion.seconds
|
import kotlin.time.Duration.Companion.seconds
|
||||||
|
|
||||||
@ -66,13 +68,18 @@ fun <S> Application.setupWebsocketServer(
|
|||||||
log.debug { "opening the connection" }
|
log.debug { "opening the connection" }
|
||||||
val input = Channel<UByteArray>(256)
|
val input = Channel<UByteArray>(256)
|
||||||
val output = Channel<UByteArray>(256)
|
val output = Channel<UByteArray>(256)
|
||||||
|
var useBinary: Boolean? = null
|
||||||
launch {
|
launch {
|
||||||
log.debug { "starting output pump" }
|
log.debug { "starting output pump" }
|
||||||
while (isActive) {
|
while (isActive) {
|
||||||
try {
|
try {
|
||||||
send(output.receive().toByteArray())
|
val block = output.receive()
|
||||||
}
|
if (useBinary == false)
|
||||||
catch(_: ClosedReceiveChannelException) {
|
send(block.asByteArray().encodeToBase64Compact())
|
||||||
|
else
|
||||||
|
send(block.toByteArray())
|
||||||
|
|
||||||
|
} catch (_: ClosedReceiveChannelException) {
|
||||||
log.debug { "closing output pump as output channel is closed" }
|
log.debug { "closing output pump as output channel is closed" }
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
@ -91,9 +98,28 @@ fun <S> Application.setupWebsocketServer(
|
|||||||
}
|
}
|
||||||
log.debug { "KSC started, looking for incoming frames" }
|
log.debug { "KSC started, looking for incoming frames" }
|
||||||
for (f in incoming) {
|
for (f in incoming) {
|
||||||
if (f is Frame.Binary)
|
|
||||||
try {
|
try {
|
||||||
|
when (f) {
|
||||||
|
is Frame.Binary -> {
|
||||||
|
if (useBinary == null) {
|
||||||
|
log.debug { "Setting binary frame mode ------------------------------------" }
|
||||||
|
useBinary = true
|
||||||
|
}
|
||||||
input.send(f.readBytes().toUByteArray())
|
input.send(f.readBytes().toUByteArray())
|
||||||
|
}
|
||||||
|
|
||||||
|
is Frame.Text -> {
|
||||||
|
if (useBinary == null) {
|
||||||
|
log.debug { "Setting text frame mode -----------------------------------" }
|
||||||
|
useBinary = false
|
||||||
|
}
|
||||||
|
input.send(f.readText().decodeBase64Compact().asUByteArray())
|
||||||
|
}
|
||||||
|
|
||||||
|
else -> {
|
||||||
|
log.warning { "unexpected frame type ${f.frameType}, ignoring" }
|
||||||
|
}
|
||||||
|
}
|
||||||
} catch (_: RemoteInterface.ClosedException) {
|
} catch (_: RemoteInterface.ClosedException) {
|
||||||
log.warning { "caught local closed exception (strange!), closing" }
|
log.warning { "caught local closed exception (strange!), closing" }
|
||||||
break
|
break
|
||||||
@ -104,8 +130,6 @@ fun <S> Application.setupWebsocketServer(
|
|||||||
log.exception { "unexpected exception, server connection will close" to t }
|
log.exception { "unexpected exception, server connection will close" to t }
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
else
|
|
||||||
log.warning { "unknown frame type ${f.frameType}, ignoring" }
|
|
||||||
}
|
}
|
||||||
log.debug { "closing the server" }
|
log.debug { "closing the server" }
|
||||||
close()
|
close()
|
||||||
|
|||||||
@ -15,10 +15,13 @@ import io.ktor.server.engine.*
|
|||||||
import io.ktor.server.netty.*
|
import io.ktor.server.netty.*
|
||||||
import kotlinx.coroutines.delay
|
import kotlinx.coroutines.delay
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
|
import kotlinx.coroutines.runBlocking
|
||||||
import kotlinx.coroutines.test.runTest
|
import kotlinx.coroutines.test.runTest
|
||||||
|
import net.sergeych.crypto2.SigningSecretKey
|
||||||
import net.sergeych.crypto2.initCrypto
|
import net.sergeych.crypto2.initCrypto
|
||||||
import net.sergeych.kiloparsec.adapter.setupWebsocketServer
|
import net.sergeych.kiloparsec.adapter.setupWebsocketServer
|
||||||
import net.sergeych.kiloparsec.adapter.websocketClient
|
import net.sergeych.kiloparsec.adapter.websocketClient
|
||||||
|
import net.sergeych.kiloparsec.adapter.websocketTransportDevice
|
||||||
import net.sergeych.mp_logger.Log
|
import net.sergeych.mp_logger.Log
|
||||||
import java.net.InetAddress
|
import java.net.InetAddress
|
||||||
import kotlin.random.Random
|
import kotlin.random.Random
|
||||||
@ -39,7 +42,7 @@ class ClientTest {
|
|||||||
|
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun webSocketTest() = runTest {
|
fun webSocketTest1() = runTest {
|
||||||
initCrypto()
|
initCrypto()
|
||||||
// fun Application.
|
// fun Application.
|
||||||
val cmdClose by command<Unit, Unit>()
|
val cmdClose by command<Unit, Unit>()
|
||||||
@ -50,6 +53,7 @@ class ClientTest {
|
|||||||
Log.connectConsole(Log.Level.DEBUG)
|
Log.connectConsole(Log.Level.DEBUG)
|
||||||
|
|
||||||
data class Session(var foo: String = "not set")
|
data class Session(var foo: String = "not set")
|
||||||
|
|
||||||
var closeCounter = 0
|
var closeCounter = 0
|
||||||
val serverInterface = KiloInterface<Session>().apply {
|
val serverInterface = KiloInterface<Session>().apply {
|
||||||
var connectedCalled = false
|
var connectedCalled = false
|
||||||
@ -73,7 +77,9 @@ class ClientTest {
|
|||||||
client.connectedStateFlow.collect {
|
client.connectedStateFlow.collect {
|
||||||
println("got: $closeCounter/$it")
|
println("got: $closeCounter/$it")
|
||||||
states += it
|
states += it
|
||||||
if( !it) { closeCounter++ }
|
if (!it) {
|
||||||
|
closeCounter++
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
assertEquals(true, client.call(cmdCheckConnected))
|
assertEquals(true, client.call(cmdCheckConnected))
|
||||||
@ -104,4 +110,101 @@ class ClientTest {
|
|||||||
// println("stopped server")
|
// println("stopped server")
|
||||||
// println("closed client")
|
// println("closed client")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun webSocketTest2() = runTest {
|
||||||
|
initCrypto()
|
||||||
|
// fun Application.
|
||||||
|
val cmdClose by command<Unit, Unit>()
|
||||||
|
val cmdGetFoo by command<Unit, String>()
|
||||||
|
val cmdSetFoo by command<String, Unit>()
|
||||||
|
val cmdCheckConnected by command<Unit, Boolean>()
|
||||||
|
|
||||||
|
Log.connectConsole(Log.Level.DEBUG)
|
||||||
|
|
||||||
|
data class Session(var foo: String = "not set")
|
||||||
|
|
||||||
|
var closeCounter = 0
|
||||||
|
val serverInterface = KiloInterface<Session>().apply {
|
||||||
|
var connectedCalled = false
|
||||||
|
onConnected { connectedCalled = true }
|
||||||
|
on(cmdGetFoo) { session.foo }
|
||||||
|
on(cmdSetFoo) { session.foo = it }
|
||||||
|
on(cmdCheckConnected) { connectedCalled }
|
||||||
|
on(cmdClose) {
|
||||||
|
throw LocalInterface.BreakConnectionException()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val 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:$port/kp", useTextFrames = true)
|
||||||
|
val states = mutableListOf<Boolean>()
|
||||||
|
val collector = launch {
|
||||||
|
client.connectedStateFlow.collect {
|
||||||
|
println("got: $closeCounter/$it")
|
||||||
|
states += it
|
||||||
|
if (!it) {
|
||||||
|
closeCounter++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
assertEquals(true, client.call(cmdCheckConnected))
|
||||||
|
assertTrue { client.connectedStateFlow.value }
|
||||||
|
assertEquals("not set", client.call(cmdGetFoo))
|
||||||
|
client.call(cmdSetFoo, "foo")
|
||||||
|
assertEquals("foo", client.call(cmdGetFoo))
|
||||||
|
|
||||||
|
client.close()
|
||||||
|
ns.stop()
|
||||||
|
collector.cancel()
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun webSocketWaitForConnectTest() = runBlocking {
|
||||||
|
initCrypto()
|
||||||
|
// fun Application.
|
||||||
|
Log.connectConsole(Log.Level.DEBUG)
|
||||||
|
|
||||||
|
val clientInterface = KiloInterface<Unit>().apply {}
|
||||||
|
|
||||||
|
val port = Random.nextInt(8080, 9090)
|
||||||
|
var clientConnectCalls = 0
|
||||||
|
// It should repeatedly reconnect, and we will count:
|
||||||
|
KiloClient(clientInterface, SigningSecretKey.new()) {
|
||||||
|
clientConnectCalls++
|
||||||
|
KiloConnectionData(websocketTransportDevice("ws://localhost:$port/kp", true), Unit)
|
||||||
|
}
|
||||||
|
delay(1200)
|
||||||
|
// and check:
|
||||||
|
// println("connection attemtps: $clientConnectCalls")
|
||||||
|
assertTrue { clientConnectCalls > 1 }
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun webSocketWaitForConnectTest2() = runBlocking {
|
||||||
|
initCrypto()
|
||||||
|
// fun Application.
|
||||||
|
Log.connectConsole(Log.Level.DEBUG)
|
||||||
|
|
||||||
|
val clientInterface = KiloInterface<Unit>().apply {}
|
||||||
|
|
||||||
|
val port = Random.nextInt(8080, 9090)
|
||||||
|
var clientConnectCalls = 0
|
||||||
|
// It should repeatedly reconnect, and we will count:
|
||||||
|
KiloClient(clientInterface, SigningSecretKey.new()) {
|
||||||
|
clientConnectCalls++
|
||||||
|
KiloConnectionData(websocketTransportDevice("ws://localhost:$port/kp", false), Unit)
|
||||||
|
}
|
||||||
|
delay(1200)
|
||||||
|
// and check:
|
||||||
|
// println("connection attemtps: $clientConnectCalls")
|
||||||
|
assertTrue { clientConnectCalls > 1 }
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
@ -52,7 +52,13 @@ fun acceptTcpDevice(port: Int, localInterface: String = "0.0.0.0"): Flow<InetTra
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend fun connectTcpDevice(address: String) = connectTcpDevice(address.toNetworkAddress())
|
|
||||||
|
/**
|
||||||
|
* Connect to the TCP/IP server (see [KiloServer]) at the specified address and provide th compatible
|
||||||
|
* [InetTransportDevice] to use with [KiloClient]. [hostPortAddress] is a string in the form of `host:port`.
|
||||||
|
*/
|
||||||
|
suspend fun connectTcpDevice(hostPortAddress: String): InetTransportDevice =
|
||||||
|
connectTcpDevice(hostPortAddress.toNetworkAddress())
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Connect to the TCP/IP server (see [KiloServer]) at the specified address and provide th compatible
|
* Connect to the TCP/IP server (see [KiloServer]) at the specified address and provide th compatible
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user