From 1419895ed634e1307d46a0d27003e27adc4af42f Mon Sep 17 00:00:00 2001 From: toki Date: Sun, 26 Apr 2026 19:24:15 +0900 Subject: [PATCH] feat: add TLS WebSocket crosstest support across all languages Co-Authored-By: Claude Sonnet 4.6 --- .claude/settings.json | 4 +- agent-task/tls_crosstest/CODE_REVIEW.md | 135 ++++ agent-task/tls_crosstest/PLAN.md | 676 ++++++++++++++++++ agent-task/tls_crosstest/code_review_0.log | 227 ++++++ agent-task/tls_crosstest/code_review_1.log | 135 ++++ agent-task/tls_crosstest/complete.log | 27 + agent-task/tls_crosstest/plan_0.log | 669 +++++++++++++++++ agent-task/tls_crosstest/plan_1.log | 676 ++++++++++++++++++ dart/crosstest/dart_kotlin.dart | 132 +++- dart/crosstest/dart_python.dart | 129 +++- dart/crosstest/dart_typescript.dart | 133 +++- dart/crosstest/kotlin_dart_client.dart | 27 +- dart/crosstest/python_dart_client.dart | 27 +- dart/crosstest/typescript_dart_client.dart | 27 +- go/crosstest/go_kotlin.go | 219 +++++- go/crosstest/go_python.go | 224 +++++- go/crosstest/go_typescript.go | 225 +++++- go/crosstest/kotlin_go_client/main.go | 59 +- go/crosstest/python_go_client/main.go | 59 +- go/crosstest/typescript_go_client/main.go | 59 +- kotlin/crosstest/dart_kotlin_client.kt | 32 +- .../tokilabs/toki_socket/crosstest/Main.kt | 32 +- kotlin/crosstest/kotlin_dart.kt | 130 +++- kotlin/crosstest/kotlin_go.kt | 130 +++- kotlin/crosstest/kotlin_python.kt | 130 +++- kotlin/crosstest/kotlin_typescript.kt | 106 ++- kotlin/crosstest/python_kotlin_client.kt | 32 +- kotlin/crosstest/typescript_kotlin_client.kt | 32 +- python/crosstest/dart_python_client.py | 34 +- python/crosstest/go_python_client.py | 35 +- python/crosstest/kotlin_python_client.py | 34 +- python/crosstest/python_dart.py | 153 +++- python/crosstest/python_go.py | 154 +++- python/crosstest/python_kotlin.py | 149 +++- python/crosstest/python_typescript.py | 153 +++- python/crosstest/typescript_python_client.py | 34 +- .../crosstest/dart_typescript_client.ts | 44 +- typescript/crosstest/go_typescript_client.ts | 44 +- .../crosstest/kotlin_typescript_client.ts | 44 +- .../crosstest/python_typescript_client.ts | 44 +- typescript/crosstest/typescript_dart.ts | 146 +++- typescript/crosstest/typescript_go.ts | 146 +++- typescript/crosstest/typescript_kotlin.ts | 85 ++- typescript/crosstest/typescript_python.ts | 146 +++- 44 files changed, 5747 insertions(+), 191 deletions(-) create mode 100644 agent-task/tls_crosstest/CODE_REVIEW.md create mode 100644 agent-task/tls_crosstest/PLAN.md create mode 100644 agent-task/tls_crosstest/code_review_0.log create mode 100644 agent-task/tls_crosstest/code_review_1.log create mode 100644 agent-task/tls_crosstest/complete.log create mode 100644 agent-task/tls_crosstest/plan_0.log create mode 100644 agent-task/tls_crosstest/plan_1.log diff --git a/.claude/settings.json b/.claude/settings.json index 68317d8..9f2aa86 100644 --- a/.claude/settings.json +++ b/.claude/settings.json @@ -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", diff --git a/agent-task/tls_crosstest/CODE_REVIEW.md b/agent-task/tls_crosstest/CODE_REVIEW.md new file mode 100644 index 0000000..effe3ba --- /dev/null +++ b/agent-task/tls_crosstest/CODE_REVIEW.md @@ -0,0 +1,135 @@ + + +# 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 +``` diff --git a/agent-task/tls_crosstest/PLAN.md b/agent-task/tls_crosstest/PLAN.md new file mode 100644 index 0000000..a8e3232 --- /dev/null +++ b/agent-task/tls_crosstest/PLAN.md @@ -0,0 +1,676 @@ + + +# 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=` 로 클라이언트에 전달한다. + +--- + +## 포트 배정표 + +| 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=` 전달 +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) +``` diff --git a/agent-task/tls_crosstest/code_review_0.log b/agent-task/tls_crosstest/code_review_0.log new file mode 100644 index 0000000..ab1e1f0 --- /dev/null +++ b/agent-task/tls_crosstest/code_review_0.log @@ -0,0 +1,227 @@ + + +# 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 +``` diff --git a/agent-task/tls_crosstest/code_review_1.log b/agent-task/tls_crosstest/code_review_1.log new file mode 100644 index 0000000..effe3ba --- /dev/null +++ b/agent-task/tls_crosstest/code_review_1.log @@ -0,0 +1,135 @@ + + +# 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 +``` diff --git a/agent-task/tls_crosstest/complete.log b/agent-task/tls_crosstest/complete.log new file mode 100644 index 0000000..769fed4 --- /dev/null +++ b/agent-task/tls_crosstest/complete.log @@ -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 diff --git a/agent-task/tls_crosstest/plan_0.log b/agent-task/tls_crosstest/plan_0.log new file mode 100644 index 0000000..67796dd --- /dev/null +++ b/agent-task/tls_crosstest/plan_0.log @@ -0,0 +1,669 @@ + + +# 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=` 로 클라이언트에 전달한다. + +--- + +## 포트 배정표 + +| 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=` 전달: + - `dart_kotlin`: Kotlin 클라이언트 `./gradlew run` 호출 시 `-PappArgs="['--mode=tls','--port=29594','--cert=...']"` 형식 + - `dart_python`: Python subprocess 호출 시 `--mode tls --port 29698 --cert ` 추가 + - `dart_typescript`: `node --import tsx` 호출 시 `--mode=tls --port=29794 --cert=` 추가 +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) +``` diff --git a/agent-task/tls_crosstest/plan_1.log b/agent-task/tls_crosstest/plan_1.log new file mode 100644 index 0000000..a8e3232 --- /dev/null +++ b/agent-task/tls_crosstest/plan_1.log @@ -0,0 +1,676 @@ + + +# 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=` 로 클라이언트에 전달한다. + +--- + +## 포트 배정표 + +| 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=` 전달 +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) +``` diff --git a/dart/crosstest/dart_kotlin.dart b/dart/crosstest/dart_kotlin.dart index f83e49e..d02eb74 100644 --- a/dart/crosstest/dart_kotlin.dart +++ b/dart/crosstest/dart_kotlin.dart @@ -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 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 _withServer(Future Function() start, } } +Future _runTls() async { + final ctx = SecurityContext() + ..useCertificateChain(_certFile) + ..usePrivateKey(_keyFile); + await _runTlsTcpSendPush(ctx); + await Future.delayed(const Duration(milliseconds: 150)); + await _runTlsTcpRequests(ctx); + await Future.delayed(const Duration(milliseconds: 150)); + await _runWssSendPush(ctx); + await Future.delayed(const Duration(milliseconds: 150)); + await _runWssRequests(ctx); +} + +Future _runTlsTcpSendPush(SecurityContext ctx) async { + final received = Completer(); + final server = _DartTlsTcpServer(_tlsTcpPort, ctx, (client) { + client.addListener((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 _runTlsTcpRequests(SecurityContext ctx) async { + final server = _DartTlsTcpServer(_tlsTcpPort, ctx, (client) { + client.addRequestListener((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 _runWssSendPush(SecurityContext ctx) async { + final received = Completer(); + final server = _DartWssServer(_wssPort, ctx, (client) { + client.addListener((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 _runWssRequests(SecurityContext ctx) async { + final server = _DartWssServer(_wssPort, ctx, (client) { + client.addRequestListener((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 _runKotlinClient( - String mode, int port, String phase, Set expectedScenarios) async { + String mode, int port, String phase, Set 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, ); diff --git a/dart/crosstest/dart_python.dart b/dart/crosstest/dart_python.dart index 49f127e..5ebda0a 100644 --- a/dart/crosstest/dart_python.dart +++ b/dart/crosstest/dart_python.dart @@ -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 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 _withServer(Future Function() start, } } +Future _runTls() async { + final ctx = SecurityContext() + ..useCertificateChain(_certFile) + ..usePrivateKey(_keyFile); + await _runTlsTcpSendPush(ctx); + await Future.delayed(const Duration(milliseconds: 150)); + await _runTlsTcpRequests(ctx); + await Future.delayed(const Duration(milliseconds: 150)); + await _runWssSendPush(ctx); + await Future.delayed(const Duration(milliseconds: 150)); + await _runWssRequests(ctx); +} + +Future _runTlsTcpSendPush(SecurityContext ctx) async { + final received = Completer(); + final server = _DartTlsTcpServer(_tlsTcpPort, ctx, (client) { + client.addListener((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 _runTlsTcpRequests(SecurityContext ctx) async { + final server = _DartTlsTcpServer(_tlsTcpPort, ctx, (client) { + client.addRequestListener((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 _runWssSendPush(SecurityContext ctx) async { + final received = Completer(); + final server = _DartWssServer(_wssPort, ctx, (client) { + client.addListener((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 _runWssRequests(SecurityContext ctx) async { + final server = _DartWssServer(_wssPort, ctx, (client) { + client.addRequestListener((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 _runPythonClient( - String mode, int port, String phase, Set expectedScenarios) async { + String mode, int port, String phase, Set expectedScenarios, + {String? certFile}) async { final process = await Process.start( 'python3', [ @@ -176,6 +302,7 @@ Future _runPythonClient( '--mode=$mode', '--port=$port', '--phase=$phase', + if (certFile != null) '--cert=$certFile', ], workingDirectory: _pythonDir, ); diff --git a/dart/crosstest/dart_typescript.dart b/dart/crosstest/dart_typescript.dart index 6aa17e1..e0bc7f4 100644 --- a/dart/crosstest/dart_typescript.dart +++ b/dart/crosstest/dart_typescript.dart @@ -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 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 _withServer(Future Function() start, } } +Future _runTls() async { + final ctx = SecurityContext() + ..useCertificateChain(_certFile) + ..usePrivateKey(_keyFile); + await _runTlsTcpSendPush(ctx); + await Future.delayed(const Duration(milliseconds: 150)); + await _runTlsTcpRequests(ctx); + await Future.delayed(const Duration(milliseconds: 150)); + await _runWssSendPush(ctx); + await Future.delayed(const Duration(milliseconds: 150)); + await _runWssRequests(ctx); +} + +Future _runTlsTcpSendPush(SecurityContext ctx) async { + final received = Completer(); + final server = _DartTlsTcpServer(_tlsTcpPort, ctx, (client) { + client.addListener((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 _runTlsTcpRequests(SecurityContext ctx) async { + final server = _DartTlsTcpServer(_tlsTcpPort, ctx, (client) { + client.addRequestListener((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 _runWssSendPush(SecurityContext ctx) async { + final received = Completer(); + final server = _DartWssServer(_wssPort, ctx, (client) { + client.addListener((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 _runWssRequests(SecurityContext ctx) async { + final server = _DartWssServer(_wssPort, ctx, (client) { + client.addRequestListener((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 _runTypescriptClient( - String mode, - int port, - String phase, - Set expectedScenarios, -) async { + String mode, int port, String phase, Set expectedScenarios, + {String? certFile}) async { final process = await Process.start( 'npx', [ @@ -181,6 +303,7 @@ Future _runTypescriptClient( '--mode=$mode', '--port=$port', '--phase=$phase', + if (certFile != null) '--cert=$certFile', ], workingDirectory: _typescriptDir, ); diff --git a/dart/crosstest/kotlin_dart_client.dart b/dart/crosstest/kotlin_dart_client.dart index 74ea9c1..ebe02d1 100644 --- a/dart/crosstest/kotlin_dart_client.dart +++ b/dart/crosstest/kotlin_dart_client.dart @@ -35,6 +35,7 @@ Future main(List 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 main(List 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 main(List 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 _runSendPush(Communicator client) async { final pushCompleter = Completer(); client.addListener((data) { diff --git a/dart/crosstest/python_dart_client.dart b/dart/crosstest/python_dart_client.dart index 3fbb85a..45eab23 100644 --- a/dart/crosstest/python_dart_client.dart +++ b/dart/crosstest/python_dart_client.dart @@ -35,6 +35,7 @@ Future main(List 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 main(List 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 main(List 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 _runSendPush(Communicator client) async { final pushCompleter = Completer(); client.addListener((data) { diff --git a/dart/crosstest/typescript_dart_client.dart b/dart/crosstest/typescript_dart_client.dart index b71a753..63b933c 100644 --- a/dart/crosstest/typescript_dart_client.dart +++ b/dart/crosstest/typescript_dart_client.dart @@ -35,6 +35,7 @@ Future main(List 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 main(List 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 main(List 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 _runSendPush(Communicator client) async { final pushCompleter = Completer(); client.addListener((data) { diff --git a/go/crosstest/go_kotlin.go b/go/crosstest/go_kotlin.go index ab77f46..4b37f6f 100644 --- a/go/crosstest/go_kotlin.go +++ b/go/crosstest/go_kotlin.go @@ -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) diff --git a/go/crosstest/go_python.go b/go/crosstest/go_python.go index 0f4b551..7cb6deb 100644 --- a/go/crosstest/go_python.go +++ b/go/crosstest/go_python.go @@ -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) diff --git a/go/crosstest/go_typescript.go b/go/crosstest/go_typescript.go index 8214ade..88fe31b 100644 --- a/go/crosstest/go_typescript.go +++ b/go/crosstest/go_typescript.go @@ -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) diff --git a/go/crosstest/kotlin_go_client/main.go b/go/crosstest/kotlin_go_client/main.go index 67c6f2a..097a15b 100644 --- a/go/crosstest/kotlin_go_client/main.go +++ b/go/crosstest/kotlin_go_client/main.go @@ -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) } diff --git a/go/crosstest/python_go_client/main.go b/go/crosstest/python_go_client/main.go index a49519b..0ffa268 100644 --- a/go/crosstest/python_go_client/main.go +++ b/go/crosstest/python_go_client/main.go @@ -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) } diff --git a/go/crosstest/typescript_go_client/main.go b/go/crosstest/typescript_go_client/main.go index 16775c9..7f015be 100644 --- a/go/crosstest/typescript_go_client/main.go +++ b/go/crosstest/typescript_go_client/main.go @@ -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) } diff --git a/kotlin/crosstest/dart_kotlin_client.kt b/kotlin/crosstest/dart_kotlin_client.kt index 9ec4eb0..78a0dd7 100644 --- a/kotlin/crosstest/dart_kotlin_client.kt +++ b/kotlin/crosstest/dart_kotlin_client.kt @@ -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) = 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()}") @@ -73,7 +76,7 @@ fun main(args: Array) = 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) = 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() addListenerTyped(client.communicator) { diff --git a/kotlin/crosstest/go_kotlin_client/src/main/kotlin/com/tokilabs/toki_socket/crosstest/Main.kt b/kotlin/crosstest/go_kotlin_client/src/main/kotlin/com/tokilabs/toki_socket/crosstest/Main.kt index ee8eda1..0ad49b9 100644 --- a/kotlin/crosstest/go_kotlin_client/src/main/kotlin/com/tokilabs/toki_socket/crosstest/Main.kt +++ b/kotlin/crosstest/go_kotlin_client/src/main/kotlin/com/tokilabs/toki_socket/crosstest/Main.kt @@ -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) = 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()}") @@ -71,7 +74,7 @@ fun main(args: Array) = 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) = 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() addListenerTyped(client.communicator) { diff --git a/kotlin/crosstest/kotlin_dart.kt b/kotlin/crosstest/kotlin_dart.kt index d6105ad..1481b02 100644 --- a/kotlin/crosstest/kotlin_dart.kt +++ b/kotlin/crosstest/kotlin_dart.kt @@ -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() +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() + server.onClientConnected = { client -> + addListenerTyped(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(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() + val server = TcpServer(HOST, TLS_TCP_PORT, serverCtx) { socket -> + TcpClient.fromSocket(socket, 0, 0, parserMap()) + } server.onClientConnected = { client -> addListenerTyped(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(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, + 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() 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) { diff --git a/kotlin/crosstest/kotlin_go.kt b/kotlin/crosstest/kotlin_go.kt index 0155cfe..d6d136c 100644 --- a/kotlin/crosstest/kotlin_go.kt +++ b/kotlin/crosstest/kotlin_go.kt @@ -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() +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() + server.onClientConnected = { client -> + addListenerTyped(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(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() + val server = TcpServer(HOST, TLS_TCP_PORT, serverCtx) { socket -> + TcpClient.fromSocket(socket, 0, 0, parserMap()) + } server.onClientConnected = { client -> addListenerTyped(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(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, + 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() 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) { diff --git a/kotlin/crosstest/kotlin_python.kt b/kotlin/crosstest/kotlin_python.kt index 5987e79..21a819d 100644 --- a/kotlin/crosstest/kotlin_python.kt +++ b/kotlin/crosstest/kotlin_python.kt @@ -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() +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() + server.onClientConnected = { client -> + addListenerTyped(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(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() + val server = TcpServer(HOST, TLS_TCP_PORT, serverCtx) { socket -> + TcpClient.fromSocket(socket, 0, 0, parserMap()) + } server.onClientConnected = { client -> addListenerTyped(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(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, + 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() 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) { diff --git a/kotlin/crosstest/kotlin_typescript.kt b/kotlin/crosstest/kotlin_typescript.kt index 768bfc1..9c33b68 100644 --- a/kotlin/crosstest/kotlin_typescript.kt +++ b/kotlin/crosstest/kotlin_typescript.kt @@ -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() + val server = TcpServer(HOST, TLS_TCP_PORT, serverCtx) { socket -> + TcpClient.fromSocket(socket, 0, 0, parserMap()) + } + server.onClientConnected = { client -> + addListenerTyped(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(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, + 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() 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) { diff --git a/kotlin/crosstest/python_kotlin_client.kt b/kotlin/crosstest/python_kotlin_client.kt index 139243d..62c9e43 100644 --- a/kotlin/crosstest/python_kotlin_client.kt +++ b/kotlin/crosstest/python_kotlin_client.kt @@ -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) = 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()}") @@ -59,7 +62,7 @@ fun main(args: Array) = 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) = 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() addListenerTyped(client.communicator) { diff --git a/kotlin/crosstest/typescript_kotlin_client.kt b/kotlin/crosstest/typescript_kotlin_client.kt index de09319..f605292 100644 --- a/kotlin/crosstest/typescript_kotlin_client.kt +++ b/kotlin/crosstest/typescript_kotlin_client.kt @@ -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) = 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()}") @@ -60,7 +63,7 @@ fun main(args: Array) = 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) = 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() addListenerTyped(client.communicator) { diff --git a/python/crosstest/dart_python_client.py b/python/crosstest/dart_python_client.py index 9b9d96c..8745fa0 100644 --- a/python/crosstest/dart_python_client.py +++ b/python/crosstest/dart_python_client.py @@ -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}") diff --git a/python/crosstest/go_python_client.py b/python/crosstest/go_python_client.py index 14058a8..292208f 100644 --- a/python/crosstest/go_python_client.py +++ b/python/crosstest/go_python_client.py @@ -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()) - diff --git a/python/crosstest/kotlin_python_client.py b/python/crosstest/kotlin_python_client.py index 21ec99c..88d56f8 100644 --- a/python/crosstest/kotlin_python_client.py +++ b/python/crosstest/kotlin_python_client.py @@ -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}") diff --git a/python/crosstest/python_dart.py b/python/crosstest/python_dart.py index be74a96..30ba68b 100644 --- a/python/crosstest/python_dart.py +++ b/python/crosstest/python_dart.py @@ -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, diff --git a/python/crosstest/python_go.py b/python/crosstest/python_go.py index 4c15107..d305c43 100644 --- a/python/crosstest/python_go.py +++ b/python/crosstest/python_go.py @@ -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()) - diff --git a/python/crosstest/python_kotlin.py b/python/crosstest/python_kotlin.py index 20801d5..628bca2 100644 --- a/python/crosstest/python_kotlin.py +++ b/python/crosstest/python_kotlin.py @@ -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, diff --git a/python/crosstest/python_typescript.py b/python/crosstest/python_typescript.py index 41122ca..93c8be5 100644 --- a/python/crosstest/python_typescript.py +++ b/python/crosstest/python_typescript.py @@ -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, diff --git a/python/crosstest/typescript_python_client.py b/python/crosstest/typescript_python_client.py index c83a120..f18ebcd 100644 --- a/python/crosstest/typescript_python_client.py +++ b/python/crosstest/typescript_python_client.py @@ -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}") diff --git a/typescript/crosstest/dart_typescript_client.ts b/typescript/crosstest/dart_typescript_client.ts index 86e11cf..240c410 100644 --- a/typescript/crosstest/dart_typescript_client.ts +++ b/typescript/crosstest/dart_typescript_client.ts @@ -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 { 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 { } } -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 { +async function dialWithRetry(mode: Mode, port: number, cert?: string): Promise { 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 { +async function dial(mode: Mode, port: number, cert?: string): Promise { 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 { diff --git a/typescript/crosstest/go_typescript_client.ts b/typescript/crosstest/go_typescript_client.ts index 66a715f..9e3e682 100644 --- a/typescript/crosstest/go_typescript_client.ts +++ b/typescript/crosstest/go_typescript_client.ts @@ -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 { 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 { } } -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 { +async function dialWithRetry(mode: Mode, port: number, cert?: string): Promise { 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 { +async function dial(mode: Mode, port: number, cert?: string): Promise { 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 { diff --git a/typescript/crosstest/kotlin_typescript_client.ts b/typescript/crosstest/kotlin_typescript_client.ts index 2489f4a..4bdde89 100644 --- a/typescript/crosstest/kotlin_typescript_client.ts +++ b/typescript/crosstest/kotlin_typescript_client.ts @@ -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 { 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 { } } -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 { +async function dialWithRetry(mode: Mode, port: number, cert?: string): Promise { 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 { +async function dial(mode: Mode, port: number, cert?: string): Promise { 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 { diff --git a/typescript/crosstest/python_typescript_client.ts b/typescript/crosstest/python_typescript_client.ts index f9a2fac..9491867 100644 --- a/typescript/crosstest/python_typescript_client.ts +++ b/typescript/crosstest/python_typescript_client.ts @@ -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 { 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 { } } -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 { +async function dialWithRetry(mode: Mode, port: number, cert?: string): Promise { 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 { +async function dial(mode: Mode, port: number, cert?: string): Promise { 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 { diff --git a/typescript/crosstest/typescript_dart.ts b/typescript/crosstest/typescript_dart.ts index 4cb8d6e..4ca20ea 100644 --- a/typescript/crosstest/typescript_dart.ts +++ b/typescript/crosstest/typescript_dart.ts @@ -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 { 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 { await runWsRequests(); } +async function runTls(): Promise { + await runTlsTcpSendPush(); + await sleep(150); + await runTlsTcpRequests(); + await sleep(150); + await runWssSendPush(); + await sleep(150); + await runWssRequests(); +} + async function runTcpSendPush(): Promise { let resolveReceived: ((value: boolean) => void) | null = null; const received = new Promise((resolve) => { @@ -143,6 +162,120 @@ async function runWsRequests(): Promise { await withServer(server, () => runDartClient("ws", WS_PORT, "requests", new Set(["3", "4"]))); } +async function runTlsTcpSendPush(): Promise { + let resolveReceived: ((value: boolean) => void) | null = null; + const received = new Promise((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 { + 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 { + let resolveReceived: ((value: boolean) => void) | null = null; + const received = new Promise((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 { + 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; stop: () => Promise }, body: () => Promise, @@ -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, + cert?: string, ): Promise { 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(promise: Promise, timeoutMs: number, label: string): Promise { let timer: ReturnType | undefined; try { diff --git a/typescript/crosstest/typescript_go.ts b/typescript/crosstest/typescript_go.ts index 7c8559f..8bdd5b3 100644 --- a/typescript/crosstest/typescript_go.ts +++ b/typescript/crosstest/typescript_go.ts @@ -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 { 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 { await runWsRequests(); } +async function runTls(): Promise { + await runTlsTcpSendPush(); + await sleep(150); + await runTlsTcpRequests(); + await sleep(150); + await runWssSendPush(); + await sleep(150); + await runWssRequests(); +} + async function runTcpSendPush(): Promise { let resolveReceived: ((value: boolean) => void) | null = null; const received = new Promise((resolve) => { @@ -143,6 +162,120 @@ async function runWsRequests(): Promise { await withServer(server, () => runGoClient("ws", WS_PORT, "requests", new Set(["3", "4"]))); } +async function runTlsTcpSendPush(): Promise { + let resolveReceived: ((value: boolean) => void) | null = null; + const received = new Promise((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 { + 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 { + let resolveReceived: ((value: boolean) => void) | null = null; + const received = new Promise((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 { + 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; stop: () => Promise }, body: () => Promise, @@ -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, + cert?: string, ): Promise { 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(promise: Promise, timeoutMs: number, label: string): Promise { let timer: ReturnType | undefined; try { diff --git a/typescript/crosstest/typescript_kotlin.ts b/typescript/crosstest/typescript_kotlin.ts index 5e6de1e..237d1ec 100644 --- a/typescript/crosstest/typescript_kotlin.ts +++ b/typescript/crosstest/typescript_kotlin.ts @@ -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 { 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 { await runWsRequests(); } +async function runTls(): Promise { + await runTlsTcpSendPush(); + await sleep(150); + await runTlsTcpRequests(); +} + async function runTcpSendPush(): Promise { let resolveReceived: ((value: boolean) => void) | null = null; const received = new Promise((resolve) => { @@ -143,6 +157,62 @@ async function runWsRequests(): Promise { await withServer(server, () => runKotlinClient("ws", WS_PORT, "requests", new Set(["3", "4"]))); } +async function runTlsTcpSendPush(): Promise { + let resolveReceived: ((value: boolean) => void) | null = null; + const received = new Promise((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 { + 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; stop: () => Promise }, body: () => Promise, @@ -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, + cert?: string, ): Promise { + 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(promise: Promise, timeoutMs: number, label: string): Promise { let timer: ReturnType | undefined; try { diff --git a/typescript/crosstest/typescript_python.ts b/typescript/crosstest/typescript_python.ts index c829b3d..fc8bae5 100644 --- a/typescript/crosstest/typescript_python.ts +++ b/typescript/crosstest/typescript_python.ts @@ -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 { 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 { await runWsRequests(); } +async function runTls(): Promise { + await runTlsTcpSendPush(); + await sleep(150); + await runTlsTcpRequests(); + await sleep(150); + await runWssSendPush(); + await sleep(150); + await runWssRequests(); +} + async function runTcpSendPush(): Promise { let resolveReceived: ((value: boolean) => void) | null = null; const received = new Promise((resolve) => { @@ -143,6 +162,120 @@ async function runWsRequests(): Promise { await withServer(server, () => runPythonClient("ws", WS_PORT, "requests", new Set(["3", "4"]))); } +async function runTlsTcpSendPush(): Promise { + let resolveReceived: ((value: boolean) => void) | null = null; + const received = new Promise((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 { + 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 { + let resolveReceived: ((value: boolean) => void) | null = null; + const received = new Promise((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 { + 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; stop: () => Promise }, body: () => Promise, @@ -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, + cert?: string, ): Promise { 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(promise: Promise, timeoutMs: number, label: string): Promise { let timer: ReturnType | undefined; try {