feat: add TLS WebSocket crosstest support across all languages

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
toki 2026-04-26 19:24:15 +09:00
parent 627c0f38c2
commit 1419895ed6
44 changed files with 5747 additions and 191 deletions

View file

@ -13,7 +13,9 @@
"Bash(chmod +x /config/workspace/toki_socket/agent-ops/sync.sh)",
"Bash(bash agent-ops/sync.sh)",
"Bash(bash agent-ops/sync.sh push)",
"Bash(git checkout:*)"
"Bash(git checkout:*)",
"Bash(cp /config/workspace/toki_socket/agent-task/tls_crosstest/CODE_REVIEW.md /config/workspace/toki_socket/agent-task/tls_crosstest/code_review_0.log)",
"Bash(cp /config/workspace/toki_socket/agent-task/tls_crosstest/PLAN.md /config/workspace/toki_socket/agent-task/tls_crosstest/plan_0.log)"
],
"additionalDirectories": [
"/config/workspace/toki_socket/tasks",

View file

@ -0,0 +1,135 @@
<!-- task=tls_crosstest plan=1 tag=TLS_CROSSTEST -->
# Code Review Reference - TLS_CROSSTEST
## 개요
date=2026-04-26
task=tls_crosstest, plan=1, tag=TLS_CROSSTEST
## 이 파일을 읽는 리뷰 에이전트에게
각 항목의 구현을 실제 소스 파일과 대조하고, `검증 결과` 섹션의 출력이 코드와 일치하는지 확인하세요.
리뷰 완료 후 반드시 아래 순서로 아카이브하세요.
1. `CODE_REVIEW.md``code_review_1.log` (N = 기존 code_review_*.log 수)
2. `PLAN.md``plan_1.log` (M = 기존 plan_*.log 수)
3. PASS인 경우 `complete.log` 작성 후 종료. WARN/FAIL인 경우 새 `PLAN.md` + `CODE_REVIEW.md` 스텁 작성.
---
## 구현 항목별 완료 여부
| 항목 | 완료 여부 |
|------|---------|
| [TLS_CROSSTEST-1] Dart 클라이언트 파일 — TLS 모드 추가 | [x] |
| [TLS_CROSSTEST-2] Go 클라이언트 파일 — TLS 모드 추가 | [x] |
| [TLS_CROSSTEST-3] Kotlin 클라이언트 파일 — TLS 모드 추가 | [x] |
| [TLS_CROSSTEST-4] Python 클라이언트 파일 — TLS 모드 추가 | [x] |
| [TLS_CROSSTEST-5] TypeScript 클라이언트 파일 — TLS 모드 추가 | [x] |
| [TLS_CROSSTEST-6] Dart orchestrator 3개 — TLS phase 추가 | [x] |
| [TLS_CROSSTEST-7] Go orchestrator 3개 — TLS phase 추가 | [x] |
| [TLS_CROSSTEST-8] Kotlin orchestrator 4개 — TLS TCP phase 추가 | [x] |
| [TLS_CROSSTEST-9] Python orchestrator 4개 — TLS phase 추가 | [x] |
| [TLS_CROSSTEST-10] TypeScript orchestrator 4개 — TLS phase 추가 | [x] |
## 계획 대비 변경 사항
- Python crosstest client 4개에 secure close timeout과 짧은 server-ready delay를 추가했다. TLS close 또는 Kotlin server listener 등록 레이스로 테스트 프로세스가 불필요하게 타임아웃되는 것을 막기 위한 안정화다.
- Kotlin server row 3개(`kotlin_dart`, `kotlin_go`, `kotlin_python`)는 기존 WS 서버를 phase마다 재시작하던 구조를 `kotlin_typescript`와 같은 단일 WS 서버 재사용 구조로 맞췄다. 같은 포트 재바인딩 타이밍으로 인한 연결 거부를 방지한다.
- Kotlin server row는 계획대로 WSS phase를 추가하지 않고 TLS TCP 2 phase만 추가했다.
## 주요 설계 결정
- 각 server orchestrator는 자기 언어의 test cert fixture를 사용한다. 단 Go server orchestrator는 Go 패키지에 cert fixture가 없어 `dart/test/certs`를 사용한다.
- Kotlin server row는 WSS를 추가하지 않고 TLS TCP만 검증한다.
- 클라이언트 helper는 기존 TCP/WS 구조를 유지하고 `--cert`가 필요한 secure mode에서만 trust context/CA를 구성한다.
## 리뷰어를 위한 체크포인트
- **포트 충돌 없음**: 포트 배정표의 신규 TLS TCP / WSS 포트(볼드체)가 각 파일의 기존 TCP/WS 포트와 겹치지 않는지 확인.
- **인증서 경로 일관성**: 각 언어 orchestrator가 자신의 `test/certs/server.crt` + `server.key`를 사용하고, `--cert` 인수로 클라이언트에 전달하는지 확인.
- **클라이언트 `--mode` 확장**: 1-5번 클라이언트 파일 전체에서 `"tls"` / `"wss"` case가 기존 `"tcp"` / `"ws"` 구조와 동일한 패턴으로 추가됐는지 확인.
- **`buildClientTLS` / `buildClientSslContext` 헬퍼 복사 정확성**: Go 3개 클라이언트 파일과 Kotlin 4개 클라이언트 파일에 복사된 헬퍼가 기준 파일 (`dart_go_client/main.go`, `tls_support` TestHelpers.kt)과 일치하는지 확인.
- **TypeScript `typescript_kotlin` orchestrator**: WSS 없는 TLS TCP 2 phase만 포함됐는지 확인 (`typescript_kotlin_client.ts` 에 `wss` 케이스는 있어도 orchestrator에서 WSS phase를 호출하지 않음).
- **Push 메시지 검증 문자열**: 각 클라이언트 파일이 올바른 서버 언어명을 포함한 push 문자열을 검증하는지 확인 (예: `python_dart_client.dart``'push from python server'`).
- **의존 관계 순서**: 1단계(클라이언트 1-5) 완료 후 2단계(orchestrator 6-10)가 구현됐는지 확인.
## 검증 결과
정적/중간 검증:
```text
$ cd dart && dart analyze crosstest/kotlin_dart_client.dart crosstest/python_dart_client.dart crosstest/typescript_dart_client.dart crosstest/dart_kotlin.dart crosstest/dart_python.dart crosstest/dart_typescript.dart
No issues found!
$ cd go && env PATH=/config/go-sdk/go/bin:$PATH GOCACHE=/tmp/go-build GOMODCACHE=/tmp/go-mod go build ./crosstest/kotlin_go_client ./crosstest/python_go_client ./crosstest/typescript_go_client
(exit 0)
$ python3 -m py_compile python/crosstest/dart_python_client.py python/crosstest/go_python_client.py python/crosstest/kotlin_python_client.py python/crosstest/typescript_python_client.py python/crosstest/python_dart.py python/crosstest/python_go.py python/crosstest/python_kotlin.py python/crosstest/python_typescript.py
(exit 0)
$ cd typescript && npx tsc --noEmit
(exit 0)
$ cd kotlin && env JAVA_HOME=/config/opt/jdk/jdk-17.0.10+7 GRADLE_USER_HOME=/tmp/gradle ./gradlew compileCrosstestKotlin
BUILD SUCCESSFUL
```
최종 crosstest:
```text
$ cd dart && env JAVA_HOME=/config/opt/jdk/jdk-17.0.10+7 dart run crosstest/dart_kotlin.dart
PASS all dart-server/kotlin-client crosstests passed
$ cd dart && dart run crosstest/dart_python.dart
PASS all dart-server/python-client crosstests passed
$ cd dart && dart run crosstest/dart_typescript.dart
PASS all dart-server/typescript-client crosstests passed
$ cd go && env PATH=/config/go-sdk/go/bin:$PATH GOCACHE=/tmp/go-build GOMODCACHE=/tmp/go-mod JAVA_HOME=/config/opt/jdk/jdk-17.0.10+7 go run ./crosstest/go_kotlin.go
PASS all go-server/kotlin-client crosstests passed
$ cd go && env PATH=/config/go-sdk/go/bin:$PATH GOCACHE=/tmp/go-build GOMODCACHE=/tmp/go-mod go run ./crosstest/go_python.go
PASS all go-server/python-client crosstests passed
$ cd go && env PATH=/config/go-sdk/go/bin:$PATH GOCACHE=/tmp/go-build GOMODCACHE=/tmp/go-mod go run ./crosstest/go_typescript.go
PASS all go-server/typescript-client crosstests passed
$ cd kotlin && env JAVA_HOME=/config/opt/jdk/jdk-17.0.10+7 GRADLE_USER_HOME=/tmp/gradle ./gradlew run -PmainClass=com.tokilabs.toki_socket.crosstest.KotlinDartKt
PASS all kotlin-server/dart-client crosstests passed
$ cd kotlin && env PATH=/config/go-sdk/go/bin:$PATH JAVA_HOME=/config/opt/jdk/jdk-17.0.10+7 GRADLE_USER_HOME=/tmp/gradle GOCACHE=/tmp/go-build GOMODCACHE=/tmp/go-mod ./gradlew run -PmainClass=com.tokilabs.toki_socket.crosstest.KotlinGoKt
PASS all kotlin-server/go-client crosstests passed
$ cd kotlin && env JAVA_HOME=/config/opt/jdk/jdk-17.0.10+7 GRADLE_USER_HOME=/tmp/gradle ./gradlew run -PmainClass=com.tokilabs.toki_socket.crosstest.KotlinPythonKt
PASS all kotlin-server/python-client crosstests passed
$ cd kotlin && env JAVA_HOME=/config/opt/jdk/jdk-17.0.10+7 GRADLE_USER_HOME=/tmp/gradle ./gradlew run -PmainClass=com.tokilabs.toki_socket.crosstest.KotlinTypescriptKt
PASS all kotlin-server/typescript-client crosstests passed
$ cd python && python3 -m crosstest.python_dart
PASS all python-server/dart-client crosstests passed
$ cd python && env PATH=/config/go-sdk/go/bin:$PATH GOCACHE=/tmp/go-build GOMODCACHE=/tmp/go-mod python3 -m crosstest.python_go
PASS all python-server/go-client crosstests passed
$ cd python && env JAVA_HOME=/config/opt/jdk/jdk-17.0.10+7 python3 -m crosstest.python_kotlin
PASS all python-server/kotlin-client crosstests passed
$ cd python && python3 -m crosstest.python_typescript
PASS all python-server/typescript-client crosstests passed
$ cd typescript && node --import tsx crosstest/typescript_dart.ts
PASS all typescript-server/dart-client crosstests passed
$ cd typescript && env PATH=/config/go-sdk/go/bin:$PATH GOCACHE=/tmp/go-build GOMODCACHE=/tmp/go-mod node --import tsx crosstest/typescript_go.ts
PASS all typescript-server/go-client crosstests passed
$ cd typescript && env JAVA_HOME=/config/opt/jdk/jdk-17.0.10+7 node --import tsx crosstest/typescript_kotlin.ts
PASS all typescript-server/kotlin-client crosstests passed
$ cd typescript && node --import tsx crosstest/typescript_python.ts
PASS all typescript-server/python-client crosstests passed
```

View file

@ -0,0 +1,676 @@
<!-- task=tls_crosstest plan=1 tag=TLS_CROSSTEST -->
# TLS 크로스테스트 전 언어 확장
## 이 파일을 읽는 구현 에이전트에게
각 항목의 체크리스트를 완료 표시하고, 중간 검증 명령을 실행한 뒤 출력을 CODE_REVIEW.md `검증 결과` 섹션에 붙여 넣는다. 최종 검증까지 완료 후 CODE_REVIEW.md 각 항목을 `[x]`로 체크한다. **의존 관계 및 구현 순서** 섹션을 반드시 지킨다.
## 배경
`dart_go`·`go_dart` 두 조합에만 TLS TCP + WSS 크로스테스트가 존재한다. Python·TypeScript·Kotlin에 TLS 구현이 추가됨에 따라 나머지 18개 쌍에도 TLS 검증이 가능해졌다. 이번 작업으로 서버·클라이언트 양쪽 모두 TLS를 지원하는 모든 조합에 TLS 크로스테스트 phase를 추가한다.
**기준 파일:** `dart/crosstest/dart_go.dart` (orchestrator), `go/crosstest/dart_go_client/main.go` (client)
**인증서 경로 규칙:** 각 언어의 `test/certs/server.crt` + `server.key` 를 사용한다 (모두 `dart/test/certs/` 원본의 사본). orchestrator는 자신의 언어 경로로 서버 cert를 로드하고 `--cert=<path>` 로 클라이언트에 전달한다.
---
## 포트 배정표
| orchestrator | TCP | WS | TLS TCP (신규) | WSS (신규) |
|---|---|---|---|---|
| dart_kotlin | 29590 | 29592 | **29594** | **29596** |
| dart_python | 29694 | 29696 | **29698** | **29700** |
| dart_typescript | 29790 | 29792 | **29794** | **29796** |
| go_kotlin | 29290 | 29292 | **29294** | **29296** |
| go_python | 29390 | 29392 | **29394** | **29396** |
| go_typescript | 29490 | 29492 | **29494** | **29496** |
| kotlin_dart | 29490 | 29492 | **29494** | — |
| kotlin_go | 29390 | 29392 | **29394** | — |
| kotlin_python | 29394 | 29396 | **29398** | — |
| kotlin_typescript | 29794 | 29796 | **29798** | — |
| python_dart | 29494 | 29496 | **29498** | **29500** |
| python_go | 29490 | 29492 | **29494** | **29496** |
| python_kotlin | 29498 | 29500 | **29502** | **29504** |
| python_typescript | 29798 | 29800 | **29802** | **29804** |
| typescript_dart | 29802 | 29804 | **29806** | **29808** |
| typescript_go | 29806 | 29808 | **29810** | **29812** |
| typescript_kotlin | 29810 | 29812 | **29814** | — |
| typescript_python | 29814 | 29816 | **29818** | **29820** |
---
## [TLS_CROSSTEST-1] Dart 클라이언트 파일 — TLS 모드 추가
### 문제
`dart/crosstest/kotlin_dart_client.dart`, `dart/crosstest/python_dart_client.dart`, `dart/crosstest/typescript_dart_client.dart` 3개 파일이 `--mode=tls/wss`, `--cert` 인수를 처리하지 않는다.
### 해결 방법
`dart/crosstest/go_dart_client.dart` 패턴을 참조한다. 각 파일에서:
1. `args`에서 `--cert` 인수 파싱 추가
2. `_connectWithRetry` 또는 동등 함수의 `switch (mode)` 블록에 `'tls'`·`'wss'` 케이스 추가:
```dart
case 'tls':
final ctx = SecurityContext()..setTrustedCertificates(certPath!);
final socket = await ProtobufClient.connectSecure(_host, port, context: ctx);
return _ClientHandle(/* TCP client */, () async => /* close */);
case 'wss':
final ctx = SecurityContext()..setTrustedCertificates(certPath!);
final ws = await WsProtobufClient.connectSecure(_host, port, context: ctx);
return _ClientHandle(/* WS client */, () async => /* close */);
```
3. 각 파일에서 push 메시지 검증 문자열이 맞는지 확인:
- `kotlin_dart_client`: `'push from kotlin server'`
- `python_dart_client`: `'push from python server'`
- `typescript_dart_client`: `'push from typescript server'`
### 수정 파일 및 체크리스트
- `dart/crosstest/kotlin_dart_client.dart`
- [x] `--cert` 파라미터 파싱 추가
- [x] `'tls'`·`'wss'` 모드 케이스 추가
- `dart/crosstest/python_dart_client.dart`
- [x] `--cert` 파라미터 파싱 추가
- [x] `'tls'`·`'wss'` 모드 케이스 추가
- `dart/crosstest/typescript_dart_client.dart`
- [x] `--cert` 파라미터 파싱 추가
- [x] `'tls'`·`'wss'` 모드 케이스 추가
### 테스트 작성
단독 실행 테스트 없음. TLS_CROSSTEST-8~10의 orchestrator 검증으로 확인.
### 중간 검증
```
$ cd dart && dart analyze crosstest/kotlin_dart_client.dart crosstest/python_dart_client.dart crosstest/typescript_dart_client.dart
No issues found!
```
---
## [TLS_CROSSTEST-2] Go 클라이언트 파일 — TLS 모드 추가
### 문제
`go/crosstest/kotlin_go_client/main.go`, `go/crosstest/python_go_client/main.go`, `go/crosstest/typescript_go_client/main.go` 3개 파일이 `--mode=tls/wss`, `--cert` 를 처리하지 않는다.
### 해결 방법
`go/crosstest/dart_go_client/main.go` 패턴을 참조한다. 각 파일에서:
1. `flag.String("cert", "", ...)` 플래그 추가
2. `dial()` 함수의 `switch mode` 블록에 `"tls"`·`"wss"` 케이스 추가:
```go
case "tls":
tlsCfg, err := buildClientTLS(*cert)
client, err = toki.DialTcpTLS(ctx, host, port, tlsCfg, 0, 0, parserMap())
case "wss":
tlsCfg, err := buildClientTLS(*cert)
client, err = toki.DialWssWithHeartbeat(ctx, host, port, wsPath, tlsCfg, 0, 0, parserMap())
```
3. `buildClientTLS(certFile string)` 헬퍼를 `dart_go_client/main.go`에서 복사 추가
4. 각 파일 push 메시지 검증 문자열 확인:
- `kotlin_go_client`: `"push from kotlin server"`
- `python_go_client`: `"push from python server"`
- `typescript_go_client`: `"push from typescript server"`
### 수정 파일 및 체크리스트
- `go/crosstest/kotlin_go_client/main.go`
- [x] `--cert` 플래그 추가
- [x] `"tls"`·`"wss"` 케이스 및 `buildClientTLS` 헬퍼 추가
- `go/crosstest/python_go_client/main.go`
- [x] 동일
- `go/crosstest/typescript_go_client/main.go`
- [x] 동일
### 테스트 작성
없음. TLS_CROSSTEST-7~9의 orchestrator 검증으로 확인.
### 중간 검증
```
$ cd go && env PATH=/config/go-sdk/go/bin:$PATH GOCACHE=/tmp/go-build GOMODCACHE=/tmp/go-mod \
go build ./crosstest/kotlin_go_client ./crosstest/python_go_client ./crosstest/typescript_go_client
(exit 0)
```
---
## [TLS_CROSSTEST-3] Kotlin 클라이언트 파일 — TLS 모드 추가
### 문제
`kotlin/crosstest/dart_kotlin_client.kt`, `go_kotlin_client/Main.kt`, `python_kotlin_client.kt`, `typescript_kotlin_client.kt` 4개 파일이 `--mode=tls/wss`, `--cert` 를 처리하지 않는다.
### 해결 방법
각 파일에서:
1. `argValue(args, "cert")` 파싱 추가
2. `connectWithRetry(mode, port, cert)` 시그니처에 `cert: String?` 추가
3. `when (mode)` 블록에 `"tls"`·`"wss"` 케이스 추가:
```kotlin
"tls" -> {
val sslCtx = buildClientSslContext(cert ?: error("--cert required for tls"))
DialTcpTls(HOST, port, sslCtx, 0, 0, parserMap)
}
"wss" -> {
val sslCtx = buildClientSslContext(cert ?: error("--cert required for wss"))
dialWss(HOST, port, WS_PATH, sslCtx, 0, 0, parserMap)
}
```
4. `buildClientSslContext(certPath: String): SSLContext` 헬퍼 추가:
```kotlin
fun buildClientSslContext(certPath: String): SSLContext {
val certFactory = java.security.cert.CertificateFactory.getInstance("X.509")
val cert = java.io.File(certPath).inputStream().use {
certFactory.generateCertificate(it) as java.security.cert.X509Certificate
}
val ts = java.security.KeyStore.getInstance("PKCS12")
ts.load(null, null)
ts.setCertificateEntry("toki", cert)
val tmf = javax.net.ssl.TrustManagerFactory.getInstance(
javax.net.ssl.TrustManagerFactory.getDefaultAlgorithm()
)
tmf.init(ts)
val ctx = javax.net.ssl.SSLContext.getInstance("TLS")
ctx.init(null, tmf.trustManagers, null)
return ctx
}
```
5. 각 파일 push 메시지 검증 문자열 확인 (`'push from dart/go/python/typescript server'`).
**참고:** `go_kotlin_client`는 Gradle 서브프로젝트 (`kotlin/crosstest/go_kotlin_client/`). 나머지 세 파일은 독립 `.kt` 파일.
### 수정 파일 및 체크리스트
- `kotlin/crosstest/dart_kotlin_client.kt`
- [x] `--cert` 파싱, `"tls"`·`"wss"` 케이스, `buildClientSslContext` 추가
- `kotlin/crosstest/go_kotlin_client/src/main/kotlin/com/tokilabs/toki_socket/crosstest/Main.kt`
- [x] 동일
- `kotlin/crosstest/python_kotlin_client.kt`
- [x] 동일
- `kotlin/crosstest/typescript_kotlin_client.kt`
- [x] 동일
### 테스트 작성
없음. TLS_CROSSTEST-6~10의 orchestrator 검증으로 확인.
### 중간 검증
```
$ cd kotlin && env JAVA_HOME=/config/opt/jdk/jdk-17.0.10+7 GRADLE_USER_HOME=/tmp/gradle \
./gradlew compileCrosstestKotlin
BUILD SUCCESSFUL
```
---
## [TLS_CROSSTEST-4] Python 클라이언트 파일 — TLS 모드 추가
### 문제
`python/crosstest/dart_python_client.py`, `go_python_client.py`, `kotlin_python_client.py`, `typescript_python_client.py` 4개 파일이 TLS 모드를 처리하지 않는다.
### 해결 방법
각 파일에서:
1. `argparse``--cert` 인수 추가 (`default=None`)
2. `choices=["tcp", "ws"]``["tcp", "ws", "tls", "wss"]` 변경
3. `dial()` 함수에 `"tls"`·`"wss"` 케이스 추가:
```python
elif mode == "tls":
ssl_ctx = ssl.create_default_context(cafile=cert)
ssl_ctx.check_hostname = False
return await connect_tcp_tls(HOST, port, ssl_ctx, 0, 0, parser_map())
elif mode == "wss":
ssl_ctx = ssl.create_default_context(cafile=cert)
ssl_ctx.check_hostname = False
return await connect_wss(HOST, port, WS_PATH, ssl_ctx, 0, 0, parser_map())
```
4. `import ssl`, `connect_tcp_tls`, `connect_wss` import 추가
### 수정 파일 및 체크리스트
- `python/crosstest/dart_python_client.py`
- [x] `import ssl`, `connect_tcp_tls`, `connect_wss` 추가
- [x] `--cert` argparse 인수 추가, choices 확장
- [x] `"tls"`·`"wss"` 분기 추가
- `python/crosstest/go_python_client.py`
- [x] 동일
- `python/crosstest/kotlin_python_client.py`
- [x] 동일
- `python/crosstest/typescript_python_client.py`
- [x] 동일
### 중간 검증
```
$ python3 -m py_compile python/crosstest/dart_python_client.py \
python/crosstest/go_python_client.py \
python/crosstest/kotlin_python_client.py \
python/crosstest/typescript_python_client.py
(exit 0)
```
---
## [TLS_CROSSTEST-5] TypeScript 클라이언트 파일 — TLS 모드 추가
### 문제
`typescript/crosstest/dart_typescript_client.ts`, `go_typescript_client.ts`, `kotlin_typescript_client.ts`, `python_typescript_client.ts` 4개 파일이 TLS 모드를 처리하지 않는다.
### 해결 방법
각 파일에서:
1. `parseArgs()``mode` 타입을 `"tcp" | "ws" | "tls" | "wss"` 로 확장
2. `--cert` 인수 파싱 추가
3. `dialWithRetry` / `dial()``cert?: string` 파라미터 추가
4. `dial()` switch 에 `"tls"`·`"wss"` 케이스 추가:
```typescript
case "tls": {
const ca = fs.readFileSync(cert!);
return connectTcpTls(HOST, port, { ca }, 0, 0, parserMap());
}
case "wss": {
const ca = fs.readFileSync(cert!);
return connectWss(HOST, port, WS_PATH, { ca }, 0, 0, parserMap());
}
```
5. `import * as fs from "node:fs"`, `connectTcpTls`, `connectWss` import 추가
### 수정 파일 및 체크리스트
- `typescript/crosstest/dart_typescript_client.ts`
- [x] `fs`, `connectTcpTls`, `connectWss` import 추가
- [x] `--cert` 파싱, mode 타입 확장, `dial()` 케이스 추가
- `typescript/crosstest/go_typescript_client.ts`
- [x] 동일
- `typescript/crosstest/kotlin_typescript_client.ts`
- [x] 동일
- `typescript/crosstest/python_typescript_client.ts`
- [x] 동일
### 중간 검증
```
$ cd typescript && npx tsc --noEmit
(exit 0)
```
---
## [TLS_CROSSTEST-6] Dart orchestrator 3개 — TLS phase 추가
### 문제
`dart/crosstest/dart_kotlin.dart`, `dart/crosstest/dart_python.dart`, `dart/crosstest/dart_typescript.dart` 에 TLS phase가 없다.
### 해결 방법
`dart/crosstest/dart_go.dart` 패턴을 참조한다. 각 파일에서:
1. TLS/WSS 포트 상수 추가:
- `dart_kotlin`: `_tlsTcpPort = 29594`, `_wssPort = 29596`
- `dart_python`: `_tlsTcpPort = 29698`, `_wssPort = 29700`
- `dart_typescript`: `_tlsTcpPort = 29794`, `_wssPort = 29796`
2. `_DartTlsTcpServer`, `_DartWssServer` 클래스 추가 (dart_go.dart 와 동일 구조)
3. `_runTls()` 메서드 추가 — TLS TCP send-push, TLS TCP requests, WSS send-push, WSS requests 4개 phase
4. `main()`에서 `await _runTls()` 호출 추가
5. 각 파일의 클라이언트 실행 헬퍼에서 `--cert=<path>` 전달
6. 인증서 경로: `dart/test/certs/server.crt`, `dart/test/certs/server.key`
### 수정 파일 및 체크리스트
- `dart/crosstest/dart_kotlin.dart`
- [x] TLS/WSS 포트 상수 추가 (29594, 29596)
- [x] `_DartTlsTcpServer`, `_DartWssServer` 클래스 추가
- [x] `_runTls()` 메서드 추가 (4 phase)
- [x] `main()`에서 `_runTls()` 호출 추가
- [x] Kotlin 클라이언트 호출에 TLS 인수 전달
- `dart/crosstest/dart_python.dart`
- [x] 동일 (포트 29698, 29700)
- `dart/crosstest/dart_typescript.dart`
- [x] 동일 (포트 29794, 29796)
### 중간 검증
```
$ cd dart && dart analyze crosstest/dart_kotlin.dart crosstest/dart_python.dart crosstest/dart_typescript.dart
No issues found!
$ cd dart && dart run crosstest/dart_python.dart
PASS all dart-server/python-client crosstests passed
$ cd dart && dart run crosstest/dart_typescript.dart
PASS all dart-server/typescript-client crosstests passed
$ cd dart && env JAVA_HOME=/config/opt/jdk/jdk-17.0.10+7 dart run crosstest/dart_kotlin.dart
PASS all dart-server/kotlin-client crosstests passed
```
---
## [TLS_CROSSTEST-7] Go orchestrator 3개 — TLS phase 추가
### 문제
`go/crosstest/go_kotlin.go`, `go/crosstest/go_python.go`, `go/crosstest/go_typescript.go` 에 TLS phase가 없다.
### 해결 방법
`go/crosstest/go_dart.go` 패턴 참조. 각 파일에서:
1. TLS/WSS 포트 상수 추가:
- `go_kotlin`: `goKotlinTLSPort = 29294`, `goKotlinWSSPort = 29296`
- `go_python`: `goPythonTLSPort = 29394`, `goPythonWSSPort = 29396`
- `go_typescript`: `goTypescriptTLSPort = 29494`, `goTypescriptWSSPort = 29496`
2. `crypto/tls` import 추가 (미존재 시)
3. `loadServerTLS(certFile, keyFile)` 헬퍼 추가 (go_dart.go 에서 복사)
4. `runTLSTCPSendPush`, `runTLSTCPRequests`, `runWSSendPushSecure`, `runWSSRequestsSecure` 추가
5. `run()` 에서 TLS phase 호출 추가
6. 인증서: `go_dart.go`와 동일하게 `dartDir``test/certs/` 에서 로드
### 수정 파일 및 체크리스트
- `go/crosstest/go_kotlin.go`
- [x] TLS 포트 상수 (29294, 29296), `loadServerTLS`, TLS phase 함수 4개 추가
- [x] `run()`에서 호출 추가
- `go/crosstest/go_python.go`
- [x] 동일 (포트 29394, 29396)
- `go/crosstest/go_typescript.go`
- [x] 동일 (포트 29494, 29496)
### 중간 검증
```
$ cd go && env PATH=/config/go-sdk/go/bin:$PATH GOCACHE=/tmp/go-build GOMODCACHE=/tmp/go-mod \
go run ./crosstest/go_python.go
PASS all go-server/python-client crosstests passed
$ cd go && env PATH=/config/go-sdk/go/bin:$PATH GOCACHE=/tmp/go-build GOMODCACHE=/tmp/go-mod \
go run ./crosstest/go_typescript.go
PASS all go-server/typescript-client crosstests passed
$ cd go && env PATH=/config/go-sdk/go/bin:$PATH JAVA_HOME=/config/opt/jdk/jdk-17.0.10+7 GOCACHE=/tmp/go-build GOMODCACHE=/tmp/go-mod \
go run ./crosstest/go_kotlin.go
PASS all go-server/kotlin-client crosstests passed
```
---
## [TLS_CROSSTEST-8] Kotlin orchestrator 4개 — TLS TCP phase 추가
### 문제
`kotlin_dart.kt`, `kotlin_go.kt`, `kotlin_python.kt`, `kotlin_typescript.kt` 에 TLS phase가 없다. Kotlin `WsServer` TLS는 이번 TLS_CROSSTEST 범위에서 사용하지 않으므로 TLS TCP 2개 phase만 추가한다.
### 해결 방법
각 파일에서:
1. TLS TCP 포트 상수 추가:
- `kotlin_dart`: TLS TCP `29494`
- `kotlin_go`: TLS TCP `29394`
- `kotlin_python`: TLS TCP `29398`
- `kotlin_typescript`: TLS TCP `29798`
2. 인증서 로딩: `kotlin/src/test/resources/server.crt` + `server.key` 경로를 repo root 기준으로 계산
3. `runTlsTcp(sslContext, certPath)` 함수 추가 — send-push, requests 2 phase
4. Kotlin `WsServer`는 TLS를 지원하지 않으므로 WSS phase는 추가하지 않는다.
5. TLS TCP 서버는 `TcpServer(..., sslContext = serverCtx)` 생성자 사용
6. main 또는 run 함수에서 `runTlsTcp()` 호출 추가
### 수정 파일 및 체크리스트
- `kotlin/crosstest/kotlin_dart.kt`
- [x] TLS TCP 포트 상수 (29494)
- [x] 인증서 경로 계산 로직 추가
- [x] `runTlsTcp()` 함수 추가 (send-push, requests)
- [x] main에서 `runTlsTcp()` 호출 추가
- [x] WSS phase 제외 (Kotlin `WsServer` TLS 미지원)
- `kotlin/crosstest/kotlin_go.kt`
- [x] 동일 (TLS TCP 포트 29394)
- `kotlin/crosstest/kotlin_python.kt`
- [x] 동일 (TLS TCP 포트 29398)
- `kotlin/crosstest/kotlin_typescript.kt`
- [x] 동일 (TLS TCP 포트 29798)
### 중간 검증
```
$ cd kotlin && env JAVA_HOME=/config/opt/jdk/jdk-17.0.10+7 GRADLE_USER_HOME=/tmp/gradle \
./gradlew run -PmainClass=com.tokilabs.toki_socket.crosstest.KotlinDartKt
PASS all kotlin-server/dart-client crosstests passed
$ cd kotlin && env JAVA_HOME=/config/opt/jdk/jdk-17.0.10+7 GRADLE_USER_HOME=/tmp/gradle \
./gradlew run -PmainClass=com.tokilabs.toki_socket.crosstest.KotlinGoKt
PASS all kotlin-server/go-client crosstests passed
$ cd kotlin && env JAVA_HOME=/config/opt/jdk/jdk-17.0.10+7 GRADLE_USER_HOME=/tmp/gradle \
./gradlew run -PmainClass=com.tokilabs.toki_socket.crosstest.KotlinPythonKt
PASS all kotlin-server/python-client crosstests passed
$ cd kotlin && env JAVA_HOME=/config/opt/jdk/jdk-17.0.10+7 GRADLE_USER_HOME=/tmp/gradle \
./gradlew run -PmainClass=com.tokilabs.toki_socket.crosstest.KotlinTypescriptKt
PASS all kotlin-server/typescript-client crosstests passed
```
---
## [TLS_CROSSTEST-9] Python orchestrator 4개 — TLS phase 추가
### 문제
`python/crosstest/python_dart.py`, `python_go.py`, `python_kotlin.py`, `python_typescript.py` 에 TLS phase가 없다.
### 해결 방법
각 파일에서:
1. TLS/WSS 포트 상수 추가:
- `python_dart`: `TLS_TCP_PORT = 29498`, `WSS_PORT = 29500`
- `python_go`: `TLS_TCP_PORT = 29494`, `WSS_PORT = 29496`
- `python_kotlin`: `TLS_TCP_PORT = 29502`, `WSS_PORT = 29504`
- `python_typescript`: `TLS_TCP_PORT = 29802`, `WSS_PORT = 29804`
2. `import ssl` 추가
3. `_make_server_ssl_context()` 헬퍼 추가:
```python
def _make_server_ssl_context() -> ssl.SSLContext:
ctx = ssl.SSLContext(ssl.PROTOCOL_TLS_SERVER)
ctx.load_cert_chain(CERT_PATH, KEY_PATH)
return ctx
```
4. `CERT_PATH`, `KEY_PATH` 상수: 각 파일 기준 repo root에서 계산
5. `run_tls()` 코루틴 추가 — TLS TCP send-push, TLS TCP requests, WSS send-push, WSS requests
6. `main()` 또는 `run()` 에서 `await run_tls()` 호출 추가
7. `--cert` 로 클라이언트 subprocess에 인증서 경로 전달
### 수정 파일 및 체크리스트
- `python/crosstest/python_dart.py`
- [x] TLS/WSS 포트 상수 (29498, 29500), cert 경로, `run_tls()` 추가
- `python/crosstest/python_go.py`
- [x] 동일 (29494, 29496)
- `python/crosstest/python_kotlin.py`
- [x] 동일 (29502, 29504)
- `python/crosstest/python_typescript.py`
- [x] 동일 (29802, 29804)
### 중간 검증
```
$ python3 -m py_compile python/crosstest/python_dart.py python/crosstest/python_go.py \
python/crosstest/python_kotlin.py python/crosstest/python_typescript.py
(exit 0)
$ cd python && env PATH=/config/go-sdk/go/bin:$PATH GOCACHE=/tmp/go-build GOMODCACHE=/tmp/go-mod python3 -m crosstest.python_go
PASS all python-server/go-client crosstests passed
$ cd python && python3 -m crosstest.python_typescript
PASS all python-server/typescript-client crosstests passed
$ cd python && python3 -m crosstest.python_dart
PASS all python-server/dart-client crosstests passed
$ cd python && env JAVA_HOME=/config/opt/jdk/jdk-17.0.10+7 python3 -m crosstest.python_kotlin
PASS all python-server/kotlin-client crosstests passed
```
---
## [TLS_CROSSTEST-10] TypeScript orchestrator 4개 — TLS phase 추가
### 문제
`typescript/crosstest/typescript_dart.ts`, `typescript_go.ts`, `typescript_kotlin.ts`, `typescript_python.ts` 에 TLS phase가 없다.
### 해결 방법
각 파일에서:
1. TLS/WSS 포트 상수 추가:
- `typescript_dart`: `TLS_TCP_PORT = 29806`, `WSS_PORT = 29808`
- `typescript_go`: `TLS_TCP_PORT = 29810`, `WSS_PORT = 29812`
- `typescript_kotlin`: `TLS_TCP_PORT = 29814` (WSS 없음)
- `typescript_python`: `TLS_TCP_PORT = 29818`, `WSS_PORT = 29820`
2. `import * as fs from "node:fs"` 추가
3. `CERT_PATH`, `KEY_PATH`: repo root 기준 계산
4. `runTls()` 함수 추가:
- `typescript_kotlin`: TLS TCP 2 phase만 (Kotlin WsServer TLS 미지원)
- 나머지: TLS TCP send-push, TLS TCP requests, WSS send-push, WSS requests
5. main 에서 `await runTls()` 호출 추가
6. `TcpServer` 생성 시 `tlsOptions: { cert, key }` 전달
7. `WsServer` 생성 시 `tlsOptions: { cert, key }` 전달 (`typescript_kotlin` 제외)
8. 클라이언트 subprocess에 `--mode=tls`, `--cert=CERT_PATH` 전달
### 수정 파일 및 체크리스트
- `typescript/crosstest/typescript_dart.ts`
- [x] TLS/WSS 포트 상수 (29806, 29808), cert 경로, `runTls()` 추가
- `typescript/crosstest/typescript_go.ts`
- [x] 동일 (29810, 29812)
- `typescript/crosstest/typescript_kotlin.ts`
- [x] TLS 포트 상수 (29814), `runTls()` — TLS TCP 2 phase만
- `typescript/crosstest/typescript_python.ts`
- [x] 동일 (29818, 29820)
### 중간 검증
```
$ cd typescript && npx tsc --noEmit
(exit 0)
$ cd typescript && env PATH=/config/go-sdk/go/bin:$PATH GOCACHE=/tmp/go-build GOMODCACHE=/tmp/go-mod node --import tsx crosstest/typescript_go.ts
PASS all typescript-server/go-client crosstests passed
$ cd typescript && node --import tsx crosstest/typescript_python.ts
PASS all typescript-server/python-client crosstests passed
$ cd typescript && node --import tsx crosstest/typescript_dart.ts
PASS all typescript-server/dart-client crosstests passed
$ cd typescript && env JAVA_HOME=/config/opt/jdk/jdk-17.0.10+7 node --import tsx crosstest/typescript_kotlin.ts
PASS all typescript-server/kotlin-client crosstests passed
```
---
## 수정 파일 요약
| 파일 | 항목 |
|------|------|
| `dart/crosstest/kotlin_dart_client.dart` | TLS_CROSSTEST-1 |
| `dart/crosstest/python_dart_client.dart` | TLS_CROSSTEST-1 |
| `dart/crosstest/typescript_dart_client.dart` | TLS_CROSSTEST-1 |
| `go/crosstest/kotlin_go_client/main.go` | TLS_CROSSTEST-2 |
| `go/crosstest/python_go_client/main.go` | TLS_CROSSTEST-2 |
| `go/crosstest/typescript_go_client/main.go` | TLS_CROSSTEST-2 |
| `kotlin/crosstest/dart_kotlin_client.kt` | TLS_CROSSTEST-3 |
| `kotlin/crosstest/go_kotlin_client/src/main/kotlin/com/tokilabs/toki_socket/crosstest/Main.kt` | TLS_CROSSTEST-3 |
| `kotlin/crosstest/python_kotlin_client.kt` | TLS_CROSSTEST-3 |
| `kotlin/crosstest/typescript_kotlin_client.kt` | TLS_CROSSTEST-3 |
| `python/crosstest/dart_python_client.py` | TLS_CROSSTEST-4 |
| `python/crosstest/go_python_client.py` | TLS_CROSSTEST-4 |
| `python/crosstest/kotlin_python_client.py` | TLS_CROSSTEST-4 |
| `python/crosstest/typescript_python_client.py` | TLS_CROSSTEST-4 |
| `typescript/crosstest/dart_typescript_client.ts` | TLS_CROSSTEST-5 |
| `typescript/crosstest/go_typescript_client.ts` | TLS_CROSSTEST-5 |
| `typescript/crosstest/kotlin_typescript_client.ts` | TLS_CROSSTEST-5 |
| `typescript/crosstest/python_typescript_client.ts` | TLS_CROSSTEST-5 |
| `dart/crosstest/dart_kotlin.dart` | TLS_CROSSTEST-6 |
| `dart/crosstest/dart_python.dart` | TLS_CROSSTEST-6 |
| `dart/crosstest/dart_typescript.dart` | TLS_CROSSTEST-6 |
| `go/crosstest/go_kotlin.go` | TLS_CROSSTEST-7 |
| `go/crosstest/go_python.go` | TLS_CROSSTEST-7 |
| `go/crosstest/go_typescript.go` | TLS_CROSSTEST-7 |
| `kotlin/crosstest/kotlin_dart.kt` | TLS_CROSSTEST-8 |
| `kotlin/crosstest/kotlin_go.kt` | TLS_CROSSTEST-8 |
| `kotlin/crosstest/kotlin_python.kt` | TLS_CROSSTEST-8 |
| `kotlin/crosstest/kotlin_typescript.kt` | TLS_CROSSTEST-8 |
| `python/crosstest/python_dart.py` | TLS_CROSSTEST-9 |
| `python/crosstest/python_go.py` | TLS_CROSSTEST-9 |
| `python/crosstest/python_kotlin.py` | TLS_CROSSTEST-9 |
| `python/crosstest/python_typescript.py` | TLS_CROSSTEST-9 |
| `typescript/crosstest/typescript_dart.ts` | TLS_CROSSTEST-10 |
| `typescript/crosstest/typescript_go.ts` | TLS_CROSSTEST-10 |
| `typescript/crosstest/typescript_kotlin.ts` | TLS_CROSSTEST-10 |
| `typescript/crosstest/typescript_python.ts` | TLS_CROSSTEST-10 |
## 의존 관계 및 구현 순서
**1단계 (클라이언트 파일 — 병렬 가능):**
TLS_CROSSTEST-1, TLS_CROSSTEST-2, TLS_CROSSTEST-3, TLS_CROSSTEST-4, TLS_CROSSTEST-5
**2단계 (orchestrator 파일 — 각 클라이언트 완료 후):**
- TLS_CROSSTEST-6 → TLS_CROSSTEST-1(Dart client), TLS_CROSSTEST-3(Kotlin), TLS_CROSSTEST-4(Python), TLS_CROSSTEST-5(TypeScript) 완료 후
- TLS_CROSSTEST-7 → TLS_CROSSTEST-2(Go), TLS_CROSSTEST-3(Kotlin), TLS_CROSSTEST-4(Python), TLS_CROSSTEST-5(TypeScript) 완료 후
- TLS_CROSSTEST-8 → TLS_CROSSTEST-1(Dart), TLS_CROSSTEST-2(Go), TLS_CROSSTEST-4(Python), TLS_CROSSTEST-5(TypeScript) 완료 후
- TLS_CROSSTEST-9 → TLS_CROSSTEST-1(Dart), TLS_CROSSTEST-2(Go), TLS_CROSSTEST-3(Kotlin), TLS_CROSSTEST-5(TypeScript) 완료 후
- TLS_CROSSTEST-10 → TLS_CROSSTEST-1(Dart), TLS_CROSSTEST-2(Go), TLS_CROSSTEST-3(Kotlin), TLS_CROSSTEST-4(Python) 완료 후
## 최종 검증
```
$ cd dart && dart run crosstest/dart_kotlin.dart
PASS all dart-server/kotlin-client crosstests passed
$ cd dart && dart run crosstest/dart_python.dart
PASS all dart-server/python-client crosstests passed
$ cd dart && dart run crosstest/dart_typescript.dart
PASS all dart-server/typescript-client crosstests passed
$ cd go && env PATH=/config/go-sdk/go/bin:$PATH GOCACHE=/tmp/go-build GOMODCACHE=/tmp/go-mod \
go run ./crosstest/go_kotlin.go && go run ./crosstest/go_python.go && go run ./crosstest/go_typescript.go
PASS all go-server/kotlin-client crosstests passed
PASS all go-server/python-client crosstests passed
PASS all go-server/typescript-client crosstests passed
$ cd kotlin && env JAVA_HOME=/config/opt/jdk/jdk-17.0.10+7 GRADLE_USER_HOME=/tmp/gradle \
./gradlew run -PmainClass=com.tokilabs.toki_socket.crosstest.KotlinDartKt && \
./gradlew run -PmainClass=com.tokilabs.toki_socket.crosstest.KotlinGoKt && \
./gradlew run -PmainClass=com.tokilabs.toki_socket.crosstest.KotlinPythonKt && \
./gradlew run -PmainClass=com.tokilabs.toki_socket.crosstest.KotlinTypescriptKt
(모두 PASS)
$ cd python && python3 -m crosstest.python_dart && python3 -m crosstest.python_go && \
python3 -m crosstest.python_kotlin && python3 -m crosstest.python_typescript
(모두 PASS)
$ cd typescript && node --import tsx crosstest/typescript_dart.ts && \
node --import tsx crosstest/typescript_go.ts && \
node --import tsx crosstest/typescript_kotlin.ts && \
node --import tsx crosstest/typescript_python.ts
(모두 PASS)
```

View file

@ -0,0 +1,227 @@
<!-- task=tls_crosstest plan=0 tag=TLS_CROSSTEST -->
# Code Review Reference - TLS_CROSSTEST
## 개요
date=2026-04-26
task=tls_crosstest, plan=0, tag=TLS_CROSSTEST
## 이 파일을 읽는 리뷰 에이전트에게
각 항목의 구현을 실제 소스 파일과 대조하고, `검증 결과` 섹션의 출력이 코드와 일치하는지 확인하세요.
리뷰 완료 후 반드시 아래 순서로 아카이브하세요.
1. `CODE_REVIEW.md` → `code_review_0.log` (N = 기존 code_review_*.log 수)
2. `PLAN.md` → `plan_0.log` (M = 기존 plan_*.log 수)
3. PASS인 경우 `complete.log` 작성 후 종료. WARN/FAIL인 경우 새 `PLAN.md` + `CODE_REVIEW.md` 스텁 작성.
---
## 구현 항목별 완료 여부
| 항목 | 완료 여부 |
|------|---------|
| [TLS_CROSSTEST-1] Dart 클라이언트 파일 — TLS 모드 추가 | [x] |
| [TLS_CROSSTEST-2] Go 클라이언트 파일 — TLS 모드 추가 | [x] |
| [TLS_CROSSTEST-3] Kotlin 클라이언트 파일 — TLS 모드 추가 | [x] |
| [TLS_CROSSTEST-4] Python 클라이언트 파일 — TLS 모드 추가 | [x] |
| [TLS_CROSSTEST-5] TypeScript 클라이언트 파일 — TLS 모드 추가 | [x] |
| [TLS_CROSSTEST-6] Dart orchestrator 3개 — TLS phase 추가 | [x] |
| [TLS_CROSSTEST-7] Go orchestrator 3개 — TLS phase 추가 | [x] |
| [TLS_CROSSTEST-8] Kotlin orchestrator 4개 — TLS TCP phase 추가 | [x] |
| [TLS_CROSSTEST-9] Python orchestrator 4개 — TLS phase 추가 | [x] |
| [TLS_CROSSTEST-10] TypeScript orchestrator 4개 — TLS phase 추가 | [x] |
## 계획 대비 변경 사항
- Kotlin server orchestrator는 최초 PLAN 일부에 WSS 항목이 섞여 있었으나, 배경/체크포인트의 "Kotlin WsServer TLS 미지원" 규칙에 맞춰 TLS TCP phase만 추가했다.
- Kotlin 클라이언트를 Gradle subprocess로 실행하는 orchestrator(`dart_kotlin`, `go_kotlin`, `python_kotlin`, `typescript_kotlin`)는 cold start가 20초를 넘을 수 있어 timeout을 60초로 늘렸다.
- Python crosstest 클라이언트는 TLS close가 완료 후 hang 되는 경우가 있어 검증 완료 후 `client.close()`를 1초 timeout으로 감쌌다.
- Kotlin server/Python client 조합에서 request listener 등록 전 request가 도착하는 race를 피하기 위해 Python crosstest 클라이언트에 연결 직후 50ms delay를 추가했다.
- `kotlin_python` WS phase는 같은 포트에서 `WsServer` 재시작이 timeout 되는 경우가 있어 `kotlin_typescript` 패턴처럼 하나의 WS 서버에서 send-push와 requests를 연속 실행하도록 맞췄다.
## 주요 설계 결정
- 각 server orchestrator는 자기 언어의 test cert fixture를 사용한다. 단 Go server orchestrator는 기존 `go_dart.go`와 동일하게 Go 패키지에 cert fixture가 없어 `dart/test/certs`를 사용한다.
- Kotlin server row는 WSS를 추가하지 않고 TLS TCP만 검증한다.
- 클라이언트 helper는 기존 TCP/WS 구조를 유지하고 `--cert`가 필요한 secure mode에서만 trust context/CA를 구성한다.
## 리뷰어를 위한 체크포인트
- **포트 충돌 없음**: 포트 배정표의 신규 TLS TCP / WSS 포트(볼드체)가 각 파일의 기존 TCP/WS 포트와 겹치지 않는지 확인.
- **인증서 경로 일관성**: 각 언어 orchestrator가 자신의 `test/certs/server.crt` + `server.key`를 사용하고, `--cert` 인수로 클라이언트에 전달하는지 확인.
- **클라이언트 `--mode` 확장**: 1-5번 클라이언트 파일 전체에서 `"tls"` / `"wss"` case가 기존 `"tcp"` / `"ws"` 구조와 동일한 패턴으로 추가됐는지 확인.
- **`buildClientTLS` / `buildClientSslContext` 헬퍼 복사 정확성**: Go 3개 클라이언트 파일과 Kotlin 4개 클라이언트 파일에 복사된 헬퍼가 기준 파일 (`dart_go_client/main.go`, `tls_support` TestHelpers.kt)과 일치하는지 확인.
- **TypeScript `typescript_kotlin` orchestrator**: WSS 없는 TLS TCP 2 phase만 포함됐는지 확인 (`typescript_kotlin_client.ts` 에 `wss` 케이스는 있어도 orchestrator에서 WSS phase를 호출하지 않음).
- **Push 메시지 검증 문자열**: 각 클라이언트 파일이 올바른 서버 언어명을 포함한 push 문자열을 검증하는지 확인 (예: `python_dart_client.dart` → `'push from python server'`).
- **의존 관계 순서**: 1단계(클라이언트 1-5) 완료 후 2단계(orchestrator 6-10)가 구현됐는지 확인.
## 검증 결과
아래 명령들은 2026-04-26에 실행했다. Gradle/Go가 필요한 조합은 로컬 SDK 경로를 환경 변수로 지정했다.
### TLS_CROSSTEST-1 중간 검증
```
$ cd dart && dart analyze crosstest/kotlin_dart_client.dart crosstest/python_dart_client.dart crosstest/typescript_dart_client.dart
No issues found!
```
### TLS_CROSSTEST-2 중간 검증
```
$ cd go && env PATH=/config/go-sdk/go/bin:$PATH GOCACHE=/tmp/go-build GOMODCACHE=/tmp/go-mod \
go build ./crosstest/kotlin_go_client ./crosstest/python_go_client ./crosstest/typescript_go_client
(exit 0)
```
### TLS_CROSSTEST-3 중간 검증
```
$ cd kotlin && env JAVA_HOME=/config/opt/jdk/jdk-17.0.10+7 GRADLE_USER_HOME=/tmp/gradle \
./gradlew compileCrosstestKotlin
BUILD SUCCESSFUL
```
### TLS_CROSSTEST-4 중간 검증
```
$ python3 -m py_compile python/crosstest/dart_python_client.py \
python/crosstest/go_python_client.py \
python/crosstest/kotlin_python_client.py \
python/crosstest/typescript_python_client.py
(exit 0)
```
### TLS_CROSSTEST-5 중간 검증
```
$ cd typescript && npx tsc --noEmit
(exit 0)
```
### TLS_CROSSTEST-6 중간 검증
```
$ cd dart && dart analyze crosstest/dart_kotlin.dart crosstest/dart_python.dart crosstest/dart_typescript.dart
No issues found!
$ cd dart && dart run crosstest/dart_python.dart
PASS all dart-server/python-client crosstests passed
$ cd dart && dart run crosstest/dart_typescript.dart
PASS all dart-server/typescript-client crosstests passed
$ cd dart && env JAVA_HOME=/config/opt/jdk/jdk-17.0.10+7 dart run crosstest/dart_kotlin.dart
PASS all dart-server/kotlin-client crosstests passed
```
### TLS_CROSSTEST-7 중간 검증
```
$ cd go && env PATH=/config/go-sdk/go/bin:$PATH GOCACHE=/tmp/go-build GOMODCACHE=/tmp/go-mod \
go run ./crosstest/go_python.go
PASS all go-server/python-client crosstests passed
$ cd go && env PATH=/config/go-sdk/go/bin:$PATH GOCACHE=/tmp/go-build GOMODCACHE=/tmp/go-mod \
go run ./crosstest/go_typescript.go
PASS all go-server/typescript-client crosstests passed
$ cd go && env PATH=/config/go-sdk/go/bin:$PATH JAVA_HOME=/config/opt/jdk/jdk-17.0.10+7 GOCACHE=/tmp/go-build GOMODCACHE=/tmp/go-mod \
go run ./crosstest/go_kotlin.go
PASS all go-server/kotlin-client crosstests passed
```
### TLS_CROSSTEST-8 중간 검증
```
$ cd kotlin && env JAVA_HOME=/config/opt/jdk/jdk-17.0.10+7 GRADLE_USER_HOME=/tmp/gradle \
./gradlew run -PmainClass=com.tokilabs.toki_socket.crosstest.KotlinDartKt
PASS all kotlin-server/dart-client crosstests passed
$ cd kotlin && env JAVA_HOME=/config/opt/jdk/jdk-17.0.10+7 GRADLE_USER_HOME=/tmp/gradle \
./gradlew run -PmainClass=com.tokilabs.toki_socket.crosstest.KotlinGoKt
PASS all kotlin-server/go-client crosstests passed
$ cd kotlin && env JAVA_HOME=/config/opt/jdk/jdk-17.0.10+7 GRADLE_USER_HOME=/tmp/gradle PATH=/config/go-sdk/go/bin:$PATH \
./gradlew run -PmainClass=com.tokilabs.toki_socket.crosstest.KotlinPythonKt
PASS all kotlin-server/python-client crosstests passed
$ cd kotlin && env JAVA_HOME=/config/opt/jdk/jdk-17.0.10+7 GRADLE_USER_HOME=/tmp/gradle \
./gradlew run -PmainClass=com.tokilabs.toki_socket.crosstest.KotlinTypescriptKt
PASS all kotlin-server/typescript-client crosstests passed
```
### TLS_CROSSTEST-9 중간 검증
```
$ python3 -m py_compile python/crosstest/python_dart.py python/crosstest/python_go.py \
python/crosstest/python_kotlin.py python/crosstest/python_typescript.py
(exit 0)
$ cd python && env PATH=/config/go-sdk/go/bin:$PATH GOCACHE=/tmp/go-build GOMODCACHE=/tmp/go-mod python3 -m crosstest.python_go
PASS all python-server/go-client crosstests passed
$ cd python && python3 -m crosstest.python_typescript
PASS all python-server/typescript-client crosstests passed
$ cd python && python3 -m crosstest.python_dart
PASS all python-server/dart-client crosstests passed
$ cd python && env JAVA_HOME=/config/opt/jdk/jdk-17.0.10+7 python3 -m crosstest.python_kotlin
PASS all python-server/kotlin-client crosstests passed
```
### TLS_CROSSTEST-10 중간 검증
```
$ cd typescript && npx tsc --noEmit
(exit 0)
$ cd typescript && env PATH=/config/go-sdk/go/bin:$PATH GOCACHE=/tmp/go-build GOMODCACHE=/tmp/go-mod node --import tsx crosstest/typescript_go.ts
PASS all typescript-server/go-client crosstests passed
$ cd typescript && node --import tsx crosstest/typescript_python.ts
PASS all typescript-server/python-client crosstests passed
$ cd typescript && node --import tsx crosstest/typescript_dart.ts
PASS all typescript-server/dart-client crosstests passed
$ cd typescript && env JAVA_HOME=/config/opt/jdk/jdk-17.0.10+7 node --import tsx crosstest/typescript_kotlin.ts
PASS all typescript-server/kotlin-client crosstests passed
```
### 최종 검증
```
$ cd dart && dart run crosstest/dart_kotlin.dart
PASS all dart-server/kotlin-client crosstests passed
$ cd dart && dart run crosstest/dart_python.dart
PASS all dart-server/python-client crosstests passed
$ cd dart && dart run crosstest/dart_typescript.dart
PASS all dart-server/typescript-client crosstests passed
$ cd go && env PATH=/config/go-sdk/go/bin:$PATH GOCACHE=/tmp/go-build GOMODCACHE=/tmp/go-mod \
go run ./crosstest/go_kotlin.go && go run ./crosstest/go_python.go && go run ./crosstest/go_typescript.go
PASS all go-server/kotlin-client crosstests passed
PASS all go-server/python-client crosstests passed
PASS all go-server/typescript-client crosstests passed
$ cd kotlin && env JAVA_HOME=/config/opt/jdk/jdk-17.0.10+7 GRADLE_USER_HOME=/tmp/gradle \
./gradlew run -PmainClass=com.tokilabs.toki_socket.crosstest.KotlinDartKt && \
./gradlew run -PmainClass=com.tokilabs.toki_socket.crosstest.KotlinGoKt && \
./gradlew run -PmainClass=com.tokilabs.toki_socket.crosstest.KotlinPythonKt && \
./gradlew run -PmainClass=com.tokilabs.toki_socket.crosstest.KotlinTypescriptKt
PASS all kotlin-server/dart-client crosstests passed
PASS all kotlin-server/go-client crosstests passed
PASS all kotlin-server/python-client crosstests passed
PASS all kotlin-server/typescript-client crosstests passed
$ cd python && python3 -m crosstest.python_dart && python3 -m crosstest.python_go && \
python3 -m crosstest.python_kotlin && python3 -m crosstest.python_typescript
PASS all python-server/dart-client crosstests passed
PASS all python-server/go-client crosstests passed
PASS all python-server/kotlin-client crosstests passed
PASS all python-server/typescript-client crosstests passed
$ cd typescript && node --import tsx crosstest/typescript_dart.ts && \
node --import tsx crosstest/typescript_go.ts && \
node --import tsx crosstest/typescript_kotlin.ts && \
node --import tsx crosstest/typescript_python.ts
PASS all typescript-server/dart-client crosstests passed
PASS all typescript-server/go-client crosstests passed
PASS all typescript-server/kotlin-client crosstests passed
PASS all typescript-server/python-client crosstests passed
```

View file

@ -0,0 +1,135 @@
<!-- task=tls_crosstest plan=1 tag=TLS_CROSSTEST -->
# Code Review Reference - TLS_CROSSTEST
## 개요
date=2026-04-26
task=tls_crosstest, plan=1, tag=TLS_CROSSTEST
## 이 파일을 읽는 리뷰 에이전트에게
각 항목의 구현을 실제 소스 파일과 대조하고, `검증 결과` 섹션의 출력이 코드와 일치하는지 확인하세요.
리뷰 완료 후 반드시 아래 순서로 아카이브하세요.
1. `CODE_REVIEW.md` → `code_review_1.log` (N = 기존 code_review_*.log 수)
2. `PLAN.md` → `plan_1.log` (M = 기존 plan_*.log 수)
3. PASS인 경우 `complete.log` 작성 후 종료. WARN/FAIL인 경우 새 `PLAN.md` + `CODE_REVIEW.md` 스텁 작성.
---
## 구현 항목별 완료 여부
| 항목 | 완료 여부 |
|------|---------|
| [TLS_CROSSTEST-1] Dart 클라이언트 파일 — TLS 모드 추가 | [x] |
| [TLS_CROSSTEST-2] Go 클라이언트 파일 — TLS 모드 추가 | [x] |
| [TLS_CROSSTEST-3] Kotlin 클라이언트 파일 — TLS 모드 추가 | [x] |
| [TLS_CROSSTEST-4] Python 클라이언트 파일 — TLS 모드 추가 | [x] |
| [TLS_CROSSTEST-5] TypeScript 클라이언트 파일 — TLS 모드 추가 | [x] |
| [TLS_CROSSTEST-6] Dart orchestrator 3개 — TLS phase 추가 | [x] |
| [TLS_CROSSTEST-7] Go orchestrator 3개 — TLS phase 추가 | [x] |
| [TLS_CROSSTEST-8] Kotlin orchestrator 4개 — TLS TCP phase 추가 | [x] |
| [TLS_CROSSTEST-9] Python orchestrator 4개 — TLS phase 추가 | [x] |
| [TLS_CROSSTEST-10] TypeScript orchestrator 4개 — TLS phase 추가 | [x] |
## 계획 대비 변경 사항
- Python crosstest client 4개에 secure close timeout과 짧은 server-ready delay를 추가했다. TLS close 또는 Kotlin server listener 등록 레이스로 테스트 프로세스가 불필요하게 타임아웃되는 것을 막기 위한 안정화다.
- Kotlin server row 3개(`kotlin_dart`, `kotlin_go`, `kotlin_python`)는 기존 WS 서버를 phase마다 재시작하던 구조를 `kotlin_typescript`와 같은 단일 WS 서버 재사용 구조로 맞췄다. 같은 포트 재바인딩 타이밍으로 인한 연결 거부를 방지한다.
- Kotlin server row는 계획대로 WSS phase를 추가하지 않고 TLS TCP 2 phase만 추가했다.
## 주요 설계 결정
- 각 server orchestrator는 자기 언어의 test cert fixture를 사용한다. 단 Go server orchestrator는 Go 패키지에 cert fixture가 없어 `dart/test/certs`를 사용한다.
- Kotlin server row는 WSS를 추가하지 않고 TLS TCP만 검증한다.
- 클라이언트 helper는 기존 TCP/WS 구조를 유지하고 `--cert`가 필요한 secure mode에서만 trust context/CA를 구성한다.
## 리뷰어를 위한 체크포인트
- **포트 충돌 없음**: 포트 배정표의 신규 TLS TCP / WSS 포트(볼드체)가 각 파일의 기존 TCP/WS 포트와 겹치지 않는지 확인.
- **인증서 경로 일관성**: 각 언어 orchestrator가 자신의 `test/certs/server.crt` + `server.key`를 사용하고, `--cert` 인수로 클라이언트에 전달하는지 확인.
- **클라이언트 `--mode` 확장**: 1-5번 클라이언트 파일 전체에서 `"tls"` / `"wss"` case가 기존 `"tcp"` / `"ws"` 구조와 동일한 패턴으로 추가됐는지 확인.
- **`buildClientTLS` / `buildClientSslContext` 헬퍼 복사 정확성**: Go 3개 클라이언트 파일과 Kotlin 4개 클라이언트 파일에 복사된 헬퍼가 기준 파일 (`dart_go_client/main.go`, `tls_support` TestHelpers.kt)과 일치하는지 확인.
- **TypeScript `typescript_kotlin` orchestrator**: WSS 없는 TLS TCP 2 phase만 포함됐는지 확인 (`typescript_kotlin_client.ts` 에 `wss` 케이스는 있어도 orchestrator에서 WSS phase를 호출하지 않음).
- **Push 메시지 검증 문자열**: 각 클라이언트 파일이 올바른 서버 언어명을 포함한 push 문자열을 검증하는지 확인 (예: `python_dart_client.dart` → `'push from python server'`).
- **의존 관계 순서**: 1단계(클라이언트 1-5) 완료 후 2단계(orchestrator 6-10)가 구현됐는지 확인.
## 검증 결과
정적/중간 검증:
```text
$ cd dart && dart analyze crosstest/kotlin_dart_client.dart crosstest/python_dart_client.dart crosstest/typescript_dart_client.dart crosstest/dart_kotlin.dart crosstest/dart_python.dart crosstest/dart_typescript.dart
No issues found!
$ cd go && env PATH=/config/go-sdk/go/bin:$PATH GOCACHE=/tmp/go-build GOMODCACHE=/tmp/go-mod go build ./crosstest/kotlin_go_client ./crosstest/python_go_client ./crosstest/typescript_go_client
(exit 0)
$ python3 -m py_compile python/crosstest/dart_python_client.py python/crosstest/go_python_client.py python/crosstest/kotlin_python_client.py python/crosstest/typescript_python_client.py python/crosstest/python_dart.py python/crosstest/python_go.py python/crosstest/python_kotlin.py python/crosstest/python_typescript.py
(exit 0)
$ cd typescript && npx tsc --noEmit
(exit 0)
$ cd kotlin && env JAVA_HOME=/config/opt/jdk/jdk-17.0.10+7 GRADLE_USER_HOME=/tmp/gradle ./gradlew compileCrosstestKotlin
BUILD SUCCESSFUL
```
최종 crosstest:
```text
$ cd dart && env JAVA_HOME=/config/opt/jdk/jdk-17.0.10+7 dart run crosstest/dart_kotlin.dart
PASS all dart-server/kotlin-client crosstests passed
$ cd dart && dart run crosstest/dart_python.dart
PASS all dart-server/python-client crosstests passed
$ cd dart && dart run crosstest/dart_typescript.dart
PASS all dart-server/typescript-client crosstests passed
$ cd go && env PATH=/config/go-sdk/go/bin:$PATH GOCACHE=/tmp/go-build GOMODCACHE=/tmp/go-mod JAVA_HOME=/config/opt/jdk/jdk-17.0.10+7 go run ./crosstest/go_kotlin.go
PASS all go-server/kotlin-client crosstests passed
$ cd go && env PATH=/config/go-sdk/go/bin:$PATH GOCACHE=/tmp/go-build GOMODCACHE=/tmp/go-mod go run ./crosstest/go_python.go
PASS all go-server/python-client crosstests passed
$ cd go && env PATH=/config/go-sdk/go/bin:$PATH GOCACHE=/tmp/go-build GOMODCACHE=/tmp/go-mod go run ./crosstest/go_typescript.go
PASS all go-server/typescript-client crosstests passed
$ cd kotlin && env JAVA_HOME=/config/opt/jdk/jdk-17.0.10+7 GRADLE_USER_HOME=/tmp/gradle ./gradlew run -PmainClass=com.tokilabs.toki_socket.crosstest.KotlinDartKt
PASS all kotlin-server/dart-client crosstests passed
$ cd kotlin && env PATH=/config/go-sdk/go/bin:$PATH JAVA_HOME=/config/opt/jdk/jdk-17.0.10+7 GRADLE_USER_HOME=/tmp/gradle GOCACHE=/tmp/go-build GOMODCACHE=/tmp/go-mod ./gradlew run -PmainClass=com.tokilabs.toki_socket.crosstest.KotlinGoKt
PASS all kotlin-server/go-client crosstests passed
$ cd kotlin && env JAVA_HOME=/config/opt/jdk/jdk-17.0.10+7 GRADLE_USER_HOME=/tmp/gradle ./gradlew run -PmainClass=com.tokilabs.toki_socket.crosstest.KotlinPythonKt
PASS all kotlin-server/python-client crosstests passed
$ cd kotlin && env JAVA_HOME=/config/opt/jdk/jdk-17.0.10+7 GRADLE_USER_HOME=/tmp/gradle ./gradlew run -PmainClass=com.tokilabs.toki_socket.crosstest.KotlinTypescriptKt
PASS all kotlin-server/typescript-client crosstests passed
$ cd python && python3 -m crosstest.python_dart
PASS all python-server/dart-client crosstests passed
$ cd python && env PATH=/config/go-sdk/go/bin:$PATH GOCACHE=/tmp/go-build GOMODCACHE=/tmp/go-mod python3 -m crosstest.python_go
PASS all python-server/go-client crosstests passed
$ cd python && env JAVA_HOME=/config/opt/jdk/jdk-17.0.10+7 python3 -m crosstest.python_kotlin
PASS all python-server/kotlin-client crosstests passed
$ cd python && python3 -m crosstest.python_typescript
PASS all python-server/typescript-client crosstests passed
$ cd typescript && node --import tsx crosstest/typescript_dart.ts
PASS all typescript-server/dart-client crosstests passed
$ cd typescript && env PATH=/config/go-sdk/go/bin:$PATH GOCACHE=/tmp/go-build GOMODCACHE=/tmp/go-mod node --import tsx crosstest/typescript_go.ts
PASS all typescript-server/go-client crosstests passed
$ cd typescript && env JAVA_HOME=/config/opt/jdk/jdk-17.0.10+7 node --import tsx crosstest/typescript_kotlin.ts
PASS all typescript-server/kotlin-client crosstests passed
$ cd typescript && node --import tsx crosstest/typescript_python.ts
PASS all typescript-server/python-client crosstests passed
```

View file

@ -0,0 +1,27 @@
task=tls_crosstest plan=1 tag=TLS_CROSSTEST
date=2026-04-26
result=PASS
모든 10개 항목 구현 완료 및 검증 통과.
## 완료 항목
- TLS_CROSSTEST-1: Dart 클라이언트 3개 — tls/wss 모드, --cert 파싱 추가
- TLS_CROSSTEST-2: Go 클라이언트 3개 — tls/wss 모드, buildClientTLS 헬퍼 추가
- TLS_CROSSTEST-3: Kotlin 클라이언트 4개 — tls/wss 모드, buildClientSslContext 헬퍼 추가
- TLS_CROSSTEST-4: Python 클라이언트 4개 — tls/wss 모드, ssl 컨텍스트 추가
- TLS_CROSSTEST-5: TypeScript 클라이언트 4개 — tls/wss 모드, connectTcpTls/connectWss 추가
- TLS_CROSSTEST-6: Dart orchestrator 3개 — TLS TCP + WSS 4 phase 추가
- TLS_CROSSTEST-7: Go orchestrator 3개 — TLS TCP + WSS 4 phase 추가
- TLS_CROSSTEST-8: Kotlin orchestrator 4개 — TLS TCP 2 phase 추가 (WSS 미지원)
- TLS_CROSSTEST-9: Python orchestrator 4개 — TLS TCP + WSS 4 phase 추가
- TLS_CROSSTEST-10: TypeScript orchestrator 4개 — TLS TCP + WSS phase 추가 (kotlin 제외 WSS)
## 최종 검증
20개 crosstest 조합 모두 PASS:
dart-server × {kotlin,python,typescript}-client
go-server × {kotlin,python,typescript}-client
kotlin-server × {dart,go,python,typescript}-client
python-server × {dart,go,kotlin,typescript}-client
typescript-server × {dart,go,kotlin,python}-client

View file

@ -0,0 +1,669 @@
<!-- task=tls_crosstest plan=0 tag=TLS_CROSSTEST -->
# TLS 크로스테스트 전 언어 확장
## 이 파일을 읽는 구현 에이전트에게
각 항목의 체크리스트를 완료 표시하고, 중간 검증 명령을 실행한 뒤 출력을 CODE_REVIEW.md `검증 결과` 섹션에 붙여 넣는다. 최종 검증까지 완료 후 CODE_REVIEW.md 각 항목을 `[x]`로 체크한다. **의존 관계 및 구현 순서** 섹션을 반드시 지킨다.
## 배경
`dart_go`·`go_dart` 두 조합에만 TLS TCP + WSS 크로스테스트가 존재한다. Python·TypeScript·Kotlin에 TLS 구현이 추가됨에 따라 나머지 18개 쌍에도 TLS 검증이 가능해졌다. 이번 작업으로 서버·클라이언트 양쪽 모두 TLS를 지원하는 모든 조합에 TLS 크로스테스트 phase를 추가한다.
**기준 파일:** `dart/crosstest/dart_go.dart` (orchestrator), `go/crosstest/dart_go_client/main.go` (client)
**인증서 경로 규칙:** 각 언어의 `test/certs/server.crt` + `server.key` 를 사용한다 (모두 `dart/test/certs/` 원본의 사본). orchestrator는 자신의 언어 경로로 서버 cert를 로드하고 `--cert=<path>` 로 클라이언트에 전달한다.
---
## 포트 배정표
| orchestrator | TCP | WS | TLS TCP (신규) | WSS (신규) |
|---|---|---|---|---|
| dart_kotlin | 29590 | 29592 | **29594** | **29596** |
| dart_python | 29694 | 29696 | **29698** | **29700** |
| dart_typescript | 29790 | 29792 | **29794** | **29796** |
| go_kotlin | 29290 | 29292 | **29294** | **29296** |
| go_python | 29390 | 29392 | **29394** | **29396** |
| go_typescript | 29490 | 29492 | **29494** | **29496** |
| kotlin_dart | 29490 | 29492 | **29494** | **29496** |
| kotlin_go | 29390 | 29392 | **29394** | **29396** |
| kotlin_python | 29394 | 29396 | **29398** | **29400** |
| kotlin_typescript | 29794 | 29796 | **29798** | **29800** |
| python_dart | 29494 | 29496 | **29498** | **29500** |
| python_go | 29490 | 29492 | **29494** | **29496** |
| python_kotlin | 29498 | 29500 | **29502** | **29504** |
| python_typescript | 29798 | 29800 | **29802** | **29804** |
| typescript_dart | 29802 | 29804 | **29806** | **29808** |
| typescript_go | 29806 | 29808 | **29810** | **29812** |
| typescript_kotlin | 29810 | 29812 | **29814** | — |
| typescript_python | 29814 | 29816 | **29818** | **29820** |
---
## [TLS_CROSSTEST-1] Dart 클라이언트 파일 — TLS 모드 추가
### 문제
`dart/crosstest/kotlin_dart_client.dart`, `dart/crosstest/python_dart_client.dart`, `dart/crosstest/typescript_dart_client.dart` 3개 파일이 `--mode=tls/wss`, `--cert` 인수를 처리하지 않는다.
### 해결 방법
`dart/crosstest/go_dart_client.dart` 패턴을 참조한다. 각 파일에서:
1. `args`에서 `--cert` 인수 파싱 추가
2. `_connectWithRetry` 또는 동등 함수의 `switch (mode)` 블록에 `'tls'`·`'wss'` 케이스 추가:
```dart
case 'tls':
final ctx = SecurityContext()..setTrustedCertificates(certPath!);
final socket = await ProtobufClient.connectSecure(_host, port, context: ctx);
return _ClientHandle(/* TCP client */, () async => /* close */);
case 'wss':
final ctx = SecurityContext()..setTrustedCertificates(certPath!);
final ws = await WsProtobufClient.connectSecure(_host, port, context: ctx);
return _ClientHandle(/* WS client */, () async => /* close */);
```
3. 각 파일에서 push 메시지 검증 문자열이 맞는지 확인:
- `kotlin_dart_client`: `'push from kotlin server'`
- `python_dart_client`: `'push from python server'`
- `typescript_dart_client`: `'push from typescript server'`
### 수정 파일 및 체크리스트
- `dart/crosstest/kotlin_dart_client.dart`
- [x] `--cert` 파라미터 파싱 추가
- [x] `'tls'`·`'wss'` 모드 케이스 추가
- `dart/crosstest/python_dart_client.dart`
- [x] `--cert` 파라미터 파싱 추가
- [x] `'tls'`·`'wss'` 모드 케이스 추가
- `dart/crosstest/typescript_dart_client.dart`
- [x] `--cert` 파라미터 파싱 추가
- [x] `'tls'`·`'wss'` 모드 케이스 추가
### 테스트 작성
단독 실행 테스트 없음. TLS_CROSSTEST-8~10의 orchestrator 검증으로 확인.
### 중간 검증
```
$ cd dart && dart analyze crosstest/kotlin_dart_client.dart crosstest/python_dart_client.dart crosstest/typescript_dart_client.dart
No issues found!
```
---
## [TLS_CROSSTEST-2] Go 클라이언트 파일 — TLS 모드 추가
### 문제
`go/crosstest/kotlin_go_client/main.go`, `go/crosstest/python_go_client/main.go`, `go/crosstest/typescript_go_client/main.go` 3개 파일이 `--mode=tls/wss`, `--cert` 를 처리하지 않는다.
### 해결 방법
`go/crosstest/dart_go_client/main.go` 패턴을 참조한다. 각 파일에서:
1. `flag.String("cert", "", ...)` 플래그 추가
2. `dial()` 함수의 `switch mode` 블록에 `"tls"`·`"wss"` 케이스 추가:
```go
case "tls":
tlsCfg, err := buildClientTLS(*cert)
client, err = toki.DialTcpTLS(ctx, host, port, tlsCfg, 0, 0, parserMap())
case "wss":
tlsCfg, err := buildClientTLS(*cert)
client, err = toki.DialWssWithHeartbeat(ctx, host, port, wsPath, tlsCfg, 0, 0, parserMap())
```
3. `buildClientTLS(certFile string)` 헬퍼를 `dart_go_client/main.go`에서 복사 추가
4. 각 파일 push 메시지 검증 문자열 확인:
- `kotlin_go_client`: `"push from kotlin server"`
- `python_go_client`: `"push from python server"`
- `typescript_go_client`: `"push from typescript server"`
### 수정 파일 및 체크리스트
- `go/crosstest/kotlin_go_client/main.go`
- [x] `--cert` 플래그 추가
- [x] `"tls"`·`"wss"` 케이스 및 `buildClientTLS` 헬퍼 추가
- `go/crosstest/python_go_client/main.go`
- [x] 동일
- `go/crosstest/typescript_go_client/main.go`
- [x] 동일
### 테스트 작성
없음. TLS_CROSSTEST-7~9의 orchestrator 검증으로 확인.
### 중간 검증
```
$ cd go && env PATH=/config/go-sdk/go/bin:$PATH GOCACHE=/tmp/go-build GOMODCACHE=/tmp/go-mod \
go build ./crosstest/kotlin_go_client ./crosstest/python_go_client ./crosstest/typescript_go_client
(exit 0)
```
---
## [TLS_CROSSTEST-3] Kotlin 클라이언트 파일 — TLS 모드 추가
### 문제
`kotlin/crosstest/dart_kotlin_client.kt`, `go_kotlin_client/Main.kt`, `python_kotlin_client.kt`, `typescript_kotlin_client.kt` 4개 파일이 `--mode=tls/wss`, `--cert` 를 처리하지 않는다.
### 해결 방법
각 파일에서:
1. `argValue(args, "cert")` 파싱 추가
2. `connectWithRetry(mode, port, cert)` 시그니처에 `cert: String?` 추가
3. `when (mode)` 블록에 `"tls"`·`"wss"` 케이스 추가:
```kotlin
"tls" -> {
val sslCtx = buildClientSslContext(cert ?: error("--cert required for tls"))
DialTcpTls(HOST, port, sslCtx, 0, 0, parserMap)
}
"wss" -> {
val sslCtx = buildClientSslContext(cert ?: error("--cert required for wss"))
dialWss(HOST, port, WS_PATH, sslCtx, 0, 0, parserMap)
}
```
4. `buildClientSslContext(certPath: String): SSLContext` 헬퍼 추가 — PEM cert를 `TrustStore`에 로드 (구현은 `tls_support` 과제에서 도입한 TestHelpers.kt 패턴 참조):
```kotlin
fun buildClientSslContext(certPath: String): SSLContext {
val certFactory = java.security.cert.CertificateFactory.getInstance("X.509")
val cert = java.io.File(certPath).inputStream().use {
certFactory.generateCertificate(it) as java.security.cert.X509Certificate
}
val ts = java.security.KeyStore.getInstance("PKCS12")
ts.load(null, null)
ts.setCertificateEntry("toki", cert)
val tmf = javax.net.ssl.TrustManagerFactory.getInstance(
javax.net.ssl.TrustManagerFactory.getDefaultAlgorithm()
)
tmf.init(ts)
val ctx = javax.net.ssl.SSLContext.getInstance("TLS")
ctx.init(null, tmf.trustManagers, null)
return ctx
}
```
5. 각 파일 push 메시지 검증 문자열 확인 (`'push from dart/go/python/typescript server'`).
**참고:** `go_kotlin_client`는 Gradle 서브프로젝트 (`kotlin/crosstest/go_kotlin_client/`). 나머지 세 파일은 독립 `.kt` 파일.
### 수정 파일 및 체크리스트
- `kotlin/crosstest/dart_kotlin_client.kt`
- [x] `--cert` 파싱, `"tls"`·`"wss"` 케이스, `buildClientSslContext` 추가
- `kotlin/crosstest/go_kotlin_client/src/main/kotlin/com/tokilabs/toki_socket/crosstest/Main.kt`
- [x] 동일
- `kotlin/crosstest/python_kotlin_client.kt`
- [x] 동일
- `kotlin/crosstest/typescript_kotlin_client.kt`
- [x] 동일
### 테스트 작성
없음. TLS_CROSSTEST-6~10의 orchestrator 검증으로 확인.
### 중간 검증
```
$ cd kotlin && env JAVA_HOME=/config/opt/jdk/jdk-17.0.10+7 GRADLE_USER_HOME=/tmp/gradle \
./gradlew compileCrosstestKotlin
BUILD SUCCESSFUL
```
---
## [TLS_CROSSTEST-4] Python 클라이언트 파일 — TLS 모드 추가
### 문제
`python/crosstest/dart_python_client.py`, `go_python_client.py`, `kotlin_python_client.py`, `typescript_python_client.py` 4개 파일이 TLS 모드를 처리하지 않는다.
### 해결 방법
각 파일에서:
1. `argparse`에 `--cert` 인수 추가 (`default=None`)
2. `choices=["tcp", "ws"]` → `["tcp", "ws", "tls", "wss"]` 변경
3. `dial()` 함수에 `"tls"`·`"wss"` 케이스 추가:
```python
elif mode == "tls":
ssl_ctx = ssl.create_default_context(cafile=cert)
ssl_ctx.check_hostname = False
return await connect_tcp_tls(HOST, port, ssl_ctx, 0, 0, parser_map())
elif mode == "wss":
ssl_ctx = ssl.create_default_context(cafile=cert)
ssl_ctx.check_hostname = False
return await connect_wss(HOST, port, WS_PATH, ssl_ctx, 0, 0, parser_map())
```
4. `import ssl`, `connect_tcp_tls`, `connect_wss` import 추가
### 수정 파일 및 체크리스트
- `python/crosstest/dart_python_client.py`
- [x] `import ssl`, `connect_tcp_tls`, `connect_wss` 추가
- [x] `--cert` argparse 인수 추가, choices 확장
- [x] `"tls"`·`"wss"` 분기 추가
- `python/crosstest/go_python_client.py`
- [x] 동일
- `python/crosstest/kotlin_python_client.py`
- [x] 동일
- `python/crosstest/typescript_python_client.py`
- [x] 동일
### 중간 검증
```
$ python3 -m py_compile python/crosstest/dart_python_client.py \
python/crosstest/go_python_client.py \
python/crosstest/kotlin_python_client.py \
python/crosstest/typescript_python_client.py
(exit 0)
```
---
## [TLS_CROSSTEST-5] TypeScript 클라이언트 파일 — TLS 모드 추가
### 문제
`typescript/crosstest/dart_typescript_client.ts`, `go_typescript_client.ts`, `kotlin_typescript_client.ts`, `python_typescript_client.ts` 4개 파일이 TLS 모드를 처리하지 않는다.
### 해결 방법
각 파일에서:
1. `parseArgs()` 내 `mode` 타입을 `"tcp" | "ws" | "tls" | "wss"` 로 확장
2. `--cert` 인수 파싱 추가
3. `dialWithRetry` / `dial()` 에 `cert?: string` 파라미터 추가
4. `dial()` switch 에 `"tls"`·`"wss"` 케이스 추가:
```typescript
case "tls": {
const ca = fs.readFileSync(cert!);
return connectTcpTls(HOST, port, { ca }, 0, 0, parserMap());
}
case "wss": {
const ca = fs.readFileSync(cert!);
return connectWss(HOST, port, WS_PATH, { ca }, 0, 0, parserMap());
}
```
5. `import * as fs from "node:fs"`, `connectTcpTls`, `connectWss` import 추가
### 수정 파일 및 체크리스트
- `typescript/crosstest/dart_typescript_client.ts`
- [x] `fs`, `connectTcpTls`, `connectWss` import 추가
- [x] `--cert` 파싱, mode 타입 확장, `dial()` 케이스 추가
- `typescript/crosstest/go_typescript_client.ts`
- [x] 동일
- `typescript/crosstest/kotlin_typescript_client.ts`
- [x] 동일
- `typescript/crosstest/python_typescript_client.ts`
- [x] 동일
### 중간 검증
```
$ cd typescript && npx tsc --noEmit
(exit 0)
```
---
## [TLS_CROSSTEST-6] Dart orchestrator 3개 — TLS phase 추가
### 문제
`dart/crosstest/dart_kotlin.dart`, `dart/crosstest/dart_python.dart`, `dart/crosstest/dart_typescript.dart` 에 TLS phase가 없다.
### 해결 방법
`dart/crosstest/dart_go.dart` 패턴을 참조한다. 각 파일에서:
1. TLS/WSS 포트 상수 추가:
- `dart_kotlin`: `_tlsTcpPort = 29594`, `_wssPort = 29596`
- `dart_python`: `_tlsTcpPort = 29698`, `_wssPort = 29700`
- `dart_typescript`: `_tlsTcpPort = 29794`, `_wssPort = 29796`
2. `_DartTlsTcpServer`, `_DartWssServer` 클래스 추가 (dart_go.dart 와 동일 구조)
3. `_runTls()` 메서드 추가 — TLS TCP send-push, TLS TCP requests, WSS send-push, WSS requests 4개 phase
4. `main()`에서 `await _runTls()` 호출 추가
5. 각 파일의 클라이언트 실행 헬퍼에서 `--cert=<path>` 전달:
- `dart_kotlin`: Kotlin 클라이언트 `./gradlew run` 호출 시 `-PappArgs="['--mode=tls','--port=29594','--cert=...']"` 형식
- `dart_python`: Python subprocess 호출 시 `--mode tls --port 29698 --cert <path>` 추가
- `dart_typescript`: `node --import tsx` 호출 시 `--mode=tls --port=29794 --cert=<path>` 추가
6. 인증서 경로: `dart/test/certs/server.crt`, `dart/test/certs/server.key` (이미 각 파일의 repo root에서 계산 가능)
### 수정 파일 및 체크리스트
- `dart/crosstest/dart_kotlin.dart`
- [x] TLS/WSS 포트 상수 추가 (29594, 29596)
- [x] `_DartTlsTcpServer`, `_DartWssServer` 클래스 추가
- [x] `_runTls()` 메서드 추가 (4 phase)
- [x] `main()`에서 `_runTls()` 호출 추가
- [x] Kotlin 클라이언트 호출에 TLS 인수 전달
- `dart/crosstest/dart_python.dart`
- [x] 동일 (포트 29698, 29700)
- `dart/crosstest/dart_typescript.dart`
- [x] 동일 (포트 29794, 29796)
### 중간 검증
```
$ cd dart && dart analyze crosstest/dart_kotlin.dart crosstest/dart_python.dart crosstest/dart_typescript.dart
No issues found!
$ cd dart && dart run crosstest/dart_python.dart
PASS all dart-server/python-client crosstests passed
$ cd dart && dart run crosstest/dart_typescript.dart
PASS all dart-server/typescript-client crosstests passed
$ cd dart && dart run crosstest/dart_kotlin.dart
PASS all dart-server/kotlin-client crosstests passed
```
---
## [TLS_CROSSTEST-7] Go orchestrator 3개 — TLS phase 추가
### 문제
`go/crosstest/go_kotlin.go`, `go/crosstest/go_python.go`, `go/crosstest/go_typescript.go` 에 TLS phase가 없다.
### 해결 방법
`go/crosstest/go_dart.go` 패턴 참조. 각 파일에서:
1. TLS/WSS 포트 상수 추가:
- `go_kotlin`: `goKotlinTLSPort = 29294`, `goKotlinWSSPort = 29296`
- `go_python`: `goPythonTLSPort = 29394`, `goPythonWSSPort = 29396`
- `go_typescript`: `goTypescriptTLSPort = 29494`, `goTypescriptWSSPort = 29496`
2. `crypto/tls` import 추가 (미존재 시)
3. `loadServerTLS(certFile, keyFile)` 헬퍼 추가 (go_dart.go 에서 복사)
4. `runTLSTCPSendPush`, `runTLSTCPRequests`, `runWSSendPushSecure`, `runWSSRequestsSecure` 추가
5. `run()` 에서 TLS phase 호출 추가
6. 인증서: `go_dart.go`와 동일하게 `dartDir`의 `test/certs/` 에서 로드
### 수정 파일 및 체크리스트
- `go/crosstest/go_kotlin.go`
- [x] TLS 포트 상수 (29294, 29296), `loadServerTLS`, TLS phase 함수 4개 추가
- [x] `run()`에서 호출 추가
- `go/crosstest/go_python.go`
- [x] 동일 (포트 29394, 29396)
- `go/crosstest/go_typescript.go`
- [x] 동일 (포트 29494, 29496)
### 중간 검증
```
$ cd go && env PATH=/config/go-sdk/go/bin:$PATH GOCACHE=/tmp/go-build GOMODCACHE=/tmp/go-mod \
go run ./crosstest/go_python.go
PASS all go-server/python-client crosstests passed
$ cd go && env PATH=/config/go-sdk/go/bin:$PATH GOCACHE=/tmp/go-build GOMODCACHE=/tmp/go-mod \
go run ./crosstest/go_typescript.go
PASS all go-server/typescript-client crosstests passed
$ cd go && env PATH=/config/go-sdk/go/bin:$PATH GOCACHE=/tmp/go-build GOMODCACHE=/tmp/go-mod \
go run ./crosstest/go_kotlin.go
PASS all go-server/kotlin-client crosstests passed
```
---
## [TLS_CROSSTEST-8] Kotlin orchestrator 4개 — TLS TCP phase 추가
### 문제
`kotlin_dart.kt`, `kotlin_go.kt`, `kotlin_python.kt`, `kotlin_typescript.kt` 에 TLS phase가 없다. Kotlin `WsServer` TLS는 이번 TLS_CROSSTEST 범위에서 사용하지 않으므로 TLS TCP 2개 phase만 추가한다.
### 해결 방법
각 파일에서:
1. TLS TCP 포트 상수 추가:
- `kotlin_dart`: TLS TCP `29494`
- `kotlin_go`: TLS TCP `29394`
- `kotlin_python`: TLS TCP `29398`
- `kotlin_typescript`: TLS TCP `29798`
2. 인증서 로딩: `kotlin/src/test/resources/server.crt` + `server.key` 경로를 repo root 기준으로 계산
3. `runTlsTcp(sslContext, certPath)` 함수 추가 — send-push, requests 2 phase
4. Kotlin `WsServer`는 TLS를 지원하지 않으므로 WSS phase는 추가하지 않는다.
5. TLS TCP 서버는 `TcpServer(..., sslContext = serverCtx)` 생성자 사용
6. main 또는 run 함수에서 `runTlsTcp()` 호출 추가
Kotlin 서버 로딩 패턴 (`tls_support` 과제에서 도입한 `createTestSslContexts` 참조):
```kotlin
val repoRoot = File(object {}.javaClass.classLoader.getResource("server.crt")!!.toURI())
.parentFile.parentFile.parentFile.parentFile.parentFile // navigate to repo root
val certPath = "$repoRoot/kotlin/src/test/resources/server.crt"
val keyPath = "$repoRoot/kotlin/src/test/resources/server.key"
val serverCtx = createServerSslContext(certPath, keyPath)
```
또는 orchestrator에서 `System.getenv("REPO_ROOT")` 환경변수를 활용하거나, 스크립트 경로로부터 repo root를 계산한다.
### 수정 파일 및 체크리스트
- `kotlin/crosstest/kotlin_dart.kt`
- [x] TLS TCP 포트 상수 (29494)
- [x] 인증서 경로 계산 로직 추가 (`kotlinDir()`)
- [x] `runTlsTcp()` 함수 추가 (send-push, requests)
- [x] main에서 `runTlsTcp()` 호출 추가
- [x] WSS phase 제외 (Kotlin `WsServer` TLS 미지원)
- `kotlin/crosstest/kotlin_go.kt`
- [x] 동일 (TLS TCP 포트 29394, WSS 제외)
- `kotlin/crosstest/kotlin_python.kt`
- [x] 동일 (TLS TCP 포트 29398, WSS 제외)
- `kotlin/crosstest/kotlin_typescript.kt`
- [x] 동일 (TLS TCP 포트 29798, WSS 제외)
### 중간 검증
```
$ cd kotlin && env JAVA_HOME=/config/opt/jdk/jdk-17.0.10+7 GRADLE_USER_HOME=/tmp/gradle \
./gradlew run -PmainClass=com.tokilabs.toki_socket.crosstest.KotlinDartKt
PASS all kotlin-server/dart-client crosstests passed
$ cd kotlin && env JAVA_HOME=/config/opt/jdk/jdk-17.0.10+7 GRADLE_USER_HOME=/tmp/gradle \
./gradlew run -PmainClass=com.tokilabs.toki_socket.crosstest.KotlinGoKt
PASS all kotlin-server/go-client crosstests passed
```
---
## [TLS_CROSSTEST-9] Python orchestrator 4개 — TLS phase 추가
### 문제
`python/crosstest/python_dart.py`, `python_go.py`, `python_kotlin.py`, `python_typescript.py` 에 TLS phase가 없다.
### 해결 방법
각 파일에서:
1. TLS/WSS 포트 상수 추가:
- `python_dart`: `TLS_TCP_PORT = 29498`, `WSS_PORT = 29500`
- `python_go`: `TLS_TCP_PORT = 29494`, `WSS_PORT = 29496`
- `python_kotlin`: `TLS_TCP_PORT = 29502`, `WSS_PORT = 29504`
- `python_typescript`: `TLS_TCP_PORT = 29802`, `WSS_PORT = 29804`
2. `import ssl` 추가
3. `_make_server_ssl_context()` 헬퍼 추가 (python/test/test_tcp.py 패턴 재사용):
```python
def _make_server_ssl_context() -> ssl.SSLContext:
ctx = ssl.SSLContext(ssl.PROTOCOL_TLS_SERVER)
ctx.load_cert_chain(CERT_PATH, KEY_PATH)
return ctx
```
4. `CERT_PATH`, `KEY_PATH` 상수: 각 파일 기준 repo root에서 계산
5. `run_tls()` 코루틴 추가 — TLS TCP send-push, TLS TCP requests, WSS send-push, WSS requests
6. `main()` 또는 `run()` 에서 `await run_tls()` 호출 추가
7. `--cert` 로 클라이언트 subprocess에 인증서 경로 전달
### 수정 파일 및 체크리스트
- `python/crosstest/python_dart.py`
- [x] TLS/WSS 포트 상수 (29498, 29500), cert 경로, `run_tls()` 추가
- `python/crosstest/python_go.py`
- [x] 동일 (29494, 29496)
- `python/crosstest/python_kotlin.py`
- [x] 동일 (29502, 29504)
- `python/crosstest/python_typescript.py`
- [x] 동일 (29802, 29804)
### 중간 검증
```
$ python3 -m py_compile python/crosstest/python_dart.py python/crosstest/python_go.py \
python/crosstest/python_kotlin.py python/crosstest/python_typescript.py
(exit 0)
$ cd python && python3 -m crosstest.python_go
PASS all python-server/go-client crosstests passed
$ cd python && python3 -m crosstest.python_typescript
PASS all python-server/typescript-client crosstests passed
```
---
## [TLS_CROSSTEST-10] TypeScript orchestrator 4개 — TLS phase 추가
### 문제
`typescript/crosstest/typescript_dart.ts`, `typescript_go.ts`, `typescript_kotlin.ts`, `typescript_python.ts` 에 TLS phase가 없다.
### 해결 방법
각 파일에서:
1. TLS/WSS 포트 상수 추가:
- `typescript_dart`: `TLS_TCP_PORT = 29806`, `WSS_PORT = 29808`
- `typescript_go`: `TLS_TCP_PORT = 29810`, `WSS_PORT = 29812`
- `typescript_kotlin`: `TLS_TCP_PORT = 29814` (WSS 없음)
- `typescript_python`: `TLS_TCP_PORT = 29818`, `WSS_PORT = 29820`
2. `import * as fs from "node:fs"` 추가
3. `CERT_PATH`, `KEY_PATH`: `path.resolve(__dirname, "../../typescript/test/certs/server.crt")` 등 repo root 기준 계산
4. `runTls()` 함수 추가 — TLS TCP send-push, TLS TCP requests, (WSS send-push, WSS requests)
- `typescript_kotlin`: WS 미지원이므로 TLS TCP 2 phase만
5. main 에서 `await runTls()` 호출 추가
6. `TcpServer` 생성 시 `tlsOptions: { cert, key }` 전달
7. `WsServer` 생성 시 `tlsOptions: { cert, key }` 전달 (typescript_kotlin 제외)
8. 클라이언트 subprocess에 `--mode=tls`, `--cert=CERT_PATH` 전달
### 수정 파일 및 체크리스트
- `typescript/crosstest/typescript_dart.ts`
- [x] TLS/WSS 포트 상수 (29806, 29808), cert 경로, `runTls()` 추가
- `typescript/crosstest/typescript_go.ts`
- [x] 동일 (29810, 29812)
- `typescript/crosstest/typescript_kotlin.ts`
- [x] TLS 포트 상수 (29814), `runTls()` — TLS TCP 2 phase만
- `typescript/crosstest/typescript_python.ts`
- [x] 동일 (29818, 29820)
### 중간 검증
```
$ cd typescript && npx tsc --noEmit
(exit 0)
$ cd typescript && node --import tsx crosstest/typescript_go.ts
PASS all typescript-server/go-client crosstests passed
$ cd typescript && node --import tsx crosstest/typescript_python.ts
PASS all typescript-server/python-client crosstests passed
```
---
## 수정 파일 요약
| 파일 | 항목 |
|------|------|
| `dart/crosstest/kotlin_dart_client.dart` | TLS_CROSSTEST-1 |
| `dart/crosstest/python_dart_client.dart` | TLS_CROSSTEST-1 |
| `dart/crosstest/typescript_dart_client.dart` | TLS_CROSSTEST-1 |
| `go/crosstest/kotlin_go_client/main.go` | TLS_CROSSTEST-2 |
| `go/crosstest/python_go_client/main.go` | TLS_CROSSTEST-2 |
| `go/crosstest/typescript_go_client/main.go` | TLS_CROSSTEST-2 |
| `kotlin/crosstest/dart_kotlin_client.kt` | TLS_CROSSTEST-3 |
| `kotlin/crosstest/go_kotlin_client/src/main/kotlin/com/tokilabs/toki_socket/crosstest/Main.kt` | TLS_CROSSTEST-3 |
| `kotlin/crosstest/python_kotlin_client.kt` | TLS_CROSSTEST-3 |
| `kotlin/crosstest/typescript_kotlin_client.kt` | TLS_CROSSTEST-3 |
| `python/crosstest/dart_python_client.py` | TLS_CROSSTEST-4 |
| `python/crosstest/go_python_client.py` | TLS_CROSSTEST-4 |
| `python/crosstest/kotlin_python_client.py` | TLS_CROSSTEST-4 |
| `python/crosstest/typescript_python_client.py` | TLS_CROSSTEST-4 |
| `typescript/crosstest/dart_typescript_client.ts` | TLS_CROSSTEST-5 |
| `typescript/crosstest/go_typescript_client.ts` | TLS_CROSSTEST-5 |
| `typescript/crosstest/kotlin_typescript_client.ts` | TLS_CROSSTEST-5 |
| `typescript/crosstest/python_typescript_client.ts` | TLS_CROSSTEST-5 |
| `dart/crosstest/dart_kotlin.dart` | TLS_CROSSTEST-6 |
| `dart/crosstest/dart_python.dart` | TLS_CROSSTEST-6 |
| `dart/crosstest/dart_typescript.dart` | TLS_CROSSTEST-6 |
| `go/crosstest/go_kotlin.go` | TLS_CROSSTEST-7 |
| `go/crosstest/go_python.go` | TLS_CROSSTEST-7 |
| `go/crosstest/go_typescript.go` | TLS_CROSSTEST-7 |
| `kotlin/crosstest/kotlin_dart.kt` | TLS_CROSSTEST-8 |
| `kotlin/crosstest/kotlin_go.kt` | TLS_CROSSTEST-8 |
| `kotlin/crosstest/kotlin_python.kt` | TLS_CROSSTEST-8 |
| `kotlin/crosstest/kotlin_typescript.kt` | TLS_CROSSTEST-8 |
| `python/crosstest/python_dart.py` | TLS_CROSSTEST-9 |
| `python/crosstest/python_go.py` | TLS_CROSSTEST-9 |
| `python/crosstest/python_kotlin.py` | TLS_CROSSTEST-9 |
| `python/crosstest/python_typescript.py` | TLS_CROSSTEST-9 |
| `typescript/crosstest/typescript_dart.ts` | TLS_CROSSTEST-10 |
| `typescript/crosstest/typescript_go.ts` | TLS_CROSSTEST-10 |
| `typescript/crosstest/typescript_kotlin.ts` | TLS_CROSSTEST-10 |
| `typescript/crosstest/typescript_python.ts` | TLS_CROSSTEST-10 |
## 의존 관계 및 구현 순서
**1단계 (클라이언트 파일 — 병렬 가능):**
TLS_CROSSTEST-1, TLS_CROSSTEST-2, TLS_CROSSTEST-3, TLS_CROSSTEST-4, TLS_CROSSTEST-5
**2단계 (orchestrator 파일 — 각 클라이언트 완료 후):**
- TLS_CROSSTEST-6 → TLS_CROSSTEST-1(Dart client), TLS_CROSSTEST-3(Kotlin), TLS_CROSSTEST-4(Python), TLS_CROSSTEST-5(TypeScript) 완료 후
- TLS_CROSSTEST-7 → TLS_CROSSTEST-2(Go), TLS_CROSSTEST-3(Kotlin), TLS_CROSSTEST-4(Python), TLS_CROSSTEST-5(TypeScript) 완료 후
- TLS_CROSSTEST-8 → TLS_CROSSTEST-1(Dart), TLS_CROSSTEST-2(Go), TLS_CROSSTEST-4(Python), TLS_CROSSTEST-5(TypeScript) 완료 후
- TLS_CROSSTEST-9 → TLS_CROSSTEST-1(Dart), TLS_CROSSTEST-2(Go), TLS_CROSSTEST-3(Kotlin), TLS_CROSSTEST-5(TypeScript) 완료 후
- TLS_CROSSTEST-10 → TLS_CROSSTEST-1(Dart), TLS_CROSSTEST-2(Go), TLS_CROSSTEST-3(Kotlin), TLS_CROSSTEST-4(Python) 완료 후
## 최종 검증
```
$ cd dart && dart run crosstest/dart_kotlin.dart
PASS all dart-server/kotlin-client crosstests passed
$ cd dart && dart run crosstest/dart_python.dart
PASS all dart-server/python-client crosstests passed
$ cd dart && dart run crosstest/dart_typescript.dart
PASS all dart-server/typescript-client crosstests passed
$ cd go && env PATH=/config/go-sdk/go/bin:$PATH GOCACHE=/tmp/go-build GOMODCACHE=/tmp/go-mod \
go run ./crosstest/go_kotlin.go && go run ./crosstest/go_python.go && go run ./crosstest/go_typescript.go
PASS all go-server/kotlin-client crosstests passed
PASS all go-server/python-client crosstests passed
PASS all go-server/typescript-client crosstests passed
$ cd kotlin && env JAVA_HOME=/config/opt/jdk/jdk-17.0.10+7 GRADLE_USER_HOME=/tmp/gradle \
./gradlew run -PmainClass=com.tokilabs.toki_socket.crosstest.KotlinDartKt && \
./gradlew run -PmainClass=com.tokilabs.toki_socket.crosstest.KotlinGoKt && \
./gradlew run -PmainClass=com.tokilabs.toki_socket.crosstest.KotlinPythonKt && \
./gradlew run -PmainClass=com.tokilabs.toki_socket.crosstest.KotlinTypescriptKt
(모두 PASS)
$ cd python && python3 -m crosstest.python_dart && python3 -m crosstest.python_go && \
python3 -m crosstest.python_kotlin && python3 -m crosstest.python_typescript
(모두 PASS)
$ cd typescript && node --import tsx crosstest/typescript_dart.ts && \
node --import tsx crosstest/typescript_go.ts && \
node --import tsx crosstest/typescript_kotlin.ts && \
node --import tsx crosstest/typescript_python.ts
(모두 PASS)
```

View file

@ -0,0 +1,676 @@
<!-- task=tls_crosstest plan=1 tag=TLS_CROSSTEST -->
# TLS 크로스테스트 전 언어 확장
## 이 파일을 읽는 구현 에이전트에게
각 항목의 체크리스트를 완료 표시하고, 중간 검증 명령을 실행한 뒤 출력을 CODE_REVIEW.md `검증 결과` 섹션에 붙여 넣는다. 최종 검증까지 완료 후 CODE_REVIEW.md 각 항목을 `[x]`로 체크한다. **의존 관계 및 구현 순서** 섹션을 반드시 지킨다.
## 배경
`dart_go`·`go_dart` 두 조합에만 TLS TCP + WSS 크로스테스트가 존재한다. Python·TypeScript·Kotlin에 TLS 구현이 추가됨에 따라 나머지 18개 쌍에도 TLS 검증이 가능해졌다. 이번 작업으로 서버·클라이언트 양쪽 모두 TLS를 지원하는 모든 조합에 TLS 크로스테스트 phase를 추가한다.
**기준 파일:** `dart/crosstest/dart_go.dart` (orchestrator), `go/crosstest/dart_go_client/main.go` (client)
**인증서 경로 규칙:** 각 언어의 `test/certs/server.crt` + `server.key` 를 사용한다 (모두 `dart/test/certs/` 원본의 사본). orchestrator는 자신의 언어 경로로 서버 cert를 로드하고 `--cert=<path>` 로 클라이언트에 전달한다.
---
## 포트 배정표
| orchestrator | TCP | WS | TLS TCP (신규) | WSS (신규) |
|---|---|---|---|---|
| dart_kotlin | 29590 | 29592 | **29594** | **29596** |
| dart_python | 29694 | 29696 | **29698** | **29700** |
| dart_typescript | 29790 | 29792 | **29794** | **29796** |
| go_kotlin | 29290 | 29292 | **29294** | **29296** |
| go_python | 29390 | 29392 | **29394** | **29396** |
| go_typescript | 29490 | 29492 | **29494** | **29496** |
| kotlin_dart | 29490 | 29492 | **29494** | — |
| kotlin_go | 29390 | 29392 | **29394** | — |
| kotlin_python | 29394 | 29396 | **29398** | — |
| kotlin_typescript | 29794 | 29796 | **29798** | — |
| python_dart | 29494 | 29496 | **29498** | **29500** |
| python_go | 29490 | 29492 | **29494** | **29496** |
| python_kotlin | 29498 | 29500 | **29502** | **29504** |
| python_typescript | 29798 | 29800 | **29802** | **29804** |
| typescript_dart | 29802 | 29804 | **29806** | **29808** |
| typescript_go | 29806 | 29808 | **29810** | **29812** |
| typescript_kotlin | 29810 | 29812 | **29814** | — |
| typescript_python | 29814 | 29816 | **29818** | **29820** |
---
## [TLS_CROSSTEST-1] Dart 클라이언트 파일 — TLS 모드 추가
### 문제
`dart/crosstest/kotlin_dart_client.dart`, `dart/crosstest/python_dart_client.dart`, `dart/crosstest/typescript_dart_client.dart` 3개 파일이 `--mode=tls/wss`, `--cert` 인수를 처리하지 않는다.
### 해결 방법
`dart/crosstest/go_dart_client.dart` 패턴을 참조한다. 각 파일에서:
1. `args`에서 `--cert` 인수 파싱 추가
2. `_connectWithRetry` 또는 동등 함수의 `switch (mode)` 블록에 `'tls'`·`'wss'` 케이스 추가:
```dart
case 'tls':
final ctx = SecurityContext()..setTrustedCertificates(certPath!);
final socket = await ProtobufClient.connectSecure(_host, port, context: ctx);
return _ClientHandle(/* TCP client */, () async => /* close */);
case 'wss':
final ctx = SecurityContext()..setTrustedCertificates(certPath!);
final ws = await WsProtobufClient.connectSecure(_host, port, context: ctx);
return _ClientHandle(/* WS client */, () async => /* close */);
```
3. 각 파일에서 push 메시지 검증 문자열이 맞는지 확인:
- `kotlin_dart_client`: `'push from kotlin server'`
- `python_dart_client`: `'push from python server'`
- `typescript_dart_client`: `'push from typescript server'`
### 수정 파일 및 체크리스트
- `dart/crosstest/kotlin_dart_client.dart`
- [x] `--cert` 파라미터 파싱 추가
- [x] `'tls'`·`'wss'` 모드 케이스 추가
- `dart/crosstest/python_dart_client.dart`
- [x] `--cert` 파라미터 파싱 추가
- [x] `'tls'`·`'wss'` 모드 케이스 추가
- `dart/crosstest/typescript_dart_client.dart`
- [x] `--cert` 파라미터 파싱 추가
- [x] `'tls'`·`'wss'` 모드 케이스 추가
### 테스트 작성
단독 실행 테스트 없음. TLS_CROSSTEST-8~10의 orchestrator 검증으로 확인.
### 중간 검증
```
$ cd dart && dart analyze crosstest/kotlin_dart_client.dart crosstest/python_dart_client.dart crosstest/typescript_dart_client.dart
No issues found!
```
---
## [TLS_CROSSTEST-2] Go 클라이언트 파일 — TLS 모드 추가
### 문제
`go/crosstest/kotlin_go_client/main.go`, `go/crosstest/python_go_client/main.go`, `go/crosstest/typescript_go_client/main.go` 3개 파일이 `--mode=tls/wss`, `--cert` 를 처리하지 않는다.
### 해결 방법
`go/crosstest/dart_go_client/main.go` 패턴을 참조한다. 각 파일에서:
1. `flag.String("cert", "", ...)` 플래그 추가
2. `dial()` 함수의 `switch mode` 블록에 `"tls"`·`"wss"` 케이스 추가:
```go
case "tls":
tlsCfg, err := buildClientTLS(*cert)
client, err = toki.DialTcpTLS(ctx, host, port, tlsCfg, 0, 0, parserMap())
case "wss":
tlsCfg, err := buildClientTLS(*cert)
client, err = toki.DialWssWithHeartbeat(ctx, host, port, wsPath, tlsCfg, 0, 0, parserMap())
```
3. `buildClientTLS(certFile string)` 헬퍼를 `dart_go_client/main.go`에서 복사 추가
4. 각 파일 push 메시지 검증 문자열 확인:
- `kotlin_go_client`: `"push from kotlin server"`
- `python_go_client`: `"push from python server"`
- `typescript_go_client`: `"push from typescript server"`
### 수정 파일 및 체크리스트
- `go/crosstest/kotlin_go_client/main.go`
- [x] `--cert` 플래그 추가
- [x] `"tls"`·`"wss"` 케이스 및 `buildClientTLS` 헬퍼 추가
- `go/crosstest/python_go_client/main.go`
- [x] 동일
- `go/crosstest/typescript_go_client/main.go`
- [x] 동일
### 테스트 작성
없음. TLS_CROSSTEST-7~9의 orchestrator 검증으로 확인.
### 중간 검증
```
$ cd go && env PATH=/config/go-sdk/go/bin:$PATH GOCACHE=/tmp/go-build GOMODCACHE=/tmp/go-mod \
go build ./crosstest/kotlin_go_client ./crosstest/python_go_client ./crosstest/typescript_go_client
(exit 0)
```
---
## [TLS_CROSSTEST-3] Kotlin 클라이언트 파일 — TLS 모드 추가
### 문제
`kotlin/crosstest/dart_kotlin_client.kt`, `go_kotlin_client/Main.kt`, `python_kotlin_client.kt`, `typescript_kotlin_client.kt` 4개 파일이 `--mode=tls/wss`, `--cert` 를 처리하지 않는다.
### 해결 방법
각 파일에서:
1. `argValue(args, "cert")` 파싱 추가
2. `connectWithRetry(mode, port, cert)` 시그니처에 `cert: String?` 추가
3. `when (mode)` 블록에 `"tls"`·`"wss"` 케이스 추가:
```kotlin
"tls" -> {
val sslCtx = buildClientSslContext(cert ?: error("--cert required for tls"))
DialTcpTls(HOST, port, sslCtx, 0, 0, parserMap)
}
"wss" -> {
val sslCtx = buildClientSslContext(cert ?: error("--cert required for wss"))
dialWss(HOST, port, WS_PATH, sslCtx, 0, 0, parserMap)
}
```
4. `buildClientSslContext(certPath: String): SSLContext` 헬퍼 추가:
```kotlin
fun buildClientSslContext(certPath: String): SSLContext {
val certFactory = java.security.cert.CertificateFactory.getInstance("X.509")
val cert = java.io.File(certPath).inputStream().use {
certFactory.generateCertificate(it) as java.security.cert.X509Certificate
}
val ts = java.security.KeyStore.getInstance("PKCS12")
ts.load(null, null)
ts.setCertificateEntry("toki", cert)
val tmf = javax.net.ssl.TrustManagerFactory.getInstance(
javax.net.ssl.TrustManagerFactory.getDefaultAlgorithm()
)
tmf.init(ts)
val ctx = javax.net.ssl.SSLContext.getInstance("TLS")
ctx.init(null, tmf.trustManagers, null)
return ctx
}
```
5. 각 파일 push 메시지 검증 문자열 확인 (`'push from dart/go/python/typescript server'`).
**참고:** `go_kotlin_client`는 Gradle 서브프로젝트 (`kotlin/crosstest/go_kotlin_client/`). 나머지 세 파일은 독립 `.kt` 파일.
### 수정 파일 및 체크리스트
- `kotlin/crosstest/dart_kotlin_client.kt`
- [x] `--cert` 파싱, `"tls"`·`"wss"` 케이스, `buildClientSslContext` 추가
- `kotlin/crosstest/go_kotlin_client/src/main/kotlin/com/tokilabs/toki_socket/crosstest/Main.kt`
- [x] 동일
- `kotlin/crosstest/python_kotlin_client.kt`
- [x] 동일
- `kotlin/crosstest/typescript_kotlin_client.kt`
- [x] 동일
### 테스트 작성
없음. TLS_CROSSTEST-6~10의 orchestrator 검증으로 확인.
### 중간 검증
```
$ cd kotlin && env JAVA_HOME=/config/opt/jdk/jdk-17.0.10+7 GRADLE_USER_HOME=/tmp/gradle \
./gradlew compileCrosstestKotlin
BUILD SUCCESSFUL
```
---
## [TLS_CROSSTEST-4] Python 클라이언트 파일 — TLS 모드 추가
### 문제
`python/crosstest/dart_python_client.py`, `go_python_client.py`, `kotlin_python_client.py`, `typescript_python_client.py` 4개 파일이 TLS 모드를 처리하지 않는다.
### 해결 방법
각 파일에서:
1. `argparse`에 `--cert` 인수 추가 (`default=None`)
2. `choices=["tcp", "ws"]` → `["tcp", "ws", "tls", "wss"]` 변경
3. `dial()` 함수에 `"tls"`·`"wss"` 케이스 추가:
```python
elif mode == "tls":
ssl_ctx = ssl.create_default_context(cafile=cert)
ssl_ctx.check_hostname = False
return await connect_tcp_tls(HOST, port, ssl_ctx, 0, 0, parser_map())
elif mode == "wss":
ssl_ctx = ssl.create_default_context(cafile=cert)
ssl_ctx.check_hostname = False
return await connect_wss(HOST, port, WS_PATH, ssl_ctx, 0, 0, parser_map())
```
4. `import ssl`, `connect_tcp_tls`, `connect_wss` import 추가
### 수정 파일 및 체크리스트
- `python/crosstest/dart_python_client.py`
- [x] `import ssl`, `connect_tcp_tls`, `connect_wss` 추가
- [x] `--cert` argparse 인수 추가, choices 확장
- [x] `"tls"`·`"wss"` 분기 추가
- `python/crosstest/go_python_client.py`
- [x] 동일
- `python/crosstest/kotlin_python_client.py`
- [x] 동일
- `python/crosstest/typescript_python_client.py`
- [x] 동일
### 중간 검증
```
$ python3 -m py_compile python/crosstest/dart_python_client.py \
python/crosstest/go_python_client.py \
python/crosstest/kotlin_python_client.py \
python/crosstest/typescript_python_client.py
(exit 0)
```
---
## [TLS_CROSSTEST-5] TypeScript 클라이언트 파일 — TLS 모드 추가
### 문제
`typescript/crosstest/dart_typescript_client.ts`, `go_typescript_client.ts`, `kotlin_typescript_client.ts`, `python_typescript_client.ts` 4개 파일이 TLS 모드를 처리하지 않는다.
### 해결 방법
각 파일에서:
1. `parseArgs()` 내 `mode` 타입을 `"tcp" | "ws" | "tls" | "wss"` 로 확장
2. `--cert` 인수 파싱 추가
3. `dialWithRetry` / `dial()` 에 `cert?: string` 파라미터 추가
4. `dial()` switch 에 `"tls"`·`"wss"` 케이스 추가:
```typescript
case "tls": {
const ca = fs.readFileSync(cert!);
return connectTcpTls(HOST, port, { ca }, 0, 0, parserMap());
}
case "wss": {
const ca = fs.readFileSync(cert!);
return connectWss(HOST, port, WS_PATH, { ca }, 0, 0, parserMap());
}
```
5. `import * as fs from "node:fs"`, `connectTcpTls`, `connectWss` import 추가
### 수정 파일 및 체크리스트
- `typescript/crosstest/dart_typescript_client.ts`
- [x] `fs`, `connectTcpTls`, `connectWss` import 추가
- [x] `--cert` 파싱, mode 타입 확장, `dial()` 케이스 추가
- `typescript/crosstest/go_typescript_client.ts`
- [x] 동일
- `typescript/crosstest/kotlin_typescript_client.ts`
- [x] 동일
- `typescript/crosstest/python_typescript_client.ts`
- [x] 동일
### 중간 검증
```
$ cd typescript && npx tsc --noEmit
(exit 0)
```
---
## [TLS_CROSSTEST-6] Dart orchestrator 3개 — TLS phase 추가
### 문제
`dart/crosstest/dart_kotlin.dart`, `dart/crosstest/dart_python.dart`, `dart/crosstest/dart_typescript.dart` 에 TLS phase가 없다.
### 해결 방법
`dart/crosstest/dart_go.dart` 패턴을 참조한다. 각 파일에서:
1. TLS/WSS 포트 상수 추가:
- `dart_kotlin`: `_tlsTcpPort = 29594`, `_wssPort = 29596`
- `dart_python`: `_tlsTcpPort = 29698`, `_wssPort = 29700`
- `dart_typescript`: `_tlsTcpPort = 29794`, `_wssPort = 29796`
2. `_DartTlsTcpServer`, `_DartWssServer` 클래스 추가 (dart_go.dart 와 동일 구조)
3. `_runTls()` 메서드 추가 — TLS TCP send-push, TLS TCP requests, WSS send-push, WSS requests 4개 phase
4. `main()`에서 `await _runTls()` 호출 추가
5. 각 파일의 클라이언트 실행 헬퍼에서 `--cert=<path>` 전달
6. 인증서 경로: `dart/test/certs/server.crt`, `dart/test/certs/server.key`
### 수정 파일 및 체크리스트
- `dart/crosstest/dart_kotlin.dart`
- [x] TLS/WSS 포트 상수 추가 (29594, 29596)
- [x] `_DartTlsTcpServer`, `_DartWssServer` 클래스 추가
- [x] `_runTls()` 메서드 추가 (4 phase)
- [x] `main()`에서 `_runTls()` 호출 추가
- [x] Kotlin 클라이언트 호출에 TLS 인수 전달
- `dart/crosstest/dart_python.dart`
- [x] 동일 (포트 29698, 29700)
- `dart/crosstest/dart_typescript.dart`
- [x] 동일 (포트 29794, 29796)
### 중간 검증
```
$ cd dart && dart analyze crosstest/dart_kotlin.dart crosstest/dart_python.dart crosstest/dart_typescript.dart
No issues found!
$ cd dart && dart run crosstest/dart_python.dart
PASS all dart-server/python-client crosstests passed
$ cd dart && dart run crosstest/dart_typescript.dart
PASS all dart-server/typescript-client crosstests passed
$ cd dart && env JAVA_HOME=/config/opt/jdk/jdk-17.0.10+7 dart run crosstest/dart_kotlin.dart
PASS all dart-server/kotlin-client crosstests passed
```
---
## [TLS_CROSSTEST-7] Go orchestrator 3개 — TLS phase 추가
### 문제
`go/crosstest/go_kotlin.go`, `go/crosstest/go_python.go`, `go/crosstest/go_typescript.go` 에 TLS phase가 없다.
### 해결 방법
`go/crosstest/go_dart.go` 패턴 참조. 각 파일에서:
1. TLS/WSS 포트 상수 추가:
- `go_kotlin`: `goKotlinTLSPort = 29294`, `goKotlinWSSPort = 29296`
- `go_python`: `goPythonTLSPort = 29394`, `goPythonWSSPort = 29396`
- `go_typescript`: `goTypescriptTLSPort = 29494`, `goTypescriptWSSPort = 29496`
2. `crypto/tls` import 추가 (미존재 시)
3. `loadServerTLS(certFile, keyFile)` 헬퍼 추가 (go_dart.go 에서 복사)
4. `runTLSTCPSendPush`, `runTLSTCPRequests`, `runWSSendPushSecure`, `runWSSRequestsSecure` 추가
5. `run()` 에서 TLS phase 호출 추가
6. 인증서: `go_dart.go`와 동일하게 `dartDir`의 `test/certs/` 에서 로드
### 수정 파일 및 체크리스트
- `go/crosstest/go_kotlin.go`
- [x] TLS 포트 상수 (29294, 29296), `loadServerTLS`, TLS phase 함수 4개 추가
- [x] `run()`에서 호출 추가
- `go/crosstest/go_python.go`
- [x] 동일 (포트 29394, 29396)
- `go/crosstest/go_typescript.go`
- [x] 동일 (포트 29494, 29496)
### 중간 검증
```
$ cd go && env PATH=/config/go-sdk/go/bin:$PATH GOCACHE=/tmp/go-build GOMODCACHE=/tmp/go-mod \
go run ./crosstest/go_python.go
PASS all go-server/python-client crosstests passed
$ cd go && env PATH=/config/go-sdk/go/bin:$PATH GOCACHE=/tmp/go-build GOMODCACHE=/tmp/go-mod \
go run ./crosstest/go_typescript.go
PASS all go-server/typescript-client crosstests passed
$ cd go && env PATH=/config/go-sdk/go/bin:$PATH JAVA_HOME=/config/opt/jdk/jdk-17.0.10+7 GOCACHE=/tmp/go-build GOMODCACHE=/tmp/go-mod \
go run ./crosstest/go_kotlin.go
PASS all go-server/kotlin-client crosstests passed
```
---
## [TLS_CROSSTEST-8] Kotlin orchestrator 4개 — TLS TCP phase 추가
### 문제
`kotlin_dart.kt`, `kotlin_go.kt`, `kotlin_python.kt`, `kotlin_typescript.kt` 에 TLS phase가 없다. Kotlin `WsServer` TLS는 이번 TLS_CROSSTEST 범위에서 사용하지 않으므로 TLS TCP 2개 phase만 추가한다.
### 해결 방법
각 파일에서:
1. TLS TCP 포트 상수 추가:
- `kotlin_dart`: TLS TCP `29494`
- `kotlin_go`: TLS TCP `29394`
- `kotlin_python`: TLS TCP `29398`
- `kotlin_typescript`: TLS TCP `29798`
2. 인증서 로딩: `kotlin/src/test/resources/server.crt` + `server.key` 경로를 repo root 기준으로 계산
3. `runTlsTcp(sslContext, certPath)` 함수 추가 — send-push, requests 2 phase
4. Kotlin `WsServer`는 TLS를 지원하지 않으므로 WSS phase는 추가하지 않는다.
5. TLS TCP 서버는 `TcpServer(..., sslContext = serverCtx)` 생성자 사용
6. main 또는 run 함수에서 `runTlsTcp()` 호출 추가
### 수정 파일 및 체크리스트
- `kotlin/crosstest/kotlin_dart.kt`
- [x] TLS TCP 포트 상수 (29494)
- [x] 인증서 경로 계산 로직 추가
- [x] `runTlsTcp()` 함수 추가 (send-push, requests)
- [x] main에서 `runTlsTcp()` 호출 추가
- [x] WSS phase 제외 (Kotlin `WsServer` TLS 미지원)
- `kotlin/crosstest/kotlin_go.kt`
- [x] 동일 (TLS TCP 포트 29394)
- `kotlin/crosstest/kotlin_python.kt`
- [x] 동일 (TLS TCP 포트 29398)
- `kotlin/crosstest/kotlin_typescript.kt`
- [x] 동일 (TLS TCP 포트 29798)
### 중간 검증
```
$ cd kotlin && env JAVA_HOME=/config/opt/jdk/jdk-17.0.10+7 GRADLE_USER_HOME=/tmp/gradle \
./gradlew run -PmainClass=com.tokilabs.toki_socket.crosstest.KotlinDartKt
PASS all kotlin-server/dart-client crosstests passed
$ cd kotlin && env JAVA_HOME=/config/opt/jdk/jdk-17.0.10+7 GRADLE_USER_HOME=/tmp/gradle \
./gradlew run -PmainClass=com.tokilabs.toki_socket.crosstest.KotlinGoKt
PASS all kotlin-server/go-client crosstests passed
$ cd kotlin && env JAVA_HOME=/config/opt/jdk/jdk-17.0.10+7 GRADLE_USER_HOME=/tmp/gradle \
./gradlew run -PmainClass=com.tokilabs.toki_socket.crosstest.KotlinPythonKt
PASS all kotlin-server/python-client crosstests passed
$ cd kotlin && env JAVA_HOME=/config/opt/jdk/jdk-17.0.10+7 GRADLE_USER_HOME=/tmp/gradle \
./gradlew run -PmainClass=com.tokilabs.toki_socket.crosstest.KotlinTypescriptKt
PASS all kotlin-server/typescript-client crosstests passed
```
---
## [TLS_CROSSTEST-9] Python orchestrator 4개 — TLS phase 추가
### 문제
`python/crosstest/python_dart.py`, `python_go.py`, `python_kotlin.py`, `python_typescript.py` 에 TLS phase가 없다.
### 해결 방법
각 파일에서:
1. TLS/WSS 포트 상수 추가:
- `python_dart`: `TLS_TCP_PORT = 29498`, `WSS_PORT = 29500`
- `python_go`: `TLS_TCP_PORT = 29494`, `WSS_PORT = 29496`
- `python_kotlin`: `TLS_TCP_PORT = 29502`, `WSS_PORT = 29504`
- `python_typescript`: `TLS_TCP_PORT = 29802`, `WSS_PORT = 29804`
2. `import ssl` 추가
3. `_make_server_ssl_context()` 헬퍼 추가:
```python
def _make_server_ssl_context() -> ssl.SSLContext:
ctx = ssl.SSLContext(ssl.PROTOCOL_TLS_SERVER)
ctx.load_cert_chain(CERT_PATH, KEY_PATH)
return ctx
```
4. `CERT_PATH`, `KEY_PATH` 상수: 각 파일 기준 repo root에서 계산
5. `run_tls()` 코루틴 추가 — TLS TCP send-push, TLS TCP requests, WSS send-push, WSS requests
6. `main()` 또는 `run()` 에서 `await run_tls()` 호출 추가
7. `--cert` 로 클라이언트 subprocess에 인증서 경로 전달
### 수정 파일 및 체크리스트
- `python/crosstest/python_dart.py`
- [x] TLS/WSS 포트 상수 (29498, 29500), cert 경로, `run_tls()` 추가
- `python/crosstest/python_go.py`
- [x] 동일 (29494, 29496)
- `python/crosstest/python_kotlin.py`
- [x] 동일 (29502, 29504)
- `python/crosstest/python_typescript.py`
- [x] 동일 (29802, 29804)
### 중간 검증
```
$ python3 -m py_compile python/crosstest/python_dart.py python/crosstest/python_go.py \
python/crosstest/python_kotlin.py python/crosstest/python_typescript.py
(exit 0)
$ cd python && env PATH=/config/go-sdk/go/bin:$PATH GOCACHE=/tmp/go-build GOMODCACHE=/tmp/go-mod python3 -m crosstest.python_go
PASS all python-server/go-client crosstests passed
$ cd python && python3 -m crosstest.python_typescript
PASS all python-server/typescript-client crosstests passed
$ cd python && python3 -m crosstest.python_dart
PASS all python-server/dart-client crosstests passed
$ cd python && env JAVA_HOME=/config/opt/jdk/jdk-17.0.10+7 python3 -m crosstest.python_kotlin
PASS all python-server/kotlin-client crosstests passed
```
---
## [TLS_CROSSTEST-10] TypeScript orchestrator 4개 — TLS phase 추가
### 문제
`typescript/crosstest/typescript_dart.ts`, `typescript_go.ts`, `typescript_kotlin.ts`, `typescript_python.ts` 에 TLS phase가 없다.
### 해결 방법
각 파일에서:
1. TLS/WSS 포트 상수 추가:
- `typescript_dart`: `TLS_TCP_PORT = 29806`, `WSS_PORT = 29808`
- `typescript_go`: `TLS_TCP_PORT = 29810`, `WSS_PORT = 29812`
- `typescript_kotlin`: `TLS_TCP_PORT = 29814` (WSS 없음)
- `typescript_python`: `TLS_TCP_PORT = 29818`, `WSS_PORT = 29820`
2. `import * as fs from "node:fs"` 추가
3. `CERT_PATH`, `KEY_PATH`: repo root 기준 계산
4. `runTls()` 함수 추가:
- `typescript_kotlin`: TLS TCP 2 phase만 (Kotlin WsServer TLS 미지원)
- 나머지: TLS TCP send-push, TLS TCP requests, WSS send-push, WSS requests
5. main 에서 `await runTls()` 호출 추가
6. `TcpServer` 생성 시 `tlsOptions: { cert, key }` 전달
7. `WsServer` 생성 시 `tlsOptions: { cert, key }` 전달 (`typescript_kotlin` 제외)
8. 클라이언트 subprocess에 `--mode=tls`, `--cert=CERT_PATH` 전달
### 수정 파일 및 체크리스트
- `typescript/crosstest/typescript_dart.ts`
- [x] TLS/WSS 포트 상수 (29806, 29808), cert 경로, `runTls()` 추가
- `typescript/crosstest/typescript_go.ts`
- [x] 동일 (29810, 29812)
- `typescript/crosstest/typescript_kotlin.ts`
- [x] TLS 포트 상수 (29814), `runTls()` — TLS TCP 2 phase만
- `typescript/crosstest/typescript_python.ts`
- [x] 동일 (29818, 29820)
### 중간 검증
```
$ cd typescript && npx tsc --noEmit
(exit 0)
$ cd typescript && env PATH=/config/go-sdk/go/bin:$PATH GOCACHE=/tmp/go-build GOMODCACHE=/tmp/go-mod node --import tsx crosstest/typescript_go.ts
PASS all typescript-server/go-client crosstests passed
$ cd typescript && node --import tsx crosstest/typescript_python.ts
PASS all typescript-server/python-client crosstests passed
$ cd typescript && node --import tsx crosstest/typescript_dart.ts
PASS all typescript-server/dart-client crosstests passed
$ cd typescript && env JAVA_HOME=/config/opt/jdk/jdk-17.0.10+7 node --import tsx crosstest/typescript_kotlin.ts
PASS all typescript-server/kotlin-client crosstests passed
```
---
## 수정 파일 요약
| 파일 | 항목 |
|------|------|
| `dart/crosstest/kotlin_dart_client.dart` | TLS_CROSSTEST-1 |
| `dart/crosstest/python_dart_client.dart` | TLS_CROSSTEST-1 |
| `dart/crosstest/typescript_dart_client.dart` | TLS_CROSSTEST-1 |
| `go/crosstest/kotlin_go_client/main.go` | TLS_CROSSTEST-2 |
| `go/crosstest/python_go_client/main.go` | TLS_CROSSTEST-2 |
| `go/crosstest/typescript_go_client/main.go` | TLS_CROSSTEST-2 |
| `kotlin/crosstest/dart_kotlin_client.kt` | TLS_CROSSTEST-3 |
| `kotlin/crosstest/go_kotlin_client/src/main/kotlin/com/tokilabs/toki_socket/crosstest/Main.kt` | TLS_CROSSTEST-3 |
| `kotlin/crosstest/python_kotlin_client.kt` | TLS_CROSSTEST-3 |
| `kotlin/crosstest/typescript_kotlin_client.kt` | TLS_CROSSTEST-3 |
| `python/crosstest/dart_python_client.py` | TLS_CROSSTEST-4 |
| `python/crosstest/go_python_client.py` | TLS_CROSSTEST-4 |
| `python/crosstest/kotlin_python_client.py` | TLS_CROSSTEST-4 |
| `python/crosstest/typescript_python_client.py` | TLS_CROSSTEST-4 |
| `typescript/crosstest/dart_typescript_client.ts` | TLS_CROSSTEST-5 |
| `typescript/crosstest/go_typescript_client.ts` | TLS_CROSSTEST-5 |
| `typescript/crosstest/kotlin_typescript_client.ts` | TLS_CROSSTEST-5 |
| `typescript/crosstest/python_typescript_client.ts` | TLS_CROSSTEST-5 |
| `dart/crosstest/dart_kotlin.dart` | TLS_CROSSTEST-6 |
| `dart/crosstest/dart_python.dart` | TLS_CROSSTEST-6 |
| `dart/crosstest/dart_typescript.dart` | TLS_CROSSTEST-6 |
| `go/crosstest/go_kotlin.go` | TLS_CROSSTEST-7 |
| `go/crosstest/go_python.go` | TLS_CROSSTEST-7 |
| `go/crosstest/go_typescript.go` | TLS_CROSSTEST-7 |
| `kotlin/crosstest/kotlin_dart.kt` | TLS_CROSSTEST-8 |
| `kotlin/crosstest/kotlin_go.kt` | TLS_CROSSTEST-8 |
| `kotlin/crosstest/kotlin_python.kt` | TLS_CROSSTEST-8 |
| `kotlin/crosstest/kotlin_typescript.kt` | TLS_CROSSTEST-8 |
| `python/crosstest/python_dart.py` | TLS_CROSSTEST-9 |
| `python/crosstest/python_go.py` | TLS_CROSSTEST-9 |
| `python/crosstest/python_kotlin.py` | TLS_CROSSTEST-9 |
| `python/crosstest/python_typescript.py` | TLS_CROSSTEST-9 |
| `typescript/crosstest/typescript_dart.ts` | TLS_CROSSTEST-10 |
| `typescript/crosstest/typescript_go.ts` | TLS_CROSSTEST-10 |
| `typescript/crosstest/typescript_kotlin.ts` | TLS_CROSSTEST-10 |
| `typescript/crosstest/typescript_python.ts` | TLS_CROSSTEST-10 |
## 의존 관계 및 구현 순서
**1단계 (클라이언트 파일 — 병렬 가능):**
TLS_CROSSTEST-1, TLS_CROSSTEST-2, TLS_CROSSTEST-3, TLS_CROSSTEST-4, TLS_CROSSTEST-5
**2단계 (orchestrator 파일 — 각 클라이언트 완료 후):**
- TLS_CROSSTEST-6 → TLS_CROSSTEST-1(Dart client), TLS_CROSSTEST-3(Kotlin), TLS_CROSSTEST-4(Python), TLS_CROSSTEST-5(TypeScript) 완료 후
- TLS_CROSSTEST-7 → TLS_CROSSTEST-2(Go), TLS_CROSSTEST-3(Kotlin), TLS_CROSSTEST-4(Python), TLS_CROSSTEST-5(TypeScript) 완료 후
- TLS_CROSSTEST-8 → TLS_CROSSTEST-1(Dart), TLS_CROSSTEST-2(Go), TLS_CROSSTEST-4(Python), TLS_CROSSTEST-5(TypeScript) 완료 후
- TLS_CROSSTEST-9 → TLS_CROSSTEST-1(Dart), TLS_CROSSTEST-2(Go), TLS_CROSSTEST-3(Kotlin), TLS_CROSSTEST-5(TypeScript) 완료 후
- TLS_CROSSTEST-10 → TLS_CROSSTEST-1(Dart), TLS_CROSSTEST-2(Go), TLS_CROSSTEST-3(Kotlin), TLS_CROSSTEST-4(Python) 완료 후
## 최종 검증
```
$ cd dart && dart run crosstest/dart_kotlin.dart
PASS all dart-server/kotlin-client crosstests passed
$ cd dart && dart run crosstest/dart_python.dart
PASS all dart-server/python-client crosstests passed
$ cd dart && dart run crosstest/dart_typescript.dart
PASS all dart-server/typescript-client crosstests passed
$ cd go && env PATH=/config/go-sdk/go/bin:$PATH GOCACHE=/tmp/go-build GOMODCACHE=/tmp/go-mod \
go run ./crosstest/go_kotlin.go && go run ./crosstest/go_python.go && go run ./crosstest/go_typescript.go
PASS all go-server/kotlin-client crosstests passed
PASS all go-server/python-client crosstests passed
PASS all go-server/typescript-client crosstests passed
$ cd kotlin && env JAVA_HOME=/config/opt/jdk/jdk-17.0.10+7 GRADLE_USER_HOME=/tmp/gradle \
./gradlew run -PmainClass=com.tokilabs.toki_socket.crosstest.KotlinDartKt && \
./gradlew run -PmainClass=com.tokilabs.toki_socket.crosstest.KotlinGoKt && \
./gradlew run -PmainClass=com.tokilabs.toki_socket.crosstest.KotlinPythonKt && \
./gradlew run -PmainClass=com.tokilabs.toki_socket.crosstest.KotlinTypescriptKt
(모두 PASS)
$ cd python && python3 -m crosstest.python_dart && python3 -m crosstest.python_go && \
python3 -m crosstest.python_kotlin && python3 -m crosstest.python_typescript
(모두 PASS)
$ cd typescript && node --import tsx crosstest/typescript_dart.ts && \
node --import tsx crosstest/typescript_go.ts && \
node --import tsx crosstest/typescript_kotlin.ts && \
node --import tsx crosstest/typescript_python.ts
(모두 PASS)
```

View file

@ -7,12 +7,16 @@ import 'package:toki_socket/toki_socket.dart';
const _host = '127.0.0.1';
const _tcpPort = 29590;
const _wsPort = 29592;
const _tlsTcpPort = 29594;
const _wssPort = 29596;
const _heartbeatIntervalSeconds = 30;
const _heartbeatWaitSeconds = 10;
const _serverObservationWindow = Duration(milliseconds: 200);
const _processTimeout = Duration(seconds: 20);
final _repoRoot = File.fromUri(Platform.script).parent.parent.parent.path;
final _kotlinDir = Directory('$_repoRoot/kotlin').path;
final _certFile = '$_repoRoot/dart/test/certs/server.crt';
final _keyFile = '$_repoRoot/dart/test/certs/server.key';
class _DartTcpClient extends ProtobufClient {
_DartTcpClient(Socket socket)
@ -38,6 +42,16 @@ class _DartTcpServer extends ProtobufServer {
void onClientConnected(ProtobufClient client) => onConnected(client);
}
class _DartTlsTcpServer extends ProtobufServer {
final void Function(ProtobufClient) onConnected;
_DartTlsTcpServer(int port, SecurityContext ctx, this.onConnected)
: super.secure(_host, port, ctx, (socket) => _DartTcpClient(socket));
@override
void onClientConnected(ProtobufClient client) => onConnected(client);
}
class _DartWsServer extends WsProtobufServer {
final void Function(WsProtobufClient) onConnected;
@ -48,12 +62,23 @@ class _DartWsServer extends WsProtobufServer {
void onClientConnected(WsProtobufClient client) => onConnected(client);
}
class _DartWssServer extends WsProtobufServer {
final void Function(WsProtobufClient) onConnected;
_DartWssServer(int port, SecurityContext ctx, this.onConnected)
: super.secure(_host, port, ctx, (ws) => _DartWsClient(ws));
@override
void onClientConnected(WsProtobufClient client) => onConnected(client);
}
Future<void> main() async {
print(
'INFO typeName dart=${TestData.getDefault().info_.qualifiedMessageName}');
try {
await _runTcp();
await _runWs();
await _runTls();
print('PASS all dart-server/kotlin-client crosstests passed');
} catch (error) {
stderr.writeln('FAIL crosstest error=$error');
@ -167,14 +192,117 @@ Future<void> _withServer(Future<void> Function() start,
}
}
Future<void> _runTls() async {
final ctx = SecurityContext()
..useCertificateChain(_certFile)
..usePrivateKey(_keyFile);
await _runTlsTcpSendPush(ctx);
await Future<void>.delayed(const Duration(milliseconds: 150));
await _runTlsTcpRequests(ctx);
await Future<void>.delayed(const Duration(milliseconds: 150));
await _runWssSendPush(ctx);
await Future<void>.delayed(const Duration(milliseconds: 150));
await _runWssRequests(ctx);
}
Future<void> _runTlsTcpSendPush(SecurityContext ctx) async {
final received = Completer<bool>();
final server = _DartTlsTcpServer(_tlsTcpPort, ctx, (client) {
client.addListener<TestData>((data) {
final valid =
data.index == 101 && data.message == 'fire from kotlin client';
if (!received.isCompleted) {
received.complete(valid);
}
if (valid) {
unawaited(client.send(TestData()
..index = 200
..message = 'push from dart server'));
}
});
});
await _withServer(server.start, server.stop, () async {
await _runKotlinClient('tls', _tlsTcpPort, 'send-push', {'1', '2'},
certFile: _certFile);
final ok = await received.future
.timeout(_serverObservationWindow, onTimeout: () => false);
if (!ok) {
throw StateError(
'TLS TCP send-push server did not receive expected data');
}
});
}
Future<void> _runTlsTcpRequests(SecurityContext ctx) async {
final server = _DartTlsTcpServer(_tlsTcpPort, ctx, (client) {
client.addRequestListener<TestData, TestData>((req) async {
return TestData()
..index = req.index * 2
..message = 'echo: ${req.message}';
});
});
await _withServer(
server.start,
server.stop,
() => _runKotlinClient('tls', _tlsTcpPort, 'requests', {'3', '4'},
certFile: _certFile),
);
}
Future<void> _runWssSendPush(SecurityContext ctx) async {
final received = Completer<bool>();
final server = _DartWssServer(_wssPort, ctx, (client) {
client.addListener<TestData>((data) {
final valid =
data.index == 101 && data.message == 'fire from kotlin client';
if (!received.isCompleted) {
received.complete(valid);
}
if (valid) {
unawaited(client.send(TestData()
..index = 200
..message = 'push from dart server'));
}
});
});
await _withServer(server.start, server.stop, () async {
await _runKotlinClient('wss', _wssPort, 'send-push', {'1', '2'},
certFile: _certFile);
final ok = await received.future
.timeout(_serverObservationWindow, onTimeout: () => false);
if (!ok) {
throw StateError('WSS send-push server did not receive expected data');
}
});
}
Future<void> _runWssRequests(SecurityContext ctx) async {
final server = _DartWssServer(_wssPort, ctx, (client) {
client.addRequestListener<TestData, TestData>((req) async {
return TestData()
..index = req.index * 2
..message = 'echo: ${req.message}';
});
});
await _withServer(
server.start,
server.stop,
() => _runKotlinClient('wss', _wssPort, 'requests', {'3', '4'},
certFile: _certFile),
);
}
Future<void> _runKotlinClient(
String mode, int port, String phase, Set<String> expectedScenarios) async {
String mode, int port, String phase, Set<String> expectedScenarios,
{String? certFile}) async {
final clientArgs = '--mode=$mode --port=$port --phase=$phase'
'${certFile == null ? '' : ' --cert=$certFile'}';
final process = await Process.start(
'./gradlew',
[
'run',
'-PmainClass=com.tokilabs.toki_socket.crosstest.DartKotlinClientKt',
'--args=--mode=$mode --port=$port --phase=$phase',
'--args=$clientArgs',
],
workingDirectory: _kotlinDir,
);

View file

@ -7,12 +7,16 @@ import 'package:toki_socket/toki_socket.dart';
const _host = '127.0.0.1';
const _tcpPort = 29694;
const _wsPort = 29696;
const _tlsTcpPort = 29698;
const _wssPort = 29700;
const _heartbeatIntervalSeconds = 30;
const _heartbeatWaitSeconds = 10;
const _serverObservationWindow = Duration(milliseconds: 200);
const _processTimeout = Duration(seconds: 20);
final _repoRoot = File.fromUri(Platform.script).parent.parent.parent.path;
final _pythonDir = Directory('$_repoRoot/python').path;
final _certFile = '$_repoRoot/dart/test/certs/server.crt';
final _keyFile = '$_repoRoot/dart/test/certs/server.key';
class _DartTcpClient extends ProtobufClient {
_DartTcpClient(Socket socket)
@ -38,6 +42,16 @@ class _DartTcpServer extends ProtobufServer {
void onClientConnected(ProtobufClient client) => onConnected(client);
}
class _DartTlsTcpServer extends ProtobufServer {
final void Function(ProtobufClient) onConnected;
_DartTlsTcpServer(int port, SecurityContext ctx, this.onConnected)
: super.secure(_host, port, ctx, (socket) => _DartTcpClient(socket));
@override
void onClientConnected(ProtobufClient client) => onConnected(client);
}
class _DartWsServer extends WsProtobufServer {
final void Function(WsProtobufClient) onConnected;
@ -48,12 +62,23 @@ class _DartWsServer extends WsProtobufServer {
void onClientConnected(WsProtobufClient client) => onConnected(client);
}
class _DartWssServer extends WsProtobufServer {
final void Function(WsProtobufClient) onConnected;
_DartWssServer(int port, SecurityContext ctx, this.onConnected)
: super.secure(_host, port, ctx, (ws) => _DartWsClient(ws));
@override
void onClientConnected(WsProtobufClient client) => onConnected(client);
}
Future<void> main() async {
print(
'INFO typeName dart=${TestData.getDefault().info_.qualifiedMessageName}');
try {
await _runTcp();
await _runWs();
await _runTls();
print('PASS all dart-server/python-client crosstests passed');
} catch (error) {
stderr.writeln('FAIL crosstest error=$error');
@ -167,8 +192,109 @@ Future<void> _withServer(Future<void> Function() start,
}
}
Future<void> _runTls() async {
final ctx = SecurityContext()
..useCertificateChain(_certFile)
..usePrivateKey(_keyFile);
await _runTlsTcpSendPush(ctx);
await Future<void>.delayed(const Duration(milliseconds: 150));
await _runTlsTcpRequests(ctx);
await Future<void>.delayed(const Duration(milliseconds: 150));
await _runWssSendPush(ctx);
await Future<void>.delayed(const Duration(milliseconds: 150));
await _runWssRequests(ctx);
}
Future<void> _runTlsTcpSendPush(SecurityContext ctx) async {
final received = Completer<bool>();
final server = _DartTlsTcpServer(_tlsTcpPort, ctx, (client) {
client.addListener<TestData>((data) {
final valid =
data.index == 101 && data.message == 'fire from python client';
if (!received.isCompleted) {
received.complete(valid);
}
if (valid) {
unawaited(client.send(TestData()
..index = 200
..message = 'push from dart server'));
}
});
});
await _withServer(server.start, server.stop, () async {
await _runPythonClient('tls', _tlsTcpPort, 'send-push', {'1', '2'},
certFile: _certFile);
final ok = await received.future
.timeout(_serverObservationWindow, onTimeout: () => false);
if (!ok) {
throw StateError(
'TLS TCP send-push server did not receive expected data');
}
});
}
Future<void> _runTlsTcpRequests(SecurityContext ctx) async {
final server = _DartTlsTcpServer(_tlsTcpPort, ctx, (client) {
client.addRequestListener<TestData, TestData>((req) async {
return TestData()
..index = req.index * 2
..message = 'echo: ${req.message}';
});
});
await _withServer(
server.start,
server.stop,
() => _runPythonClient('tls', _tlsTcpPort, 'requests', {'3', '4'},
certFile: _certFile),
);
}
Future<void> _runWssSendPush(SecurityContext ctx) async {
final received = Completer<bool>();
final server = _DartWssServer(_wssPort, ctx, (client) {
client.addListener<TestData>((data) {
final valid =
data.index == 101 && data.message == 'fire from python client';
if (!received.isCompleted) {
received.complete(valid);
}
if (valid) {
unawaited(client.send(TestData()
..index = 200
..message = 'push from dart server'));
}
});
});
await _withServer(server.start, server.stop, () async {
await _runPythonClient('wss', _wssPort, 'send-push', {'1', '2'},
certFile: _certFile);
final ok = await received.future
.timeout(_serverObservationWindow, onTimeout: () => false);
if (!ok) {
throw StateError('WSS send-push server did not receive expected data');
}
});
}
Future<void> _runWssRequests(SecurityContext ctx) async {
final server = _DartWssServer(_wssPort, ctx, (client) {
client.addRequestListener<TestData, TestData>((req) async {
return TestData()
..index = req.index * 2
..message = 'echo: ${req.message}';
});
});
await _withServer(
server.start,
server.stop,
() => _runPythonClient('wss', _wssPort, 'requests', {'3', '4'},
certFile: _certFile),
);
}
Future<void> _runPythonClient(
String mode, int port, String phase, Set<String> expectedScenarios) async {
String mode, int port, String phase, Set<String> expectedScenarios,
{String? certFile}) async {
final process = await Process.start(
'python3',
[
@ -176,6 +302,7 @@ Future<void> _runPythonClient(
'--mode=$mode',
'--port=$port',
'--phase=$phase',
if (certFile != null) '--cert=$certFile',
],
workingDirectory: _pythonDir,
);

View file

@ -7,12 +7,16 @@ import 'package:toki_socket/toki_socket.dart';
const _host = '127.0.0.1';
const _tcpPort = 29790;
const _wsPort = 29792;
const _tlsTcpPort = 29794;
const _wssPort = 29796;
const _heartbeatIntervalSeconds = 30;
const _heartbeatWaitSeconds = 10;
const _serverObservationWindow = Duration(milliseconds: 200);
const _processTimeout = Duration(seconds: 20);
final _repoRoot = File.fromUri(Platform.script).parent.parent.parent.path;
final _typescriptDir = Directory('$_repoRoot/typescript').path;
final _certFile = '$_repoRoot/dart/test/certs/server.crt';
final _keyFile = '$_repoRoot/dart/test/certs/server.key';
class _DartTcpClient extends ProtobufClient {
_DartTcpClient(Socket socket)
@ -38,6 +42,16 @@ class _DartTcpServer extends ProtobufServer {
void onClientConnected(ProtobufClient client) => onConnected(client);
}
class _DartTlsTcpServer extends ProtobufServer {
final void Function(ProtobufClient) onConnected;
_DartTlsTcpServer(int port, SecurityContext ctx, this.onConnected)
: super.secure(_host, port, ctx, (socket) => _DartTcpClient(socket));
@override
void onClientConnected(ProtobufClient client) => onConnected(client);
}
class _DartWsServer extends WsProtobufServer {
final void Function(WsProtobufClient) onConnected;
@ -48,12 +62,23 @@ class _DartWsServer extends WsProtobufServer {
void onClientConnected(WsProtobufClient client) => onConnected(client);
}
class _DartWssServer extends WsProtobufServer {
final void Function(WsProtobufClient) onConnected;
_DartWssServer(int port, SecurityContext ctx, this.onConnected)
: super.secure(_host, port, ctx, (ws) => _DartWsClient(ws));
@override
void onClientConnected(WsProtobufClient client) => onConnected(client);
}
Future<void> main() async {
print(
'INFO typeName dart=${TestData.getDefault().info_.qualifiedMessageName}');
try {
await _runTcp();
await _runWs();
await _runTls();
print('PASS all dart-server/typescript-client crosstests passed');
} catch (error) {
stderr.writeln('FAIL crosstest error=$error');
@ -167,12 +192,109 @@ Future<void> _withServer(Future<void> Function() start,
}
}
Future<void> _runTls() async {
final ctx = SecurityContext()
..useCertificateChain(_certFile)
..usePrivateKey(_keyFile);
await _runTlsTcpSendPush(ctx);
await Future<void>.delayed(const Duration(milliseconds: 150));
await _runTlsTcpRequests(ctx);
await Future<void>.delayed(const Duration(milliseconds: 150));
await _runWssSendPush(ctx);
await Future<void>.delayed(const Duration(milliseconds: 150));
await _runWssRequests(ctx);
}
Future<void> _runTlsTcpSendPush(SecurityContext ctx) async {
final received = Completer<bool>();
final server = _DartTlsTcpServer(_tlsTcpPort, ctx, (client) {
client.addListener<TestData>((data) {
final valid =
data.index == 101 && data.message == 'fire from typescript client';
if (!received.isCompleted) {
received.complete(valid);
}
if (valid) {
unawaited(client.send(TestData()
..index = 200
..message = 'push from dart server'));
}
});
});
await _withServer(server.start, server.stop, () async {
await _runTypescriptClient('tls', _tlsTcpPort, 'send-push', {'1', '2'},
certFile: _certFile);
final ok = await received.future
.timeout(_serverObservationWindow, onTimeout: () => false);
if (!ok) {
throw StateError(
'TLS TCP send-push server did not receive expected data');
}
});
}
Future<void> _runTlsTcpRequests(SecurityContext ctx) async {
final server = _DartTlsTcpServer(_tlsTcpPort, ctx, (client) {
client.addRequestListener<TestData, TestData>((req) async {
return TestData()
..index = req.index * 2
..message = 'echo: ${req.message}';
});
});
await _withServer(
server.start,
server.stop,
() => _runTypescriptClient('tls', _tlsTcpPort, 'requests', {'3', '4'},
certFile: _certFile),
);
}
Future<void> _runWssSendPush(SecurityContext ctx) async {
final received = Completer<bool>();
final server = _DartWssServer(_wssPort, ctx, (client) {
client.addListener<TestData>((data) {
final valid =
data.index == 101 && data.message == 'fire from typescript client';
if (!received.isCompleted) {
received.complete(valid);
}
if (valid) {
unawaited(client.send(TestData()
..index = 200
..message = 'push from dart server'));
}
});
});
await _withServer(server.start, server.stop, () async {
await _runTypescriptClient('wss', _wssPort, 'send-push', {'1', '2'},
certFile: _certFile);
final ok = await received.future
.timeout(_serverObservationWindow, onTimeout: () => false);
if (!ok) {
throw StateError('WSS send-push server did not receive expected data');
}
});
}
Future<void> _runWssRequests(SecurityContext ctx) async {
final server = _DartWssServer(_wssPort, ctx, (client) {
client.addRequestListener<TestData, TestData>((req) async {
return TestData()
..index = req.index * 2
..message = 'echo: ${req.message}';
});
});
await _withServer(
server.start,
server.stop,
() => _runTypescriptClient('wss', _wssPort, 'requests', {'3', '4'},
certFile: _certFile),
);
}
Future<void> _runTypescriptClient(
String mode,
int port,
String phase,
Set<String> expectedScenarios,
) async {
String mode, int port, String phase, Set<String> expectedScenarios,
{String? certFile}) async {
final process = await Process.start(
'npx',
[
@ -181,6 +303,7 @@ Future<void> _runTypescriptClient(
'--mode=$mode',
'--port=$port',
'--phase=$phase',
if (certFile != null) '--cert=$certFile',
],
workingDirectory: _typescriptDir,
);

View file

@ -35,6 +35,7 @@ Future<void> main(List<String> args) async {
final mode = _argValue(args, 'mode') ?? 'tcp';
final phase = _argValue(args, 'phase') ?? 'send-push';
final port = int.tryParse(_argValue(args, 'port') ?? '');
final certPath = _argValue(args, 'cert');
print(
'INFO typeName dart=${TestData.getDefault().info_.qualifiedMessageName}');
@ -47,7 +48,7 @@ Future<void> main(List<String> args) async {
late _ClientHandle handle;
try {
handle = await _connectWithRetry(mode, port);
handle = await _connectWithRetry(mode, port, certPath: certPath);
} catch (error) {
_fail('setup', error.toString());
exitCode = 1;
@ -76,7 +77,8 @@ Future<void> main(List<String> args) async {
exit(exitCode);
}
Future<_ClientHandle> _connectWithRetry(String mode, int port) async {
Future<_ClientHandle> _connectWithRetry(String mode, int port,
{String? certPath}) async {
final deadline = DateTime.now().add(_connectWindow);
Object? lastError;
@ -93,6 +95,20 @@ Future<_ClientHandle> _connectWithRetry(String mode, int port) async {
.timeout(const Duration(milliseconds: 300));
final client = _WsClient(ws);
return _ClientHandle(client, client.close);
case 'tls':
final ctx = _buildClientSecurityContext(certPath, mode);
final socket =
await ProtobufClient.connectSecure(_host, port, context: ctx)
.timeout(const Duration(milliseconds: 300));
final client = _TcpClient(socket);
return _ClientHandle(client, client.close);
case 'wss':
final ctx = _buildClientSecurityContext(certPath, mode);
final ws = await WsProtobufClient.connectSecure(_host, port,
path: _wsPath, context: ctx)
.timeout(const Duration(milliseconds: 300));
final client = _WsClient(ws);
return _ClientHandle(client, client.close);
default:
throw ArgumentError('unknown mode "$mode"');
}
@ -106,6 +122,13 @@ Future<_ClientHandle> _connectWithRetry(String mode, int port) async {
'connect $mode:$port timed out: $lastError', _connectWindow);
}
SecurityContext _buildClientSecurityContext(String? certPath, String mode) {
if (certPath == null || certPath.isEmpty) {
throw ArgumentError('--cert is required for $mode mode');
}
return SecurityContext()..setTrustedCertificates(certPath);
}
Future<bool> _runSendPush(Communicator client) async {
final pushCompleter = Completer<TestData>();
client.addListener<TestData>((data) {

View file

@ -35,6 +35,7 @@ Future<void> main(List<String> args) async {
final mode = _argValue(args, 'mode') ?? 'tcp';
final phase = _argValue(args, 'phase') ?? 'send-push';
final port = int.tryParse(_argValue(args, 'port') ?? '');
final certPath = _argValue(args, 'cert');
print(
'INFO typeName dart=${TestData.getDefault().info_.qualifiedMessageName}');
@ -47,7 +48,7 @@ Future<void> main(List<String> args) async {
late _ClientHandle handle;
try {
handle = await _connectWithRetry(mode, port);
handle = await _connectWithRetry(mode, port, certPath: certPath);
} catch (error) {
_fail('setup', error.toString());
exitCode = 1;
@ -76,7 +77,8 @@ Future<void> main(List<String> args) async {
exit(exitCode);
}
Future<_ClientHandle> _connectWithRetry(String mode, int port) async {
Future<_ClientHandle> _connectWithRetry(String mode, int port,
{String? certPath}) async {
final deadline = DateTime.now().add(_connectWindow);
Object? lastError;
@ -93,6 +95,20 @@ Future<_ClientHandle> _connectWithRetry(String mode, int port) async {
.timeout(const Duration(milliseconds: 300));
final client = _WsClient(ws);
return _ClientHandle(client, client.close);
case 'tls':
final ctx = _buildClientSecurityContext(certPath, mode);
final socket =
await ProtobufClient.connectSecure(_host, port, context: ctx)
.timeout(const Duration(milliseconds: 300));
final client = _TcpClient(socket);
return _ClientHandle(client, client.close);
case 'wss':
final ctx = _buildClientSecurityContext(certPath, mode);
final ws = await WsProtobufClient.connectSecure(_host, port,
path: _wsPath, context: ctx)
.timeout(const Duration(milliseconds: 300));
final client = _WsClient(ws);
return _ClientHandle(client, client.close);
default:
throw ArgumentError('unknown mode "$mode"');
}
@ -106,6 +122,13 @@ Future<_ClientHandle> _connectWithRetry(String mode, int port) async {
'connect $mode:$port timed out: $lastError', _connectWindow);
}
SecurityContext _buildClientSecurityContext(String? certPath, String mode) {
if (certPath == null || certPath.isEmpty) {
throw ArgumentError('--cert is required for $mode mode');
}
return SecurityContext()..setTrustedCertificates(certPath);
}
Future<bool> _runSendPush(Communicator client) async {
final pushCompleter = Completer<TestData>();
client.addListener<TestData>((data) {

View file

@ -35,6 +35,7 @@ Future<void> main(List<String> args) async {
final mode = _argValue(args, 'mode') ?? 'tcp';
final phase = _argValue(args, 'phase') ?? 'send-push';
final port = int.tryParse(_argValue(args, 'port') ?? '');
final certPath = _argValue(args, 'cert');
print(
'INFO typeName dart=${TestData.getDefault().info_.qualifiedMessageName}');
@ -47,7 +48,7 @@ Future<void> main(List<String> args) async {
late _ClientHandle handle;
try {
handle = await _connectWithRetry(mode, port);
handle = await _connectWithRetry(mode, port, certPath: certPath);
} catch (error) {
_fail('setup', error.toString());
exitCode = 1;
@ -76,7 +77,8 @@ Future<void> main(List<String> args) async {
exit(exitCode);
}
Future<_ClientHandle> _connectWithRetry(String mode, int port) async {
Future<_ClientHandle> _connectWithRetry(String mode, int port,
{String? certPath}) async {
final deadline = DateTime.now().add(_connectWindow);
Object? lastError;
@ -93,6 +95,20 @@ Future<_ClientHandle> _connectWithRetry(String mode, int port) async {
.timeout(const Duration(milliseconds: 300));
final client = _WsClient(ws);
return _ClientHandle(client, client.close);
case 'tls':
final ctx = _buildClientSecurityContext(certPath, mode);
final socket =
await ProtobufClient.connectSecure(_host, port, context: ctx)
.timeout(const Duration(milliseconds: 300));
final client = _TcpClient(socket);
return _ClientHandle(client, client.close);
case 'wss':
final ctx = _buildClientSecurityContext(certPath, mode);
final ws = await WsProtobufClient.connectSecure(_host, port,
path: _wsPath, context: ctx)
.timeout(const Duration(milliseconds: 300));
final client = _WsClient(ws);
return _ClientHandle(client, client.close);
default:
throw ArgumentError('unknown mode "$mode"');
}
@ -106,6 +122,13 @@ Future<_ClientHandle> _connectWithRetry(String mode, int port) async {
'connect $mode:$port timed out: $lastError', _connectWindow);
}
SecurityContext _buildClientSecurityContext(String? certPath, String mode) {
if (certPath == null || certPath.isEmpty) {
throw ArgumentError('--cert is required for $mode mode');
}
return SecurityContext()..setTrustedCertificates(certPath);
}
Future<bool> _runSendPush(Communicator client) async {
final pushCompleter = Completer<TestData>();
client.addListener<TestData>((data) {

View file

@ -5,6 +5,7 @@ package main
import (
"bufio"
"context"
"crypto/tls"
"errors"
"fmt"
"io"
@ -28,6 +29,8 @@ const (
host = "127.0.0.1"
goKotlinTCPPort = 29290
goKotlinWSPort = 29292
goKotlinTLSPort = 29294
goKotlinWSSPort = 29296
wsPath = "/"
processTimeout = 20 * time.Second
serverObservationWindow = 200 * time.Millisecond
@ -64,7 +67,43 @@ func run() error {
return err
}
time.Sleep(150 * time.Millisecond)
return runWSRequests()
if err := runWSRequests(); err != nil {
return err
}
time.Sleep(150 * time.Millisecond)
dartDir, err := dartPackageDir()
if err != nil {
return fmt.Errorf("cannot find dart package dir: %w", err)
}
certFile := filepath.Join(dartDir, "test", "certs", "server.crt")
keyFile := filepath.Join(dartDir, "test", "certs", "server.key")
serverTLS, err := loadServerTLS(certFile, keyFile)
if err != nil {
return fmt.Errorf("load TLS certs: %w", err)
}
if err := runTLSTCPSendPush(serverTLS, certFile); err != nil {
return err
}
time.Sleep(150 * time.Millisecond)
if err := runTLSTCPRequests(serverTLS, certFile); err != nil {
return err
}
time.Sleep(150 * time.Millisecond)
if err := runWSSendPushSecure(serverTLS, certFile); err != nil {
return err
}
time.Sleep(150 * time.Millisecond)
return runWSSRequestsSecure(serverTLS, certFile)
}
func loadServerTLS(certFile, keyFile string) (*tls.Config, error) {
cert, err := tls.LoadX509KeyPair(certFile, keyFile)
if err != nil {
return nil, err
}
return &tls.Config{Certificates: []tls.Certificate{cert}}, nil
}
func runTCPSendPush() error {
@ -193,7 +232,133 @@ func runWSRequests() error {
return runKotlinClient("ws", goKotlinWSPort, "requests", map[string]bool{"3": true, "4": true})
}
func runKotlinClient(mode string, port int, phase string, expected map[string]bool) error {
func runTLSTCPSendPush(serverTLS *tls.Config, certFile string) error {
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
received := make(chan bool, 1)
server := toki.NewTcpServerTLS(host, goKotlinTLSPort, serverTLS, func(conn net.Conn) *toki.TcpClient {
return toki.NewTcpClient(conn, 0, 0, parserMap())
})
server.OnClientConnected = func(client *toki.TcpClient) {
toki.AddListenerTyped[*packets.TestData](&client.Communicator, func(data *packets.TestData) {
fmt.Printf("SERVER_RECEIVED index=%d message=%s\n", data.GetIndex(), data.GetMessage())
valid := data.GetIndex() == 101 && data.GetMessage() == "fire from kotlin client"
select {
case received <- valid:
default:
}
if valid {
_ = client.Send(&packets.TestData{Index: 200, Message: "push from go server"})
}
})
}
if err := server.Start(ctx); err != nil {
return err
}
defer server.Stop()
if err := runKotlinClient("tls", goKotlinTLSPort, "send-push", map[string]bool{"1": true, "2": true}, certFile); err != nil {
return err
}
select {
case ok := <-received:
if !ok {
return errors.New("TLS TCP send-push server received unexpected data")
}
case <-time.After(serverObservationWindow):
return errors.New("TLS TCP send-push server did not receive expected data")
}
return nil
}
func runTLSTCPRequests(serverTLS *tls.Config, certFile string) error {
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
server := toki.NewTcpServerTLS(host, goKotlinTLSPort, serverTLS, func(conn net.Conn) *toki.TcpClient {
return toki.NewTcpClient(conn, 0, 0, parserMap())
})
server.OnClientConnected = func(client *toki.TcpClient) {
toki.AddRequestListenerTyped[*packets.TestData, *packets.TestData](&client.Communicator, func(req *packets.TestData) (*packets.TestData, error) {
return &packets.TestData{
Index: req.GetIndex() * 2,
Message: "echo: " + req.GetMessage(),
}, nil
})
}
if err := server.Start(ctx); err != nil {
return err
}
defer server.Stop()
return runKotlinClient("tls", goKotlinTLSPort, "requests", map[string]bool{"3": true, "4": true}, certFile)
}
func runWSSendPushSecure(serverTLS *tls.Config, certFile string) error {
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
received := make(chan bool, 1)
server := toki.NewWsServerTLS(host, goKotlinWSSPort, wsPath, serverTLS, func(conn *websocket.Conn) *toki.WsClient {
return toki.NewWsClient(conn, 0, 0, parserMap())
})
server.OnClientConnected = func(client *toki.WsClient) {
toki.AddListenerTyped[*packets.TestData](&client.Communicator, func(data *packets.TestData) {
fmt.Printf("SERVER_RECEIVED index=%d message=%s\n", data.GetIndex(), data.GetMessage())
valid := data.GetIndex() == 101 && data.GetMessage() == "fire from kotlin client"
select {
case received <- valid:
default:
}
if valid {
_ = client.Send(&packets.TestData{Index: 200, Message: "push from go server"})
}
})
}
if err := server.Start(ctx); err != nil {
return err
}
defer server.Stop()
if err := runKotlinClient("wss", goKotlinWSSPort, "send-push", map[string]bool{"1": true, "2": true}, certFile); err != nil {
return err
}
select {
case ok := <-received:
if !ok {
return errors.New("WSS send-push server received unexpected data")
}
case <-time.After(serverObservationWindow):
return errors.New("WSS send-push server did not receive expected data")
}
return nil
}
func runWSSRequestsSecure(serverTLS *tls.Config, certFile string) error {
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
server := toki.NewWsServerTLS(host, goKotlinWSSPort, wsPath, serverTLS, func(conn *websocket.Conn) *toki.WsClient {
return toki.NewWsClient(conn, 0, 0, parserMap())
})
server.OnClientConnected = func(client *toki.WsClient) {
toki.AddRequestListenerTyped[*packets.TestData, *packets.TestData](&client.Communicator, func(req *packets.TestData) (*packets.TestData, error) {
return &packets.TestData{
Index: req.GetIndex() * 2,
Message: "echo: " + req.GetMessage(),
}, nil
})
}
if err := server.Start(ctx); err != nil {
return err
}
defer server.Stop()
return runKotlinClient("wss", goKotlinWSSPort, "requests", map[string]bool{"3": true, "4": true}, certFile)
}
func runKotlinClient(mode string, port int, phase string, expected map[string]bool, certFile ...string) error {
ctx, cancel := context.WithTimeout(context.Background(), processTimeout)
defer cancel()
@ -202,9 +367,11 @@ func runKotlinClient(mode string, port int, phase string, expected map[string]bo
return err
}
cmd := exec.CommandContext(ctx, "./gradlew", "run",
"--args=--mode="+mode+" --port="+fmt.Sprint(port)+" --phase="+phase,
)
clientArgs := "--mode=" + mode + " --port=" + fmt.Sprint(port) + " --phase=" + phase
if len(certFile) > 0 && certFile[0] != "" {
clientArgs += " --cert=" + certFile[0]
}
cmd := exec.CommandContext(ctx, "./gradlew", "run", "--args="+clientArgs)
cmd.Dir = kotlinDir
stdoutPipe, err := cmd.StdoutPipe()
@ -276,6 +443,48 @@ func isKotlinPackageDir(dir string) bool {
return err == nil && !info.IsDir()
}
func dartPackageDir() (string, error) {
candidates := make([]string, 0, 3)
_, filename, _, ok := runtime.Caller(0)
if ok {
repoRoot := filepath.Dir(filepath.Dir(filepath.Dir(filename)))
candidates = append(candidates, filepath.Join(repoRoot, "dart"))
}
if wd, err := os.Getwd(); err == nil {
candidates = append(candidates, findDartPackageCandidates(wd)...)
}
if executable, err := os.Executable(); err == nil {
candidates = append(candidates, findDartPackageCandidates(filepath.Dir(executable))...)
}
for _, candidate := range candidates {
if isDartPackageDir(candidate) {
return candidate, nil
}
}
return "", fmt.Errorf("cannot resolve dart package directory from candidates %v", candidates)
}
func findDartPackageCandidates(start string) []string {
candidates := make([]string, 0)
for dir := start; ; dir = filepath.Dir(dir) {
candidates = append(candidates, filepath.Join(dir, "dart"))
if filepath.Base(dir) == "dart" {
candidates = append(candidates, dir)
}
parent := filepath.Dir(dir)
if parent == dir {
return candidates
}
}
}
func isDartPackageDir(dir string) bool {
info, err := os.Stat(filepath.Join(dir, "pubspec.yaml"))
return err == nil && !info.IsDir()
}
func scanLines(wg *sync.WaitGroup, reader io.Reader, writer *os.File, mu *sync.Mutex, resultLines *[]string) {
defer wg.Done()
scanner := bufio.NewScanner(reader)

View file

@ -5,6 +5,7 @@ package main
import (
"bufio"
"context"
"crypto/tls"
"errors"
"fmt"
"io"
@ -28,6 +29,8 @@ const (
host = "127.0.0.1"
goPythonTCPPort = 29390
goPythonWSPort = 29392
goPythonTLSPort = 29394
goPythonWSSPort = 29396
wsPath = "/"
processTimeout = 20 * time.Second
serverObservationWindow = 200 * time.Millisecond
@ -64,7 +67,43 @@ func run() error {
return err
}
time.Sleep(150 * time.Millisecond)
return runWSRequests()
if err := runWSRequests(); err != nil {
return err
}
time.Sleep(150 * time.Millisecond)
dartDir, err := dartPackageDir()
if err != nil {
return fmt.Errorf("cannot find dart package dir: %w", err)
}
certFile := filepath.Join(dartDir, "test", "certs", "server.crt")
keyFile := filepath.Join(dartDir, "test", "certs", "server.key")
serverTLS, err := loadServerTLS(certFile, keyFile)
if err != nil {
return fmt.Errorf("load TLS certs: %w", err)
}
if err := runTLSTCPSendPush(serverTLS, certFile); err != nil {
return err
}
time.Sleep(150 * time.Millisecond)
if err := runTLSTCPRequests(serverTLS, certFile); err != nil {
return err
}
time.Sleep(150 * time.Millisecond)
if err := runWSSendPushSecure(serverTLS, certFile); err != nil {
return err
}
time.Sleep(150 * time.Millisecond)
return runWSSRequestsSecure(serverTLS, certFile)
}
func loadServerTLS(certFile, keyFile string) (*tls.Config, error) {
cert, err := tls.LoadX509KeyPair(certFile, keyFile)
if err != nil {
return nil, err
}
return &tls.Config{Certificates: []tls.Certificate{cert}}, nil
}
func runTCPSendPush() error {
@ -193,7 +232,133 @@ func runWSRequests() error {
return runPythonClient("ws", goPythonWSPort, "requests", map[string]bool{"3": true, "4": true})
}
func runPythonClient(mode string, port int, phase string, expected map[string]bool) error {
func runTLSTCPSendPush(serverTLS *tls.Config, certFile string) error {
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
received := make(chan bool, 1)
server := toki.NewTcpServerTLS(host, goPythonTLSPort, serverTLS, func(conn net.Conn) *toki.TcpClient {
return toki.NewTcpClient(conn, 0, 0, parserMap())
})
server.OnClientConnected = func(client *toki.TcpClient) {
toki.AddListenerTyped[*packets.TestData](&client.Communicator, func(data *packets.TestData) {
fmt.Printf("SERVER_RECEIVED index=%d message=%s\n", data.GetIndex(), data.GetMessage())
valid := data.GetIndex() == 101 && data.GetMessage() == "fire from python client"
select {
case received <- valid:
default:
}
if valid {
_ = client.Send(&packets.TestData{Index: 200, Message: "push from go server"})
}
})
}
if err := server.Start(ctx); err != nil {
return err
}
defer server.Stop()
if err := runPythonClient("tls", goPythonTLSPort, "send-push", map[string]bool{"1": true, "2": true}, certFile); err != nil {
return err
}
select {
case ok := <-received:
if !ok {
return errors.New("TLS TCP send-push server received unexpected data")
}
case <-time.After(serverObservationWindow):
return errors.New("TLS TCP send-push server did not receive expected data")
}
return nil
}
func runTLSTCPRequests(serverTLS *tls.Config, certFile string) error {
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
server := toki.NewTcpServerTLS(host, goPythonTLSPort, serverTLS, func(conn net.Conn) *toki.TcpClient {
return toki.NewTcpClient(conn, 0, 0, parserMap())
})
server.OnClientConnected = func(client *toki.TcpClient) {
toki.AddRequestListenerTyped[*packets.TestData, *packets.TestData](&client.Communicator, func(req *packets.TestData) (*packets.TestData, error) {
return &packets.TestData{
Index: req.GetIndex() * 2,
Message: "echo: " + req.GetMessage(),
}, nil
})
}
if err := server.Start(ctx); err != nil {
return err
}
defer server.Stop()
return runPythonClient("tls", goPythonTLSPort, "requests", map[string]bool{"3": true, "4": true}, certFile)
}
func runWSSendPushSecure(serverTLS *tls.Config, certFile string) error {
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
received := make(chan bool, 1)
server := toki.NewWsServerTLS(host, goPythonWSSPort, wsPath, serverTLS, func(conn *websocket.Conn) *toki.WsClient {
return toki.NewWsClient(conn, 0, 0, parserMap())
})
server.OnClientConnected = func(client *toki.WsClient) {
toki.AddListenerTyped[*packets.TestData](&client.Communicator, func(data *packets.TestData) {
fmt.Printf("SERVER_RECEIVED index=%d message=%s\n", data.GetIndex(), data.GetMessage())
valid := data.GetIndex() == 101 && data.GetMessage() == "fire from python client"
select {
case received <- valid:
default:
}
if valid {
_ = client.Send(&packets.TestData{Index: 200, Message: "push from go server"})
}
})
}
if err := server.Start(ctx); err != nil {
return err
}
defer server.Stop()
if err := runPythonClient("wss", goPythonWSSPort, "send-push", map[string]bool{"1": true, "2": true}, certFile); err != nil {
return err
}
select {
case ok := <-received:
if !ok {
return errors.New("WSS send-push server received unexpected data")
}
case <-time.After(serverObservationWindow):
return errors.New("WSS send-push server did not receive expected data")
}
return nil
}
func runWSSRequestsSecure(serverTLS *tls.Config, certFile string) error {
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
server := toki.NewWsServerTLS(host, goPythonWSSPort, wsPath, serverTLS, func(conn *websocket.Conn) *toki.WsClient {
return toki.NewWsClient(conn, 0, 0, parserMap())
})
server.OnClientConnected = func(client *toki.WsClient) {
toki.AddRequestListenerTyped[*packets.TestData, *packets.TestData](&client.Communicator, func(req *packets.TestData) (*packets.TestData, error) {
return &packets.TestData{
Index: req.GetIndex() * 2,
Message: "echo: " + req.GetMessage(),
}, nil
})
}
if err := server.Start(ctx); err != nil {
return err
}
defer server.Stop()
return runPythonClient("wss", goPythonWSSPort, "requests", map[string]bool{"3": true, "4": true}, certFile)
}
func runPythonClient(mode string, port int, phase string, expected map[string]bool, certFile ...string) error {
ctx, cancel := context.WithTimeout(context.Background(), processTimeout)
defer cancel()
@ -202,11 +367,16 @@ func runPythonClient(mode string, port int, phase string, expected map[string]bo
return err
}
cmd := exec.CommandContext(ctx, "python3", "crosstest/go_python_client.py",
"--mode="+mode,
args := []string{
"crosstest/go_python_client.py",
"--mode=" + mode,
fmt.Sprintf("--port=%d", port),
"--phase="+phase,
)
"--phase=" + phase,
}
if len(certFile) > 0 && certFile[0] != "" {
args = append(args, "--cert="+certFile[0])
}
cmd := exec.CommandContext(ctx, "python3", args...)
cmd.Dir = pythonDir
stdoutPipe, err := cmd.StdoutPipe()
@ -278,6 +448,48 @@ func isPythonPackageDir(dir string) bool {
return err == nil && !info.IsDir()
}
func dartPackageDir() (string, error) {
candidates := make([]string, 0, 3)
_, filename, _, ok := runtime.Caller(0)
if ok {
repoRoot := filepath.Dir(filepath.Dir(filepath.Dir(filename)))
candidates = append(candidates, filepath.Join(repoRoot, "dart"))
}
if wd, err := os.Getwd(); err == nil {
candidates = append(candidates, findDartPackageCandidates(wd)...)
}
if executable, err := os.Executable(); err == nil {
candidates = append(candidates, findDartPackageCandidates(filepath.Dir(executable))...)
}
for _, candidate := range candidates {
if isDartPackageDir(candidate) {
return candidate, nil
}
}
return "", fmt.Errorf("cannot resolve dart package directory from candidates %v", candidates)
}
func findDartPackageCandidates(start string) []string {
candidates := make([]string, 0)
for dir := start; ; dir = filepath.Dir(dir) {
candidates = append(candidates, filepath.Join(dir, "dart"))
if filepath.Base(dir) == "dart" {
candidates = append(candidates, dir)
}
parent := filepath.Dir(dir)
if parent == dir {
return candidates
}
}
}
func isDartPackageDir(dir string) bool {
info, err := os.Stat(filepath.Join(dir, "pubspec.yaml"))
return err == nil && !info.IsDir()
}
func scanLines(wg *sync.WaitGroup, reader io.Reader, writer *os.File, mu *sync.Mutex, resultLines *[]string) {
defer wg.Done()
scanner := bufio.NewScanner(reader)

View file

@ -5,6 +5,7 @@ package main
import (
"bufio"
"context"
"crypto/tls"
"errors"
"fmt"
"io"
@ -28,6 +29,8 @@ const (
host = "127.0.0.1"
goTypescriptTCPPort = 29490
goTypescriptWSPort = 29492
goTypescriptTLSPort = 29494
goTypescriptWSSPort = 29496
wsPath = "/"
processTimeout = 20 * time.Second
serverObservationWindow = 200 * time.Millisecond
@ -64,7 +67,43 @@ func run() error {
return err
}
time.Sleep(150 * time.Millisecond)
return runWSRequests()
if err := runWSRequests(); err != nil {
return err
}
time.Sleep(150 * time.Millisecond)
dartDir, err := dartPackageDir()
if err != nil {
return fmt.Errorf("cannot find dart package dir: %w", err)
}
certFile := filepath.Join(dartDir, "test", "certs", "server.crt")
keyFile := filepath.Join(dartDir, "test", "certs", "server.key")
serverTLS, err := loadServerTLS(certFile, keyFile)
if err != nil {
return fmt.Errorf("load TLS certs: %w", err)
}
if err := runTLSTCPSendPush(serverTLS, certFile); err != nil {
return err
}
time.Sleep(150 * time.Millisecond)
if err := runTLSTCPRequests(serverTLS, certFile); err != nil {
return err
}
time.Sleep(150 * time.Millisecond)
if err := runWSSendPushSecure(serverTLS, certFile); err != nil {
return err
}
time.Sleep(150 * time.Millisecond)
return runWSSRequestsSecure(serverTLS, certFile)
}
func loadServerTLS(certFile, keyFile string) (*tls.Config, error) {
cert, err := tls.LoadX509KeyPair(certFile, keyFile)
if err != nil {
return nil, err
}
return &tls.Config{Certificates: []tls.Certificate{cert}}, nil
}
func runTCPSendPush() error {
@ -193,7 +232,133 @@ func runWSRequests() error {
return runTypescriptClient("ws", goTypescriptWSPort, "requests", map[string]bool{"3": true, "4": true})
}
func runTypescriptClient(mode string, port int, phase string, expected map[string]bool) error {
func runTLSTCPSendPush(serverTLS *tls.Config, certFile string) error {
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
received := make(chan bool, 1)
server := toki.NewTcpServerTLS(host, goTypescriptTLSPort, serverTLS, func(conn net.Conn) *toki.TcpClient {
return toki.NewTcpClient(conn, 0, 0, parserMap())
})
server.OnClientConnected = func(client *toki.TcpClient) {
toki.AddListenerTyped[*packets.TestData](&client.Communicator, func(data *packets.TestData) {
fmt.Printf("SERVER_RECEIVED index=%d message=%s\n", data.GetIndex(), data.GetMessage())
valid := data.GetIndex() == 101 && data.GetMessage() == "fire from typescript client"
select {
case received <- valid:
default:
}
if valid {
_ = client.Send(&packets.TestData{Index: 200, Message: "push from go server"})
}
})
}
if err := server.Start(ctx); err != nil {
return err
}
defer server.Stop()
if err := runTypescriptClient("tls", goTypescriptTLSPort, "send-push", map[string]bool{"1": true, "2": true}, certFile); err != nil {
return err
}
select {
case ok := <-received:
if !ok {
return errors.New("TLS TCP send-push server received unexpected data")
}
case <-time.After(serverObservationWindow):
return errors.New("TLS TCP send-push server did not receive expected data")
}
return nil
}
func runTLSTCPRequests(serverTLS *tls.Config, certFile string) error {
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
server := toki.NewTcpServerTLS(host, goTypescriptTLSPort, serverTLS, func(conn net.Conn) *toki.TcpClient {
return toki.NewTcpClient(conn, 0, 0, parserMap())
})
server.OnClientConnected = func(client *toki.TcpClient) {
toki.AddRequestListenerTyped[*packets.TestData, *packets.TestData](&client.Communicator, func(req *packets.TestData) (*packets.TestData, error) {
return &packets.TestData{
Index: req.GetIndex() * 2,
Message: "echo: " + req.GetMessage(),
}, nil
})
}
if err := server.Start(ctx); err != nil {
return err
}
defer server.Stop()
return runTypescriptClient("tls", goTypescriptTLSPort, "requests", map[string]bool{"3": true, "4": true}, certFile)
}
func runWSSendPushSecure(serverTLS *tls.Config, certFile string) error {
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
received := make(chan bool, 1)
server := toki.NewWsServerTLS(host, goTypescriptWSSPort, wsPath, serverTLS, func(conn *websocket.Conn) *toki.WsClient {
return toki.NewWsClient(conn, 0, 0, parserMap())
})
server.OnClientConnected = func(client *toki.WsClient) {
toki.AddListenerTyped[*packets.TestData](&client.Communicator, func(data *packets.TestData) {
fmt.Printf("SERVER_RECEIVED index=%d message=%s\n", data.GetIndex(), data.GetMessage())
valid := data.GetIndex() == 101 && data.GetMessage() == "fire from typescript client"
select {
case received <- valid:
default:
}
if valid {
_ = client.Send(&packets.TestData{Index: 200, Message: "push from go server"})
}
})
}
if err := server.Start(ctx); err != nil {
return err
}
defer server.Stop()
if err := runTypescriptClient("wss", goTypescriptWSSPort, "send-push", map[string]bool{"1": true, "2": true}, certFile); err != nil {
return err
}
select {
case ok := <-received:
if !ok {
return errors.New("WSS send-push server received unexpected data")
}
case <-time.After(serverObservationWindow):
return errors.New("WSS send-push server did not receive expected data")
}
return nil
}
func runWSSRequestsSecure(serverTLS *tls.Config, certFile string) error {
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
server := toki.NewWsServerTLS(host, goTypescriptWSSPort, wsPath, serverTLS, func(conn *websocket.Conn) *toki.WsClient {
return toki.NewWsClient(conn, 0, 0, parserMap())
})
server.OnClientConnected = func(client *toki.WsClient) {
toki.AddRequestListenerTyped[*packets.TestData, *packets.TestData](&client.Communicator, func(req *packets.TestData) (*packets.TestData, error) {
return &packets.TestData{
Index: req.GetIndex() * 2,
Message: "echo: " + req.GetMessage(),
}, nil
})
}
if err := server.Start(ctx); err != nil {
return err
}
defer server.Stop()
return runTypescriptClient("wss", goTypescriptWSSPort, "requests", map[string]bool{"3": true, "4": true}, certFile)
}
func runTypescriptClient(mode string, port int, phase string, expected map[string]bool, certFile ...string) error {
ctx, cancel := context.WithTimeout(context.Background(), processTimeout)
defer cancel()
@ -202,11 +367,17 @@ func runTypescriptClient(mode string, port int, phase string, expected map[strin
return err
}
cmd := exec.CommandContext(ctx, "npx", "tsx", "crosstest/go_typescript_client.ts",
"--mode="+mode,
args := []string{
"tsx",
"crosstest/go_typescript_client.ts",
"--mode=" + mode,
fmt.Sprintf("--port=%d", port),
"--phase="+phase,
)
"--phase=" + phase,
}
if len(certFile) > 0 && certFile[0] != "" {
args = append(args, "--cert="+certFile[0])
}
cmd := exec.CommandContext(ctx, "npx", args...)
cmd.Dir = typescriptDir
stdoutPipe, err := cmd.StdoutPipe()
@ -282,6 +453,48 @@ func isTypescriptPackageDir(dir string) bool {
return err == nil && !tsconfig.IsDir()
}
func dartPackageDir() (string, error) {
candidates := make([]string, 0, 3)
_, filename, _, ok := runtime.Caller(0)
if ok {
repoRoot := filepath.Dir(filepath.Dir(filepath.Dir(filename)))
candidates = append(candidates, filepath.Join(repoRoot, "dart"))
}
if wd, err := os.Getwd(); err == nil {
candidates = append(candidates, findDartPackageCandidates(wd)...)
}
if executable, err := os.Executable(); err == nil {
candidates = append(candidates, findDartPackageCandidates(filepath.Dir(executable))...)
}
for _, candidate := range candidates {
if isDartPackageDir(candidate) {
return candidate, nil
}
}
return "", fmt.Errorf("cannot resolve dart package directory from candidates %v", candidates)
}
func findDartPackageCandidates(start string) []string {
candidates := make([]string, 0)
for dir := start; ; dir = filepath.Dir(dir) {
candidates = append(candidates, filepath.Join(dir, "dart"))
if filepath.Base(dir) == "dart" {
candidates = append(candidates, dir)
}
parent := filepath.Dir(dir)
if parent == dir {
return candidates
}
}
}
func isDartPackageDir(dir string) bool {
info, err := os.Stat(filepath.Join(dir, "pubspec.yaml"))
return err == nil && !info.IsDir()
}
func scanLines(wg *sync.WaitGroup, reader io.Reader, writer *os.File, mu *sync.Mutex, resultLines *[]string) {
defer wg.Done()
scanner := bufio.NewScanner(reader)

View file

@ -2,6 +2,8 @@ package main
import (
"context"
"crypto/tls"
"crypto/x509"
"flag"
"fmt"
"os"
@ -38,9 +40,10 @@ func parserMap() toki.ParserMap {
}
func main() {
mode := flag.String("mode", "tcp", "transport mode: tcp or ws")
mode := flag.String("mode", "tcp", "transport mode: tcp, ws, tls, or wss")
port := flag.Int("port", 0, "server port")
phase := flag.String("phase", "send-push", "test phase: send-push or requests")
cert := flag.String("cert", "", "path to PEM certificate for TLS verification")
flag.Parse()
fmt.Printf("INFO typeName go=%s\n", toki.TypeNameOf(&packets.TestData{}))
@ -50,7 +53,7 @@ func main() {
os.Exit(1)
}
client, err := dialWithRetry(*mode, *port)
client, err := dialWithRetry(*mode, *port, *cert)
if err != nil {
fail("setup", err.Error())
os.Exit(1)
@ -73,12 +76,24 @@ func main() {
}
}
func dialWithRetry(mode string, port int) (*clientHandle, error) {
func buildClientTLS(certFile string) (*tls.Config, error) {
certPEM, err := os.ReadFile(certFile)
if err != nil {
return nil, fmt.Errorf("read cert %s: %w", certFile, err)
}
pool := x509.NewCertPool()
if !pool.AppendCertsFromPEM(certPEM) {
return nil, fmt.Errorf("no valid PEM certificate found in %s", certFile)
}
return &tls.Config{RootCAs: pool}, nil
}
func dialWithRetry(mode string, port int, certFile string) (*clientHandle, error) {
deadline := time.Now().Add(connectWindow)
var lastErr error
for time.Now().Before(deadline) {
ctx, cancel := context.WithTimeout(context.Background(), 300*time.Millisecond)
handle, err := dial(ctx, mode, port)
handle, err := dial(ctx, mode, port, certFile)
cancel()
if err == nil {
return handle, nil
@ -89,7 +104,7 @@ func dialWithRetry(mode string, port int) (*clientHandle, error) {
return nil, fmt.Errorf("connect %s:%d timed out: %w", mode, port, lastErr)
}
func dial(ctx context.Context, mode string, port int) (*clientHandle, error) {
func dial(ctx context.Context, mode string, port int, certFile string) (*clientHandle, error) {
switch mode {
case "tcp":
client, err := toki.DialTcp(ctx, host, port, 0, 0, parserMap())
@ -111,6 +126,40 @@ func dial(ctx context.Context, mode string, port int) (*clientHandle, error) {
send: client.Send,
close: client.Close,
}, nil
case "tls":
if certFile == "" {
return nil, fmt.Errorf("--cert is required for tls mode")
}
tlsCfg, err := buildClientTLS(certFile)
if err != nil {
return nil, err
}
client, err := toki.DialTcpTLS(ctx, host, port, tlsCfg, 0, 0, parserMap())
if err != nil {
return nil, err
}
return &clientHandle{
communicator: &client.Communicator,
send: client.Send,
close: client.Close,
}, nil
case "wss":
if certFile == "" {
return nil, fmt.Errorf("--cert is required for wss mode")
}
tlsCfg, err := buildClientTLS(certFile)
if err != nil {
return nil, err
}
client, err := toki.DialWssWithHeartbeat(ctx, host, port, wsPath, tlsCfg, 0, 0, parserMap())
if err != nil {
return nil, err
}
return &clientHandle{
communicator: &client.Communicator,
send: client.Send,
close: client.Close,
}, nil
default:
return nil, fmt.Errorf("unknown mode %q", mode)
}

View file

@ -2,6 +2,8 @@ package main
import (
"context"
"crypto/tls"
"crypto/x509"
"flag"
"fmt"
"os"
@ -37,9 +39,10 @@ func parserMap() toki.ParserMap {
}
func main() {
mode := flag.String("mode", "tcp", "transport mode: tcp or ws")
mode := flag.String("mode", "tcp", "transport mode: tcp, ws, tls, or wss")
port := flag.Int("port", 0, "server port")
phase := flag.String("phase", "send-push", "test phase: send-push or requests")
cert := flag.String("cert", "", "path to PEM certificate for TLS verification")
flag.Parse()
fmt.Printf("INFO typeName go=%s\n", toki.TypeNameOf(&packets.TestData{}))
@ -49,7 +52,7 @@ func main() {
os.Exit(1)
}
client, err := dialWithRetry(*mode, *port)
client, err := dialWithRetry(*mode, *port, *cert)
if err != nil {
fail("setup", err.Error())
os.Exit(1)
@ -71,12 +74,24 @@ func main() {
}
}
func dialWithRetry(mode string, port int) (*clientHandle, error) {
func buildClientTLS(certFile string) (*tls.Config, error) {
certPEM, err := os.ReadFile(certFile)
if err != nil {
return nil, fmt.Errorf("read cert %s: %w", certFile, err)
}
pool := x509.NewCertPool()
if !pool.AppendCertsFromPEM(certPEM) {
return nil, fmt.Errorf("no valid PEM certificate found in %s", certFile)
}
return &tls.Config{RootCAs: pool}, nil
}
func dialWithRetry(mode string, port int, certFile string) (*clientHandle, error) {
deadline := time.Now().Add(connectWindow)
var lastErr error
for time.Now().Before(deadline) {
ctx, cancel := context.WithTimeout(context.Background(), 300*time.Millisecond)
handle, err := dial(ctx, mode, port)
handle, err := dial(ctx, mode, port, certFile)
cancel()
if err == nil {
return handle, nil
@ -87,7 +102,7 @@ func dialWithRetry(mode string, port int) (*clientHandle, error) {
return nil, fmt.Errorf("connect %s:%d timed out: %w", mode, port, lastErr)
}
func dial(ctx context.Context, mode string, port int) (*clientHandle, error) {
func dial(ctx context.Context, mode string, port int, certFile string) (*clientHandle, error) {
switch mode {
case "tcp":
client, err := toki.DialTcp(ctx, host, port, 0, 0, parserMap())
@ -109,6 +124,40 @@ func dial(ctx context.Context, mode string, port int) (*clientHandle, error) {
send: client.Send,
close: client.Close,
}, nil
case "tls":
if certFile == "" {
return nil, fmt.Errorf("--cert is required for tls mode")
}
tlsCfg, err := buildClientTLS(certFile)
if err != nil {
return nil, err
}
client, err := toki.DialTcpTLS(ctx, host, port, tlsCfg, 0, 0, parserMap())
if err != nil {
return nil, err
}
return &clientHandle{
communicator: &client.Communicator,
send: client.Send,
close: client.Close,
}, nil
case "wss":
if certFile == "" {
return nil, fmt.Errorf("--cert is required for wss mode")
}
tlsCfg, err := buildClientTLS(certFile)
if err != nil {
return nil, err
}
client, err := toki.DialWssWithHeartbeat(ctx, host, port, wsPath, tlsCfg, 0, 0, parserMap())
if err != nil {
return nil, err
}
return &clientHandle{
communicator: &client.Communicator,
send: client.Send,
close: client.Close,
}, nil
default:
return nil, fmt.Errorf("unknown mode %q", mode)
}

View file

@ -2,6 +2,8 @@ package main
import (
"context"
"crypto/tls"
"crypto/x509"
"flag"
"fmt"
"os"
@ -38,9 +40,10 @@ func parserMap() toki.ParserMap {
}
func main() {
mode := flag.String("mode", "tcp", "transport mode: tcp or ws")
mode := flag.String("mode", "tcp", "transport mode: tcp, ws, tls, or wss")
port := flag.Int("port", 0, "server port")
phase := flag.String("phase", "send-push", "test phase: send-push or requests")
cert := flag.String("cert", "", "path to PEM certificate for TLS verification")
flag.Parse()
fmt.Printf("INFO typeName go=%s\n", toki.TypeNameOf(&packets.TestData{}))
@ -50,7 +53,7 @@ func main() {
os.Exit(1)
}
client, err := dialWithRetry(*mode, *port)
client, err := dialWithRetry(*mode, *port, *cert)
if err != nil {
fail("setup", err.Error())
os.Exit(1)
@ -73,12 +76,24 @@ func main() {
}
}
func dialWithRetry(mode string, port int) (*clientHandle, error) {
func buildClientTLS(certFile string) (*tls.Config, error) {
certPEM, err := os.ReadFile(certFile)
if err != nil {
return nil, fmt.Errorf("read cert %s: %w", certFile, err)
}
pool := x509.NewCertPool()
if !pool.AppendCertsFromPEM(certPEM) {
return nil, fmt.Errorf("no valid PEM certificate found in %s", certFile)
}
return &tls.Config{RootCAs: pool}, nil
}
func dialWithRetry(mode string, port int, certFile string) (*clientHandle, error) {
deadline := time.Now().Add(connectWindow)
var lastErr error
for time.Now().Before(deadline) {
ctx, cancel := context.WithTimeout(context.Background(), 300*time.Millisecond)
handle, err := dial(ctx, mode, port)
handle, err := dial(ctx, mode, port, certFile)
cancel()
if err == nil {
return handle, nil
@ -89,7 +104,7 @@ func dialWithRetry(mode string, port int) (*clientHandle, error) {
return nil, fmt.Errorf("connect %s:%d timed out: %w", mode, port, lastErr)
}
func dial(ctx context.Context, mode string, port int) (*clientHandle, error) {
func dial(ctx context.Context, mode string, port int, certFile string) (*clientHandle, error) {
switch mode {
case "tcp":
client, err := toki.DialTcp(ctx, host, port, 0, 0, parserMap())
@ -111,6 +126,40 @@ func dial(ctx context.Context, mode string, port int) (*clientHandle, error) {
send: client.Send,
close: client.Close,
}, nil
case "tls":
if certFile == "" {
return nil, fmt.Errorf("--cert is required for tls mode")
}
tlsCfg, err := buildClientTLS(certFile)
if err != nil {
return nil, err
}
client, err := toki.DialTcpTLS(ctx, host, port, tlsCfg, 0, 0, parserMap())
if err != nil {
return nil, err
}
return &clientHandle{
communicator: &client.Communicator,
send: client.Send,
close: client.Close,
}, nil
case "wss":
if certFile == "" {
return nil, fmt.Errorf("--cert is required for wss mode")
}
tlsCfg, err := buildClientTLS(certFile)
if err != nil {
return nil, err
}
client, err := toki.DialWssWithHeartbeat(ctx, host, port, wsPath, tlsCfg, 0, 0, parserMap())
if err != nil {
return nil, err
}
return &clientHandle{
communicator: &client.Communicator,
send: client.Send,
close: client.Close,
}, nil
default:
return nil, fmt.Errorf("unknown mode %q", mode)
}

View file

@ -3,7 +3,9 @@
package com.tokilabs.toki_socket.crosstest
import com.tokilabs.toki_socket.DialTcp
import com.tokilabs.toki_socket.DialTcpTls
import com.tokilabs.toki_socket.DialWs
import com.tokilabs.toki_socket.DialWss
import com.tokilabs.toki_socket.ParserMap
import com.tokilabs.toki_socket.WsClient
import com.tokilabs.toki_socket.TcpClient
@ -64,6 +66,7 @@ fun main(args: Array<String>) = runBlocking {
val mode = argValue(args, "mode") ?: "tcp"
val phase = argValue(args, "phase") ?: "send-push"
val port = argValue(args, "port")?.toIntOrNull()
val cert = argValue(args, "cert")
println("INFO typeName kotlin=${typeNameOf<TestData>()}")
@ -73,7 +76,7 @@ fun main(args: Array<String>) = runBlocking {
}
val client = try {
connectWithRetry(mode, port)
connectWithRetry(mode, port, cert)
} catch (error: Throwable) {
fail("setup", error.message ?: error.toString())
exitProcess(1)
@ -95,7 +98,7 @@ fun main(args: Array<String>) = runBlocking {
if (!ok) exitProcess(1)
}
private suspend fun connectWithRetry(mode: String, port: Int): DartClientHandle {
private suspend fun connectWithRetry(mode: String, port: Int, cert: String?): DartClientHandle {
val deadline = System.nanoTime() + CONNECT_WINDOW_MS * 1_000_000L
var lastError: Throwable? = null
while (System.nanoTime() < deadline) {
@ -103,6 +106,14 @@ private suspend fun connectWithRetry(mode: String, port: Int): DartClientHandle
return when (mode) {
"tcp" -> DartTcpHandle(DialTcp(HOST, port, 0, 0, parserMap()))
"ws" -> DartWsHandle(DialWs(HOST, port, WS_PATH, 0, 0, parserMap()))
"tls" -> {
val sslCtx = buildClientSslContext(cert ?: error("--cert required for tls"))
DartTcpHandle(DialTcpTls(HOST, port, sslCtx, 0, 0, parserMap()))
}
"wss" -> {
val sslCtx = buildClientSslContext(cert ?: error("--cert required for wss"))
DartWsHandle(DialWss(HOST, port, WS_PATH, sslCtx, 0, 0, parserMap()))
}
else -> error("unknown mode $mode")
}
} catch (error: Throwable) {
@ -113,6 +124,23 @@ private suspend fun connectWithRetry(mode: String, port: Int): DartClientHandle
error("connect $mode:$port timed out: $lastError")
}
private fun buildClientSslContext(certPath: String): javax.net.ssl.SSLContext {
val certFactory = java.security.cert.CertificateFactory.getInstance("X.509")
val cert = java.io.File(certPath).inputStream().use {
certFactory.generateCertificate(it) as java.security.cert.X509Certificate
}
val trustStore = java.security.KeyStore.getInstance("PKCS12")
trustStore.load(null, null)
trustStore.setCertificateEntry("toki", cert)
val trustManagerFactory = javax.net.ssl.TrustManagerFactory.getInstance(
javax.net.ssl.TrustManagerFactory.getDefaultAlgorithm(),
)
trustManagerFactory.init(trustStore)
val ctx = javax.net.ssl.SSLContext.getInstance("TLS")
ctx.init(null, trustManagerFactory.trustManagers, null)
return ctx
}
private suspend fun runSendPush(client: DartClientHandle): Boolean {
val push = CompletableDeferred<TestData>()
addListenerTyped<TestData>(client.communicator) {

View file

@ -1,7 +1,9 @@
package com.tokilabs.toki_socket.crosstest
import com.tokilabs.toki_socket.DialTcp
import com.tokilabs.toki_socket.DialTcpTls
import com.tokilabs.toki_socket.DialWs
import com.tokilabs.toki_socket.DialWss
import com.tokilabs.toki_socket.ParserMap
import com.tokilabs.toki_socket.WsClient
import com.tokilabs.toki_socket.TcpClient
@ -62,6 +64,7 @@ fun main(args: Array<String>) = runBlocking {
val mode = argValue(args, "mode") ?: "tcp"
val phase = argValue(args, "phase") ?: "send-push"
val port = argValue(args, "port")?.toIntOrNull()
val cert = argValue(args, "cert")
println("INFO typeName kotlin=${typeNameOf<TestData>()}")
@ -71,7 +74,7 @@ fun main(args: Array<String>) = runBlocking {
}
val client = try {
connectWithRetry(mode, port)
connectWithRetry(mode, port, cert)
} catch (error: Throwable) {
fail("setup", error.message ?: error.toString())
exitProcess(1)
@ -93,7 +96,7 @@ fun main(args: Array<String>) = runBlocking {
if (!ok) exitProcess(1)
}
private suspend fun connectWithRetry(mode: String, port: Int): ClientHandle {
private suspend fun connectWithRetry(mode: String, port: Int, cert: String?): ClientHandle {
val deadline = System.nanoTime() + CONNECT_WINDOW_MS * 1_000_000L
var lastError: Throwable? = null
while (System.nanoTime() < deadline) {
@ -101,6 +104,14 @@ private suspend fun connectWithRetry(mode: String, port: Int): ClientHandle {
return when (mode) {
"tcp" -> TcpHandle(DialTcp(HOST, port, 0, 0, parserMap()))
"ws" -> WsHandle(DialWs(HOST, port, WS_PATH, 0, 0, parserMap()))
"tls" -> {
val sslCtx = buildClientSslContext(cert ?: error("--cert required for tls"))
TcpHandle(DialTcpTls(HOST, port, sslCtx, 0, 0, parserMap()))
}
"wss" -> {
val sslCtx = buildClientSslContext(cert ?: error("--cert required for wss"))
WsHandle(DialWss(HOST, port, WS_PATH, sslCtx, 0, 0, parserMap()))
}
else -> error("unknown mode $mode")
}
} catch (error: Throwable) {
@ -111,6 +122,23 @@ private suspend fun connectWithRetry(mode: String, port: Int): ClientHandle {
error("connect $mode:$port timed out: $lastError")
}
private fun buildClientSslContext(certPath: String): javax.net.ssl.SSLContext {
val certFactory = java.security.cert.CertificateFactory.getInstance("X.509")
val cert = java.io.File(certPath).inputStream().use {
certFactory.generateCertificate(it) as java.security.cert.X509Certificate
}
val trustStore = java.security.KeyStore.getInstance("PKCS12")
trustStore.load(null, null)
trustStore.setCertificateEntry("toki", cert)
val trustManagerFactory = javax.net.ssl.TrustManagerFactory.getInstance(
javax.net.ssl.TrustManagerFactory.getDefaultAlgorithm(),
)
trustManagerFactory.init(trustStore)
val ctx = javax.net.ssl.SSLContext.getInstance("TLS")
ctx.init(null, trustManagerFactory.trustManagers, null)
return ctx
}
private suspend fun runSendPush(client: ClientHandle): Boolean {
val push = CompletableDeferred<TestData>()
addListenerTyped<TestData>(client.communicator) {

View file

@ -20,12 +20,21 @@ import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.withContext
import kotlinx.coroutines.withTimeoutOrNull
import java.io.File
import java.security.KeyFactory
import java.security.KeyStore
import java.security.cert.CertificateFactory
import java.security.cert.X509Certificate
import java.security.spec.PKCS8EncodedKeySpec
import java.util.Base64
import java.util.concurrent.TimeUnit
import javax.net.ssl.KeyManagerFactory
import javax.net.ssl.SSLContext
import kotlin.system.exitProcess
private const val HOST = "127.0.0.1"
private const val TCP_PORT = 29490
private const val WS_PORT = 29492
private const val TLS_TCP_PORT = 29494
private const val WS_PATH = "/"
private const val PROCESS_TIMEOUT_MS = 20_000L
private const val SERVER_OBSERVATION_MS = 200L
@ -37,9 +46,9 @@ fun main() = runBlocking {
delay(150)
runTcpRequests()
delay(150)
runWsSendPush()
runWs()
delay(150)
runWsRequests()
runTlsTcp()
println("PASS all kotlin-server/dart-client crosstests passed")
} catch (error: Throwable) {
System.err.println("FAIL crosstest error=${error.message ?: error}")
@ -93,11 +102,71 @@ private suspend fun runTcpRequests() {
}
}
private suspend fun runWsSendPush() = coroutineScope {
val received = CompletableDeferred<Boolean>()
private suspend fun runWs() = coroutineScope {
val server = WsServer(HOST, WS_PORT, WS_PATH) { conn ->
WsClient.forServer(conn, 0, 0, parserMap())
}
server.start()
delay(100)
try {
runWsSendPush(server)
delay(150)
runWsRequests(server)
} finally {
server.stop()
}
}
private suspend fun runWsSendPush(server: WsServer) = coroutineScope {
val received = CompletableDeferred<Boolean>()
server.onClientConnected = { client ->
addListenerTyped<TestData>(client.communicator) { data ->
println("SERVER_RECEIVED index=${data.index} message=${data.message}")
val valid = data.index == 101 && data.message == "fire from dart client"
received.complete(valid)
if (valid) {
launch {
client.send(
TestData.newBuilder()
.setIndex(200)
.setMessage("push from kotlin server")
.build(),
)
}
}
}
}
runDartClient("ws", WS_PORT, "send-push", setOf("1", "2"))
val ok = withTimeoutOrNull(SERVER_OBSERVATION_MS) { received.await() } ?: false
check(ok) { "WS send-push server did not receive expected data" }
}
private suspend fun runWsRequests(server: WsServer) {
server.onClientConnected = { client ->
addRequestListenerTyped<TestData, TestData>(client.communicator) { req ->
TestData.newBuilder()
.setIndex(req.index * 2)
.setMessage("echo: ${req.message}")
.build()
}
}
runDartClient("ws", WS_PORT, "requests", setOf("3", "4"))
}
private suspend fun runTlsTcp() {
val certFile = kotlinResourceFile("server.crt")
val keyFile = kotlinResourceFile("server.key")
val serverCtx = buildServerSslContext(certFile.path, keyFile.path)
runTlsTcpSendPush(serverCtx, certFile.path)
delay(150)
runTlsTcpRequests(serverCtx, certFile.path)
}
private suspend fun runTlsTcpSendPush(serverCtx: SSLContext, certPath: String) = coroutineScope {
val received = CompletableDeferred<Boolean>()
val server = TcpServer(HOST, TLS_TCP_PORT, serverCtx) { socket ->
TcpClient.fromSocket(socket, 0, 0, parserMap())
}
server.onClientConnected = { client ->
addListenerTyped<TestData>(client.communicator) { data ->
println("SERVER_RECEIVED index=${data.index} message=${data.message}")
@ -116,15 +185,15 @@ private suspend fun runWsSendPush() = coroutineScope {
}
}
withServer(server::start, server::stop) {
runDartClient("ws", WS_PORT, "send-push", setOf("1", "2"))
runDartClient("tls", TLS_TCP_PORT, "send-push", setOf("1", "2"), certPath)
val ok = withTimeoutOrNull(SERVER_OBSERVATION_MS) { received.await() } ?: false
check(ok) { "WS send-push server did not receive expected data" }
check(ok) { "TLS TCP send-push server did not receive expected data" }
}
}
private suspend fun runWsRequests() {
val server = WsServer(HOST, WS_PORT, WS_PATH) { conn ->
WsClient.forServer(conn, 0, 0, parserMap())
private suspend fun runTlsTcpRequests(serverCtx: SSLContext, certPath: String) {
val server = TcpServer(HOST, TLS_TCP_PORT, serverCtx) { socket ->
TcpClient.fromSocket(socket, 0, 0, parserMap())
}
server.onClientConnected = { client ->
addRequestListenerTyped<TestData, TestData>(client.communicator) { req ->
@ -135,7 +204,7 @@ private suspend fun runWsRequests() {
}
}
withServer(server::start, server::stop) {
runDartClient("ws", WS_PORT, "requests", setOf("3", "4"))
runDartClient("tls", TLS_TCP_PORT, "requests", setOf("3", "4"), certPath)
}
}
@ -145,6 +214,7 @@ private suspend fun withServer(
body: suspend () -> Unit,
) {
start()
delay(100)
try {
body()
} finally {
@ -157,8 +227,9 @@ private suspend fun runDartClient(
port: Int,
phase: String,
expectedScenarios: Set<String>,
certPath: String? = null,
) = withContext(Dispatchers.IO) {
val process = ProcessBuilder(
val args = mutableListOf(
"dart",
"run",
"crosstest/kotlin_dart_client.dart",
@ -166,6 +237,10 @@ private suspend fun runDartClient(
"--port=$port",
"--phase=$phase",
)
if (certPath != null) {
args += "--cert=$certPath"
}
val process = ProcessBuilder(args)
.directory(dartDir())
.redirectErrorStream(false)
.start()
@ -222,6 +297,39 @@ private fun validateResultLines(
private fun parserMap(): ParserMap =
mapOf(typeNameOf<TestData>() to { TestData.parseFrom(it) })
private fun buildServerSslContext(certPath: String, keyPath: String): SSLContext {
val certFactory = CertificateFactory.getInstance("X.509")
val cert = File(certPath).inputStream().use {
certFactory.generateCertificate(it) as X509Certificate
}
val keyPem = File(keyPath).readText()
.replace("-----BEGIN PRIVATE KEY-----", "")
.replace("-----END PRIVATE KEY-----", "")
.replace("\\s+".toRegex(), "")
val privateKey = KeyFactory.getInstance("RSA")
.generatePrivate(PKCS8EncodedKeySpec(Base64.getDecoder().decode(keyPem)))
val keyStore = KeyStore.getInstance("PKCS12")
keyStore.load(null, null)
keyStore.setKeyEntry("toki", privateKey, CharArray(0), arrayOf(cert))
val keyManagerFactory = KeyManagerFactory.getInstance(KeyManagerFactory.getDefaultAlgorithm())
keyManagerFactory.init(keyStore, CharArray(0))
val ctx = SSLContext.getInstance("TLS")
ctx.init(keyManagerFactory.keyManagers, null, null)
return ctx
}
private fun kotlinResourceFile(name: String): File {
var dir = File(System.getProperty("user.dir")).absoluteFile
while (dir.parentFile != null) {
val direct = File(dir, "src/test/resources/$name").canonicalFile
if (direct.isFile) return direct
val nested = File(dir, "kotlin/src/test/resources/$name").canonicalFile
if (nested.isFile) return nested
dir = dir.parentFile
}
error("cannot resolve kotlin test resource $name")
}
private fun dartDir(): File {
var dir = File(System.getProperty("user.dir")).absoluteFile
while (dir.parentFile != null) {

View file

@ -20,12 +20,21 @@ import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.withContext
import kotlinx.coroutines.withTimeoutOrNull
import java.io.File
import java.security.KeyFactory
import java.security.KeyStore
import java.security.cert.CertificateFactory
import java.security.cert.X509Certificate
import java.security.spec.PKCS8EncodedKeySpec
import java.util.Base64
import java.util.concurrent.TimeUnit
import javax.net.ssl.KeyManagerFactory
import javax.net.ssl.SSLContext
import kotlin.system.exitProcess
private const val HOST = "127.0.0.1"
private const val TCP_PORT = 29390
private const val WS_PORT = 29392
private const val TLS_TCP_PORT = 29394
private const val WS_PATH = "/"
private const val PROCESS_TIMEOUT_MS = 20_000L
private const val SERVER_OBSERVATION_MS = 200L
@ -37,9 +46,9 @@ fun main() = runBlocking {
delay(150)
runTcpRequests()
delay(150)
runWsSendPush()
runWs()
delay(150)
runWsRequests()
runTlsTcp()
println("PASS all kotlin-server/go-client crosstests passed")
} catch (error: Throwable) {
System.err.println("FAIL crosstest error=${error.message ?: error}")
@ -93,11 +102,71 @@ private suspend fun runTcpRequests() {
}
}
private suspend fun runWsSendPush() = coroutineScope {
val received = CompletableDeferred<Boolean>()
private suspend fun runWs() = coroutineScope {
val server = WsServer(HOST, WS_PORT, WS_PATH) { conn ->
WsClient.forServer(conn, 0, 0, parserMap())
}
server.start()
delay(100)
try {
runWsSendPush(server)
delay(150)
runWsRequests(server)
} finally {
server.stop()
}
}
private suspend fun runWsSendPush(server: WsServer) = coroutineScope {
val received = CompletableDeferred<Boolean>()
server.onClientConnected = { client ->
addListenerTyped<TestData>(client.communicator) { data ->
println("SERVER_RECEIVED index=${data.index} message=${data.message}")
val valid = data.index == 101 && data.message == "fire from go client"
received.complete(valid)
if (valid) {
launch {
client.send(
TestData.newBuilder()
.setIndex(200)
.setMessage("push from kotlin server")
.build(),
)
}
}
}
}
runGoClient("ws", WS_PORT, "send-push", setOf("1", "2"))
val ok = withTimeoutOrNull(SERVER_OBSERVATION_MS) { received.await() } ?: false
check(ok) { "WS send-push server did not receive expected data" }
}
private suspend fun runWsRequests(server: WsServer) {
server.onClientConnected = { client ->
addRequestListenerTyped<TestData, TestData>(client.communicator) { req ->
TestData.newBuilder()
.setIndex(req.index * 2)
.setMessage("echo: ${req.message}")
.build()
}
}
runGoClient("ws", WS_PORT, "requests", setOf("3", "4"))
}
private suspend fun runTlsTcp() {
val certFile = kotlinResourceFile("server.crt")
val keyFile = kotlinResourceFile("server.key")
val serverCtx = buildServerSslContext(certFile.path, keyFile.path)
runTlsTcpSendPush(serverCtx, certFile.path)
delay(150)
runTlsTcpRequests(serverCtx, certFile.path)
}
private suspend fun runTlsTcpSendPush(serverCtx: SSLContext, certPath: String) = coroutineScope {
val received = CompletableDeferred<Boolean>()
val server = TcpServer(HOST, TLS_TCP_PORT, serverCtx) { socket ->
TcpClient.fromSocket(socket, 0, 0, parserMap())
}
server.onClientConnected = { client ->
addListenerTyped<TestData>(client.communicator) { data ->
println("SERVER_RECEIVED index=${data.index} message=${data.message}")
@ -116,15 +185,15 @@ private suspend fun runWsSendPush() = coroutineScope {
}
}
withServer(server::start, server::stop) {
runGoClient("ws", WS_PORT, "send-push", setOf("1", "2"))
runGoClient("tls", TLS_TCP_PORT, "send-push", setOf("1", "2"), certPath)
val ok = withTimeoutOrNull(SERVER_OBSERVATION_MS) { received.await() } ?: false
check(ok) { "WS send-push server did not receive expected data" }
check(ok) { "TLS TCP send-push server did not receive expected data" }
}
}
private suspend fun runWsRequests() {
val server = WsServer(HOST, WS_PORT, WS_PATH) { conn ->
WsClient.forServer(conn, 0, 0, parserMap())
private suspend fun runTlsTcpRequests(serverCtx: SSLContext, certPath: String) {
val server = TcpServer(HOST, TLS_TCP_PORT, serverCtx) { socket ->
TcpClient.fromSocket(socket, 0, 0, parserMap())
}
server.onClientConnected = { client ->
addRequestListenerTyped<TestData, TestData>(client.communicator) { req ->
@ -135,7 +204,7 @@ private suspend fun runWsRequests() {
}
}
withServer(server::start, server::stop) {
runGoClient("ws", WS_PORT, "requests", setOf("3", "4"))
runGoClient("tls", TLS_TCP_PORT, "requests", setOf("3", "4"), certPath)
}
}
@ -145,6 +214,7 @@ private suspend fun withServer(
body: suspend () -> Unit,
) {
start()
delay(100)
try {
body()
} finally {
@ -157,8 +227,9 @@ private suspend fun runGoClient(
port: Int,
phase: String,
expectedScenarios: Set<String>,
certPath: String? = null,
) = withContext(Dispatchers.IO) {
val process = ProcessBuilder(
val args = mutableListOf(
"go",
"run",
"./crosstest/kotlin_go_client",
@ -166,6 +237,10 @@ private suspend fun runGoClient(
"--port=$port",
"--phase=$phase",
)
if (certPath != null) {
args += "--cert=$certPath"
}
val process = ProcessBuilder(args)
.directory(goDir())
.redirectErrorStream(false)
.start()
@ -222,6 +297,39 @@ private fun validateResultLines(
private fun parserMap(): ParserMap =
mapOf(typeNameOf<TestData>() to { TestData.parseFrom(it) })
private fun buildServerSslContext(certPath: String, keyPath: String): SSLContext {
val certFactory = CertificateFactory.getInstance("X.509")
val cert = File(certPath).inputStream().use {
certFactory.generateCertificate(it) as X509Certificate
}
val keyPem = File(keyPath).readText()
.replace("-----BEGIN PRIVATE KEY-----", "")
.replace("-----END PRIVATE KEY-----", "")
.replace("\\s+".toRegex(), "")
val privateKey = KeyFactory.getInstance("RSA")
.generatePrivate(PKCS8EncodedKeySpec(Base64.getDecoder().decode(keyPem)))
val keyStore = KeyStore.getInstance("PKCS12")
keyStore.load(null, null)
keyStore.setKeyEntry("toki", privateKey, CharArray(0), arrayOf(cert))
val keyManagerFactory = KeyManagerFactory.getInstance(KeyManagerFactory.getDefaultAlgorithm())
keyManagerFactory.init(keyStore, CharArray(0))
val ctx = SSLContext.getInstance("TLS")
ctx.init(keyManagerFactory.keyManagers, null, null)
return ctx
}
private fun kotlinResourceFile(name: String): File {
var dir = File(System.getProperty("user.dir")).absoluteFile
while (dir.parentFile != null) {
val direct = File(dir, "src/test/resources/$name").canonicalFile
if (direct.isFile) return direct
val nested = File(dir, "kotlin/src/test/resources/$name").canonicalFile
if (nested.isFile) return nested
dir = dir.parentFile
}
error("cannot resolve kotlin test resource $name")
}
private fun goDir(): File {
var dir = File(System.getProperty("user.dir")).absoluteFile
while (dir.parentFile != null) {

View file

@ -20,12 +20,21 @@ import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.withContext
import kotlinx.coroutines.withTimeoutOrNull
import java.io.File
import java.security.KeyFactory
import java.security.KeyStore
import java.security.cert.CertificateFactory
import java.security.cert.X509Certificate
import java.security.spec.PKCS8EncodedKeySpec
import java.util.Base64
import java.util.concurrent.TimeUnit
import javax.net.ssl.KeyManagerFactory
import javax.net.ssl.SSLContext
import kotlin.system.exitProcess
private const val HOST = "127.0.0.1"
private const val TCP_PORT = 29394
private const val WS_PORT = 29396
private const val TLS_TCP_PORT = 29398
private const val WS_PATH = "/"
private const val PROCESS_TIMEOUT_MS = 20_000L
private const val SERVER_OBSERVATION_MS = 200L
@ -37,9 +46,9 @@ fun main() = runBlocking {
delay(150)
runTcpRequests()
delay(150)
runWsSendPush()
runWs()
delay(150)
runWsRequests()
runTlsTcp()
println("PASS all kotlin-server/python-client crosstests passed")
} catch (error: Throwable) {
System.err.println("FAIL crosstest error=${error.message ?: error}")
@ -93,11 +102,71 @@ private suspend fun runTcpRequests() {
}
}
private suspend fun runWsSendPush() = coroutineScope {
val received = CompletableDeferred<Boolean>()
private suspend fun runWs() = coroutineScope {
val server = WsServer(HOST, WS_PORT, WS_PATH) { conn ->
WsClient.forServer(conn, 0, 0, parserMap())
}
server.start()
delay(100)
try {
runWsSendPush(server)
delay(150)
runWsRequests(server)
} finally {
server.stop()
}
}
private suspend fun runWsSendPush(server: WsServer) = coroutineScope {
val received = CompletableDeferred<Boolean>()
server.onClientConnected = { client ->
addListenerTyped<TestData>(client.communicator) { data ->
println("SERVER_RECEIVED index=${data.index} message=${data.message}")
val valid = data.index == 101 && data.message == "fire from python client"
received.complete(valid)
if (valid) {
launch {
client.send(
TestData.newBuilder()
.setIndex(200)
.setMessage("push from kotlin server")
.build(),
)
}
}
}
}
runPythonClient("ws", WS_PORT, "send-push", setOf("1", "2"))
val ok = withTimeoutOrNull(SERVER_OBSERVATION_MS) { received.await() } ?: false
check(ok) { "WS send-push server did not receive expected data" }
}
private suspend fun runWsRequests(server: WsServer) {
server.onClientConnected = { client ->
addRequestListenerTyped<TestData, TestData>(client.communicator) { req ->
TestData.newBuilder()
.setIndex(req.index * 2)
.setMessage("echo: ${req.message}")
.build()
}
}
runPythonClient("ws", WS_PORT, "requests", setOf("3", "4"))
}
private suspend fun runTlsTcp() {
val certFile = kotlinResourceFile("server.crt")
val keyFile = kotlinResourceFile("server.key")
val serverCtx = buildServerSslContext(certFile.path, keyFile.path)
runTlsTcpSendPush(serverCtx, certFile.path)
delay(150)
runTlsTcpRequests(serverCtx, certFile.path)
}
private suspend fun runTlsTcpSendPush(serverCtx: SSLContext, certPath: String) = coroutineScope {
val received = CompletableDeferred<Boolean>()
val server = TcpServer(HOST, TLS_TCP_PORT, serverCtx) { socket ->
TcpClient.fromSocket(socket, 0, 0, parserMap())
}
server.onClientConnected = { client ->
addListenerTyped<TestData>(client.communicator) { data ->
println("SERVER_RECEIVED index=${data.index} message=${data.message}")
@ -116,15 +185,15 @@ private suspend fun runWsSendPush() = coroutineScope {
}
}
withServer(server::start, server::stop) {
runPythonClient("ws", WS_PORT, "send-push", setOf("1", "2"))
runPythonClient("tls", TLS_TCP_PORT, "send-push", setOf("1", "2"), certPath)
val ok = withTimeoutOrNull(SERVER_OBSERVATION_MS) { received.await() } ?: false
check(ok) { "WS send-push server did not receive expected data" }
check(ok) { "TLS TCP send-push server did not receive expected data" }
}
}
private suspend fun runWsRequests() {
val server = WsServer(HOST, WS_PORT, WS_PATH) { conn ->
WsClient.forServer(conn, 0, 0, parserMap())
private suspend fun runTlsTcpRequests(serverCtx: SSLContext, certPath: String) {
val server = TcpServer(HOST, TLS_TCP_PORT, serverCtx) { socket ->
TcpClient.fromSocket(socket, 0, 0, parserMap())
}
server.onClientConnected = { client ->
addRequestListenerTyped<TestData, TestData>(client.communicator) { req ->
@ -135,7 +204,7 @@ private suspend fun runWsRequests() {
}
}
withServer(server::start, server::stop) {
runPythonClient("ws", WS_PORT, "requests", setOf("3", "4"))
runPythonClient("tls", TLS_TCP_PORT, "requests", setOf("3", "4"), certPath)
}
}
@ -145,6 +214,7 @@ private suspend fun withServer(
body: suspend () -> Unit,
) {
start()
delay(100)
try {
body()
} finally {
@ -157,14 +227,19 @@ private suspend fun runPythonClient(
port: Int,
phase: String,
expectedScenarios: Set<String>,
certPath: String? = null,
) = withContext(Dispatchers.IO) {
val process = ProcessBuilder(
val args = mutableListOf(
"python3",
"crosstest/kotlin_python_client.py",
"--mode=$mode",
"--port=$port",
"--phase=$phase",
)
if (certPath != null) {
args += "--cert=$certPath"
}
val process = ProcessBuilder(args)
.directory(pythonDir())
.redirectErrorStream(false)
.start()
@ -221,6 +296,39 @@ private fun validateResultLines(
private fun parserMap(): ParserMap =
mapOf(typeNameOf<TestData>() to { TestData.parseFrom(it) })
private fun buildServerSslContext(certPath: String, keyPath: String): SSLContext {
val certFactory = CertificateFactory.getInstance("X.509")
val cert = File(certPath).inputStream().use {
certFactory.generateCertificate(it) as X509Certificate
}
val keyPem = File(keyPath).readText()
.replace("-----BEGIN PRIVATE KEY-----", "")
.replace("-----END PRIVATE KEY-----", "")
.replace("\\s+".toRegex(), "")
val privateKey = KeyFactory.getInstance("RSA")
.generatePrivate(PKCS8EncodedKeySpec(Base64.getDecoder().decode(keyPem)))
val keyStore = KeyStore.getInstance("PKCS12")
keyStore.load(null, null)
keyStore.setKeyEntry("toki", privateKey, CharArray(0), arrayOf(cert))
val keyManagerFactory = KeyManagerFactory.getInstance(KeyManagerFactory.getDefaultAlgorithm())
keyManagerFactory.init(keyStore, CharArray(0))
val ctx = SSLContext.getInstance("TLS")
ctx.init(keyManagerFactory.keyManagers, null, null)
return ctx
}
private fun kotlinResourceFile(name: String): File {
var dir = File(System.getProperty("user.dir")).absoluteFile
while (dir.parentFile != null) {
val direct = File(dir, "src/test/resources/$name").canonicalFile
if (direct.isFile) return direct
val nested = File(dir, "kotlin/src/test/resources/$name").canonicalFile
if (nested.isFile) return nested
dir = dir.parentFile
}
error("cannot resolve kotlin test resource $name")
}
private fun pythonDir(): File {
var dir = File(System.getProperty("user.dir")).absoluteFile
while (dir.parentFile != null) {

View file

@ -20,12 +20,21 @@ import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.withContext
import kotlinx.coroutines.withTimeoutOrNull
import java.io.File
import java.security.KeyFactory
import java.security.KeyStore
import java.security.cert.CertificateFactory
import java.security.cert.X509Certificate
import java.security.spec.PKCS8EncodedKeySpec
import java.util.Base64
import java.util.concurrent.TimeUnit
import javax.net.ssl.KeyManagerFactory
import javax.net.ssl.SSLContext
import kotlin.system.exitProcess
private const val HOST = "127.0.0.1"
private const val TCP_PORT = 29794
private const val WS_PORT = 29796
private const val TLS_TCP_PORT = 29798
private const val WS_PATH = "/"
private const val PROCESS_TIMEOUT_MS = 20_000L
private const val SERVER_OBSERVATION_MS = 200L
@ -38,6 +47,8 @@ fun main() = runBlocking {
runTcpRequests()
delay(150)
runWs()
delay(150)
runTlsTcp()
println("PASS all kotlin-server/typescript-client crosstests passed")
} catch (error: Throwable) {
System.err.println("FAIL crosstest error=${error.message ?: error}")
@ -142,6 +153,61 @@ private suspend fun runWsRequests(server: WsServer) {
runTypescriptClient("ws", WS_PORT, "requests", setOf("3", "4"))
}
private suspend fun runTlsTcp() {
val certFile = kotlinResourceFile("server.crt")
val keyFile = kotlinResourceFile("server.key")
val serverCtx = buildServerSslContext(certFile.path, keyFile.path)
runTlsTcpSendPush(serverCtx, certFile.path)
delay(150)
runTlsTcpRequests(serverCtx, certFile.path)
}
private suspend fun runTlsTcpSendPush(serverCtx: SSLContext, certPath: String) = coroutineScope {
val received = CompletableDeferred<Boolean>()
val server = TcpServer(HOST, TLS_TCP_PORT, serverCtx) { socket ->
TcpClient.fromSocket(socket, 0, 0, parserMap())
}
server.onClientConnected = { client ->
addListenerTyped<TestData>(client.communicator) { data ->
println("SERVER_RECEIVED index=${data.index} message=${data.message}")
val valid = data.index == 101 && data.message == "fire from typescript client"
received.complete(valid)
if (valid) {
launch {
client.send(
TestData.newBuilder()
.setIndex(200)
.setMessage("push from kotlin server")
.build(),
)
}
}
}
}
withServer(server::start, server::stop) {
runTypescriptClient("tls", TLS_TCP_PORT, "send-push", setOf("1", "2"), certPath)
val ok = withTimeoutOrNull(SERVER_OBSERVATION_MS) { received.await() } ?: false
check(ok) { "TLS TCP send-push server did not receive expected data" }
}
}
private suspend fun runTlsTcpRequests(serverCtx: SSLContext, certPath: String) {
val server = TcpServer(HOST, TLS_TCP_PORT, serverCtx) { socket ->
TcpClient.fromSocket(socket, 0, 0, parserMap())
}
server.onClientConnected = { client ->
addRequestListenerTyped<TestData, TestData>(client.communicator) { req ->
TestData.newBuilder()
.setIndex(req.index * 2)
.setMessage("echo: ${req.message}")
.build()
}
}
withServer(server::start, server::stop) {
runTypescriptClient("tls", TLS_TCP_PORT, "requests", setOf("3", "4"), certPath)
}
}
private suspend fun withServer(
start: () -> Unit,
stop: () -> Unit,
@ -161,8 +227,9 @@ private suspend fun runTypescriptClient(
port: Int,
phase: String,
expectedScenarios: Set<String>,
certPath: String? = null,
) = withContext(Dispatchers.IO) {
val process = ProcessBuilder(
val args = mutableListOf(
"npx",
"tsx",
"crosstest/kotlin_typescript_client.ts",
@ -170,6 +237,10 @@ private suspend fun runTypescriptClient(
"--port=$port",
"--phase=$phase",
)
if (certPath != null) {
args += "--cert=$certPath"
}
val process = ProcessBuilder(args)
.directory(typescriptDir())
.redirectErrorStream(false)
.start()
@ -226,6 +297,39 @@ private fun validateResultLines(
private fun parserMap(): ParserMap =
mapOf(typeNameOf<TestData>() to { TestData.parseFrom(it) })
private fun buildServerSslContext(certPath: String, keyPath: String): SSLContext {
val certFactory = CertificateFactory.getInstance("X.509")
val cert = File(certPath).inputStream().use {
certFactory.generateCertificate(it) as X509Certificate
}
val keyPem = File(keyPath).readText()
.replace("-----BEGIN PRIVATE KEY-----", "")
.replace("-----END PRIVATE KEY-----", "")
.replace("\\s+".toRegex(), "")
val privateKey = KeyFactory.getInstance("RSA")
.generatePrivate(PKCS8EncodedKeySpec(Base64.getDecoder().decode(keyPem)))
val keyStore = KeyStore.getInstance("PKCS12")
keyStore.load(null, null)
keyStore.setKeyEntry("toki", privateKey, CharArray(0), arrayOf(cert))
val keyManagerFactory = KeyManagerFactory.getInstance(KeyManagerFactory.getDefaultAlgorithm())
keyManagerFactory.init(keyStore, CharArray(0))
val ctx = SSLContext.getInstance("TLS")
ctx.init(keyManagerFactory.keyManagers, null, null)
return ctx
}
private fun kotlinResourceFile(name: String): File {
var dir = File(System.getProperty("user.dir")).absoluteFile
while (dir.parentFile != null) {
val direct = File(dir, "src/test/resources/$name").canonicalFile
if (direct.isFile) return direct
val nested = File(dir, "kotlin/src/test/resources/$name").canonicalFile
if (nested.isFile) return nested
dir = dir.parentFile
}
error("cannot resolve kotlin test resource $name")
}
private fun typescriptDir(): File {
var dir = File(System.getProperty("user.dir")).absoluteFile
while (dir.parentFile != null) {

View file

@ -3,7 +3,9 @@
package com.tokilabs.toki_socket.crosstest
import com.tokilabs.toki_socket.DialTcp
import com.tokilabs.toki_socket.DialTcpTls
import com.tokilabs.toki_socket.DialWs
import com.tokilabs.toki_socket.DialWss
import com.tokilabs.toki_socket.ParserMap
import com.tokilabs.toki_socket.WsClient
import com.tokilabs.toki_socket.TcpClient
@ -50,6 +52,7 @@ fun main(args: Array<String>) = runBlocking {
val mode = argValue(args, "mode") ?: "tcp"
val phase = argValue(args, "phase") ?: "send-push"
val port = argValue(args, "port")?.toIntOrNull()
val cert = argValue(args, "cert")
println("INFO typeName kotlin=${typeNameOf<TestData>()}")
@ -59,7 +62,7 @@ fun main(args: Array<String>) = runBlocking {
}
val client = try {
connectWithRetry(mode, port)
connectWithRetry(mode, port, cert)
} catch (error: Throwable) {
fail("setup", error.message ?: error.toString())
exitProcess(1)
@ -81,7 +84,7 @@ fun main(args: Array<String>) = runBlocking {
if (!ok) exitProcess(1)
}
private suspend fun connectWithRetry(mode: String, port: Int): PythonClientHandle {
private suspend fun connectWithRetry(mode: String, port: Int, cert: String?): PythonClientHandle {
val deadline = System.nanoTime() + CONNECT_WINDOW_MS * 1_000_000L
var lastError: Throwable? = null
while (System.nanoTime() < deadline) {
@ -89,6 +92,14 @@ private suspend fun connectWithRetry(mode: String, port: Int): PythonClientHandl
return when (mode) {
"tcp" -> PythonTcpHandle(DialTcp(HOST, port, 0, 0, parserMap()))
"ws" -> PythonWsHandle(DialWs(HOST, port, WS_PATH, 0, 0, parserMap()))
"tls" -> {
val sslCtx = buildClientSslContext(cert ?: error("--cert required for tls"))
PythonTcpHandle(DialTcpTls(HOST, port, sslCtx, 0, 0, parserMap()))
}
"wss" -> {
val sslCtx = buildClientSslContext(cert ?: error("--cert required for wss"))
PythonWsHandle(DialWss(HOST, port, WS_PATH, sslCtx, 0, 0, parserMap()))
}
else -> error("unknown mode $mode")
}
} catch (error: Throwable) {
@ -99,6 +110,23 @@ private suspend fun connectWithRetry(mode: String, port: Int): PythonClientHandl
error("connect $mode:$port timed out: $lastError")
}
private fun buildClientSslContext(certPath: String): javax.net.ssl.SSLContext {
val certFactory = java.security.cert.CertificateFactory.getInstance("X.509")
val cert = java.io.File(certPath).inputStream().use {
certFactory.generateCertificate(it) as java.security.cert.X509Certificate
}
val trustStore = java.security.KeyStore.getInstance("PKCS12")
trustStore.load(null, null)
trustStore.setCertificateEntry("toki", cert)
val trustManagerFactory = javax.net.ssl.TrustManagerFactory.getInstance(
javax.net.ssl.TrustManagerFactory.getDefaultAlgorithm(),
)
trustManagerFactory.init(trustStore)
val ctx = javax.net.ssl.SSLContext.getInstance("TLS")
ctx.init(null, trustManagerFactory.trustManagers, null)
return ctx
}
private suspend fun runSendPush(client: PythonClientHandle): Boolean {
val push = CompletableDeferred<TestData>()
addListenerTyped<TestData>(client.communicator) {

View file

@ -4,7 +4,9 @@ package com.tokilabs.toki_socket.crosstest
import com.tokilabs.toki_socket.Communicator
import com.tokilabs.toki_socket.DialTcp
import com.tokilabs.toki_socket.DialTcpTls
import com.tokilabs.toki_socket.DialWs
import com.tokilabs.toki_socket.DialWss
import com.tokilabs.toki_socket.ParserMap
import com.tokilabs.toki_socket.TcpClient
import com.tokilabs.toki_socket.WsClient
@ -51,6 +53,7 @@ fun main(args: Array<String>) = runBlocking {
val mode = argValue(args, "mode") ?: "tcp"
val phase = argValue(args, "phase") ?: "send-push"
val port = argValue(args, "port")?.toIntOrNull()
val cert = argValue(args, "cert")
println("INFO typeName kotlin=${typeNameOf<TestData>()}")
@ -60,7 +63,7 @@ fun main(args: Array<String>) = runBlocking {
}
val client = try {
connectWithRetry(mode, port)
connectWithRetry(mode, port, cert)
} catch (error: Throwable) {
fail("setup", error.message ?: error.toString())
exitProcess(1)
@ -82,7 +85,7 @@ fun main(args: Array<String>) = runBlocking {
if (!ok) exitProcess(1)
}
private suspend fun connectWithRetry(mode: String, port: Int): TypescriptClientHandle {
private suspend fun connectWithRetry(mode: String, port: Int, cert: String?): TypescriptClientHandle {
val deadline = System.nanoTime() + CONNECT_WINDOW_MS * 1_000_000L
var lastError: Throwable? = null
while (System.nanoTime() < deadline) {
@ -90,6 +93,14 @@ private suspend fun connectWithRetry(mode: String, port: Int): TypescriptClientH
return when (mode) {
"tcp" -> TypescriptTcpHandle(DialTcp(HOST, port, 0, 0, parserMap()))
"ws" -> TypescriptWsHandle(DialWs(HOST, port, WS_PATH, 0, 0, parserMap()))
"tls" -> {
val sslCtx = buildClientSslContext(cert ?: error("--cert required for tls"))
TypescriptTcpHandle(DialTcpTls(HOST, port, sslCtx, 0, 0, parserMap()))
}
"wss" -> {
val sslCtx = buildClientSslContext(cert ?: error("--cert required for wss"))
TypescriptWsHandle(DialWss(HOST, port, WS_PATH, sslCtx, 0, 0, parserMap()))
}
else -> error("unknown mode $mode")
}
} catch (error: Throwable) {
@ -100,6 +111,23 @@ private suspend fun connectWithRetry(mode: String, port: Int): TypescriptClientH
error("connect $mode:$port timed out: $lastError")
}
private fun buildClientSslContext(certPath: String): javax.net.ssl.SSLContext {
val certFactory = java.security.cert.CertificateFactory.getInstance("X.509")
val cert = java.io.File(certPath).inputStream().use {
certFactory.generateCertificate(it) as java.security.cert.X509Certificate
}
val trustStore = java.security.KeyStore.getInstance("PKCS12")
trustStore.load(null, null)
trustStore.setCertificateEntry("toki", cert)
val trustManagerFactory = javax.net.ssl.TrustManagerFactory.getInstance(
javax.net.ssl.TrustManagerFactory.getDefaultAlgorithm(),
)
trustManagerFactory.init(trustStore)
val ctx = javax.net.ssl.SSLContext.getInstance("TLS")
ctx.init(null, trustManagerFactory.trustManagers, null)
return ctx
}
private suspend fun runSendPush(client: TypescriptClientHandle): Boolean {
val push = CompletableDeferred<TestData>()
addListenerTyped<TestData>(client.communicator) {

View file

@ -2,20 +2,23 @@ from __future__ import annotations
import argparse
import asyncio
import ssl
import sys
from contextlib import suppress
from pathlib import Path
sys.path.insert(0, str(Path(__file__).resolve().parents[1]))
from toki_socket.communicator import type_name_of
from toki_socket.packets.message_common_pb2 import TestData
from toki_socket.tcp_client import connect_tcp
from toki_socket.ws_client import connect_ws
from toki_socket.tcp_client import connect_tcp, connect_tcp_tls
from toki_socket.ws_client import connect_ws, connect_wss
HOST = "127.0.0.1"
WS_PATH = "/"
CONNECT_WINDOW = 3.0
REQUEST_WINDOW = 2.0
SERVER_READY_DELAY = 0.05
def parser_map():
@ -24,18 +27,20 @@ def parser_map():
async def main() -> None:
parser = argparse.ArgumentParser()
parser.add_argument("--mode", choices=["tcp", "ws"], default="tcp")
parser.add_argument("--mode", choices=["tcp", "ws", "tls", "wss"], default="tcp")
parser.add_argument("--port", type=int, required=True)
parser.add_argument("--phase", choices=["send-push", "requests"], default="send-push")
parser.add_argument("--cert", default=None)
args = parser.parse_args()
print(f"INFO typeName python={type_name_of(TestData)}")
try:
client = await dial_with_retry(args.mode, args.port)
client = await dial_with_retry(args.mode, args.port, args.cert)
except Exception as exc:
fail("setup", str(exc))
raise SystemExit(1) from exc
await asyncio.sleep(SERVER_READY_DELAY)
try:
if args.phase == "send-push":
@ -43,29 +48,42 @@ async def main() -> None:
else:
ok = await run_requests(client)
finally:
await client.close()
with suppress(Exception):
await asyncio.wait_for(client.close(), 1.0)
if not ok:
raise SystemExit(1)
async def dial_with_retry(mode: str, port: int):
async def dial_with_retry(mode: str, port: int, cert: str | None):
deadline = asyncio.get_running_loop().time() + CONNECT_WINDOW
last_error: Exception | None = None
while asyncio.get_running_loop().time() < deadline:
try:
return await asyncio.wait_for(dial(mode, port), 0.3)
return await asyncio.wait_for(dial(mode, port, cert), 0.3)
except Exception as exc:
last_error = exc
await asyncio.sleep(0.1)
raise TimeoutError(f"connect {mode}:{port} timed out: {last_error}")
async def dial(mode: str, port: int):
async def dial(mode: str, port: int, cert: str | None):
if mode == "tcp":
return await connect_tcp(HOST, port, 0, 0, parser_map())
if mode == "ws":
return await connect_ws(HOST, port, WS_PATH, 0, 0, parser_map())
if mode == "tls":
if cert is None:
raise ValueError("--cert is required for tls mode")
ssl_ctx = ssl.create_default_context(cafile=cert)
ssl_ctx.check_hostname = False
return await connect_tcp_tls(HOST, port, ssl_ctx, 0, 0, parser_map())
if mode == "wss":
if cert is None:
raise ValueError("--cert is required for wss mode")
ssl_ctx = ssl.create_default_context(cafile=cert)
ssl_ctx.check_hostname = False
return await connect_wss(HOST, port, WS_PATH, ssl_ctx, 0, 0, parser_map())
raise ValueError(f"unknown mode {mode!r}")

View file

@ -2,20 +2,23 @@ from __future__ import annotations
import argparse
import asyncio
import ssl
import sys
from contextlib import suppress
from pathlib import Path
sys.path.insert(0, str(Path(__file__).resolve().parents[1]))
from toki_socket.communicator import type_name_of
from toki_socket.packets.message_common_pb2 import TestData
from toki_socket.tcp_client import connect_tcp
from toki_socket.ws_client import connect_ws
from toki_socket.tcp_client import connect_tcp, connect_tcp_tls
from toki_socket.ws_client import connect_ws, connect_wss
HOST = "127.0.0.1"
WS_PATH = "/"
CONNECT_WINDOW = 3.0
REQUEST_WINDOW = 2.0
SERVER_READY_DELAY = 0.05
def parser_map():
@ -24,18 +27,20 @@ def parser_map():
async def main() -> None:
parser = argparse.ArgumentParser()
parser.add_argument("--mode", choices=["tcp", "ws"], default="tcp")
parser.add_argument("--mode", choices=["tcp", "ws", "tls", "wss"], default="tcp")
parser.add_argument("--port", type=int, required=True)
parser.add_argument("--phase", choices=["send-push", "requests"], default="send-push")
parser.add_argument("--cert", default=None)
args = parser.parse_args()
print(f"INFO typeName python={type_name_of(TestData)}")
try:
client = await dial_with_retry(args.mode, args.port)
client = await dial_with_retry(args.mode, args.port, args.cert)
except Exception as exc:
fail("setup", str(exc))
raise SystemExit(1) from exc
await asyncio.sleep(SERVER_READY_DELAY)
try:
if args.phase == "send-push":
@ -43,29 +48,42 @@ async def main() -> None:
else:
ok = await run_requests(client)
finally:
await client.close()
with suppress(Exception):
await asyncio.wait_for(client.close(), 1.0)
if not ok:
raise SystemExit(1)
async def dial_with_retry(mode: str, port: int):
async def dial_with_retry(mode: str, port: int, cert: str | None):
deadline = asyncio.get_running_loop().time() + CONNECT_WINDOW
last_error: Exception | None = None
while asyncio.get_running_loop().time() < deadline:
try:
return await asyncio.wait_for(dial(mode, port), 0.3)
return await asyncio.wait_for(dial(mode, port, cert), 0.3)
except Exception as exc:
last_error = exc
await asyncio.sleep(0.1)
raise TimeoutError(f"connect {mode}:{port} timed out: {last_error}")
async def dial(mode: str, port: int):
async def dial(mode: str, port: int, cert: str | None):
if mode == "tcp":
return await connect_tcp(HOST, port, 0, 0, parser_map())
if mode == "ws":
return await connect_ws(HOST, port, WS_PATH, 0, 0, parser_map())
if mode == "tls":
if cert is None:
raise ValueError("--cert is required for tls mode")
ssl_ctx = ssl.create_default_context(cafile=cert)
ssl_ctx.check_hostname = False
return await connect_tcp_tls(HOST, port, ssl_ctx, 0, 0, parser_map())
if mode == "wss":
if cert is None:
raise ValueError("--cert is required for wss mode")
ssl_ctx = ssl.create_default_context(cafile=cert)
ssl_ctx.check_hostname = False
return await connect_wss(HOST, port, WS_PATH, ssl_ctx, 0, 0, parser_map())
raise ValueError(f"unknown mode {mode!r}")
@ -137,4 +155,3 @@ def fail(scenario: str, error: str) -> None:
if __name__ == "__main__":
asyncio.run(main())

View file

@ -2,20 +2,23 @@ from __future__ import annotations
import argparse
import asyncio
import ssl
import sys
from contextlib import suppress
from pathlib import Path
sys.path.insert(0, str(Path(__file__).resolve().parents[1]))
from toki_socket.communicator import type_name_of
from toki_socket.packets.message_common_pb2 import TestData
from toki_socket.tcp_client import connect_tcp
from toki_socket.ws_client import connect_ws
from toki_socket.tcp_client import connect_tcp, connect_tcp_tls
from toki_socket.ws_client import connect_ws, connect_wss
HOST = "127.0.0.1"
WS_PATH = "/"
CONNECT_WINDOW = 3.0
REQUEST_WINDOW = 2.0
SERVER_READY_DELAY = 0.05
def parser_map():
@ -24,18 +27,20 @@ def parser_map():
async def main() -> None:
parser = argparse.ArgumentParser()
parser.add_argument("--mode", choices=["tcp", "ws"], default="tcp")
parser.add_argument("--mode", choices=["tcp", "ws", "tls", "wss"], default="tcp")
parser.add_argument("--port", type=int, required=True)
parser.add_argument("--phase", choices=["send-push", "requests"], default="send-push")
parser.add_argument("--cert", default=None)
args = parser.parse_args()
print(f"INFO typeName python={type_name_of(TestData)}")
try:
client = await dial_with_retry(args.mode, args.port)
client = await dial_with_retry(args.mode, args.port, args.cert)
except Exception as exc:
fail("setup", str(exc))
raise SystemExit(1) from exc
await asyncio.sleep(SERVER_READY_DELAY)
try:
if args.phase == "send-push":
@ -43,29 +48,42 @@ async def main() -> None:
else:
ok = await run_requests(client)
finally:
await client.close()
with suppress(Exception):
await asyncio.wait_for(client.close(), 1.0)
if not ok:
raise SystemExit(1)
async def dial_with_retry(mode: str, port: int):
async def dial_with_retry(mode: str, port: int, cert: str | None):
deadline = asyncio.get_running_loop().time() + CONNECT_WINDOW
last_error: Exception | None = None
while asyncio.get_running_loop().time() < deadline:
try:
return await asyncio.wait_for(dial(mode, port), 0.3)
return await asyncio.wait_for(dial(mode, port, cert), 0.3)
except Exception as exc:
last_error = exc
await asyncio.sleep(0.1)
raise TimeoutError(f"connect {mode}:{port} timed out: {last_error}")
async def dial(mode: str, port: int):
async def dial(mode: str, port: int, cert: str | None):
if mode == "tcp":
return await connect_tcp(HOST, port, 0, 0, parser_map())
if mode == "ws":
return await connect_ws(HOST, port, WS_PATH, 0, 0, parser_map())
if mode == "tls":
if cert is None:
raise ValueError("--cert is required for tls mode")
ssl_ctx = ssl.create_default_context(cafile=cert)
ssl_ctx.check_hostname = False
return await connect_tcp_tls(HOST, port, ssl_ctx, 0, 0, parser_map())
if mode == "wss":
if cert is None:
raise ValueError("--cert is required for wss mode")
ssl_ctx = ssl.create_default_context(cafile=cert)
ssl_ctx.check_hostname = False
return await connect_wss(HOST, port, WS_PATH, ssl_ctx, 0, 0, parser_map())
raise ValueError(f"unknown mode {mode!r}")

View file

@ -2,6 +2,7 @@ from __future__ import annotations
import asyncio
import re
import ssl
import sys
from pathlib import Path
@ -15,9 +16,13 @@ from toki_socket.ws_server import WsServer
HOST = "127.0.0.1"
PYTHON_DART_TCP_PORT = 29494
PYTHON_DART_WS_PORT = 29496
TLS_TCP_PORT = 29498
WSS_PORT = 29500
WS_PATH = "/"
PROCESS_TIMEOUT = 20.0
SERVER_OBSERVATION_WINDOW = 0.2
CERT_PATH = Path(__file__).resolve().parents[1] / "test" / "certs" / "server.crt"
KEY_PATH = Path(__file__).resolve().parents[1] / "test" / "certs" / "server.key"
def parser_map():
@ -34,6 +39,8 @@ async def main() -> None:
await run_ws_send_push()
await asyncio.sleep(0.15)
await run_ws_requests()
await asyncio.sleep(0.15)
await run_tls()
except Exception as exc:
print(f"FAIL crosstest error={exc}", file=sys.stderr)
raise SystemExit(1) from exc
@ -122,15 +129,155 @@ async def run_ws_requests() -> None:
await server.stop()
async def run_dart_client(mode: str, port: int, phase: str, expected: set[str]) -> None:
def _make_server_ssl_context() -> ssl.SSLContext:
ctx = ssl.SSLContext(ssl.PROTOCOL_TLS_SERVER)
ctx.load_cert_chain(CERT_PATH, KEY_PATH)
return ctx
async def run_tls() -> None:
ssl_context = _make_server_ssl_context()
await run_tls_tcp_send_push(ssl_context)
await asyncio.sleep(0.15)
await run_tls_tcp_requests(ssl_context)
await asyncio.sleep(0.15)
await run_wss_send_push(ssl_context)
await asyncio.sleep(0.15)
await run_wss_requests(ssl_context)
async def run_tls_tcp_send_push(ssl_context: ssl.SSLContext) -> None:
received = asyncio.get_running_loop().create_future()
server = TcpServer(
HOST,
TLS_TCP_PORT,
interval_sec=0,
wait_sec=0,
parser_map=parser_map(),
ssl_context=ssl_context,
)
def on_connected(client):
def on_message(data):
print(f"SERVER_RECEIVED index={data.index} message={data.message}")
valid = data.index == 101 and data.message == "fire from dart client"
if not received.done():
received.set_result(valid)
if valid:
asyncio.create_task(
client.communicator.send(TestData(index=200, message="push from python server"))
)
client.communicator.add_listener(type_name_of(TestData), on_message)
server.on_client_connected = on_connected
await server.start()
try:
await run_dart_client("tls", TLS_TCP_PORT, "send-push", {"1", "2"}, CERT_PATH)
ok = await asyncio.wait_for(received, SERVER_OBSERVATION_WINDOW)
if not ok:
raise RuntimeError("TLS TCP send-push server received unexpected data")
finally:
await server.stop()
async def run_tls_tcp_requests(ssl_context: ssl.SSLContext) -> None:
server = TcpServer(
HOST,
TLS_TCP_PORT,
interval_sec=0,
wait_sec=0,
parser_map=parser_map(),
ssl_context=ssl_context,
)
server.on_client_connected = lambda client: client.communicator.add_request_listener(
type_name_of(TestData),
lambda req: TestData(index=req.index * 2, message="echo: " + req.message),
)
await server.start()
try:
await run_dart_client("tls", TLS_TCP_PORT, "requests", {"3", "4"}, CERT_PATH)
finally:
await server.stop()
async def run_wss_send_push(ssl_context: ssl.SSLContext) -> None:
received = asyncio.get_running_loop().create_future()
server = WsServer(
HOST,
WSS_PORT,
WS_PATH,
interval_sec=0,
wait_sec=0,
parser_map=parser_map(),
ssl_context=ssl_context,
)
def on_connected(client):
def on_message(data):
print(f"SERVER_RECEIVED index={data.index} message={data.message}")
valid = data.index == 101 and data.message == "fire from dart client"
if not received.done():
received.set_result(valid)
if valid:
asyncio.create_task(
client.communicator.send(TestData(index=200, message="push from python server"))
)
client.communicator.add_listener(type_name_of(TestData), on_message)
server.on_client_connected = on_connected
await server.start()
try:
await run_dart_client("wss", WSS_PORT, "send-push", {"1", "2"}, CERT_PATH)
ok = await asyncio.wait_for(received, SERVER_OBSERVATION_WINDOW)
if not ok:
raise RuntimeError("WSS send-push server received unexpected data")
finally:
await server.stop()
async def run_wss_requests(ssl_context: ssl.SSLContext) -> None:
server = WsServer(
HOST,
WSS_PORT,
WS_PATH,
interval_sec=0,
wait_sec=0,
parser_map=parser_map(),
ssl_context=ssl_context,
)
server.on_client_connected = lambda client: client.communicator.add_request_listener(
type_name_of(TestData),
lambda req: TestData(index=req.index * 2, message="echo: " + req.message),
)
await server.start()
try:
await run_dart_client("wss", WSS_PORT, "requests", {"3", "4"}, CERT_PATH)
finally:
await server.stop()
async def run_dart_client(
mode: str,
port: int,
phase: str,
expected: set[str],
cert: Path | None = None,
) -> None:
dart_dir = dart_package_dir()
process = await asyncio.create_subprocess_exec(
"dart",
args = [
"run",
"crosstest/python_dart_client.dart",
f"--mode={mode}",
f"--port={port}",
f"--phase={phase}",
]
if cert is not None:
args.append(f"--cert={cert}")
process = await asyncio.create_subprocess_exec(
"dart",
*args,
cwd=dart_dir,
stdout=asyncio.subprocess.PIPE,
stderr=asyncio.subprocess.PIPE,

View file

@ -2,6 +2,7 @@ from __future__ import annotations
import asyncio
import re
import ssl
import sys
from pathlib import Path
@ -15,9 +16,13 @@ from toki_socket.ws_server import WsServer
HOST = "127.0.0.1"
PYTHON_GO_TCP_PORT = 29490
PYTHON_GO_WS_PORT = 29492
TLS_TCP_PORT = 29494
WSS_PORT = 29496
WS_PATH = "/"
PROCESS_TIMEOUT = 20.0
SERVER_OBSERVATION_WINDOW = 0.2
CERT_PATH = Path(__file__).resolve().parents[1] / "test" / "certs" / "server.crt"
KEY_PATH = Path(__file__).resolve().parents[1] / "test" / "certs" / "server.key"
def parser_map():
@ -34,6 +39,8 @@ async def main() -> None:
await run_ws_send_push()
await asyncio.sleep(0.15)
await run_ws_requests()
await asyncio.sleep(0.15)
await run_tls()
except Exception as exc:
print(f"FAIL crosstest error={exc}", file=sys.stderr)
raise SystemExit(1) from exc
@ -122,15 +129,155 @@ async def run_ws_requests() -> None:
await server.stop()
async def run_go_client(mode: str, port: int, phase: str, expected: set[str]) -> None:
def _make_server_ssl_context() -> ssl.SSLContext:
ctx = ssl.SSLContext(ssl.PROTOCOL_TLS_SERVER)
ctx.load_cert_chain(CERT_PATH, KEY_PATH)
return ctx
async def run_tls() -> None:
ssl_context = _make_server_ssl_context()
await run_tls_tcp_send_push(ssl_context)
await asyncio.sleep(0.15)
await run_tls_tcp_requests(ssl_context)
await asyncio.sleep(0.15)
await run_wss_send_push(ssl_context)
await asyncio.sleep(0.15)
await run_wss_requests(ssl_context)
async def run_tls_tcp_send_push(ssl_context: ssl.SSLContext) -> None:
received = asyncio.get_running_loop().create_future()
server = TcpServer(
HOST,
TLS_TCP_PORT,
interval_sec=0,
wait_sec=0,
parser_map=parser_map(),
ssl_context=ssl_context,
)
def on_connected(client):
def on_message(data):
print(f"SERVER_RECEIVED index={data.index} message={data.message}")
valid = data.index == 101 and data.message == "fire from go client"
if not received.done():
received.set_result(valid)
if valid:
asyncio.create_task(
client.communicator.send(TestData(index=200, message="push from python server"))
)
client.communicator.add_listener(type_name_of(TestData), on_message)
server.on_client_connected = on_connected
await server.start()
try:
await run_go_client("tls", TLS_TCP_PORT, "send-push", {"1", "2"}, CERT_PATH)
ok = await asyncio.wait_for(received, SERVER_OBSERVATION_WINDOW)
if not ok:
raise RuntimeError("TLS TCP send-push server received unexpected data")
finally:
await server.stop()
async def run_tls_tcp_requests(ssl_context: ssl.SSLContext) -> None:
server = TcpServer(
HOST,
TLS_TCP_PORT,
interval_sec=0,
wait_sec=0,
parser_map=parser_map(),
ssl_context=ssl_context,
)
server.on_client_connected = lambda client: client.communicator.add_request_listener(
type_name_of(TestData),
lambda req: TestData(index=req.index * 2, message="echo: " + req.message),
)
await server.start()
try:
await run_go_client("tls", TLS_TCP_PORT, "requests", {"3", "4"}, CERT_PATH)
finally:
await server.stop()
async def run_wss_send_push(ssl_context: ssl.SSLContext) -> None:
received = asyncio.get_running_loop().create_future()
server = WsServer(
HOST,
WSS_PORT,
WS_PATH,
interval_sec=0,
wait_sec=0,
parser_map=parser_map(),
ssl_context=ssl_context,
)
def on_connected(client):
def on_message(data):
print(f"SERVER_RECEIVED index={data.index} message={data.message}")
valid = data.index == 101 and data.message == "fire from go client"
if not received.done():
received.set_result(valid)
if valid:
asyncio.create_task(
client.communicator.send(TestData(index=200, message="push from python server"))
)
client.communicator.add_listener(type_name_of(TestData), on_message)
server.on_client_connected = on_connected
await server.start()
try:
await run_go_client("wss", WSS_PORT, "send-push", {"1", "2"}, CERT_PATH)
ok = await asyncio.wait_for(received, SERVER_OBSERVATION_WINDOW)
if not ok:
raise RuntimeError("WSS send-push server received unexpected data")
finally:
await server.stop()
async def run_wss_requests(ssl_context: ssl.SSLContext) -> None:
server = WsServer(
HOST,
WSS_PORT,
WS_PATH,
interval_sec=0,
wait_sec=0,
parser_map=parser_map(),
ssl_context=ssl_context,
)
server.on_client_connected = lambda client: client.communicator.add_request_listener(
type_name_of(TestData),
lambda req: TestData(index=req.index * 2, message="echo: " + req.message),
)
await server.start()
try:
await run_go_client("wss", WSS_PORT, "requests", {"3", "4"}, CERT_PATH)
finally:
await server.stop()
async def run_go_client(
mode: str,
port: int,
phase: str,
expected: set[str],
cert: Path | None = None,
) -> None:
go_dir = go_package_dir()
process = await asyncio.create_subprocess_exec(
"go",
args = [
"run",
"./crosstest/python_go_client",
f"--mode={mode}",
f"--port={port}",
f"--phase={phase}",
]
if cert is not None:
args.append(f"--cert={cert}")
process = await asyncio.create_subprocess_exec(
"go",
*args,
cwd=go_dir,
stdout=asyncio.subprocess.PIPE,
stderr=asyncio.subprocess.PIPE,
@ -176,4 +323,3 @@ def go_package_dir() -> Path:
if __name__ == "__main__":
asyncio.run(main())

View file

@ -2,6 +2,7 @@ from __future__ import annotations
import asyncio
import re
import ssl
import sys
from pathlib import Path
@ -15,9 +16,13 @@ from toki_socket.ws_server import WsServer
HOST = "127.0.0.1"
PYTHON_KOTLIN_TCP_PORT = 29498
PYTHON_KOTLIN_WS_PORT = 29500
TLS_TCP_PORT = 29502
WSS_PORT = 29504
WS_PATH = "/"
PROCESS_TIMEOUT = 20.0
SERVER_OBSERVATION_WINDOW = 0.2
CERT_PATH = Path(__file__).resolve().parents[1] / "test" / "certs" / "server.crt"
KEY_PATH = Path(__file__).resolve().parents[1] / "test" / "certs" / "server.key"
def parser_map():
@ -34,6 +39,8 @@ async def main() -> None:
await run_ws_send_push()
await asyncio.sleep(0.15)
await run_ws_requests()
await asyncio.sleep(0.15)
await run_tls()
except Exception as exc:
print(f"FAIL crosstest error={exc}", file=sys.stderr)
raise SystemExit(1) from exc
@ -122,13 +129,151 @@ async def run_ws_requests() -> None:
await server.stop()
async def run_kotlin_client(mode: str, port: int, phase: str, expected: set[str]) -> None:
def _make_server_ssl_context() -> ssl.SSLContext:
ctx = ssl.SSLContext(ssl.PROTOCOL_TLS_SERVER)
ctx.load_cert_chain(CERT_PATH, KEY_PATH)
return ctx
async def run_tls() -> None:
ssl_context = _make_server_ssl_context()
await run_tls_tcp_send_push(ssl_context)
await asyncio.sleep(0.15)
await run_tls_tcp_requests(ssl_context)
await asyncio.sleep(0.15)
await run_wss_send_push(ssl_context)
await asyncio.sleep(0.15)
await run_wss_requests(ssl_context)
async def run_tls_tcp_send_push(ssl_context: ssl.SSLContext) -> None:
received = asyncio.get_running_loop().create_future()
server = TcpServer(
HOST,
TLS_TCP_PORT,
interval_sec=0,
wait_sec=0,
parser_map=parser_map(),
ssl_context=ssl_context,
)
def on_connected(client):
def on_message(data):
print(f"SERVER_RECEIVED index={data.index} message={data.message}")
valid = data.index == 101 and data.message == "fire from kotlin client"
if not received.done():
received.set_result(valid)
if valid:
asyncio.create_task(
client.communicator.send(TestData(index=200, message="push from python server"))
)
client.communicator.add_listener(type_name_of(TestData), on_message)
server.on_client_connected = on_connected
await server.start()
try:
await run_kotlin_client("tls", TLS_TCP_PORT, "send-push", {"1", "2"}, CERT_PATH)
ok = await asyncio.wait_for(received, SERVER_OBSERVATION_WINDOW)
if not ok:
raise RuntimeError("TLS TCP send-push server received unexpected data")
finally:
await server.stop()
async def run_tls_tcp_requests(ssl_context: ssl.SSLContext) -> None:
server = TcpServer(
HOST,
TLS_TCP_PORT,
interval_sec=0,
wait_sec=0,
parser_map=parser_map(),
ssl_context=ssl_context,
)
server.on_client_connected = lambda client: client.communicator.add_request_listener(
type_name_of(TestData),
lambda req: TestData(index=req.index * 2, message="echo: " + req.message),
)
await server.start()
try:
await run_kotlin_client("tls", TLS_TCP_PORT, "requests", {"3", "4"}, CERT_PATH)
finally:
await server.stop()
async def run_wss_send_push(ssl_context: ssl.SSLContext) -> None:
received = asyncio.get_running_loop().create_future()
server = WsServer(
HOST,
WSS_PORT,
WS_PATH,
interval_sec=0,
wait_sec=0,
parser_map=parser_map(),
ssl_context=ssl_context,
)
def on_connected(client):
def on_message(data):
print(f"SERVER_RECEIVED index={data.index} message={data.message}")
valid = data.index == 101 and data.message == "fire from kotlin client"
if not received.done():
received.set_result(valid)
if valid:
asyncio.create_task(
client.communicator.send(TestData(index=200, message="push from python server"))
)
client.communicator.add_listener(type_name_of(TestData), on_message)
server.on_client_connected = on_connected
await server.start()
try:
await run_kotlin_client("wss", WSS_PORT, "send-push", {"1", "2"}, CERT_PATH)
ok = await asyncio.wait_for(received, SERVER_OBSERVATION_WINDOW)
if not ok:
raise RuntimeError("WSS send-push server received unexpected data")
finally:
await server.stop()
async def run_wss_requests(ssl_context: ssl.SSLContext) -> None:
server = WsServer(
HOST,
WSS_PORT,
WS_PATH,
interval_sec=0,
wait_sec=0,
parser_map=parser_map(),
ssl_context=ssl_context,
)
server.on_client_connected = lambda client: client.communicator.add_request_listener(
type_name_of(TestData),
lambda req: TestData(index=req.index * 2, message="echo: " + req.message),
)
await server.start()
try:
await run_kotlin_client("wss", WSS_PORT, "requests", {"3", "4"}, CERT_PATH)
finally:
await server.stop()
async def run_kotlin_client(
mode: str,
port: int,
phase: str,
expected: set[str],
cert: Path | None = None,
) -> None:
kotlin_dir = kotlin_package_dir()
client_args = f"--mode={mode} --port={port} --phase={phase}"
if cert is not None:
client_args += f" --cert={cert}"
process = await asyncio.create_subprocess_exec(
"./gradlew",
"run",
f"-PmainClass=com.tokilabs.toki_socket.crosstest.PythonKotlinClientKt",
f"--args=--mode={mode} --port={port} --phase={phase}",
f"--args={client_args}",
cwd=kotlin_dir,
stdout=asyncio.subprocess.PIPE,
stderr=asyncio.subprocess.PIPE,

View file

@ -2,6 +2,7 @@ from __future__ import annotations
import asyncio
import re
import ssl
import sys
from pathlib import Path
@ -15,9 +16,13 @@ from toki_socket.ws_server import WsServer
HOST = "127.0.0.1"
PYTHON_TYPESCRIPT_TCP_PORT = 29798
PYTHON_TYPESCRIPT_WS_PORT = 29800
TLS_TCP_PORT = 29802
WSS_PORT = 29804
WS_PATH = "/"
PROCESS_TIMEOUT = 20.0
SERVER_OBSERVATION_WINDOW = 0.2
CERT_PATH = Path(__file__).resolve().parents[1] / "test" / "certs" / "server.crt"
KEY_PATH = Path(__file__).resolve().parents[1] / "test" / "certs" / "server.key"
def parser_map():
@ -34,6 +39,8 @@ async def main() -> None:
await run_ws_send_push()
await asyncio.sleep(0.15)
await run_ws_requests()
await asyncio.sleep(0.15)
await run_tls()
except Exception as exc:
print(f"FAIL crosstest error={exc}", file=sys.stderr)
raise SystemExit(1) from exc
@ -122,15 +129,155 @@ async def run_ws_requests() -> None:
await server.stop()
async def run_typescript_client(mode: str, port: int, phase: str, expected: set[str]) -> None:
def _make_server_ssl_context() -> ssl.SSLContext:
ctx = ssl.SSLContext(ssl.PROTOCOL_TLS_SERVER)
ctx.load_cert_chain(CERT_PATH, KEY_PATH)
return ctx
async def run_tls() -> None:
ssl_context = _make_server_ssl_context()
await run_tls_tcp_send_push(ssl_context)
await asyncio.sleep(0.15)
await run_tls_tcp_requests(ssl_context)
await asyncio.sleep(0.15)
await run_wss_send_push(ssl_context)
await asyncio.sleep(0.15)
await run_wss_requests(ssl_context)
async def run_tls_tcp_send_push(ssl_context: ssl.SSLContext) -> None:
received = asyncio.get_running_loop().create_future()
server = TcpServer(
HOST,
TLS_TCP_PORT,
interval_sec=0,
wait_sec=0,
parser_map=parser_map(),
ssl_context=ssl_context,
)
def on_connected(client):
def on_message(data):
print(f"SERVER_RECEIVED index={data.index} message={data.message}")
valid = data.index == 101 and data.message == "fire from typescript client"
if not received.done():
received.set_result(valid)
if valid:
asyncio.create_task(
client.communicator.send(TestData(index=200, message="push from python server"))
)
client.communicator.add_listener(type_name_of(TestData), on_message)
server.on_client_connected = on_connected
await server.start()
try:
await run_typescript_client("tls", TLS_TCP_PORT, "send-push", {"1", "2"}, CERT_PATH)
ok = await asyncio.wait_for(received, SERVER_OBSERVATION_WINDOW)
if not ok:
raise RuntimeError("TLS TCP send-push server received unexpected data")
finally:
await server.stop()
async def run_tls_tcp_requests(ssl_context: ssl.SSLContext) -> None:
server = TcpServer(
HOST,
TLS_TCP_PORT,
interval_sec=0,
wait_sec=0,
parser_map=parser_map(),
ssl_context=ssl_context,
)
server.on_client_connected = lambda client: client.communicator.add_request_listener(
type_name_of(TestData),
lambda req: TestData(index=req.index * 2, message="echo: " + req.message),
)
await server.start()
try:
await run_typescript_client("tls", TLS_TCP_PORT, "requests", {"3", "4"}, CERT_PATH)
finally:
await server.stop()
async def run_wss_send_push(ssl_context: ssl.SSLContext) -> None:
received = asyncio.get_running_loop().create_future()
server = WsServer(
HOST,
WSS_PORT,
WS_PATH,
interval_sec=0,
wait_sec=0,
parser_map=parser_map(),
ssl_context=ssl_context,
)
def on_connected(client):
def on_message(data):
print(f"SERVER_RECEIVED index={data.index} message={data.message}")
valid = data.index == 101 and data.message == "fire from typescript client"
if not received.done():
received.set_result(valid)
if valid:
asyncio.create_task(
client.communicator.send(TestData(index=200, message="push from python server"))
)
client.communicator.add_listener(type_name_of(TestData), on_message)
server.on_client_connected = on_connected
await server.start()
try:
await run_typescript_client("wss", WSS_PORT, "send-push", {"1", "2"}, CERT_PATH)
ok = await asyncio.wait_for(received, SERVER_OBSERVATION_WINDOW)
if not ok:
raise RuntimeError("WSS send-push server received unexpected data")
finally:
await server.stop()
async def run_wss_requests(ssl_context: ssl.SSLContext) -> None:
server = WsServer(
HOST,
WSS_PORT,
WS_PATH,
interval_sec=0,
wait_sec=0,
parser_map=parser_map(),
ssl_context=ssl_context,
)
server.on_client_connected = lambda client: client.communicator.add_request_listener(
type_name_of(TestData),
lambda req: TestData(index=req.index * 2, message="echo: " + req.message),
)
await server.start()
try:
await run_typescript_client("wss", WSS_PORT, "requests", {"3", "4"}, CERT_PATH)
finally:
await server.stop()
async def run_typescript_client(
mode: str,
port: int,
phase: str,
expected: set[str],
cert: Path | None = None,
) -> None:
ts_dir = typescript_package_dir()
process = await asyncio.create_subprocess_exec(
"npx",
args = [
"tsx",
"crosstest/python_typescript_client.ts",
f"--mode={mode}",
f"--port={port}",
f"--phase={phase}",
]
if cert is not None:
args.append(f"--cert={cert}")
process = await asyncio.create_subprocess_exec(
"npx",
*args,
cwd=ts_dir,
stdout=asyncio.subprocess.PIPE,
stderr=asyncio.subprocess.PIPE,

View file

@ -2,20 +2,23 @@ from __future__ import annotations
import argparse
import asyncio
import ssl
import sys
from contextlib import suppress
from pathlib import Path
sys.path.insert(0, str(Path(__file__).resolve().parents[1]))
from toki_socket.communicator import type_name_of
from toki_socket.packets.message_common_pb2 import TestData
from toki_socket.tcp_client import connect_tcp
from toki_socket.ws_client import connect_ws
from toki_socket.tcp_client import connect_tcp, connect_tcp_tls
from toki_socket.ws_client import connect_ws, connect_wss
HOST = "127.0.0.1"
WS_PATH = "/"
CONNECT_WINDOW = 3.0
REQUEST_WINDOW = 2.0
SERVER_READY_DELAY = 0.05
def parser_map():
@ -24,18 +27,20 @@ def parser_map():
async def main() -> None:
parser = argparse.ArgumentParser()
parser.add_argument("--mode", choices=["tcp", "ws"], default="tcp")
parser.add_argument("--mode", choices=["tcp", "ws", "tls", "wss"], default="tcp")
parser.add_argument("--port", type=int, required=True)
parser.add_argument("--phase", choices=["send-push", "requests"], default="send-push")
parser.add_argument("--cert", default=None)
args = parser.parse_args()
print(f"INFO typeName python={type_name_of(TestData)}")
try:
client = await dial_with_retry(args.mode, args.port)
client = await dial_with_retry(args.mode, args.port, args.cert)
except Exception as exc:
fail("setup", str(exc))
raise SystemExit(1) from exc
await asyncio.sleep(SERVER_READY_DELAY)
try:
if args.phase == "send-push":
@ -43,29 +48,42 @@ async def main() -> None:
else:
ok = await run_requests(client)
finally:
await client.close()
with suppress(Exception):
await asyncio.wait_for(client.close(), 1.0)
if not ok:
raise SystemExit(1)
async def dial_with_retry(mode: str, port: int):
async def dial_with_retry(mode: str, port: int, cert: str | None):
deadline = asyncio.get_running_loop().time() + CONNECT_WINDOW
last_error: Exception | None = None
while asyncio.get_running_loop().time() < deadline:
try:
return await asyncio.wait_for(dial(mode, port), 0.3)
return await asyncio.wait_for(dial(mode, port, cert), 0.3)
except Exception as exc:
last_error = exc
await asyncio.sleep(0.1)
raise TimeoutError(f"connect {mode}:{port} timed out: {last_error}")
async def dial(mode: str, port: int):
async def dial(mode: str, port: int, cert: str | None):
if mode == "tcp":
return await connect_tcp(HOST, port, 0, 0, parser_map())
if mode == "ws":
return await connect_ws(HOST, port, WS_PATH, 0, 0, parser_map())
if mode == "tls":
if cert is None:
raise ValueError("--cert is required for tls mode")
ssl_ctx = ssl.create_default_context(cafile=cert)
ssl_ctx.check_hostname = False
return await connect_tcp_tls(HOST, port, ssl_ctx, 0, 0, parser_map())
if mode == "wss":
if cert is None:
raise ValueError("--cert is required for wss mode")
ssl_ctx = ssl.create_default_context(cafile=cert)
ssl_ctx.check_hostname = False
return await connect_wss(HOST, port, WS_PATH, ssl_ctx, 0, 0, parser_map())
raise ValueError(f"unknown mode {mode!r}")

View file

@ -1,3 +1,5 @@
import * as fs from "node:fs";
import { create } from "@bufbuild/protobuf";
import type { BaseClient } from "../src/base_client.js";
@ -7,21 +9,24 @@ import {
sendRequestTyped,
} from "../src/communicator.js";
import { TestDataSchema, type TestData } from "../src/packets/message_common_pb.js";
import { connectTcp } from "../src/tcp_client.js";
import { connectWs } from "../src/ws_client.js";
import { connectTcp, connectTcpTls } from "../src/tcp_client.js";
import { connectWs, connectWss } from "../src/ws_client.js";
const HOST = "127.0.0.1";
const WS_PATH = "/";
const CONNECT_WINDOW_MS = 3000;
const REQUEST_WINDOW_MS = 2000;
type Mode = "tcp" | "ws" | "tls" | "wss";
type Phase = "send-push" | "requests";
async function main(): Promise<void> {
const args = parseArgs(process.argv.slice(2));
console.log(`INFO typeName ts=${TestDataSchema.typeName}`);
let client: BaseClient;
try {
client = await dialWithRetry(args.mode, args.port);
client = await dialWithRetry(args.mode, args.port, args.cert);
} catch (err) {
fail("setup", errorMessage(err));
process.exitCode = 1;
@ -40,15 +45,16 @@ async function main(): Promise<void> {
}
}
function parseArgs(argv: string[]): { mode: "tcp" | "ws"; port: number; phase: "send-push" | "requests" } {
let mode: "tcp" | "ws" = "tcp";
function parseArgs(argv: string[]): { mode: Mode; port: number; phase: Phase; cert?: string } {
let mode: Mode = "tcp";
let port: number | null = null;
let phase: "send-push" | "requests" = "send-push";
let phase: Phase = "send-push";
let cert: string | undefined;
for (const arg of argv) {
if (arg.startsWith("--mode=")) {
const value = arg.slice("--mode=".length);
if (value === "tcp" || value === "ws") {
if (value === "tcp" || value === "ws" || value === "tls" || value === "wss") {
mode = value;
}
continue;
@ -62,21 +68,25 @@ function parseArgs(argv: string[]): { mode: "tcp" | "ws"; port: number; phase: "
if (value === "send-push" || value === "requests") {
phase = value;
}
continue;
}
if (arg.startsWith("--cert=")) {
cert = arg.slice("--cert=".length);
}
}
if (port === null || Number.isNaN(port)) {
throw new Error("missing required --port");
}
return { mode, port, phase };
return { mode, port, phase, cert };
}
async function dialWithRetry(mode: "tcp" | "ws", port: number): Promise<BaseClient> {
async function dialWithRetry(mode: Mode, port: number, cert?: string): Promise<BaseClient> {
const deadline = Date.now() + CONNECT_WINDOW_MS;
let lastError: unknown;
while (Date.now() < deadline) {
try {
return await dial(mode, port);
return await dial(mode, port, cert);
} catch (err) {
lastError = err;
await sleep(100);
@ -85,12 +95,22 @@ async function dialWithRetry(mode: "tcp" | "ws", port: number): Promise<BaseClie
throw new Error(`connect ${mode}:${port} timed out: ${errorMessage(lastError)}`);
}
async function dial(mode: "tcp" | "ws", port: number): Promise<BaseClient> {
async function dial(mode: Mode, port: number, cert?: string): Promise<BaseClient> {
const parserMap = new Map([[TestDataSchema.typeName, parserFromSchema(TestDataSchema)]]);
if (mode === "tcp") {
return connectTcp(HOST, port, 0, 0, parserMap);
}
return connectWs(HOST, port, WS_PATH, 0, 0, parserMap);
if (mode === "ws") {
return connectWs(HOST, port, WS_PATH, 0, 0, parserMap);
}
if (cert === undefined || cert.length === 0) {
throw new Error(`--cert is required for ${mode} mode`);
}
const ca = fs.readFileSync(cert);
if (mode === "tls") {
return connectTcpTls(HOST, port, { ca }, 0, 0, parserMap);
}
return connectWss(HOST, port, WS_PATH, { ca }, 0, 0, parserMap);
}
async function runSendPush(client: BaseClient): Promise<boolean> {

View file

@ -1,3 +1,5 @@
import * as fs from "node:fs";
import { create } from "@bufbuild/protobuf";
import type { BaseClient } from "../src/base_client.js";
@ -7,21 +9,24 @@ import {
sendRequestTyped,
} from "../src/communicator.js";
import { TestDataSchema, type TestData } from "../src/packets/message_common_pb.js";
import { connectTcp } from "../src/tcp_client.js";
import { connectWs } from "../src/ws_client.js";
import { connectTcp, connectTcpTls } from "../src/tcp_client.js";
import { connectWs, connectWss } from "../src/ws_client.js";
const HOST = "127.0.0.1";
const WS_PATH = "/";
const CONNECT_WINDOW_MS = 3000;
const REQUEST_WINDOW_MS = 2000;
type Mode = "tcp" | "ws" | "tls" | "wss";
type Phase = "send-push" | "requests";
async function main(): Promise<void> {
const args = parseArgs(process.argv.slice(2));
console.log(`INFO typeName ts=${TestDataSchema.typeName}`);
let client: BaseClient;
try {
client = await dialWithRetry(args.mode, args.port);
client = await dialWithRetry(args.mode, args.port, args.cert);
} catch (err) {
fail("setup", errorMessage(err));
process.exitCode = 1;
@ -40,15 +45,16 @@ async function main(): Promise<void> {
}
}
function parseArgs(argv: string[]): { mode: "tcp" | "ws"; port: number; phase: "send-push" | "requests" } {
let mode: "tcp" | "ws" = "tcp";
function parseArgs(argv: string[]): { mode: Mode; port: number; phase: Phase; cert?: string } {
let mode: Mode = "tcp";
let port: number | null = null;
let phase: "send-push" | "requests" = "send-push";
let phase: Phase = "send-push";
let cert: string | undefined;
for (const arg of argv) {
if (arg.startsWith("--mode=")) {
const value = arg.slice("--mode=".length);
if (value === "tcp" || value === "ws") {
if (value === "tcp" || value === "ws" || value === "tls" || value === "wss") {
mode = value;
}
continue;
@ -62,21 +68,25 @@ function parseArgs(argv: string[]): { mode: "tcp" | "ws"; port: number; phase: "
if (value === "send-push" || value === "requests") {
phase = value;
}
continue;
}
if (arg.startsWith("--cert=")) {
cert = arg.slice("--cert=".length);
}
}
if (port === null || Number.isNaN(port)) {
throw new Error("missing required --port");
}
return { mode, port, phase };
return { mode, port, phase, cert };
}
async function dialWithRetry(mode: "tcp" | "ws", port: number): Promise<BaseClient> {
async function dialWithRetry(mode: Mode, port: number, cert?: string): Promise<BaseClient> {
const deadline = Date.now() + CONNECT_WINDOW_MS;
let lastError: unknown;
while (Date.now() < deadline) {
try {
return await dial(mode, port);
return await dial(mode, port, cert);
} catch (err) {
lastError = err;
await sleep(100);
@ -85,12 +95,22 @@ async function dialWithRetry(mode: "tcp" | "ws", port: number): Promise<BaseClie
throw new Error(`connect ${mode}:${port} timed out: ${errorMessage(lastError)}`);
}
async function dial(mode: "tcp" | "ws", port: number): Promise<BaseClient> {
async function dial(mode: Mode, port: number, cert?: string): Promise<BaseClient> {
const parserMap = new Map([[TestDataSchema.typeName, parserFromSchema(TestDataSchema)]]);
if (mode === "tcp") {
return connectTcp(HOST, port, 0, 0, parserMap);
}
return connectWs(HOST, port, WS_PATH, 0, 0, parserMap);
if (mode === "ws") {
return connectWs(HOST, port, WS_PATH, 0, 0, parserMap);
}
if (cert === undefined || cert.length === 0) {
throw new Error(`--cert is required for ${mode} mode`);
}
const ca = fs.readFileSync(cert);
if (mode === "tls") {
return connectTcpTls(HOST, port, { ca }, 0, 0, parserMap);
}
return connectWss(HOST, port, WS_PATH, { ca }, 0, 0, parserMap);
}
async function runSendPush(client: BaseClient): Promise<boolean> {

View file

@ -1,3 +1,5 @@
import * as fs from "node:fs";
import { create } from "@bufbuild/protobuf";
import type { BaseClient } from "../src/base_client.js";
@ -7,8 +9,8 @@ import {
sendRequestTyped,
} from "../src/communicator.js";
import { TestDataSchema, type TestData } from "../src/packets/message_common_pb.js";
import { connectTcp } from "../src/tcp_client.js";
import { connectWs } from "../src/ws_client.js";
import { connectTcp, connectTcpTls } from "../src/tcp_client.js";
import { connectWs, connectWss } from "../src/ws_client.js";
const HOST = "127.0.0.1";
const WS_PATH = "/";
@ -16,13 +18,16 @@ const CONNECT_WINDOW_MS = 10000;
const REQUEST_WINDOW_MS = 2000;
const SERVER_READY_DELAY_MS = 50;
type Mode = "tcp" | "ws" | "tls" | "wss";
type Phase = "send-push" | "requests";
async function main(): Promise<void> {
const args = parseArgs(process.argv.slice(2));
console.log(`INFO typeName ts=${TestDataSchema.typeName}`);
let client: BaseClient;
try {
client = await dialWithRetry(args.mode, args.port);
client = await dialWithRetry(args.mode, args.port, args.cert);
} catch (err) {
fail("setup", errorMessage(err));
process.exitCode = 1;
@ -42,15 +47,16 @@ async function main(): Promise<void> {
}
}
function parseArgs(argv: string[]): { mode: "tcp" | "ws"; port: number; phase: "send-push" | "requests" } {
let mode: "tcp" | "ws" = "tcp";
function parseArgs(argv: string[]): { mode: Mode; port: number; phase: Phase; cert?: string } {
let mode: Mode = "tcp";
let port: number | null = null;
let phase: "send-push" | "requests" = "send-push";
let phase: Phase = "send-push";
let cert: string | undefined;
for (const arg of argv) {
if (arg.startsWith("--mode=")) {
const value = arg.slice("--mode=".length);
if (value === "tcp" || value === "ws") {
if (value === "tcp" || value === "ws" || value === "tls" || value === "wss") {
mode = value;
}
continue;
@ -64,21 +70,25 @@ function parseArgs(argv: string[]): { mode: "tcp" | "ws"; port: number; phase: "
if (value === "send-push" || value === "requests") {
phase = value;
}
continue;
}
if (arg.startsWith("--cert=")) {
cert = arg.slice("--cert=".length);
}
}
if (port === null || Number.isNaN(port)) {
throw new Error("missing required --port");
}
return { mode, port, phase };
return { mode, port, phase, cert };
}
async function dialWithRetry(mode: "tcp" | "ws", port: number): Promise<BaseClient> {
async function dialWithRetry(mode: Mode, port: number, cert?: string): Promise<BaseClient> {
const deadline = Date.now() + CONNECT_WINDOW_MS;
let lastError: unknown;
while (Date.now() < deadline) {
try {
return await dial(mode, port);
return await dial(mode, port, cert);
} catch (err) {
lastError = err;
await sleep(100);
@ -87,12 +97,22 @@ async function dialWithRetry(mode: "tcp" | "ws", port: number): Promise<BaseClie
throw new Error(`connect ${mode}:${port} timed out: ${errorMessage(lastError)}`);
}
async function dial(mode: "tcp" | "ws", port: number): Promise<BaseClient> {
async function dial(mode: Mode, port: number, cert?: string): Promise<BaseClient> {
const parserMap = new Map([[TestDataSchema.typeName, parserFromSchema(TestDataSchema)]]);
if (mode === "tcp") {
return connectTcp(HOST, port, 0, 0, parserMap);
}
return connectWs(HOST, port, WS_PATH, 0, 0, parserMap);
if (mode === "ws") {
return connectWs(HOST, port, WS_PATH, 0, 0, parserMap);
}
if (cert === undefined || cert.length === 0) {
throw new Error(`--cert is required for ${mode} mode`);
}
const ca = fs.readFileSync(cert);
if (mode === "tls") {
return connectTcpTls(HOST, port, { ca }, 0, 0, parserMap);
}
return connectWss(HOST, port, WS_PATH, { ca }, 0, 0, parserMap);
}
async function runSendPush(client: BaseClient): Promise<boolean> {

View file

@ -1,3 +1,5 @@
import * as fs from "node:fs";
import { create } from "@bufbuild/protobuf";
import type { BaseClient } from "../src/base_client.js";
@ -7,21 +9,24 @@ import {
sendRequestTyped,
} from "../src/communicator.js";
import { TestDataSchema, type TestData } from "../src/packets/message_common_pb.js";
import { connectTcp } from "../src/tcp_client.js";
import { connectWs } from "../src/ws_client.js";
import { connectTcp, connectTcpTls } from "../src/tcp_client.js";
import { connectWs, connectWss } from "../src/ws_client.js";
const HOST = "127.0.0.1";
const WS_PATH = "/";
const CONNECT_WINDOW_MS = 3000;
const REQUEST_WINDOW_MS = 2000;
type Mode = "tcp" | "ws" | "tls" | "wss";
type Phase = "send-push" | "requests";
async function main(): Promise<void> {
const args = parseArgs(process.argv.slice(2));
console.log(`INFO typeName ts=${TestDataSchema.typeName}`);
let client: BaseClient;
try {
client = await dialWithRetry(args.mode, args.port);
client = await dialWithRetry(args.mode, args.port, args.cert);
} catch (err) {
fail("setup", errorMessage(err));
process.exitCode = 1;
@ -40,15 +45,16 @@ async function main(): Promise<void> {
}
}
function parseArgs(argv: string[]): { mode: "tcp" | "ws"; port: number; phase: "send-push" | "requests" } {
let mode: "tcp" | "ws" = "tcp";
function parseArgs(argv: string[]): { mode: Mode; port: number; phase: Phase; cert?: string } {
let mode: Mode = "tcp";
let port: number | null = null;
let phase: "send-push" | "requests" = "send-push";
let phase: Phase = "send-push";
let cert: string | undefined;
for (const arg of argv) {
if (arg.startsWith("--mode=")) {
const value = arg.slice("--mode=".length);
if (value === "tcp" || value === "ws") {
if (value === "tcp" || value === "ws" || value === "tls" || value === "wss") {
mode = value;
}
continue;
@ -62,21 +68,25 @@ function parseArgs(argv: string[]): { mode: "tcp" | "ws"; port: number; phase: "
if (value === "send-push" || value === "requests") {
phase = value;
}
continue;
}
if (arg.startsWith("--cert=")) {
cert = arg.slice("--cert=".length);
}
}
if (port === null || Number.isNaN(port)) {
throw new Error("missing required --port");
}
return { mode, port, phase };
return { mode, port, phase, cert };
}
async function dialWithRetry(mode: "tcp" | "ws", port: number): Promise<BaseClient> {
async function dialWithRetry(mode: Mode, port: number, cert?: string): Promise<BaseClient> {
const deadline = Date.now() + CONNECT_WINDOW_MS;
let lastError: unknown;
while (Date.now() < deadline) {
try {
return await dial(mode, port);
return await dial(mode, port, cert);
} catch (err) {
lastError = err;
await sleep(100);
@ -85,12 +95,22 @@ async function dialWithRetry(mode: "tcp" | "ws", port: number): Promise<BaseClie
throw new Error(`connect ${mode}:${port} timed out: ${errorMessage(lastError)}`);
}
async function dial(mode: "tcp" | "ws", port: number): Promise<BaseClient> {
async function dial(mode: Mode, port: number, cert?: string): Promise<BaseClient> {
const parserMap = new Map([[TestDataSchema.typeName, parserFromSchema(TestDataSchema)]]);
if (mode === "tcp") {
return connectTcp(HOST, port, 0, 0, parserMap);
}
return connectWs(HOST, port, WS_PATH, 0, 0, parserMap);
if (mode === "ws") {
return connectWs(HOST, port, WS_PATH, 0, 0, parserMap);
}
if (cert === undefined || cert.length === 0) {
throw new Error(`--cert is required for ${mode} mode`);
}
const ca = fs.readFileSync(cert);
if (mode === "tls") {
return connectTcpTls(HOST, port, { ca }, 0, 0, parserMap);
}
return connectWss(HOST, port, WS_PATH, { ca }, 0, 0, parserMap);
}
async function runSendPush(client: BaseClient): Promise<boolean> {

View file

@ -1,4 +1,5 @@
import * as childProcess from "node:child_process";
import * as fs from "node:fs";
import * as path from "node:path";
import * as readline from "node:readline";
import { fileURLToPath } from "node:url";
@ -19,19 +20,27 @@ import { WsServer } from "../src/ws_server.js";
const __filename = fileURLToPath(import.meta.url);
const repoRoot = path.resolve(path.dirname(__filename), "../..");
const dartDir = path.join(repoRoot, "dart");
const CERT_PATH = path.join(repoRoot, "typescript/test/certs/server.crt");
const KEY_PATH = path.join(repoRoot, "typescript/test/certs/server.key");
const HOST = "127.0.0.1";
const TCP_PORT = 29802;
const WS_PORT = 29804;
const TLS_TCP_PORT = 29806;
const WSS_PORT = 29808;
const WS_PATH = "/";
const PROCESS_TIMEOUT_MS = 20_000;
const SERVER_OBSERVATION_MS = 200;
type Mode = "tcp" | "ws" | "tls" | "wss";
type Phase = "send-push" | "requests";
async function main(): Promise<void> {
console.log(`INFO typeName ts=${TestDataSchema.typeName}`);
try {
await runTcp();
await runWs();
await runTls();
console.log("PASS all typescript-server/dart-client crosstests passed");
} catch (err) {
console.error(`FAIL crosstest error=${errorMessage(err)}`);
@ -51,6 +60,16 @@ async function runWs(): Promise<void> {
await runWsRequests();
}
async function runTls(): Promise<void> {
await runTlsTcpSendPush();
await sleep(150);
await runTlsTcpRequests();
await sleep(150);
await runWssSendPush();
await sleep(150);
await runWssRequests();
}
async function runTcpSendPush(): Promise<void> {
let resolveReceived: ((value: boolean) => void) | null = null;
const received = new Promise<boolean>((resolve) => {
@ -143,6 +162,120 @@ async function runWsRequests(): Promise<void> {
await withServer(server, () => runDartClient("ws", WS_PORT, "requests", new Set(["3", "4"])));
}
async function runTlsTcpSendPush(): Promise<void> {
let resolveReceived: ((value: boolean) => void) | null = null;
const received = new Promise<boolean>((resolve) => {
resolveReceived = resolve;
});
const server = new TcpServer(
HOST,
TLS_TCP_PORT,
(socket) => new TcpClient(socket, 0, 0, parserMap()),
serverTlsOptions(),
);
server.onClientConnected = (client) => {
addListenerTyped(client.communicator, TestDataSchema, (data) => {
console.log(`SERVER_RECEIVED index=${data.index} message=${data.message}`);
const valid = data.index === 101 && data.message === "fire from dart client";
if (resolveReceived !== null) {
resolveReceived(valid);
resolveReceived = null;
}
if (valid) {
void client.communicator.send(
create(TestDataSchema, {
index: 200,
message: "push from typescript server",
}),
);
}
});
};
await withServer(server, async () => {
await runDartClient("tls", TLS_TCP_PORT, "send-push", new Set(["1", "2"]), CERT_PATH);
const ok = await withTimeout(received, SERVER_OBSERVATION_MS, "TLS TCP send-push server did not receive expected data");
if (!ok) {
throw new Error("TLS TCP send-push server received unexpected data");
}
});
}
async function runTlsTcpRequests(): Promise<void> {
const server = new TcpServer(
HOST,
TLS_TCP_PORT,
(socket) => new TcpClient(socket, 0, 0, parserMap()),
serverTlsOptions(),
);
server.onClientConnected = (client) => {
addRequestListenerTyped(client.communicator, TestDataSchema, (req) =>
create(TestDataSchema, {
index: req.index * 2,
message: `echo: ${req.message}`,
}),
);
};
await withServer(server, () => runDartClient("tls", TLS_TCP_PORT, "requests", new Set(["3", "4"]), CERT_PATH));
}
async function runWssSendPush(): Promise<void> {
let resolveReceived: ((value: boolean) => void) | null = null;
const received = new Promise<boolean>((resolve) => {
resolveReceived = resolve;
});
const server = new WsServer(
HOST,
WSS_PORT,
WS_PATH,
(ws) => new WsClient(ws, 0, 0, parserMap()),
serverTlsOptions(),
);
server.onClientConnected = (client) => {
addListenerTyped(client.communicator, TestDataSchema, (data) => {
console.log(`SERVER_RECEIVED index=${data.index} message=${data.message}`);
const valid = data.index === 101 && data.message === "fire from dart client";
if (resolveReceived !== null) {
resolveReceived(valid);
resolveReceived = null;
}
if (valid) {
void client.communicator.send(
create(TestDataSchema, {
index: 200,
message: "push from typescript server",
}),
);
}
});
};
await withServer(server, async () => {
await runDartClient("wss", WSS_PORT, "send-push", new Set(["1", "2"]), CERT_PATH);
const ok = await withTimeout(received, SERVER_OBSERVATION_MS, "WSS send-push server did not receive expected data");
if (!ok) {
throw new Error("WSS send-push server received unexpected data");
}
});
}
async function runWssRequests(): Promise<void> {
const server = new WsServer(
HOST,
WSS_PORT,
WS_PATH,
(ws) => new WsClient(ws, 0, 0, parserMap()),
serverTlsOptions(),
);
server.onClientConnected = (client) => {
addRequestListenerTyped(client.communicator, TestDataSchema, (req) =>
create(TestDataSchema, {
index: req.index * 2,
message: `echo: ${req.message}`,
}),
);
};
await withServer(server, () => runDartClient("wss", WSS_PORT, "requests", new Set(["3", "4"]), CERT_PATH));
}
async function withServer(
server: { start: () => Promise<void>; stop: () => Promise<void> },
body: () => Promise<void>,
@ -156,10 +289,11 @@ async function withServer(
}
async function runDartClient(
mode: "tcp" | "ws",
mode: Mode,
port: number,
phase: "send-push" | "requests",
phase: Phase,
expected: Set<string>,
cert?: string,
): Promise<void> {
await runClientProcess(
"dart",
@ -169,6 +303,7 @@ async function runDartClient(
`--mode=${mode}`,
`--port=${port}`,
`--phase=${phase}`,
...(cert === undefined ? [] : [`--cert=${cert}`]),
],
dartDir,
`dart-client ${mode}/${phase}`,
@ -259,6 +394,13 @@ function parserMap() {
return new Map([[TestDataSchema.typeName, parserFromSchema(TestDataSchema)]]);
}
function serverTlsOptions(): { cert: Buffer; key: Buffer } {
return {
cert: fs.readFileSync(CERT_PATH),
key: fs.readFileSync(KEY_PATH),
};
}
async function withTimeout<T>(promise: Promise<T>, timeoutMs: number, label: string): Promise<T> {
let timer: ReturnType<typeof setTimeout> | undefined;
try {

View file

@ -1,4 +1,5 @@
import * as childProcess from "node:child_process";
import * as fs from "node:fs";
import * as path from "node:path";
import * as readline from "node:readline";
import { fileURLToPath } from "node:url";
@ -19,19 +20,27 @@ import { WsServer } from "../src/ws_server.js";
const __filename = fileURLToPath(import.meta.url);
const repoRoot = path.resolve(path.dirname(__filename), "../..");
const goDir = path.join(repoRoot, "go");
const CERT_PATH = path.join(repoRoot, "typescript/test/certs/server.crt");
const KEY_PATH = path.join(repoRoot, "typescript/test/certs/server.key");
const HOST = "127.0.0.1";
const TCP_PORT = 29806;
const WS_PORT = 29808;
const TLS_TCP_PORT = 29810;
const WSS_PORT = 29812;
const WS_PATH = "/";
const PROCESS_TIMEOUT_MS = 20_000;
const SERVER_OBSERVATION_MS = 200;
type Mode = "tcp" | "ws" | "tls" | "wss";
type Phase = "send-push" | "requests";
async function main(): Promise<void> {
console.log(`INFO typeName ts=${TestDataSchema.typeName}`);
try {
await runTcp();
await runWs();
await runTls();
console.log("PASS all typescript-server/go-client crosstests passed");
} catch (err) {
console.error(`FAIL crosstest error=${errorMessage(err)}`);
@ -51,6 +60,16 @@ async function runWs(): Promise<void> {
await runWsRequests();
}
async function runTls(): Promise<void> {
await runTlsTcpSendPush();
await sleep(150);
await runTlsTcpRequests();
await sleep(150);
await runWssSendPush();
await sleep(150);
await runWssRequests();
}
async function runTcpSendPush(): Promise<void> {
let resolveReceived: ((value: boolean) => void) | null = null;
const received = new Promise<boolean>((resolve) => {
@ -143,6 +162,120 @@ async function runWsRequests(): Promise<void> {
await withServer(server, () => runGoClient("ws", WS_PORT, "requests", new Set(["3", "4"])));
}
async function runTlsTcpSendPush(): Promise<void> {
let resolveReceived: ((value: boolean) => void) | null = null;
const received = new Promise<boolean>((resolve) => {
resolveReceived = resolve;
});
const server = new TcpServer(
HOST,
TLS_TCP_PORT,
(socket) => new TcpClient(socket, 0, 0, parserMap()),
serverTlsOptions(),
);
server.onClientConnected = (client) => {
addListenerTyped(client.communicator, TestDataSchema, (data) => {
console.log(`SERVER_RECEIVED index=${data.index} message=${data.message}`);
const valid = data.index === 101 && data.message === "fire from go client";
if (resolveReceived !== null) {
resolveReceived(valid);
resolveReceived = null;
}
if (valid) {
void client.communicator.send(
create(TestDataSchema, {
index: 200,
message: "push from typescript server",
}),
);
}
});
};
await withServer(server, async () => {
await runGoClient("tls", TLS_TCP_PORT, "send-push", new Set(["1", "2"]), CERT_PATH);
const ok = await withTimeout(received, SERVER_OBSERVATION_MS, "TLS TCP send-push server did not receive expected data");
if (!ok) {
throw new Error("TLS TCP send-push server received unexpected data");
}
});
}
async function runTlsTcpRequests(): Promise<void> {
const server = new TcpServer(
HOST,
TLS_TCP_PORT,
(socket) => new TcpClient(socket, 0, 0, parserMap()),
serverTlsOptions(),
);
server.onClientConnected = (client) => {
addRequestListenerTyped(client.communicator, TestDataSchema, (req) =>
create(TestDataSchema, {
index: req.index * 2,
message: `echo: ${req.message}`,
}),
);
};
await withServer(server, () => runGoClient("tls", TLS_TCP_PORT, "requests", new Set(["3", "4"]), CERT_PATH));
}
async function runWssSendPush(): Promise<void> {
let resolveReceived: ((value: boolean) => void) | null = null;
const received = new Promise<boolean>((resolve) => {
resolveReceived = resolve;
});
const server = new WsServer(
HOST,
WSS_PORT,
WS_PATH,
(ws) => new WsClient(ws, 0, 0, parserMap()),
serverTlsOptions(),
);
server.onClientConnected = (client) => {
addListenerTyped(client.communicator, TestDataSchema, (data) => {
console.log(`SERVER_RECEIVED index=${data.index} message=${data.message}`);
const valid = data.index === 101 && data.message === "fire from go client";
if (resolveReceived !== null) {
resolveReceived(valid);
resolveReceived = null;
}
if (valid) {
void client.communicator.send(
create(TestDataSchema, {
index: 200,
message: "push from typescript server",
}),
);
}
});
};
await withServer(server, async () => {
await runGoClient("wss", WSS_PORT, "send-push", new Set(["1", "2"]), CERT_PATH);
const ok = await withTimeout(received, SERVER_OBSERVATION_MS, "WSS send-push server did not receive expected data");
if (!ok) {
throw new Error("WSS send-push server received unexpected data");
}
});
}
async function runWssRequests(): Promise<void> {
const server = new WsServer(
HOST,
WSS_PORT,
WS_PATH,
(ws) => new WsClient(ws, 0, 0, parserMap()),
serverTlsOptions(),
);
server.onClientConnected = (client) => {
addRequestListenerTyped(client.communicator, TestDataSchema, (req) =>
create(TestDataSchema, {
index: req.index * 2,
message: `echo: ${req.message}`,
}),
);
};
await withServer(server, () => runGoClient("wss", WSS_PORT, "requests", new Set(["3", "4"]), CERT_PATH));
}
async function withServer(
server: { start: () => Promise<void>; stop: () => Promise<void> },
body: () => Promise<void>,
@ -156,10 +289,11 @@ async function withServer(
}
async function runGoClient(
mode: "tcp" | "ws",
mode: Mode,
port: number,
phase: "send-push" | "requests",
phase: Phase,
expected: Set<string>,
cert?: string,
): Promise<void> {
await runClientProcess(
"go",
@ -169,6 +303,7 @@ async function runGoClient(
`--mode=${mode}`,
`--port=${port}`,
`--phase=${phase}`,
...(cert === undefined ? [] : [`--cert=${cert}`]),
],
goDir,
`go-client ${mode}/${phase}`,
@ -259,6 +394,13 @@ function parserMap() {
return new Map([[TestDataSchema.typeName, parserFromSchema(TestDataSchema)]]);
}
function serverTlsOptions(): { cert: Buffer; key: Buffer } {
return {
cert: fs.readFileSync(CERT_PATH),
key: fs.readFileSync(KEY_PATH),
};
}
async function withTimeout<T>(promise: Promise<T>, timeoutMs: number, label: string): Promise<T> {
let timer: ReturnType<typeof setTimeout> | undefined;
try {

View file

@ -1,4 +1,5 @@
import * as childProcess from "node:child_process";
import * as fs from "node:fs";
import * as path from "node:path";
import * as readline from "node:readline";
import { fileURLToPath } from "node:url";
@ -19,19 +20,26 @@ import { WsServer } from "../src/ws_server.js";
const __filename = fileURLToPath(import.meta.url);
const repoRoot = path.resolve(path.dirname(__filename), "../..");
const kotlinDir = path.join(repoRoot, "kotlin");
const CERT_PATH = path.join(repoRoot, "typescript/test/certs/server.crt");
const KEY_PATH = path.join(repoRoot, "typescript/test/certs/server.key");
const HOST = "127.0.0.1";
const TCP_PORT = 29810;
const WS_PORT = 29812;
const TLS_TCP_PORT = 29814;
const WS_PATH = "/";
const PROCESS_TIMEOUT_MS = 20_000;
const SERVER_OBSERVATION_MS = 200;
type Mode = "tcp" | "ws" | "tls" | "wss";
type Phase = "send-push" | "requests";
async function main(): Promise<void> {
console.log(`INFO typeName ts=${TestDataSchema.typeName}`);
try {
await runTcp();
await runWs();
await runTls();
console.log("PASS all typescript-server/kotlin-client crosstests passed");
} catch (err) {
console.error(`FAIL crosstest error=${errorMessage(err)}`);
@ -51,6 +59,12 @@ async function runWs(): Promise<void> {
await runWsRequests();
}
async function runTls(): Promise<void> {
await runTlsTcpSendPush();
await sleep(150);
await runTlsTcpRequests();
}
async function runTcpSendPush(): Promise<void> {
let resolveReceived: ((value: boolean) => void) | null = null;
const received = new Promise<boolean>((resolve) => {
@ -143,6 +157,62 @@ async function runWsRequests(): Promise<void> {
await withServer(server, () => runKotlinClient("ws", WS_PORT, "requests", new Set(["3", "4"])));
}
async function runTlsTcpSendPush(): Promise<void> {
let resolveReceived: ((value: boolean) => void) | null = null;
const received = new Promise<boolean>((resolve) => {
resolveReceived = resolve;
});
const server = new TcpServer(
HOST,
TLS_TCP_PORT,
(socket) => new TcpClient(socket, 0, 0, parserMap()),
serverTlsOptions(),
);
server.onClientConnected = (client) => {
addListenerTyped(client.communicator, TestDataSchema, (data) => {
console.log(`SERVER_RECEIVED index=${data.index} message=${data.message}`);
const valid = data.index === 101 && data.message === "fire from kotlin client";
if (resolveReceived !== null) {
resolveReceived(valid);
resolveReceived = null;
}
if (valid) {
void client.communicator.send(
create(TestDataSchema, {
index: 200,
message: "push from typescript server",
}),
);
}
});
};
await withServer(server, async () => {
await runKotlinClient("tls", TLS_TCP_PORT, "send-push", new Set(["1", "2"]), CERT_PATH);
const ok = await withTimeout(received, SERVER_OBSERVATION_MS, "TLS TCP send-push server did not receive expected data");
if (!ok) {
throw new Error("TLS TCP send-push server received unexpected data");
}
});
}
async function runTlsTcpRequests(): Promise<void> {
const server = new TcpServer(
HOST,
TLS_TCP_PORT,
(socket) => new TcpClient(socket, 0, 0, parserMap()),
serverTlsOptions(),
);
server.onClientConnected = (client) => {
addRequestListenerTyped(client.communicator, TestDataSchema, (req) =>
create(TestDataSchema, {
index: req.index * 2,
message: `echo: ${req.message}`,
}),
);
};
await withServer(server, () => runKotlinClient("tls", TLS_TCP_PORT, "requests", new Set(["3", "4"]), CERT_PATH));
}
async function withServer(
server: { start: () => Promise<void>; stop: () => Promise<void> },
body: () => Promise<void>,
@ -156,17 +226,19 @@ async function withServer(
}
async function runKotlinClient(
mode: "tcp" | "ws",
mode: Mode,
port: number,
phase: "send-push" | "requests",
phase: Phase,
expected: Set<string>,
cert?: string,
): Promise<void> {
const clientArgs = `--mode=${mode} --port=${port} --phase=${phase}${cert === undefined ? "" : ` --cert=${cert}`}`;
await runClientProcess(
"./gradlew",
[
"run",
"-PmainClass=com.tokilabs.toki_socket.crosstest.TypescriptKotlinClientKt",
`--args=--mode=${mode} --port=${port} --phase=${phase}`,
`--args=${clientArgs}`,
],
kotlinDir,
`kotlin-client ${mode}/${phase}`,
@ -257,6 +329,13 @@ function parserMap() {
return new Map([[TestDataSchema.typeName, parserFromSchema(TestDataSchema)]]);
}
function serverTlsOptions(): { cert: Buffer; key: Buffer } {
return {
cert: fs.readFileSync(CERT_PATH),
key: fs.readFileSync(KEY_PATH),
};
}
async function withTimeout<T>(promise: Promise<T>, timeoutMs: number, label: string): Promise<T> {
let timer: ReturnType<typeof setTimeout> | undefined;
try {

View file

@ -1,4 +1,5 @@
import * as childProcess from "node:child_process";
import * as fs from "node:fs";
import * as path from "node:path";
import * as readline from "node:readline";
import { fileURLToPath } from "node:url";
@ -19,19 +20,27 @@ import { WsServer } from "../src/ws_server.js";
const __filename = fileURLToPath(import.meta.url);
const repoRoot = path.resolve(path.dirname(__filename), "../..");
const pythonDir = path.join(repoRoot, "python");
const CERT_PATH = path.join(repoRoot, "typescript/test/certs/server.crt");
const KEY_PATH = path.join(repoRoot, "typescript/test/certs/server.key");
const HOST = "127.0.0.1";
const TCP_PORT = 29814;
const WS_PORT = 29816;
const TLS_TCP_PORT = 29818;
const WSS_PORT = 29820;
const WS_PATH = "/";
const PROCESS_TIMEOUT_MS = 20_000;
const SERVER_OBSERVATION_MS = 200;
type Mode = "tcp" | "ws" | "tls" | "wss";
type Phase = "send-push" | "requests";
async function main(): Promise<void> {
console.log(`INFO typeName ts=${TestDataSchema.typeName}`);
try {
await runTcp();
await runWs();
await runTls();
console.log("PASS all typescript-server/python-client crosstests passed");
} catch (err) {
console.error(`FAIL crosstest error=${errorMessage(err)}`);
@ -51,6 +60,16 @@ async function runWs(): Promise<void> {
await runWsRequests();
}
async function runTls(): Promise<void> {
await runTlsTcpSendPush();
await sleep(150);
await runTlsTcpRequests();
await sleep(150);
await runWssSendPush();
await sleep(150);
await runWssRequests();
}
async function runTcpSendPush(): Promise<void> {
let resolveReceived: ((value: boolean) => void) | null = null;
const received = new Promise<boolean>((resolve) => {
@ -143,6 +162,120 @@ async function runWsRequests(): Promise<void> {
await withServer(server, () => runPythonClient("ws", WS_PORT, "requests", new Set(["3", "4"])));
}
async function runTlsTcpSendPush(): Promise<void> {
let resolveReceived: ((value: boolean) => void) | null = null;
const received = new Promise<boolean>((resolve) => {
resolveReceived = resolve;
});
const server = new TcpServer(
HOST,
TLS_TCP_PORT,
(socket) => new TcpClient(socket, 0, 0, parserMap()),
serverTlsOptions(),
);
server.onClientConnected = (client) => {
addListenerTyped(client.communicator, TestDataSchema, (data) => {
console.log(`SERVER_RECEIVED index=${data.index} message=${data.message}`);
const valid = data.index === 101 && data.message === "fire from python client";
if (resolveReceived !== null) {
resolveReceived(valid);
resolveReceived = null;
}
if (valid) {
void client.communicator.send(
create(TestDataSchema, {
index: 200,
message: "push from typescript server",
}),
);
}
});
};
await withServer(server, async () => {
await runPythonClient("tls", TLS_TCP_PORT, "send-push", new Set(["1", "2"]), CERT_PATH);
const ok = await withTimeout(received, SERVER_OBSERVATION_MS, "TLS TCP send-push server did not receive expected data");
if (!ok) {
throw new Error("TLS TCP send-push server received unexpected data");
}
});
}
async function runTlsTcpRequests(): Promise<void> {
const server = new TcpServer(
HOST,
TLS_TCP_PORT,
(socket) => new TcpClient(socket, 0, 0, parserMap()),
serverTlsOptions(),
);
server.onClientConnected = (client) => {
addRequestListenerTyped(client.communicator, TestDataSchema, (req) =>
create(TestDataSchema, {
index: req.index * 2,
message: `echo: ${req.message}`,
}),
);
};
await withServer(server, () => runPythonClient("tls", TLS_TCP_PORT, "requests", new Set(["3", "4"]), CERT_PATH));
}
async function runWssSendPush(): Promise<void> {
let resolveReceived: ((value: boolean) => void) | null = null;
const received = new Promise<boolean>((resolve) => {
resolveReceived = resolve;
});
const server = new WsServer(
HOST,
WSS_PORT,
WS_PATH,
(ws) => new WsClient(ws, 0, 0, parserMap()),
serverTlsOptions(),
);
server.onClientConnected = (client) => {
addListenerTyped(client.communicator, TestDataSchema, (data) => {
console.log(`SERVER_RECEIVED index=${data.index} message=${data.message}`);
const valid = data.index === 101 && data.message === "fire from python client";
if (resolveReceived !== null) {
resolveReceived(valid);
resolveReceived = null;
}
if (valid) {
void client.communicator.send(
create(TestDataSchema, {
index: 200,
message: "push from typescript server",
}),
);
}
});
};
await withServer(server, async () => {
await runPythonClient("wss", WSS_PORT, "send-push", new Set(["1", "2"]), CERT_PATH);
const ok = await withTimeout(received, SERVER_OBSERVATION_MS, "WSS send-push server did not receive expected data");
if (!ok) {
throw new Error("WSS send-push server received unexpected data");
}
});
}
async function runWssRequests(): Promise<void> {
const server = new WsServer(
HOST,
WSS_PORT,
WS_PATH,
(ws) => new WsClient(ws, 0, 0, parserMap()),
serverTlsOptions(),
);
server.onClientConnected = (client) => {
addRequestListenerTyped(client.communicator, TestDataSchema, (req) =>
create(TestDataSchema, {
index: req.index * 2,
message: `echo: ${req.message}`,
}),
);
};
await withServer(server, () => runPythonClient("wss", WSS_PORT, "requests", new Set(["3", "4"]), CERT_PATH));
}
async function withServer(
server: { start: () => Promise<void>; stop: () => Promise<void> },
body: () => Promise<void>,
@ -156,10 +289,11 @@ async function withServer(
}
async function runPythonClient(
mode: "tcp" | "ws",
mode: Mode,
port: number,
phase: "send-push" | "requests",
phase: Phase,
expected: Set<string>,
cert?: string,
): Promise<void> {
await runClientProcess(
"python3",
@ -169,6 +303,7 @@ async function runPythonClient(
`--mode=${mode}`,
`--port=${port}`,
`--phase=${phase}`,
...(cert === undefined ? [] : [`--cert=${cert}`]),
],
pythonDir,
`python-client ${mode}/${phase}`,
@ -259,6 +394,13 @@ function parserMap() {
return new Map([[TestDataSchema.typeName, parserFromSchema(TestDataSchema)]]);
}
function serverTlsOptions(): { cert: Buffer; key: Buffer } {
return {
cert: fs.readFileSync(CERT_PATH),
key: fs.readFileSync(KEY_PATH),
};
}
async function withTimeout<T>(promise: Promise<T>, timeoutMs: number, label: string): Promise<T> {
let timer: ReturnType<typeof setTimeout> | undefined;
try {