This commit is contained in:
zoe
2026-09-11 12:18:55 +08:00
parent 7cd92bc2d1
commit af0303d582
102 changed files with 377 additions and 629 deletions
+13 -13
View File
@@ -1,18 +1,18 @@
# Copy values into the service environment; this file is not loaded automatically.
# Development default: skip login, loopback management only.
# Set true to require administrator login (required for remote deployment).
AGENTDB_AUTH_ENABLED=false
# Generate a unique key with: agentdb generate-key
AGENTDB_MASTER_KEY=
AGENTDB_ADMIN_USER=admin
AGENTDB_ADMIN_PASSWORD=
AGENTDB_DATA=data/agentdb.sqlite
AGENTDB_HTTP_ADDR=127.0.0.1:8080
AGENTDB_SECURE_COOKIE=false
SERVICEPORT_AUTH_ENABLED=false
# Generate a unique key with: serviceport generate-key
SERVICEPORT_MASTER_KEY=
SERVICEPORT_ADMIN_USER=admin
SERVICEPORT_ADMIN_PASSWORD=
SERVICEPORT_DATA=data/serviceport.sqlite
SERVICEPORT_HTTP_ADDR=127.0.0.1:8080
SERVICEPORT_SECURE_COOKIE=false
# Database gateways require a certificate and key. SSH does not use these.
AGENTDB_TLS_CERT=
AGENTDB_TLS_KEY=
SERVICEPORT_TLS_CERT=
SERVICEPORT_TLS_KEY=
# Windows without AGENTDB_MASTER_KEY: automatic local desktop startup.
# Data/key/TLS default to %APPDATA%\AgentDB; DPAPI key is bound to this Windows user.
# AGENTDB_NO_BROWSER=1
# Windows without SERVICEPORT_MASTER_KEY: automatic local desktop startup.
# Data/key/TLS default to %APPDATA%\serviceport; DPAPI key is bound to this Windows user.
# SERVICEPORT_NO_BROWSER=1
+3 -3
View File
@@ -1,4 +1,4 @@
# AgentDB collaboration
# serviceport collaboration
## Work ownership
@@ -13,12 +13,12 @@
- Use Go 1.25.0 (`GOTOOLCHAIN=go1.25.0`); newer installed toolchains may not compile the pinned Vitess dependency.
- Backend: `go test ./... -count=1 -timeout=120s` and `go vet ./...`.
- Frontend: `npm.cmd --prefix web run build`. Run the Playwright suite in `web` against a freshly built executable using `AGENTDB_E2E_BINARY`.
- Frontend: `npm.cmd --prefix web run build`. Run the Playwright suite in `web` against a freshly built executable using `SERVICEPORT_E2E_BINARY`.
- Do not build embedded Go assets while Vite is replacing `web/dist`. Restore tracked `web/dist/.gitkeep` after a frontend build if needed.
- Describe whether protocol tests used a real server or a fixture; do not present fixture coverage as live-server validation.
## Local data
- Preserve `data/agentdb.sqlite`, `data/audit`, TLS material, and `data/master-key.dpapi`.
- Preserve `data/serviceport.sqlite`, `data/audit`, TLS material, and `data/master-key.dpapi`.
- Stop the owned local service and back up the complete `data` directory before deploying a migration. Do not modify real target data for smoke tests.
- Use isolated test databases and ports. Do not start subagents unless the user has enabled them for the active work.
+6 -6
View File
@@ -15,13 +15,13 @@ ARG VERSION
ARG COMMIT=unknown
ARG BUILD_TIME
RUN version="${VERSION:-$(cat VERSION)}" && build_time="${BUILD_TIME:-$(date -u '+%Y-%m-%dT%H:%M:%SZ')}" && \
CGO_ENABLED=0 go build -trimpath -ldflags "-s -w -X agentdb/internal/buildinfo.Version=$version -X agentdb/internal/buildinfo.Commit=$COMMIT -X agentdb/internal/buildinfo.BuildTime=$build_time" -o /agentdb ./cmd/agentdb
CGO_ENABLED=0 go build -trimpath -ldflags "-s -w -X serviceport/internal/buildinfo.Version=$version -X serviceport/internal/buildinfo.Commit=$COMMIT -X serviceport/internal/buildinfo.BuildTime=$build_time" -o /serviceport ./cmd/serviceport
FROM alpine:3.22.1
RUN apk add --no-cache ca-certificates && adduser -D -u 10001 agentdb && mkdir /data && chown agentdb /data
USER agentdb
RUN apk add --no-cache ca-certificates && adduser -D -u 10001 serviceport && mkdir /data && chown serviceport /data
USER serviceport
WORKDIR /app
COPY --from=backend /agentdb /app/agentdb
ENV AGENTDB_HTTP_ADDR=0.0.0.0:8080 AGENTDB_DATA=/data/agentdb.sqlite AGENTDB_AUTH_ENABLED=true
COPY --from=backend /serviceport /app/serviceport
ENV SERVICEPORT_HTTP_ADDR=0.0.0.0:8080 SERVICEPORT_DATA=/data/serviceport.sqlite SERVICEPORT_AUTH_ENABLED=true
EXPOSE 8080
ENTRYPOINT ["/app/agentdb"]
ENTRYPOINT ["/app/serviceport"]
+32 -32
View File
@@ -1,6 +1,6 @@
# AgentDB
# serviceport
开发时双击根目录 `start-dev.cmd`,打开 `http://127.0.0.1:8080`(默认免登录)。该入口固定使用项目根目录下的 `data/agentdb.sqlite`,与启动时的工作目录无关,重启和重新构建后继续保留目标服务和网关配置。数据库、`master-key.dpapi` 和 `tls` 均保存在 `data` 中,备份时关闭服务并复制整个目录;密钥绑定当前 Windows 用户。直接运行发布包仍使用 `%APPDATA%/AgentDB`。
开发时双击根目录 `start-dev.cmd`,打开 `http://127.0.0.1:8080`(默认免登录)。该入口固定使用项目根目录下的 `data/serviceport.sqlite`,与启动时的工作目录无关,重启和重新构建后继续保留目标服务和网关配置。数据库、`master-key.dpapi` 和 `tls` 均保存在 `data` 中,备份时关闭服务并复制整个目录;密钥绑定当前 Windows 用户。直接运行发布包仍使用 `%APPDATA%/serviceport`。
使用 Go 和 Vue 3 构建的 MySQL、MongoDB、PostgreSQL、Redis、RabbitMQ、SSH 连接代理管理后台。目标服务独立保存地址、凭据及直连 / SOCKS5 / SSH 转发出口;网关引用目标,独立管理监听端口、访问凭据及可选只读策略。一个目标可被多个同协议网关复用。
@@ -23,14 +23,14 @@
目标版本与实测版本请区分,见 [验证说明](docs/testing.md)。
「拖拽连接」可将已停止的网关拖到同协议目标并保存关联,也支持点击选择。命令审计与连接事件按 UTC 日期保存至 `data/audit/YYYY-MM-DD.sqlite`(每库分别包含 commands/events),保留 7 天。旧记录启动时自动迁移;配置库继续固定使用 `data/agentdb.sqlite`。两类日志均支持跨天筛选、分页和 CSV 全量导出;命令内容仍加密保存,导出只包含当前保留且已记录的内容。备份请停止服务并复制整个 `data` 目录。
「拖拽连接」可将已停止的网关拖到同协议目标并保存关联,也支持点击选择。命令审计与连接事件按 UTC 日期保存至 `data/audit/YYYY-MM-DD.sqlite`(每库分别包含 commands/events),保留 7 天。旧记录启动时自动迁移;配置库继续固定使用 `data/serviceport.sqlite`。两类日志均支持跨天筛选、分页和 CSV 全量导出;命令内容仍加密保存,导出只包含当前保留且已记录的内容。备份请停止服务并复制整个 `data` 目录。
## 构建
项目包含 `.drone.yml`,Drone CI 会自动执行 Go 全量测试、`go vet`、前端生产构建和无 CGO Linux amd64 编译。流水线不连接真实数据库,不读取 `data/`;真实服务矩阵仍使用 `scripts/test-matrix.ps1` 或 `scripts/test-matrix.sh`。
Drone tag 构建还会生成 Linux 发布包,并在配置 `ipfs_api` / `ipfs_token` Secret 后上传 IPFS,详见 [Drone/IPFS 发布说明](docs/ipfs.md)。
项目版本统一维护在根目录 `VERSION`。`agentdb.exe version` 或 `agentdb.exe --version --json` 可查看版本、Git 提交、构建时间与平台,管理界面顶部也可查看版本信息。构建/打包脚本自动注入元数据;直接 `go build` 仍使用 VERSION 的版本值,但提交和时间可能显示 `unknown`。
项目版本统一维护在根目录 `VERSION`。`serviceport.exe version` 或 `serviceport.exe --version --json` 可查看版本、Git 提交、构建时间与平台,管理界面顶部也可查看版本信息。构建/打包脚本自动注入元数据;直接 `go build` 仍使用 VERSION 的版本值,但提交和时间可能显示 `unknown`。
目标出口新增内置 `K8s port-forward`,不依赖 kubectl,支持 Pod、Service 和 Deployment。配置与权限见 [Kubernetes 说明](docs/kubernetes.md)。
@@ -48,29 +48,29 @@ Linux / macOS:
sh scripts/build.sh
```
脚本安装前端依赖、构建 Vue、执行 Go 测试并生成 `bin/agentdb`(Windows 为 `.exe`)。前端嵌入可执行文件,部署不需要 Node.js。
脚本安装前端依赖、构建 Vue、执行 Go 测试并生成 `bin/serviceport`(Windows 为 `.exe`)。前端嵌入可执行文件,部署不需要 Node.js。
## 启动
Windows 本地使用:解压发行包后双击 `agentdb.exe`,浏览器自动打开,**开发阶段默认免登录**,直接进入控制台。无需安装 Go/Node.js,无需设置环境变量。保持程序窗口运行,按 Ctrl+C 停止服务。
Windows 本地使用:解压发行包后双击 `serviceport.exe`,浏览器自动打开,**开发阶段默认免登录**,直接进入控制台。无需安装 Go/Node.js,无需设置环境变量。保持程序窗口运行,按 Ctrl+C 停止服务。
先在「目标服务」中添加 MySQL/MongoDB/SSH 目标,再在「网关服务」中选择目标并配置监听、网关账号和访问策略。目标被引用时不能删除;编辑目标前需要停止所有引用它的网关。旧版配置会在事务中自动拆分,保留网关 ID、凭据、SSH 指纹和启用状态,升级前先停止服务并备份数据目录。
右上角可切换浅色、深色、海蓝及跟随系统主题,选择保存在当前浏览器中。
开发模式仅允许回环地址管理访问,仍校验请求来源与 CSRF。设置 `AGENTDB_AUTH_ENABLED=true` 可启用管理员登录;Windows 本地模式会在尚无管理员时打开首次设置页面,已有管理员直接登录。远程服务器部署必须开启认证。
开发模式仅允许回环地址管理访问,仍校验请求来源与 CSRF。设置 `SERVICEPORT_AUTH_ENABLED=true` 可启用管理员登录;Windows 本地模式会在尚无管理员时打开首次设置页面,已有管理员直接登录。远程服务器部署必须开启认证。
未设置 `AGENTDB_MASTER_KEY` 时,Windows 自动进入本地模式:数据固定保存到 `%APPDATA%\AgentDB`,密钥使用当前 Windows 用户的 DPAPI 加密保存,自动创建有效期一年的 localhost TLS 证书。备份整个目录;该密钥绑定 Windows 用户,不能直接复制到其他电脑使用。高级部署及跨机器迁移应使用自行保管的环境变量主密钥。详见 [Windows 使用说明](packaging/windows/README.md)。
未设置 `SERVICEPORT_MASTER_KEY` 时,Windows 自动进入本地模式:数据固定保存到 `%APPDATA%\serviceport`,密钥使用当前 Windows 用户的 DPAPI 加密保存,自动创建有效期一年的 localhost TLS 证书。备份整个目录;该密钥绑定 Windows 用户,不能直接复制到其他电脑使用。高级部署及跨机器迁移应使用自行保管的环境变量主密钥。详见 [Windows 使用说明](packaging/windows/README.md)。
以下为服务器部署 / 手动管理密钥的 PowerShell 示例(设置主密钥后保留原有启动行为):
```powershell
$env:AGENTDB_AUTH_ENABLED = 'true'
$env:AGENTDB_MASTER_KEY = .\bin\agentdb.exe generate-key
$env:AGENTDB_ADMIN_USER = 'admin'
$env:SERVICEPORT_AUTH_ENABLED = 'true'
$env:SERVICEPORT_MASTER_KEY = .\bin\serviceport.exe generate-key
$env:SERVICEPORT_ADMIN_USER = 'admin'
$adminSecret = Read-Host '初始管理员密码(12–72 字节)' -AsSecureString
$env:AGENTDB_ADMIN_PASSWORD = [Net.NetworkCredential]::new('', $adminSecret).Password
.\bin\agentdb.exe
$env:SERVICEPORT_ADMIN_PASSWORD = [Net.NetworkCredential]::new('', $adminSecret).Password
.\bin\serviceport.exe
```
打开 `http://127.0.0.1:8080`。初始管理员只在首次运行创建,之后不会用环境变量覆盖密码。
@@ -79,15 +79,15 @@ $env:AGENTDB_ADMIN_PASSWORD = [Net.NetworkCredential]::new('', $adminSecret).Pas
| 环境变量 | 默认值 / 用途 |
| --- | --- |
| `AGENTDB_AUTH_ENABLED` | 默认 `false`,开发阶段免登录且仅限本机;`true` 启用管理员登录 |
| `AGENTDB_MASTER_KEY` | 服务器模式必填,32 字节随机密钥的 Base64;Windows 未设置时启用本地模式 |
| `AGENTDB_ADMIN_USER` | 手动密钥模式且启用认证时,首次启动必填 |
| `AGENTDB_ADMIN_PASSWORD` | 手动密钥模式且启用认证时,首次启动必填,12–72 字节 |
| `AGENTDB_DATA` | `data/agentdb.sqlite` |
| `AGENTDB_HTTP_ADDR` | `127.0.0.1:8080` |
| `AGENTDB_NO_BROWSER` | Windows 本地模式设为 `1` 可禁用自动打开浏览器 |
| `AGENTDB_SECURE_COOKIE` | 默认 `false`;管理界面经 HTTPS 访问时设为 `true` |
| `AGENTDB_TLS_CERT` / `AGENTDB_TLS_KEY` | 数据库网关客户端 TLS 证书和私钥路径 |
| `SERVICEPORT_AUTH_ENABLED` | 默认 `false`,开发阶段免登录且仅限本机;`true` 启用管理员登录 |
| `SERVICEPORT_MASTER_KEY` | 服务器模式必填,32 字节随机密钥的 Base64;Windows 未设置时启用本地模式 |
| `SERVICEPORT_ADMIN_USER` | 手动密钥模式且启用认证时,首次启动必填 |
| `SERVICEPORT_ADMIN_PASSWORD` | 手动密钥模式且启用认证时,首次启动必填,12–72 字节 |
| `SERVICEPORT_DATA` | `data/serviceport.sqlite` |
| `SERVICEPORT_HTTP_ADDR` | `127.0.0.1:8080` |
| `SERVICEPORT_NO_BROWSER` | Windows 本地模式设为 `1` 可禁用自动打开浏览器 |
| `SERVICEPORT_SECURE_COOKIE` | 默认 `false`;管理界面经 HTTPS 访问时设为 `true` |
| `SERVICEPORT_TLS_CERT` / `SERVICEPORT_TLS_KEY` | 数据库网关客户端 TLS 证书和私钥路径 |
`.env.example` 仅为示例,程序不自动加载 `.env`。远程管理使用 HTTPS 反向代理。
@@ -100,9 +100,9 @@ MySQL/MongoDB 网关允许普通连接;配置证书后同一端口也支持客
```powershell
$env:GOTOOLCHAIN = 'go1.25.0'
go run ./cmd/devcert
$env:AGENTDB_TLS_CERT = 'data/tls/cert.pem'
$env:AGENTDB_TLS_KEY = 'data/tls/key.pem'
.\bin\agentdb.exe
$env:SERVICEPORT_TLS_CERT = 'data/tls/cert.pem'
$env:SERVICEPORT_TLS_KEY = 'data/tls/key.pem'
.\bin\serviceport.exe
```
客户端将 `cert.pem` 作为 CA 验证;局域网正式使用时换成覆盖实际网关域名的受信任证书。目标数据库 TLS 独立配置,默认验证系统 CA 或配置中提供的 PEM CA,不提供跳过验证模式。
@@ -116,7 +116,7 @@ $env:AGENTDB_TLS_KEY = 'data/tls/key.pem'
目标、网关、SOCKS5 密码和 SSH 私钥口令可在重新验证管理员密码后查看,30 秒自动隐藏并记录审计。SSH 私钥不可下载。管理员登录密码仅保存 bcrypt 哈希,只能重置:
```powershell
.\bin\agentdb.exe reset-password
.\bin\serviceport.exe reset-password
```
重置命令使用同一个数据路径和主密钥,交互输入密码并使旧会话失效。
@@ -128,13 +128,13 @@ $env:GOTOOLCHAIN = 'go1.25.0'
go test ./...
go vet ./...
npm.cmd --prefix web run build
go build -o bin/agentdb.exe ./cmd/agentdb
go build -o bin/serviceport.exe ./cmd/serviceport
npm.cmd --prefix web test
```
前端开发:`npm --prefix web run dev`,Vite 将 `/api` 转发到本机 8080。浏览器测试启动隔离的临时数据服务,默认使用已安装的 Edge;其他平台可设置 `AGENTDB_BROWSER_CHANNEL=chromium` 并先运行 `npx playwright install chromium`。
前端开发:`npm --prefix web run dev`,Vite 将 `/api` 转发到本机 8080。浏览器测试启动隔离的临时数据服务,默认使用已安装的 Edge;其他平台可设置 `SERVICEPORT_BROWSER_CHANNEL=chromium` 并先运行 `npx playwright install chromium`。
Docker 构建:`docker build -t agentdb .`。容器默认以 UID 10001 运行,数据卷须可写;代理监听端口需要自行映射。数据库版本矩阵见 [验证说明](docs/testing.md)。
Docker 构建:`docker build -t serviceport .`。容器默认以 UID 10001 运行,数据卷须可写;代理监听端口需要自行映射。数据库版本矩阵见 [验证说明](docs/testing.md)。
## Windows 便携包
@@ -148,11 +148,11 @@ Docker 构建:`docker build -t agentdb .`。容器默认以 UID 10001 运行
powershell.exe -NoProfile -ExecutionPolicy Bypass -File scripts/package.ps1
```
安装 Go、Node.js/npm 后,可双击根目录 `package.cmd` 打包;窗口保留结果,命令行自动化可设置 `AGENTDB_PACKAGE_NO_PAUSE=1` 取消暂停。版本默认读取 `VERSION`。
安装 Go、Node.js/npm 后,可双击根目录 `package.cmd` 打包;窗口保留结果,命令行自动化可设置 `SERVICEPORT_PACKAGE_NO_PAUSE=1` 取消暂停。版本默认读取 `VERSION`。
默认使用快速 ZIP 压缩,并输出前端、Go 编译、验证、压缩各阶段耗时。需要更小的压缩包时运行 `.\package.cmd -Compression Optimal`,耗时会增加。前端按源码、配置、依赖锁文件、Node 版本及产物内容校验缓存,变化或缺失时自动重建;依赖变化自动执行 `npm ci`。可用 `.\package.cmd -RebuildWeb` 强制重建前端。缓存位于 `artifacts/build-cache`。Go 每次构建当前代码、注入最新版本信息,并自动复用编译缓存;发布包验证始终执行。
`start-dev.cmd` 用于直接启动:先停止当前目录中的旧服务并等待退出,将整个 `data` 备份到 `artifacts/backups/<时间>/`。源码目录每次调用 `scripts/build.ps1` 构建最新前后端并运行 Go 测试,成功后运行 `bin/agentdb.exe`,失败不会启动旧程序;发布包内没有构建脚本时直接运行根目录的 `agentdb.exe`。发布包无需安装 Go 或 Node.js。此入口固定使用入口所在目录的 `data/agentdb.sqlite`,默认免登录,访问 http://127.0.0.1:8080。
`start-dev.cmd` 用于直接启动:先停止当前目录中的旧服务并等待退出,将整个 `data` 备份到 `artifacts/backups/<时间>/`。源码目录每次调用 `scripts/build.ps1` 构建最新前后端并运行 Go 测试,成功后运行 `bin/serviceport.exe`,失败不会启动旧程序;发布包内没有构建脚本时直接运行根目录的 `serviceport.exe`。发布包无需安装 Go 或 Node.js。此入口固定使用入口所在目录的 `data/serviceport.sqlite`,默认免登录,访问 http://127.0.0.1:8080。
打包脚本按需构建前端并编译 Windows x64 程序,自动验证文件校验值、隔离启动、网页资源与管理员登录,通过后在 `artifacts/releases/<时间>/` 生成 ZIP 及 SHA-256 校验文件。包内包含主程序、开发证书工具、独立启动说明和配置示例,不包含本地数据库或凭据。解压后也可用 `node scripts/verify-package.mjs <解压后的包目录>` 重复验证。打包不会替换正在运行的服务。
@@ -164,7 +164,7 @@ powershell.exe -NoProfile -ExecutionPolicy Bypass -File scripts/package.ps1
## 目标压缩与命令审计
MySQL/MongoDB 目标可选择 zlib 压缩(默认关闭),通过协议协商启用,目标不支持时保持未压缩连接;连接测试显示实际协商结果。压缩仅作用于 AgentDB 到目标服务一侧,直连和 SOCKS5 均适用。当前 SSH 库仅支持未压缩传输。
MySQL/MongoDB 目标可选择 zlib 压缩(默认关闭),通过协议协商启用,目标不支持时保持未压缩连接;连接测试显示实际协商结果。压缩仅作用于 serviceport 到目标服务一侧,直连和 SOCKS5 均适用。当前 SSH 库仅支持未压缩传输。
「命令审计」记录 MySQL SQL/预编译模板、MongoDB 请求、SSH exec、交互输入行和 SFTP 操作,可按网关/协议筛选并分页查看。内容加密保存,保留 7 天,每条最多 16 KiB,截断会标记。放行/拦截是网关决策,不表示目标执行成功。审计写入失败时不继续转发该命令。
-209
View File
@@ -1,209 +0,0 @@
package main
import (
"agentdb/internal/api"
"agentdb/internal/buildinfo"
"agentdb/internal/desktop"
"agentdb/internal/gateway"
"agentdb/internal/localcert"
"agentdb/internal/model"
"agentdb/internal/protocol/mongoproxy"
"agentdb/internal/protocol/mysqlproxy"
"agentdb/internal/protocol/pgproxy"
"agentdb/internal/protocol/rabbitproxy"
"agentdb/internal/protocol/redisproxy"
"agentdb/internal/protocol/sshproxy"
"agentdb/internal/security"
"agentdb/internal/store"
"agentdb/web"
"context"
"crypto/rand"
"crypto/tls"
"encoding/base64"
"encoding/json"
"fmt"
"golang.org/x/term"
"log"
"net"
"net/http"
"os"
"os/signal"
"path/filepath"
"strings"
"syscall"
"time"
)
func main() {
if err := run(); err != nil {
log.Print(err)
if desktop.Enabled() && len(os.Args) == 1 && term.IsTerminal(int(os.Stdin.Fd())) {
fmt.Println("启动失败。请查看以上错误;按 Enter 关闭窗口。")
fmt.Scanln()
}
os.Exit(1)
}
}
func run() error {
if len(os.Args) > 1 && (os.Args[1] == "version" || os.Args[1] == "--version" || os.Args[1] == "-version") {
info := buildinfo.Current()
if len(os.Args) == 3 && os.Args[2] == "--json" {
return json.NewEncoder(os.Stdout).Encode(info)
}
if len(os.Args) != 2 {
return fmt.Errorf("usage: agentdb version [--json]")
}
fmt.Println(info.String())
return nil
}
if len(os.Args) > 1 && os.Args[1] == "generate-key" {
b := make([]byte, 32)
if _, err := rand.Read(b); err != nil {
return err
}
fmt.Println(base64.StdEncoding.EncodeToString(b))
return nil
}
log.Print(buildinfo.Current().String())
masterKey := os.Getenv("AGENTDB_MASTER_KEY")
path := env("AGENTDB_DATA", "data/agentdb.sqlite")
var local *desktop.Config
if desktop.Enabled() {
var err error
local, err = desktop.Prepare()
if err != nil {
return err
}
defer local.Close()
masterKey, path = local.Key, local.Data
}
vault, err := security.NewVault(masterKey)
if err != nil {
return err
}
if err = os.MkdirAll(filepath.Dir(path), 0700); err != nil {
return err
}
s, err := store.Open(path, vault)
if err != nil {
return err
}
defer s.Close()
if len(os.Args) > 1 && os.Args[1] == "reset-password" {
fmt.Print("New administrator password: ")
a, e := term.ReadPassword(int(os.Stdin.Fd()))
fmt.Println()
if e != nil {
return e
}
fmt.Print("Confirm password: ")
b, e := term.ReadPassword(int(os.Stdin.Fd()))
fmt.Println()
if e != nil {
return e
}
if string(a) != string(b) {
return fmt.Errorf("passwords do not match")
}
return s.ResetPassword(string(a))
}
authEnabled := strings.EqualFold(os.Getenv("AGENTDB_AUTH_ENABLED"), "true")
if (authEnabled && local == nil) || os.Getenv("AGENTDB_ADMIN_PASSWORD") != "" {
if err = s.InitAdmin(os.Getenv("AGENTDB_ADMIN_USER"), os.Getenv("AGENTDB_ADMIN_PASSWORD")); err != nil {
return err
}
}
var tlsConfig *tls.Config
cert, key := os.Getenv("AGENTDB_TLS_CERT"), os.Getenv("AGENTDB_TLS_KEY")
if local != nil && cert == "" && key == "" {
dir := filepath.Join(local.Dir, "tls")
cert, key = filepath.Join(dir, "cert.pem"), filepath.Join(dir, "key.pem")
if _, e := os.Stat(cert); os.IsNotExist(e) {
if e = localcert.Generate(dir, 365*24*time.Hour); e != nil {
return e
}
}
}
if cert != "" || key != "" {
pair, e := tls.LoadX509KeyPair(cert, key)
if e != nil {
return fmt.Errorf("load gateway TLS certificate: %w", e)
}
tlsConfig = &tls.Config{Certificates: []tls.Certificate{pair}, MinVersion: tls.VersionTLS12}
}
handlers := map[string]model.Handler{"mysql": mysqlproxy.New(), "mongodb": mongoproxy.New(), "ssh": sshproxy.New(), "redis": redisproxy.New(), "rabbitmq": rabbitproxy.New(), "pgsql": pgproxy.New()}
m := gateway.New(s, tlsConfig, handlers)
defer m.Shutdown()
ps, err := s.List()
if err != nil {
return fmt.Errorf("cannot load configurations; verify master key: %w", err)
}
for _, p := range ps {
if p.Enabled {
if e := m.Start(p); e != nil {
log.Printf("proxy %s restore failed: %v", p.ID, e)
}
}
}
server := api.New(s, m, strings.EqualFold(os.Getenv("AGENTDB_SECURE_COOKIE"), "true"), authEnabled)
srv := &http.Server{Addr: env("AGENTDB_HTTP_ADDR", "127.0.0.1:8080"), Handler: server.Handler(web.Handler()), ReadHeaderTimeout: 5 * time.Second, ReadTimeout: 30 * time.Second, WriteTimeout: 45 * time.Second, IdleTimeout: 60 * time.Second, MaxHeaderBytes: 1 << 16}
if local != nil || !authEnabled {
host, _, e := net.SplitHostPort(srv.Addr)
if e != nil || net.ParseIP(host) == nil || !net.ParseIP(host).IsLoopback() {
return fmt.Errorf("desktop/development mode requires a loopback AGENTDB_HTTP_ADDR, such as 127.0.0.1:8080; enable AGENTDB_AUTH_ENABLED for remote deployment")
}
}
listener, err := net.Listen("tcp", srv.Addr)
if err != nil {
return fmt.Errorf("cannot listen on %s (AgentDB may already be running): %w", srv.Addr, err)
}
defer listener.Close()
url := "http://" + listener.Addr().String()
if local != nil {
var setupPath string
if authEnabled {
srv.Handler, setupPath, err = desktop.Setup(s, srv.Handler, listener.Addr().String())
if err != nil {
return err
}
}
url += setupPath
fmt.Printf("数据目录:%s\n关闭服务请按 Ctrl+C。\n", local.Dir)
}
ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM)
defer stop()
done := make(chan error, 1)
go func() { log.Printf("AgentDB management listening on %s", listener.Addr()); done <- srv.Serve(listener) }()
if local != nil {
fmt.Println("管理页面:" + url)
if os.Getenv("AGENTDB_NO_BROWSER") != "1" {
if e := desktop.OpenBrowser(url); e != nil {
log.Printf("请手动打开管理页面:%v", e)
}
}
}
s.Prune()
ticker := time.NewTicker(time.Hour)
defer ticker.Stop()
for {
select {
case e := <-done:
if e == http.ErrServerClosed {
return nil
}
return e
case <-ticker.C:
s.Prune()
case <-ctx.Done():
shutdown, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel()
return srv.Shutdown(shutdown)
}
}
}
func env(key, fallback string) string {
if value := os.Getenv(key); value != "" {
return value
}
return fallback
}
-43
View File
@@ -1,43 +0,0 @@
package main
import (
"agentdb/internal/buildinfo"
"encoding/json"
"io"
"os"
"path/filepath"
"testing"
)
func TestVersionWithoutDataInitialization(t *testing.T) {
data := filepath.Join(t.TempDir(), "must-not-exist", "agentdb.sqlite")
t.Setenv("AGENTDB_DATA", data)
t.Setenv("AGENTDB_MASTER_KEY", "invalid")
oldArgs, oldStdout := os.Args, os.Stdout
r, w, err := os.Pipe()
if err != nil {
t.Fatal(err)
}
defer r.Close()
defer w.Close()
t.Cleanup(func() { os.Args, os.Stdout = oldArgs, oldStdout })
os.Args, os.Stdout = []string{"agentdb", "--version", "--json"}, w
if err := run(); err != nil {
t.Fatal(err)
}
w.Close()
body, err := io.ReadAll(r)
if err != nil {
t.Fatal(err)
}
var got buildinfo.Info
if err := json.Unmarshal(body, &got); err != nil {
t.Fatal(err)
}
if got != buildinfo.Current() {
t.Fatalf("CLI metadata: %+v", got)
}
if _, err := os.Stat(filepath.Dir(data)); !os.IsNotExist(err) {
t.Fatalf("version initialized data: %v", err)
}
}
+1 -1
View File
@@ -2,7 +2,7 @@
package main
import (
"agentdb/internal/localcert"
"serviceport/internal/localcert"
"fmt"
"os"
"time"
+14 -14
View File
@@ -1,6 +1,6 @@
# Disposable integration targets only. All passwords below are public TEST credentials.
# No production directories or data volumes are mounted.
name: agentdb-test
name: serviceport-test
services:
cert-init:
build: ./fixtures
@@ -11,7 +11,7 @@ services:
image: mysql:5.7.44
ports: ["127.0.0.1:13357:3306"]
environment:
MYSQL_ROOT_PASSWORD: agentdb-test-password
MYSQL_ROOT_PASSWORD: serviceport-test-password
MYSQL_ROOT_HOST: "%"
command: ["--ssl-ca=/certs/ca.pem", "--ssl-cert=/certs/server-cert.pem", "--ssl-key=/certs/server-key.pem", "--require-secure-transport=ON"]
volumes:
@@ -22,7 +22,7 @@ services:
cert-init:
condition: service_completed_successfully
healthcheck:
test: ["CMD-SHELL", "MYSQL_PWD=agentdb-test-password mysqladmin ping -uroot --silent"]
test: ["CMD-SHELL", "MYSQL_PWD=serviceport-test-password mysqladmin ping -uroot --silent"]
interval: 5s
timeout: 5s
retries: 60
@@ -30,7 +30,7 @@ services:
image: mysql:8.0.43
ports: ["127.0.0.1:13380:3306"]
environment:
MYSQL_ROOT_PASSWORD: agentdb-test-password
MYSQL_ROOT_PASSWORD: serviceport-test-password
MYSQL_ROOT_HOST: "%"
command: ["--ssl-ca=/certs/ca.pem", "--ssl-cert=/certs/server-cert.pem", "--ssl-key=/certs/server-key.pem", "--require-secure-transport=ON"]
volumes:
@@ -41,7 +41,7 @@ services:
cert-init:
condition: service_completed_successfully
healthcheck:
test: ["CMD-SHELL", "MYSQL_PWD=agentdb-test-password mysqladmin ping -uroot --silent"]
test: ["CMD-SHELL", "MYSQL_PWD=serviceport-test-password mysqladmin ping -uroot --silent"]
interval: 5s
timeout: 5s
retries: 60
@@ -49,7 +49,7 @@ services:
image: mysql:8.4.6
ports: ["127.0.0.1:13384:3306"]
environment:
MYSQL_ROOT_PASSWORD: agentdb-test-password
MYSQL_ROOT_PASSWORD: serviceport-test-password
MYSQL_ROOT_HOST: "%"
command: ["--ssl-ca=/certs/ca.pem", "--ssl-cert=/certs/server-cert.pem", "--ssl-key=/certs/server-key.pem", "--require-secure-transport=ON"]
volumes:
@@ -60,7 +60,7 @@ services:
cert-init:
condition: service_completed_successfully
healthcheck:
test: ["CMD-SHELL", "MYSQL_PWD=agentdb-test-password mysqladmin ping -uroot --silent"]
test: ["CMD-SHELL", "MYSQL_PWD=serviceport-test-password mysqladmin ping -uroot --silent"]
interval: 5s
timeout: 5s
retries: 60
@@ -69,7 +69,7 @@ services:
ports: ["127.0.0.1:27060:27017"]
environment:
MONGO_INITDB_ROOT_USERNAME: root
MONGO_INITDB_ROOT_PASSWORD: agentdb-test-password
MONGO_INITDB_ROOT_PASSWORD: serviceport-test-password
command: ["mongod", "--bind_ip_all", "--tlsMode", "requireTLS", "--tlsCertificateKeyFile", "/certs/mongo.pem", "--tlsCAFile", "/certs/ca.pem", "--tlsAllowConnectionsWithoutCertificates"]
volumes:
- fixture-certs:/certs:ro
@@ -80,7 +80,7 @@ services:
cert-init:
condition: service_completed_successfully
healthcheck:
test: ["CMD", "mongosh", "--quiet", "--host", "localhost", "--tls", "--tlsCAFile", "/certs/ca.pem", "-u", "root", "-p", "agentdb-test-password", "--authenticationDatabase", "admin", "--eval", "db.adminCommand({ping:1})"]
test: ["CMD", "mongosh", "--quiet", "--host", "localhost", "--tls", "--tlsCAFile", "/certs/ca.pem", "-u", "root", "-p", "serviceport-test-password", "--authenticationDatabase", "admin", "--eval", "db.adminCommand({ping:1})"]
interval: 5s
timeout: 5s
retries: 60
@@ -89,7 +89,7 @@ services:
ports: ["127.0.0.1:27070:27017"]
environment:
MONGO_INITDB_ROOT_USERNAME: root
MONGO_INITDB_ROOT_PASSWORD: agentdb-test-password
MONGO_INITDB_ROOT_PASSWORD: serviceport-test-password
command: ["mongod", "--bind_ip_all", "--tlsMode", "requireTLS", "--tlsCertificateKeyFile", "/certs/mongo.pem", "--tlsCAFile", "/certs/ca.pem", "--tlsAllowConnectionsWithoutCertificates"]
volumes:
- fixture-certs:/certs:ro
@@ -100,7 +100,7 @@ services:
cert-init:
condition: service_completed_successfully
healthcheck:
test: ["CMD", "mongosh", "--quiet", "--host", "localhost", "--tls", "--tlsCAFile", "/certs/ca.pem", "-u", "root", "-p", "agentdb-test-password", "--authenticationDatabase", "admin", "--eval", "db.adminCommand({ping:1})"]
test: ["CMD", "mongosh", "--quiet", "--host", "localhost", "--tls", "--tlsCAFile", "/certs/ca.pem", "-u", "root", "-p", "serviceport-test-password", "--authenticationDatabase", "admin", "--eval", "db.adminCommand({ping:1})"]
interval: 5s
timeout: 5s
retries: 60
@@ -109,7 +109,7 @@ services:
ports: ["127.0.0.1:27080:27017"]
environment:
MONGO_INITDB_ROOT_USERNAME: root
MONGO_INITDB_ROOT_PASSWORD: agentdb-test-password
MONGO_INITDB_ROOT_PASSWORD: serviceport-test-password
entrypoint: ["bash", "/fixture-start.sh"]
volumes:
- fixture-certs:/certs:ro
@@ -121,14 +121,14 @@ services:
cert-init:
condition: service_completed_successfully
healthcheck:
test: ["CMD", "mongosh", "--quiet", "--host", "localhost", "--tls", "--tlsCAFile", "/certs/ca.pem", "-u", "root", "-p", "agentdb-test-password", "--authenticationDatabase", "admin", "--eval", "db.adminCommand({ping:1})"]
test: ["CMD", "mongosh", "--quiet", "--host", "localhost", "--tls", "--tlsCAFile", "/certs/ca.pem", "-u", "root", "-p", "serviceport-test-password", "--authenticationDatabase", "admin", "--eval", "db.adminCommand({ping:1})"]
interval: 5s
timeout: 5s
retries: 60
mongo80-init:
image: mongo:8.0.13
profiles: ["replica-init"]
entrypoint: ["mongosh", "--quiet", "--host", "mongo80", "--tls", "--tlsCAFile", "/certs/ca.pem", "-u", "root", "-p", "agentdb-test-password", "--authenticationDatabase", "admin", "/init-replica.js"]
entrypoint: ["mongosh", "--quiet", "--host", "mongo80", "--tls", "--tlsCAFile", "/certs/ca.pem", "-u", "root", "-p", "serviceport-test-password", "--authenticationDatabase", "admin", "/init-replica.js"]
volumes:
- fixture-certs:/certs:ro
- ./mongo/init-replica.js:/init-replica.js:ro
+1 -1
View File
@@ -65,7 +65,7 @@ func certs(dir string) error {
if err != nil {
return err
}
ca := &x509.Certificate{SerialNumber: big.NewInt(1), Subject: pkix.Name{CommonName: "agentdb disposable test CA"}, NotBefore: time.Now().Add(-time.Hour), NotAfter: time.Now().AddDate(1, 0, 0), IsCA: true, BasicConstraintsValid: true, KeyUsage: x509.KeyUsageCertSign | x509.KeyUsageDigitalSignature}
ca := &x509.Certificate{SerialNumber: big.NewInt(1), Subject: pkix.Name{CommonName: "serviceport disposable test CA"}, NotBefore: time.Now().Add(-time.Hour), NotAfter: time.Now().AddDate(1, 0, 0), IsCA: true, BasicConstraintsValid: true, KeyUsage: x509.KeyUsageCertSign | x509.KeyUsageDigitalSignature}
der, err := x509.CreateCertificate(rand.Reader, ca, ca, &key.PublicKey, key)
if err != nil {
return err
+1 -1
View File
@@ -2,7 +2,7 @@ try {
rs.status();
} catch (e) {
if (e.code !== 94) throw e;
const result = rs.initiate({_id: "agentdb-test", members: [{_id: 0, host: "mongo80:27017"}]});
const result = rs.initiate({_id: "serviceport-test", members: [{_id: 0, host: "mongo80:27017"}]});
if (result.ok !== 1) throw new Error(JSON.stringify(result));
}
let primary = false;
+4 -4
View File
@@ -1,6 +1,6 @@
#!/usr/bin/env bash
set -euo pipefail
cp /certs/replica-key /tmp/agentdb-replica-key
chown mongodb:mongodb /tmp/agentdb-replica-key
chmod 400 /tmp/agentdb-replica-key
exec /usr/local/bin/docker-entrypoint.sh mongod --bind_ip_all --replSet agentdb-test --keyFile /tmp/agentdb-replica-key --tlsMode requireTLS --tlsCertificateKeyFile /certs/mongo.pem --tlsCAFile /certs/ca.pem --tlsAllowConnectionsWithoutCertificates
cp /certs/replica-key /tmp/serviceport-replica-key
chown mongodb:mongodb /tmp/serviceport-replica-key
chmod 400 /tmp/serviceport-replica-key
exec /usr/local/bin/docker-entrypoint.sh mongod --bind_ip_all --replSet serviceport-test --keyFile /tmp/serviceport-replica-key --tlsMode requireTLS --tlsCertificateKeyFile /certs/mongo.pem --tlsCAFile /certs/ca.pem --tlsAllowConnectionsWithoutCertificates
+2 -2
View File
@@ -1,6 +1,6 @@
FROM debian:12.11-slim
RUN apt-get update && apt-get install -y --no-install-recommends openssh-server ca-certificates && rm -rf /var/lib/apt/lists/* \
&& mkdir -p /run/sshd && useradd -m -s /bin/bash agentdb \
&& echo 'agentdb:agentdb-test-password' | chpasswd
&& mkdir -p /run/sshd && useradd -m -s /bin/bash serviceport \
&& echo 'serviceport:serviceport-test-password' | chpasswd
COPY sshd_config /etc/ssh/sshd_config
CMD ["sh", "-c", "ssh-keygen -A && exec /usr/sbin/sshd -D -e"]
+1 -1
View File
@@ -5,7 +5,7 @@ PasswordAuthentication yes
PubkeyAuthentication yes
PermitRootLogin no
UsePAM no
AllowUsers agentdb
AllowUsers serviceport
Subsystem sftp internal-sftp
PrintMotd no
LogLevel VERBOSE
+6 -6
View File
@@ -1,4 +1,4 @@
# AgentDB 开发计划
# serviceport 开发计划
## 0.3.0:Kubernetes、版本与模块拆分
@@ -51,7 +51,7 @@
## 3. 密码、密钥与管理员认证
- 开发阶段默认关闭管理员登录;设置 AGENTDB_AUTH_ENABLED=true 后,通过环境变量或 Windows 首次设置页面初始化管理员。登录密码使用 bcrypt,不保存可恢复副本,不提供原文查看。
- 开发阶段默认关闭管理员登录;设置 SERVICEPORT_AUTH_ENABLED=true 后,通过环境变量或 Windows 首次设置页面初始化管理员。登录密码使用 bcrypt,不保存可恢复副本,不提供原文查看。
- 提供本机交互式密码重置命令,重置后注销已有会话。
- 使用服务端会话、HttpOnly Cookie 和 CSRF 校验。
- 目标密码、网关密码、SOCKS5 密码、SSH 上游私钥及私钥口令使用 AES-GCM 加密保存,每次加密使用独立随机 nonce。
@@ -209,7 +209,7 @@
### Windows 双击启动(0.1.1)
- 已实现无环境凭据启动:自动打开浏览器,首次页面设置管理员,后续复用账号。
- 默认数据目录 `%APPDATA%\AgentDB`;随机主密钥使用当前 Windows 用户 DPAPI 加密保存,独占锁阻止多个进程同时启动。
- 默认数据目录 `%APPDATA%\serviceport`;随机主密钥使用当前 Windows 用户 DPAPI 加密保存,独占锁阻止多个进程同时启动。
- 自动生成一年有效的 localhost TLS 证书;保持服务器环境变量部署兼容。
- 本地初始化仅监听回环地址,校验 Host、Origin 和一次性随机 token;初始化完成后关闭设置入口。
- 错误保留在控制台窗口;提供打包后的首次启动、浏览器设置、密钥持久化和重启验证脚本 `scripts/verify-desktop.mjs`。
@@ -224,11 +224,11 @@
- 旧数据通过事务自动拆分为 targets/gateways 两张加密配置表,引用有外键约束;迁移保留原网关 ID、密码、SSH 主机身份和启用状态。回滚版本须恢复升级前备份。
- 新增 /api/v1/targets CRUD、连接测试、密码查看和 SSH 指纹确认;网关仍使用 /api/v1/proxies。旧版内联创建请求兼容导入为独立目标,但网关更新禁止修改目标参数;新界面只使用 targetId 创建网关。
- 管理界面增加浅色、深色、海蓝及跟随系统主题,浏览器持久化选择。
- 开发阶段默认 AGENTDB_AUTH_ENABLED=false:跳过管理员创建与登录,直达控制台;仅限回环访问,保留 Host/Origin/CSRF 校验。设置 true 恢复 bcrypt 管理员登录、会话及密码查看重新认证。远程部署必须显式开启认证。
- 开发阶段默认 SERVICEPORT_AUTH_ENABLED=false:跳过管理员创建与登录,直达控制台;仅限回环访问,保留 Host/Origin/CSRF 校验。设置 true 恢复 bcrypt 管理员登录、会话及密码查看重新认证。远程部署必须显式开启认证。
### 目标压缩与逐命令审计
- 开发启动入口:根目录 `start-dev.cmd` 调用 `scripts/start-dev.ps1`,固定使用项目 `data/agentdb.sqlite` 与同目录 DPAPI 密钥、TLS 文件,重启复用配置,默认回环免登录。测试数据库单独存放,不使用开发配置库。
- 开发启动入口:根目录 `start-dev.cmd` 调用 `scripts/start-dev.ps1`,固定使用项目 `data/serviceport.sqlite` 与同目录 DPAPI 密钥、TLS 文件,重启复用配置,默认回环免登录。测试数据库单独存放,不使用开发配置库。
- 2026-09-09 实测 Windows MySQL Community 5.7.44:目标 TLS 开启,分别使用 none/zlib 压缩,通过读写、预编译、事务提交/回滚、中文与 emoji、只读阻止 DELETE/UPDATE prepare、错误密码拒绝、网关明文连接拒绝和命令审计。临时数据库自动删除;日志在 `artifacts/mysql-verification/real-mysql57-*.log`。本次真实实例测试为直连,未覆盖 SOCKS5 或 MySQL 8.x。
- MySQL 5.7 兼容 zlib CLIENT_COMPRESS,先读取并回放目标 greeting,仅在目标声明支持时请求压缩;MongoDB hello 协商 zlib OP_COMPRESSED,限制解压尺寸、校验包结构/校验和,避免压缩认证与凭据管理命令。目标不支持时回退为未压缩,测试返回实际算法。SSH 当前仅 none。
@@ -238,7 +238,7 @@
### 审计分库、导出与连接编排
- 配置固定在 `data/agentdb.sqlite`;命令审计与连接/安全事件写入 `data/audit/YYYY-MM-DD.sqlite`,按 UTC 日期分库,同日分别使用 commands/events 表。
- 配置固定在 `data/serviceport.sqlite`;命令审计与连接/安全事件写入 `data/audit/YYYY-MM-DD.sqlite`,按 UTC 日期分库,同日分别使用 commands/events 表。
- 启动自动迁移旧日志,命令密文保持不变,迁移中断可重试;升级前停止服务并备份整个 data 目录。日志保留 7 天,过期分库清理;查询和导出跨分库进行。
- 命令审计支持网关、协议、目标、来源 IP、决策、操作类型、关键词和时间筛选;连接与审计支持服务、来源 IP、事件类型、关键词和时间筛选。两页均支持游标分页和按筛选条件导出全部 CSV,不受当前页限制。
- CSV 使用 UTF-8 BOM、正确转义及公式防护,通过临时文件下载,避免完整导出占用服务端内存。
+5 -5
View File
@@ -1,12 +1,12 @@
# Kubernetes port-forward
AgentDB 内置 Kubernetes 客户端,不需要安装 `kubectl`。先在独立「代理」菜单新增 `K8s port-forward` 代理,再在目标服务的「代理出口」下拉框选择它。MySQL、MongoDB、SSH、Redis、RabbitMQ、PostgreSQL 均可引用同一个代理配置。
serviceport 内置 Kubernetes 客户端,不需要安装 `kubectl`。先在独立「代理」菜单新增 `K8s port-forward` 代理,再在目标服务的「代理出口」下拉框选择它。MySQL、MongoDB、SSH、Redis、RabbitMQ、PostgreSQL 均可引用同一个代理配置。
## 配置
| 字段 | 含义 |
| --- | --- |
| Kubeconfig 路径 | AgentDB 所在机器上的配置文件路径;留空使用 `KUBECONFIG` 或当前用户的 `~/.kube/config`,集群内部可使用 ServiceAccount |
| Kubeconfig 路径 | serviceport 所在机器上的配置文件路径;留空使用 `KUBECONFIG` 或当前用户的 `~/.kube/config`,集群内部可使用 ServiceAccount |
| Context | kubeconfig 中的 context 名称;留空使用当前 context |
| 命名空间 | 留空使用所选 context 的命名空间,未设置时为 `default` |
| Kubernetes 资源 | `pod/mysql-0`、`service/mysql` 或 `deployment/mysql` |
@@ -15,12 +15,12 @@ AgentDB 内置 Kubernetes 客户端,不需要安装 `kubectl`。先在独立
例如 MySQL Service 在 `database` 命名空间暴露 `3306`:资源填 `service/mysql`,命名空间填 `database`,目标端口填 `3306`;TLS 开启时目标主机须匹配数据库证书,例如 `mysql.database.svc`。数据库账号密码仍填目标服务自己的凭据,客户端使用网关账号。
配置文件仅保存路径,不把 kubeconfig 内容上传到管理接口。AgentDB 运行用户须能读取该文件及它引用的证书、私钥。使用云厂商 exec 登录插件的配置仍依赖对应认证插件;不支持交互式登录提示。不要使用来源不可信的 kubeconfig。
配置文件仅保存路径,不把 kubeconfig 内容上传到管理接口。serviceport 运行用户须能读取该文件及它引用的证书、私钥。使用云厂商 exec 登录插件的配置仍依赖对应认证插件;不支持交互式登录提示。不要使用来源不可信的 kubeconfig。
## 权限与连接
集群 API 必须可从 AgentDB 所在机器访问。凭据需要所选资源的读取权限、Service/Deployment 对应 Pod 的列表权限,以及 `pods/portforward` 的创建权限。Service 必须有 Pod selector,选出的 Pod 必须处于 Running 状态,优先选择 Ready Pod。
集群 API 必须可从 serviceport 所在机器访问。凭据需要所选资源的读取权限、Service/Deployment 对应 Pod 的列表权限,以及 `pods/portforward` 的创建权限。Service 必须有 Pod selector,选出的 Pod 必须处于 Running 状态,优先选择 Ready Pod。
每条上游连接使用独立的 port-forward 通道和随机本地回环端口。连接关闭、网关停止或建连失败时清理通道和监听端口,不回退直连。建立成功的数据库会话不受建连超时影响。Pod 重启或网络中断后,原会话断开;客户端重连时重新选择 Pod,不自动重放 SQL 或消息。
当前使用 Kubernetes SPDY port-forward 协议,仅支持 TCP,要求 AgentDB 直接访问集群 API;暂不支持 kubeconfig 的 `proxy-url` 或匹配 API 地址的 HTTP(S) 代理环境变量,配置这些代理时明确返回失败。集群 API 自身的反向代理需要允许流式连接升级。普通 kubeconfig 证书、Token 认证无需 kubectl;特定云平台认证和真实集群兼容性需在实际环境验证。
当前使用 Kubernetes SPDY port-forward 协议,仅支持 TCP,要求 serviceport 直接访问集群 API;暂不支持 kubeconfig 的 `proxy-url` 或匹配 API 地址的 HTTP(S) 代理环境变量,配置这些代理时明确返回失败。集群 API 自身的反向代理需要允许流式连接升级。普通 kubeconfig 证书、Token 认证无需 kubectl;特定云平台认证和真实集群兼容性需在实际环境验证。
+7 -7
View File
@@ -32,11 +32,11 @@ $env:GOTOOLCHAIN = 'go1.25.0'
go test ./...
go vet ./...
npm.cmd --prefix web run build
go build -o bin/agentdb.exe ./cmd/agentdb
go build -o bin/serviceport.exe ./cmd/serviceport
npm.cmd --prefix web test
```
浏览器测试使用隔离的临时 SQLite、随机主密钥和仅供测试的管理员密码,监听 `127.0.0.1:18081`,结束后清理。不连接用户的数据库。默认使用已安装的 Edge;其他平台可安装 Playwright Chromium 并设置 `AGENTDB_BROWSER_CHANNEL=chromium`。截图输出到忽略版本控制的 `web/test-results`。
浏览器测试使用隔离的临时 SQLite、随机主密钥和仅供测试的管理员密码,监听 `127.0.0.1:18081`,结束后清理。不连接用户的数据库。默认使用已安装的 Edge;其他平台可安装 Playwright Chromium 并设置 `SERVICEPORT_BROWSER_CHANNEL=chromium`。截图输出到忽略版本控制的 `web/test-results`。
## 已有自动化覆盖
@@ -85,11 +85,11 @@ docker compose -f deploy/compose.test.yml run --rm --no-deps mongo80-init
独立连接已准备好的目标测试实例:
- MySQL:`AGENTDB_MYSQL_TEST_ADDR`、`AGENTDB_MYSQL_TEST_USER`、`AGENTDB_MYSQL_TEST_PASSWORD`、可选 `AGENTDB_MYSQL_TEST_CA`(CA 文件路径),执行 `go test ./internal/protocol/mysqlproxy -run TestRealMySQL -count=1 -v`。
- MongoDB:`AGENTDB_MONGO_TEST_ADDR`、`AGENTDB_MONGO_TEST_USER`、`AGENTDB_MONGO_TEST_PASSWORD`、可选 `AGENTDB_MONGO_TEST_CA`(PEM 内容),执行 `go test ./internal/protocol/mongoproxy -run TestRealMongo -count=1 -v`。
- SSH:`AGENTDB_SSH_TEST_ADDR`、`AGENTDB_SSH_TEST_USER`、`AGENTDB_SSH_TEST_PASSWORD`,执行 `go test ./internal/protocol/sshproxy -run TestRealSSH -count=1 -v`。验证远程输出和退出码、SFTP 上传下载及清理、禁止 TCP 转发。
- MySQL:`SERVICEPORT_MYSQL_TEST_ADDR`、`SERVICEPORT_MYSQL_TEST_USER`、`SERVICEPORT_MYSQL_TEST_PASSWORD`、可选 `SERVICEPORT_MYSQL_TEST_CA`(CA 文件路径),执行 `go test ./internal/protocol/mysqlproxy -run TestRealMySQL -count=1 -v`。
- MongoDB:`SERVICEPORT_MONGO_TEST_ADDR`、`SERVICEPORT_MONGO_TEST_USER`、`SERVICEPORT_MONGO_TEST_PASSWORD`、可选 `SERVICEPORT_MONGO_TEST_CA`(PEM 内容),执行 `go test ./internal/protocol/mongoproxy -run TestRealMongo -count=1 -v`。
- SSH:`SERVICEPORT_SSH_TEST_ADDR`、`SERVICEPORT_SSH_TEST_USER`、`SERVICEPORT_SSH_TEST_PASSWORD`,执行 `go test ./internal/protocol/sshproxy -run TestRealSSH -count=1 -v`。验证远程输出和退出码、SFTP 上传下载及清理、禁止 TCP 转发。
三类测试可额外设置 `AGENTDB_TEST_SOCKS_ADDR` 和可选 `AGENTDB_TEST_SOCKS_USER/PASSWORD`,使用真实生产出站层经 SOCKS5 连接上述目标。测试中的首次 SSH 指纹探测仅适用于已明确指定的一次性测试服务,不替代生产环境的管理员独立核对流程。
三类测试可额外设置 `SERVICEPORT_TEST_SOCKS_ADDR` 和可选 `SERVICEPORT_TEST_SOCKS_USER/PASSWORD`,使用真实生产出站层经 SOCKS5 连接上述目标。测试中的首次 SSH 指纹探测仅适用于已明确指定的一次性测试服务,不替代生产环境的管理员独立核对流程。
测试账号须允许创建和删除独立测试库;未设置地址时真实版本测试明确跳过。省略上游 CA 时测试按明文上游运行,但网关客户端仍执行 TLS。
@@ -107,7 +107,7 @@ docker compose -f deploy/compose.test.yml run --rm --no-deps mongo80-init
```powershell
node scripts/verify-package.mjs <解压后的目录>
node scripts/verify-desktop.mjs <解压后的目录>/agentdb.exe
node scripts/verify-desktop.mjs <解压后的目录>/serviceport.exe
```
脚本只在临时用户数据目录中测试,不读取实际用户配置。桌面脚本用 Edge 自动操作首次设置页面,禁用系统浏览器弹窗并使用随机本机端口;系统默认浏览器关联及资源管理器双击交互未由此脚本覆盖。
+1 -1
View File
@@ -1,4 +1,4 @@
module agentdb
module serviceport
go 1.25.0
+3 -3
View File
@@ -1,8 +1,8 @@
package api
import (
"agentdb/internal/model"
"agentdb/internal/security"
"serviceport/internal/model"
"serviceport/internal/security"
"net/http"
"time"
)
@@ -52,7 +52,7 @@ func (s *Server) login(w http.ResponseWriter, r *http.Request) {
}
s.sessions[token] = sess
s.mu.Unlock()
http.SetCookie(w, &http.Cookie{Name: "agentdb_session", Value: token, Path: "/", HttpOnly: true, Secure: s.secure, SameSite: http.SameSiteStrictMode, MaxAge: 43200})
http.SetCookie(w, &http.Cookie{Name: "serviceport_session", Value: token, Path: "/", HttpOnly: true, Secure: s.secure, SameSite: http.SameSiteStrictMode, MaxAge: 43200})
respond(w, 200, map[string]string{"username": sess.User, "csrfToken": sess.CSRF})
}
+4 -4
View File
@@ -1,7 +1,7 @@
package api
import (
"agentdb/internal/store"
"serviceport/internal/store"
"encoding/csv"
"net/http"
"os"
@@ -70,7 +70,7 @@ func csvCell(value string) string {
return value
}
func (s *Server) exportCommands(w http.ResponseWriter, r *http.Request, f store.CommandFilter) {
file, err := os.CreateTemp("", "agentdb-export-*.csv")
file, err := os.CreateTemp("", "serviceport-export-*.csv")
if err != nil {
fail(w, 500, "cannot create export")
return
@@ -97,7 +97,7 @@ func (s *Server) exportCommands(w http.ResponseWriter, r *http.Request, f store.
return
}
w.Header().Set("Content-Type", "text/csv; charset=utf-8")
w.Header().Set("Content-Disposition", "attachment; filename=agentdb-commands.csv")
w.Header().Set("Content-Disposition", "attachment; filename=serviceport-commands.csv")
w.Header().Set("Cache-Control", "no-store")
http.ServeContent(w, r, "agentdb-commands.csv", time.Time{}, file)
http.ServeContent(w, r, "serviceport-commands.csv", time.Time{}, file)
}
+1 -1
View File
@@ -1,7 +1,7 @@
package api
import (
"agentdb/internal/model"
"serviceport/internal/model"
"encoding/csv"
"encoding/json"
"strings"
+4 -4
View File
@@ -1,7 +1,7 @@
package api
import (
"agentdb/internal/store"
"serviceport/internal/store"
"encoding/csv"
"net/http"
"os"
@@ -45,7 +45,7 @@ func (s *Server) events(w http.ResponseWriter, r *http.Request) {
}
}
if q.Get("export") == "csv" {
file, err := os.CreateTemp("", "agentdb-events-*.csv")
file, err := os.CreateTemp("", "serviceport-events-*.csv")
if err != nil {
fail(w, 500, "cannot create export")
return
@@ -72,9 +72,9 @@ func (s *Server) events(w http.ResponseWriter, r *http.Request) {
return
}
w.Header().Set("Content-Type", "text/csv; charset=utf-8")
w.Header().Set("Content-Disposition", "attachment; filename=agentdb-events.csv")
w.Header().Set("Content-Disposition", "attachment; filename=serviceport-events.csv")
w.Header().Set("Cache-Control", "no-store")
http.ServeContent(w, r, "agentdb-events.csv", time.Time{}, file)
http.ServeContent(w, r, "serviceport-events.csv", time.Time{}, file)
return
}
records, err := s.store.FilterEvents(r.Context(), f, before)
+4 -4
View File
@@ -1,10 +1,10 @@
package api
import (
"agentdb/internal/gateway"
"agentdb/internal/model"
"agentdb/internal/outbound"
"agentdb/internal/security"
"serviceport/internal/gateway"
"serviceport/internal/model"
"serviceport/internal/outbound"
"serviceport/internal/security"
"net/http"
"strings"
"time"
+1 -1
View File
@@ -1,7 +1,7 @@
package api
import (
"agentdb/internal/model"
"serviceport/internal/model"
"encoding/json"
"strings"
"testing"
+3 -3
View File
@@ -1,9 +1,9 @@
package api
import (
"agentdb/internal/model"
"agentdb/internal/protocol/sshproxy"
"agentdb/internal/security"
"serviceport/internal/model"
"serviceport/internal/protocol/sshproxy"
"serviceport/internal/security"
"golang.org/x/crypto/ssh"
"net/http"
)
+8 -8
View File
@@ -1,12 +1,12 @@
package api
import (
"agentdb/internal/buildinfo"
"agentdb/internal/gateway"
"agentdb/internal/outbound"
"agentdb/internal/protocol/sshproxy"
"agentdb/internal/security"
"agentdb/internal/store"
"serviceport/internal/buildinfo"
"serviceport/internal/gateway"
"serviceport/internal/outbound"
"serviceport/internal/protocol/sshproxy"
"serviceport/internal/security"
"serviceport/internal/store"
"context"
"net"
"net/http"
@@ -99,7 +99,7 @@ func (s *Server) serve(w http.ResponseWriter, r *http.Request) {
sess = session{User: "开发者", CSRF: s.devCSRF}
} else {
var err error
cookie, err = r.Cookie("agentdb_session")
cookie, err = r.Cookie("serviceport_session")
if err != nil {
fail(w, 401, "login required")
return
@@ -139,7 +139,7 @@ func (s *Server) serve(w http.ResponseWriter, r *http.Request) {
s.mu.Lock()
delete(s.sessions, cookie.Value)
s.mu.Unlock()
http.SetCookie(w, &http.Cookie{Name: "agentdb_session", Path: "/", MaxAge: -1, HttpOnly: true, Secure: s.secure, SameSite: http.SameSiteStrictMode})
http.SetCookie(w, &http.Cookie{Name: "serviceport_session", Path: "/", MaxAge: -1, HttpOnly: true, Secure: s.secure, SameSite: http.SameSiteStrictMode})
respond(w, 200, map[string]bool{"ok": true})
return
}
+9 -9
View File
@@ -16,11 +16,11 @@ import (
"testing"
"time"
"agentdb/internal/gateway"
"agentdb/internal/model"
"agentdb/internal/protocol/sshproxy"
"agentdb/internal/security"
"agentdb/internal/store"
"serviceport/internal/gateway"
"serviceport/internal/model"
"serviceport/internal/protocol/sshproxy"
"serviceport/internal/security"
"serviceport/internal/store"
"golang.org/x/crypto/ssh"
)
@@ -108,7 +108,7 @@ func (f *fixture) request(method, path string, body any, csrf bool) *httptest.Re
f.t.Fatal(err)
}
}
req := httptest.NewRequest(method, "https://agentdb.test"+path, bytes.NewReader(data))
req := httptest.NewRequest(method, "https://serviceport.test"+path, bytes.NewReader(data))
req.RemoteAddr = "127.0.0.1:12345"
req.Header.Set("Content-Type", "application/json")
if f.cookie != nil {
@@ -217,7 +217,7 @@ func TestSessionsCSRFAndPasswordReset(t *testing.T) {
}
requireStatus(t, f.request("GET", "/api/v1/session", nil, false), 200)
requireStatus(t, f.request("POST", "/api/v1/proxies", config(t, "mysql"), false), 403)
req := httptest.NewRequest("POST", "https://agentdb.test/api/v1/logout", nil)
req := httptest.NewRequest("POST", "https://serviceport.test/api/v1/logout", nil)
req.RemoteAddr = "127.0.0.1:12345"
req.AddCookie(f.cookie)
req.Header.Set("X-CSRF-Token", f.csrf)
@@ -289,7 +289,7 @@ func TestRevealRequiresReauthenticationAndLimitsBothSessionAndIP(t *testing.T) {
requireStatus(t, f.request("POST", path, body, true), 200)
requireStatus(t, f.request("POST", path, body, true), 429)
encoded, _ := json.Marshal(body)
req := httptest.NewRequest("POST", "https://agentdb.test"+path, bytes.NewReader(encoded))
req := httptest.NewRequest("POST", "https://serviceport.test"+path, bytes.NewReader(encoded))
req.RemoteAddr = "127.0.0.2:12345"
req.AddCookie(f.cookie)
req.Header.Set("X-CSRF-Token", f.csrf)
@@ -369,7 +369,7 @@ func TestSSHHostKeyCannotBeInjectedAndPrivateKeyIsRedacted(t *testing.T) {
func TestLogoutInvalidatesSessionAndMalformedJSONRejected(t *testing.T) {
f := newFixture(t)
f.login()
req := httptest.NewRequest("POST", "https://agentdb.test/api/v1/proxies", io.NopCloser(strings.NewReader(`{} {}`)))
req := httptest.NewRequest("POST", "https://serviceport.test/api/v1/proxies", io.NopCloser(strings.NewReader(`{} {}`)))
req.AddCookie(f.cookie)
req.Header.Set("X-CSRF-Token", f.csrf)
w := httptest.NewRecorder()
+5 -5
View File
@@ -1,11 +1,11 @@
package api
import (
"agentdb/internal/gateway"
"agentdb/internal/model"
"agentdb/internal/outbound"
"agentdb/internal/protocol/sshproxy"
"agentdb/internal/security"
"serviceport/internal/gateway"
"serviceport/internal/model"
"serviceport/internal/outbound"
"serviceport/internal/protocol/sshproxy"
"serviceport/internal/security"
"context"
"errors"
"net/http"
+1 -1
View File
@@ -1,7 +1,7 @@
package api
import (
"agentdb/internal/model"
"serviceport/internal/model"
"encoding/json"
"net/http"
"net/http/httptest"
+1 -1
View File
@@ -1,7 +1,7 @@
package api
import (
"agentdb/internal/model"
"serviceport/internal/model"
"errors"
"golang.org/x/crypto/ssh"
"strings"
+1 -1
View File
@@ -1,7 +1,7 @@
package api
import (
"agentdb/internal/buildinfo"
"serviceport/internal/buildinfo"
"encoding/json"
"testing"
)
+3 -3
View File
@@ -1,7 +1,7 @@
package buildinfo
import (
"agentdb"
"serviceport"
"fmt"
"runtime"
)
@@ -22,11 +22,11 @@ type Info struct {
func Current() Info {
v := Version
if v == "" {
v = agentdb.Version()
v = serviceport.Version()
}
return Info{v, Commit, BuildTime, runtime.Version(), runtime.GOOS + "/" + runtime.GOARCH}
}
func (i Info) String() string {
return fmt.Sprintf("AgentDB %s (commit %s, built %s, %s, %s)", i.Version, i.Commit, i.BuildTime, i.GoVersion, i.Platform)
return fmt.Sprintf("serviceport %s (commit %s, built %s, %s, %s)", i.Version, i.Commit, i.BuildTime, i.GoVersion, i.Platform)
}
+3 -3
View File
@@ -1,7 +1,7 @@
package buildinfo
import (
"agentdb"
"serviceport"
"strings"
"testing"
)
@@ -10,7 +10,7 @@ func TestCurrent(t *testing.T) {
oldVersion, oldCommit, oldTime := Version, Commit, BuildTime
t.Cleanup(func() { Version, Commit, BuildTime = oldVersion, oldCommit, oldTime })
Version = ""
if Current().Version != agentdb.Version() {
if Current().Version != serviceport.Version() {
t.Fatal("default must use embedded VERSION")
}
Version, Commit, BuildTime = "1.2.3-rc.1", "abc123-dirty", "2026-09-09T00:00:00Z"
@@ -18,7 +18,7 @@ func TestCurrent(t *testing.T) {
if i.Version != Version || i.Commit != Commit || i.BuildTime != BuildTime || i.Platform == "" || i.GoVersion == "" {
t.Fatalf("metadata mismatch: %+v", i)
}
if !strings.Contains(i.String(), "AgentDB 1.2.3-rc.1") {
if !strings.Contains(i.String(), "serviceport 1.2.3-rc.1") {
t.Fatal(i.String())
}
}
+7 -7
View File
@@ -2,7 +2,7 @@
package desktop
import (
"agentdb/internal/security"
"serviceport/internal/security"
"crypto/rand"
"encoding/base64"
"errors"
@@ -12,7 +12,7 @@ import (
"runtime"
)
func Enabled() bool { return runtime.GOOS == "windows" && os.Getenv("AGENTDB_MASTER_KEY") == "" }
func Enabled() bool { return runtime.GOOS == "windows" && os.Getenv("SERVICEPORT_MASTER_KEY") == "" }
type Config struct {
Key string
@@ -26,8 +26,8 @@ func Prepare() (*Config, error) {
if err != nil {
return nil, err
}
dir = filepath.Join(dir, "AgentDB")
data := os.Getenv("AGENTDB_DATA")
dir = filepath.Join(dir, "serviceport")
data := os.Getenv("SERVICEPORT_DATA")
if data != "" {
data, err = filepath.Abs(data)
if err != nil {
@@ -35,14 +35,14 @@ func Prepare() (*Config, error) {
}
dir = filepath.Dir(data)
} else {
data = filepath.Join(dir, "agentdb.sqlite")
data = filepath.Join(dir, "serviceport.sqlite")
}
if err := os.MkdirAll(dir, 0700); err != nil {
return nil, err
}
release, err := lock(filepath.Join(dir, "desktop.lock"))
if err != nil {
return nil, fmt.Errorf("AgentDB may already be running; close its existing window before restarting: %w", err)
return nil, fmt.Errorf("serviceport may already be running; close its existing window before restarting: %w", err)
}
key, err := loadKey(filepath.Join(dir, "master-key.dpapi"), data)
if err != nil {
@@ -69,7 +69,7 @@ func loadKey(path, data string) (string, error) {
return "", err
}
if _, err = os.Stat(data); !errors.Is(err, os.ErrNotExist) {
return "", fmt.Errorf("database already exists but local key is missing; restore master-key.dpapi or set the original AGENTDB_MASTER_KEY")
return "", fmt.Errorf("database already exists but local key is missing; restore master-key.dpapi or set the original SERVICEPORT_MASTER_KEY")
}
raw := make([]byte, 32)
if _, err = rand.Read(raw); err != nil {
+1 -1
View File
@@ -9,7 +9,7 @@ import (
func TestLocalKeyPersistenceAndLock(t *testing.T) {
dir := t.TempDir()
t.Setenv("AGENTDB_DATA", filepath.Join(dir, "test.sqlite"))
t.Setenv("SERVICEPORT_DATA", filepath.Join(dir, "test.sqlite"))
c, err := Prepare()
if err != nil {
t.Fatal(err)
+4 -4
View File
@@ -1,8 +1,8 @@
package desktop
import (
"agentdb/internal/security"
"agentdb/internal/store"
"serviceport/internal/security"
"serviceport/internal/store"
"crypto/subtle"
"html/template"
"net/http"
@@ -84,8 +84,8 @@ func Setup(s *store.Store, next http.Handler, host string) (http.Handler, string
}
var setupPage = template.Must(template.New("setup").Parse(`<!doctype html>
<html lang="zh-CN"><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>AgentDB · 首次设置</title>
<html lang="zh-CN"><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>serviceport · 首次设置</title>
<style>body{margin:0;background:#f2f5fa;color:#18304a;font:16px system-ui;display:grid;min-height:100vh;place-items:center}main{box-sizing:border-box;background:white;padding:36px;border-radius:18px;box-shadow:0 12px 50px #18304a12;width:min(440px,92vw)}h1{margin-top:0}p{color:#62758a;line-height:1.6}label{display:block;margin-top:18px}input,button{box-sizing:border-box;width:100%;padding:12px;border-radius:8px;font:inherit}input{margin-top:7px;border:1px solid #cbd5e1}button{margin-top:24px;border:0;background:#2563eb;color:white;cursor:pointer}.error{color:#b42318}</style>
<main><h1>欢迎使用 AgentDB</h1><p>设置管理员账号后即可管理 MySQL、MongoDB 和 SSH 代理。下次启动直接登录。</p>
<main><h1>欢迎使用 serviceport</h1><p>设置管理员账号后即可管理 MySQL、MongoDB 和 SSH 代理。下次启动直接登录。</p>
{{if .}}<p class="error" role="alert">{{.}}</p>{{end}}
<form method="post"><label>管理员用户名<input name="username" autocomplete="username" maxlength="128" value="admin" required></label><label>管理员密码<input type="password" name="password" autocomplete="new-password" minlength="12" maxlength="72" required></label><label>确认密码<input type="password" name="confirm" autocomplete="new-password" minlength="12" maxlength="72" required></label><p>密码须为 12–72 字节(建议使用英文、数字和符号)。</p><button type="submit">保存并进入登录页面</button></form></main></html>`))
+2 -2
View File
@@ -1,8 +1,8 @@
package desktop
import (
"agentdb/internal/security"
"agentdb/internal/store"
"serviceport/internal/security"
"serviceport/internal/store"
"encoding/base64"
"net/http"
"net/http/httptest"
+3 -3
View File
@@ -1,9 +1,9 @@
package gateway
import (
"agentdb/internal/model"
"agentdb/internal/outbound"
"agentdb/internal/store"
"serviceport/internal/model"
"serviceport/internal/outbound"
"serviceport/internal/store"
"context"
"crypto/tls"
"errors"
+3 -3
View File
@@ -1,9 +1,9 @@
package gateway
import (
"agentdb/internal/model"
"agentdb/internal/security"
"agentdb/internal/store"
"serviceport/internal/model"
"serviceport/internal/security"
"serviceport/internal/store"
"bytes"
"context"
"crypto/tls"
+1 -1
View File
@@ -32,7 +32,7 @@ func Generate(dir string, validity time.Duration) error {
if err != nil {
return err
}
cert := &x509.Certificate{SerialNumber: serial, Subject: pkix.Name{CommonName: "AgentDB localhost development"}, DNSNames: []string{"localhost"}, IPAddresses: []net.IP{net.ParseIP("127.0.0.1"), net.ParseIP("::1")}, NotBefore: time.Now().Add(-time.Minute), NotAfter: time.Now().Add(validity), KeyUsage: x509.KeyUsageDigitalSignature | x509.KeyUsageCertSign, ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth}, BasicConstraintsValid: true, IsCA: true}
cert := &x509.Certificate{SerialNumber: serial, Subject: pkix.Name{CommonName: "serviceport localhost development"}, DNSNames: []string{"localhost"}, IPAddresses: []net.IP{net.ParseIP("127.0.0.1"), net.ParseIP("::1")}, NotBefore: time.Now().Add(-time.Minute), NotAfter: time.Now().Add(validity), KeyUsage: x509.KeyUsageDigitalSignature | x509.KeyUsageCertSign, ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth}, BasicConstraintsValid: true, IsCA: true}
der, err := x509.CreateCertificate(rand.Reader, cert, cert, &key.PublicKey, key)
if err != nil {
return err
+1 -1
View File
@@ -1,7 +1,7 @@
package outbound
import (
"agentdb/internal/model"
"serviceport/internal/model"
"context"
"encoding/binary"
"errors"
+1 -1
View File
@@ -1,7 +1,7 @@
package outbound
import (
"agentdb/internal/model"
"serviceport/internal/model"
"bytes"
"context"
"encoding/binary"
+1 -1
View File
@@ -1,7 +1,7 @@
package outbound
import (
"agentdb/internal/model"
"serviceport/internal/model"
"context"
"crypto/tls"
"errors"
+1 -1
View File
@@ -1,7 +1,7 @@
package outbound
import (
"agentdb/internal/model"
"serviceport/internal/model"
"context"
"errors"
"fmt"
+1 -1
View File
@@ -1,7 +1,7 @@
package outbound
import (
"agentdb/internal/model"
"serviceport/internal/model"
"context"
"crypto/ed25519"
"crypto/rand"
@@ -1,7 +1,7 @@
package mongoproxy
import (
"agentdb/internal/model"
"serviceport/internal/model"
"bytes"
"context"
"encoding/binary"
+1 -1
View File
@@ -1,7 +1,7 @@
package mongoproxy
import (
"agentdb/internal/model"
"serviceport/internal/model"
"context"
"crypto/tls"
"go.mongodb.org/mongo-driver/v2/bson"
+1 -1
View File
@@ -14,7 +14,7 @@ import (
"sync/atomic"
"time"
"agentdb/internal/model"
"serviceport/internal/model"
"github.com/xdg-go/scram"
"go.mongodb.org/mongo-driver/v2/bson"
)
+1 -1
View File
@@ -16,7 +16,7 @@ import (
"testing"
"time"
"agentdb/internal/model"
"serviceport/internal/model"
"go.mongodb.org/mongo-driver/v2/bson"
)
@@ -10,20 +10,20 @@ import (
"testing"
"time"
"agentdb/internal/model"
"agentdb/internal/testutil"
"serviceport/internal/model"
"serviceport/internal/testutil"
"go.mongodb.org/mongo-driver/v2/bson"
"go.mongodb.org/mongo-driver/v2/mongo"
"go.mongodb.org/mongo-driver/v2/mongo/options"
)
// Run against each version fixture with AGENTDB_MONGO_TEST_ADDR=host:port,
// AGENTDB_MONGO_TEST_USER and AGENTDB_MONGO_TEST_PASSWORD. Tests create and drop
// an isolated database. TLS is enabled when AGENTDB_MONGO_TEST_CA contains PEM.
// Run against each version fixture with SERVICEPORT_MONGO_TEST_ADDR=host:port,
// SERVICEPORT_MONGO_TEST_USER and SERVICEPORT_MONGO_TEST_PASSWORD. Tests create and drop
// an isolated database. TLS is enabled when SERVICEPORT_MONGO_TEST_CA contains PEM.
func TestRealMongo(t *testing.T) {
address := os.Getenv("AGENTDB_MONGO_TEST_ADDR")
address := os.Getenv("SERVICEPORT_MONGO_TEST_ADDR")
if address == "" {
t.Skip("set AGENTDB_MONGO_TEST_ADDR for real MongoDB version verification")
t.Skip("set SERVICEPORT_MONGO_TEST_ADDR for real MongoDB version verification")
}
host, portText, e := net.SplitHostPort(address)
if e != nil {
@@ -36,7 +36,7 @@ func TestRealMongo(t *testing.T) {
ctx, cancel := context.WithTimeout(context.Background(), 60*time.Second)
defer cancel()
serverTLS, roots := testTLS(t)
p := model.Proxy{TargetHost: host, TargetPort: port, TargetUser: os.Getenv("AGENTDB_MONGO_TEST_USER"), TargetPassword: os.Getenv("AGENTDB_MONGO_TEST_PASSWORD"), AuthSource: "admin", GatewayUser: "gateway", GatewayPassword: "gateway-test-secret", TargetCA: os.Getenv("AGENTDB_MONGO_TEST_CA")}
p := model.Proxy{TargetHost: host, TargetPort: port, TargetUser: os.Getenv("SERVICEPORT_MONGO_TEST_USER"), TargetPassword: os.Getenv("SERVICEPORT_MONGO_TEST_PASSWORD"), AuthSource: "admin", GatewayUser: "gateway", GatewayPassword: "gateway-test-secret", TargetCA: os.Getenv("SERVICEPORT_MONGO_TEST_CA")}
dial := testutil.FixtureDial(t, &p)
p.TargetTLS = p.TargetCA != ""
info, e := New().Test(ctx, p, dial)
@@ -91,7 +91,7 @@ func TestRealMongo(t *testing.T) {
t.Fatal(e)
}
}
dbName := "agentdb_gateway_test_" + strconv.FormatInt(time.Now().UnixNano(), 10)
dbName := "serviceport_gateway_test_" + strconv.FormatInt(time.Now().UnixNano(), 10)
rw := clients["readwrite"].Database(dbName)
ro := clients["readonly"].Database(dbName)
defer rw.Drop(context.Background())
+1 -1
View File
@@ -6,7 +6,7 @@ import (
"strings"
)
var errReadonly = errors.New("request rejected by AgentDB read-only policy")
var errReadonly = errors.New("request rejected by serviceport read-only policy")
func set(s string) map[string]bool {
m := map[string]bool{}
+1 -1
View File
@@ -1,7 +1,7 @@
package mysqlproxy
import (
"agentdb/internal/model"
"serviceport/internal/model"
"context"
"encoding/binary"
"fmt"
+1 -1
View File
@@ -1,7 +1,7 @@
package mysqlproxy
import (
"agentdb/internal/model"
"serviceport/internal/model"
"errors"
"github.com/go-mysql-org/go-mysql/client"
"strings"
@@ -1,7 +1,7 @@
package mysqlproxy
import (
"agentdb/internal/model"
"serviceport/internal/model"
"bytes"
"compress/zlib"
"context"
+3 -3
View File
@@ -2,7 +2,7 @@
package mysqlproxy
import (
"agentdb/internal/model"
"serviceport/internal/model"
"bytes"
"context"
"crypto/tls"
@@ -128,7 +128,7 @@ func (h *Handler) Serve(ctx context.Context, raw net.Conn, p model.Proxy, dial m
defer stop()
_ = raw.SetDeadline(time.Now().Add(15 * time.Second))
hh := &handshakeHandler{}
s := server.NewServer("8.0.11-AgentDB", 45, mysql.AUTH_NATIVE_PASSWORD, nil, tc)
s := server.NewServer("8.0.11-serviceport", 45, mysql.AUTH_NATIVE_PASSWORD, nil, tc)
downstream, e := s.NewConn(&clientGreetingConn{Conn: raw}, p.GatewayUser, p.GatewayPassword, hh)
if e != nil {
return e
@@ -160,7 +160,7 @@ func (h *Handler) Serve(ctx context.Context, raw net.Conn, p model.Proxy, dial m
}
event(category, "mysql:"+rule)
}
return downstream.WriteValue(mysql.NewError(mysql.ER_OPTION_PREVENTS_STATEMENT, "AgentDB rejected request: "+rule))
return downstream.WriteValue(mysql.NewError(mysql.ER_OPTION_PREVENTS_STATEMENT, "serviceport rejected request: "+rule))
}
for {
data, e := downstream.ReadPacket()
+1 -1
View File
@@ -1,7 +1,7 @@
package mysqlproxy
import (
"agentdb/internal/model"
"serviceport/internal/model"
"context"
"crypto/ecdsa"
"crypto/elliptic"
@@ -1,8 +1,8 @@
package mysqlproxy
import (
"agentdb/internal/model"
"agentdb/internal/testutil"
"serviceport/internal/model"
"serviceport/internal/testutil"
"context"
"crypto/tls"
"crypto/x509"
@@ -21,9 +21,9 @@ import (
// TestRealMySQL is opt-in and runs unchanged against the 5.7, 8.0 and 8.4
// fixtures. The fixture account must be able to create a temporary database.
func TestRealMySQL(t *testing.T) {
address := os.Getenv("AGENTDB_MYSQL_TEST_ADDR")
address := os.Getenv("SERVICEPORT_MYSQL_TEST_ADDR")
if address == "" {
t.Skip("set AGENTDB_MYSQL_TEST_ADDR, USER, PASSWORD and optional CA")
t.Skip("set SERVICEPORT_MYSQL_TEST_ADDR, USER, PASSWORD and optional CA")
}
host, port, e := net.SplitHostPort(address)
if e != nil {
@@ -33,10 +33,10 @@ func TestRealMySQL(t *testing.T) {
if e != nil {
t.Fatal(e)
}
p := model.Proxy{TargetHost: host, TargetPort: pn, TargetUser: os.Getenv("AGENTDB_MYSQL_TEST_USER"), TargetPassword: os.Getenv("AGENTDB_MYSQL_TEST_PASSWORD"), GatewayUser: "integration", GatewayPassword: "integration-password"}
p.TargetCompression = os.Getenv("AGENTDB_MYSQL_TEST_COMPRESSION")
p := model.Proxy{TargetHost: host, TargetPort: pn, TargetUser: os.Getenv("SERVICEPORT_MYSQL_TEST_USER"), TargetPassword: os.Getenv("SERVICEPORT_MYSQL_TEST_PASSWORD"), GatewayUser: "integration", GatewayPassword: "integration-password"}
p.TargetCompression = os.Getenv("SERVICEPORT_MYSQL_TEST_COMPRESSION")
var targetTLS *tls.Config
if path := os.Getenv("AGENTDB_MYSQL_TEST_CA"); path != "" {
if path := os.Getenv("SERVICEPORT_MYSQL_TEST_CA"); path != "" {
pem, e := os.ReadFile(path)
if e != nil {
t.Fatal(e)
@@ -65,7 +65,7 @@ func TestRealMySQL(t *testing.T) {
wrong.Close()
t.Fatal("invalid upstream password accepted")
}
database := fmt.Sprintf("agentdb_test_%d", time.Now().UnixNano())
database := fmt.Sprintf("serviceport_test_%d", time.Now().UnixNano())
if _, e = control.Execute("CREATE DATABASE " + database + " CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci"); e != nil {
t.Fatal(e)
}
+3 -3
View File
@@ -1,8 +1,8 @@
package pgproxy
import (
"agentdb/internal/model"
"agentdb/internal/protocol/transport"
"serviceport/internal/model"
"serviceport/internal/protocol/transport"
"context"
"crypto/subtle"
"crypto/tls"
@@ -40,7 +40,7 @@ func open(ctx context.Context, p model.Proxy, dial model.DialFunc, database stri
cfg.DialFunc = pgconn.DialFunc(dial)
// Keep DNS resolution on the configured proxy/jump host.
cfg.LookupFunc = func(_ context.Context, host string) ([]string, error) { return []string{host}, nil }
cfg.RuntimeParams = map[string]string{"application_name": "AgentDB", "client_encoding": "UTF8"}
cfg.RuntimeParams = map[string]string{"application_name": "serviceport", "client_encoding": "UTF8"}
return pgconn.ConnectConfig(ctx, cfg)
}
func (h *Handler) Test(ctx context.Context, p model.Proxy, dial model.DialFunc) (model.TestResult, error) {
+1 -1
View File
@@ -1,7 +1,7 @@
package pgproxy
import (
"agentdb/internal/model"
"serviceport/internal/model"
"context"
"github.com/jackc/pgx/v5/pgconn"
"github.com/jackc/pgx/v5/pgproto3"
+2 -2
View File
@@ -1,8 +1,8 @@
package rabbitproxy
import (
"agentdb/internal/model"
"agentdb/internal/protocol/transport"
"serviceport/internal/model"
"serviceport/internal/protocol/transport"
"context"
"crypto/subtle"
"crypto/tls"
+1 -1
View File
@@ -1,7 +1,7 @@
package rabbitproxy
import (
"agentdb/internal/model"
"serviceport/internal/model"
"context"
"encoding/binary"
amqp "github.com/rabbitmq/amqp091-go"
+2 -2
View File
@@ -1,8 +1,8 @@
package redisproxy
import (
"agentdb/internal/model"
"agentdb/internal/protocol/transport"
"serviceport/internal/model"
"serviceport/internal/protocol/transport"
"bufio"
"context"
"crypto/subtle"
+2 -2
View File
@@ -1,8 +1,8 @@
package redisproxy
import (
"agentdb/internal/model"
"agentdb/internal/protocol/transport"
"serviceport/internal/model"
"serviceport/internal/protocol/transport"
"bufio"
"bytes"
"context"
+1 -1
View File
@@ -1,7 +1,7 @@
package sshproxy
import (
"agentdb/internal/model"
"serviceport/internal/model"
"context"
"encoding/binary"
"fmt"
+1 -1
View File
@@ -1,7 +1,7 @@
package sshproxy
import (
"agentdb/internal/model"
"serviceport/internal/model"
"bytes"
"context"
"encoding/binary"
@@ -1,8 +1,8 @@
package sshproxy
import (
"agentdb/internal/model"
"agentdb/internal/testutil"
"serviceport/internal/model"
"serviceport/internal/testutil"
"bytes"
"context"
"errors"
@@ -20,9 +20,9 @@ import (
// TestRealSSH writes only a uniquely named temporary file on the disposable
// OpenSSH fixture, then removes it. No production host should be used here.
func TestRealSSH(t *testing.T) {
address := os.Getenv("AGENTDB_SSH_TEST_ADDR")
address := os.Getenv("SERVICEPORT_SSH_TEST_ADDR")
if address == "" {
t.Skip("set AGENTDB_SSH_TEST_ADDR/USER/PASSWORD for OpenSSH fixture")
t.Skip("set SERVICEPORT_SSH_TEST_ADDR/USER/PASSWORD for OpenSSH fixture")
}
host, port, err := net.SplitHostPort(address)
if err != nil {
@@ -33,7 +33,7 @@ func TestRealSSH(t *testing.T) {
t.Fatal(err)
}
hostKey, signer := testSigner(t)
p := model.Proxy{TargetHost: host, TargetPort: n, TargetUser: os.Getenv("AGENTDB_SSH_TEST_USER"), TargetPassword: os.Getenv("AGENTDB_SSH_TEST_PASSWORD"), GatewayUser: "integration", GatewayPassword: "disposable-gateway-password", SSH: model.SSHConfig{AuthMethod: "password", HostKey: hostKey}}
p := model.Proxy{TargetHost: host, TargetPort: n, TargetUser: os.Getenv("SERVICEPORT_SSH_TEST_USER"), TargetPassword: os.Getenv("SERVICEPORT_SSH_TEST_PASSWORD"), GatewayUser: "integration", GatewayPassword: "disposable-gateway-password", SSH: model.SSHConfig{AuthMethod: "password", HostKey: hostKey}}
dial := testutil.FixtureDial(t, &p)
ctx, cancel := context.WithTimeout(context.Background(), 40*time.Second)
defer cancel()
@@ -77,10 +77,10 @@ func TestRealSSH(t *testing.T) {
if err != nil {
t.Fatal(err)
}
output, err := session.Output("printf agentdb-fixture; exit 7")
output, err := session.Output("printf serviceport-fixture; exit 7")
session.Close()
var exit *ssh.ExitError
if string(output) != "agentdb-fixture" || !errors.As(err, &exit) || exit.ExitStatus() != 7 {
if string(output) != "serviceport-fixture" || !errors.As(err, &exit) || exit.ExitStatus() != 7 {
t.Fatalf("exec output/exit mismatch: %q %v", output, err)
}
transfer, err := sftp.NewClient(client)
@@ -88,8 +88,8 @@ func TestRealSSH(t *testing.T) {
t.Fatal(err)
}
defer transfer.Close()
remote := fmt.Sprintf("/tmp/agentdb-fixture-%d", time.Now().UnixNano())
payload := []byte("AgentDB SFTP roundtrip 中文 😀\n")
remote := fmt.Sprintf("/tmp/serviceport-fixture-%d", time.Now().UnixNano())
payload := []byte("serviceport SFTP roundtrip 中文 😀\n")
f, err := transfer.OpenFile(remote, os.O_WRONLY|os.O_CREATE|os.O_EXCL)
if err != nil {
t.Fatal(err)
+3 -3
View File
@@ -21,7 +21,7 @@ import (
"sync"
"time"
"agentdb/internal/model"
"serviceport/internal/model"
"golang.org/x/crypto/ssh"
)
@@ -64,7 +64,7 @@ func serverConfig(p model.Proxy) (*ssh.ServerConfig, error) {
if signer.PublicKey().Type() != ssh.KeyAlgoED25519 {
return nil, errors.New("gateway host key must be Ed25519")
}
cfg := &ssh.ServerConfig{Config: algorithms(), MaxAuthTries: 5, ServerVersion: "SSH-2.0-AgentDB", PublicKeyAuthAlgorithms: []string{ssh.KeyAlgoED25519, ssh.KeyAlgoRSASHA256, ssh.KeyAlgoRSASHA512}}
cfg := &ssh.ServerConfig{Config: algorithms(), MaxAuthTries: 5, ServerVersion: "SSH-2.0-serviceport", PublicKeyAuthAlgorithms: []string{ssh.KeyAlgoED25519, ssh.KeyAlgoRSASHA256, ssh.KeyAlgoRSASHA512}}
cfg.AddHostKey(signer)
if p.GatewayPassword != "" {
cfg.PasswordCallback = func(c ssh.ConnMetadata, password []byte) (*ssh.Permissions, error) {
@@ -193,7 +193,7 @@ func (h *Handler) Test(ctx context.Context, p model.Proxy, dial model.DialFunc)
func Probe(ctx context.Context, p model.Proxy, dial model.DialFunc) (model.TestResult, error) {
var result model.TestResult
captured := errors.New("host key captured")
cfg := &ssh.ClientConfig{Config: algorithms(), User: "agentdb-host-key-probe", HostKeyAlgorithms: []string{ssh.KeyAlgoED25519, ssh.KeyAlgoRSASHA512, ssh.KeyAlgoRSASHA256}, HostKeyCallback: func(_ string, _ net.Addr, key ssh.PublicKey) error {
cfg := &ssh.ClientConfig{Config: algorithms(), User: "serviceport-host-key-probe", HostKeyAlgorithms: []string{ssh.KeyAlgoED25519, ssh.KeyAlgoRSASHA512, ssh.KeyAlgoRSASHA256}, HostKeyCallback: func(_ string, _ net.Addr, key ssh.PublicKey) error {
result.Fingerprint = ssh.FingerprintSHA256(key)
result.HostKey = string(ssh.MarshalAuthorizedKey(key))
return captured
+1 -1
View File
@@ -13,7 +13,7 @@ import (
"testing"
"time"
"agentdb/internal/model"
"serviceport/internal/model"
"golang.org/x/crypto/ssh"
)
+1 -1
View File
@@ -1,7 +1,7 @@
package transport
import (
"agentdb/internal/model"
"serviceport/internal/model"
"bufio"
"context"
"crypto/tls"
+1 -1
View File
@@ -14,7 +14,7 @@ type Vault struct{ aead cipher.AEAD }
func NewVault(key string) (*Vault, error) {
b, err := base64.StdEncoding.DecodeString(key)
if err != nil || len(b) != 32 {
return nil, errors.New("AGENTDB_MASTER_KEY must be base64 encoding of 32 random bytes")
return nil, errors.New("SERVICEPORT_MASTER_KEY must be base64 encoding of 32 random bytes")
}
block, err := aes.NewCipher(b)
if err != nil {
+3 -3
View File
@@ -1,8 +1,8 @@
package store
import (
"agentdb/internal/model"
"agentdb/internal/security"
"serviceport/internal/model"
"serviceport/internal/security"
"bytes"
"context"
"database/sql"
@@ -22,7 +22,7 @@ func shardFixture(t *testing.T) (*Store, string, *security.Vault) {
if err != nil {
t.Fatal(err)
}
path := filepath.Join(t.TempDir(), "agentdb.sqlite")
path := filepath.Join(t.TempDir(), "serviceport.sqlite")
s, err := Open(path, v)
if err != nil {
t.Fatal(err)
+2 -2
View File
@@ -1,8 +1,8 @@
package store
import (
"agentdb/internal/model"
"agentdb/internal/security"
"serviceport/internal/model"
"serviceport/internal/security"
"context"
"database/sql"
"errors"
+2 -2
View File
@@ -1,8 +1,8 @@
package store
import (
"agentdb/internal/model"
"agentdb/internal/security"
"serviceport/internal/model"
"serviceport/internal/security"
"bytes"
"encoding/base64"
"path/filepath"
+1 -1
View File
@@ -1,7 +1,7 @@
package store
import (
"agentdb/internal/security"
"serviceport/internal/security"
"context"
"encoding/base64"
"path/filepath"
+2 -2
View File
@@ -1,8 +1,8 @@
package store
import (
"agentdb/internal/model"
"agentdb/internal/security"
"serviceport/internal/model"
"serviceport/internal/security"
"encoding/base64"
"path/filepath"
"strings"
+2 -2
View File
@@ -1,8 +1,8 @@
package store
import (
"agentdb/internal/model"
"agentdb/internal/security"
"serviceport/internal/model"
"serviceport/internal/security"
"database/sql"
"errors"
)
+3 -3
View File
@@ -1,8 +1,8 @@
package store
import (
"agentdb/internal/model"
"agentdb/internal/security"
"serviceport/internal/model"
"serviceport/internal/security"
"bytes"
"encoding/base64"
"path/filepath"
@@ -11,7 +11,7 @@ import (
func outboundTestStore(t *testing.T) (*Store, string, *security.Vault) {
t.Helper()
path := filepath.Join(t.TempDir(), "agentdb.sqlite")
path := filepath.Join(t.TempDir(), "serviceport.sqlite")
vault, err := security.NewVault(base64.StdEncoding.EncodeToString(bytes.Repeat([]byte{3}, 32)))
if err != nil {
t.Fatal(err)
+1 -1
View File
@@ -1,7 +1,7 @@
package store
import (
"agentdb/internal/model"
"serviceport/internal/model"
"database/sql"
"encoding/json"
"errors"
+2 -2
View File
@@ -1,7 +1,7 @@
package store
import (
"agentdb/internal/security"
"serviceport/internal/security"
"database/sql"
"path/filepath"
"sync"
@@ -98,7 +98,7 @@ func (s *Store) InitAdmin(user, password string) error {
return nil
}
if len(user) == 0 || len(password) < 12 || len(password) > 72 {
return errors.New("first startup requires AGENTDB_ADMIN_USER and AGENTDB_ADMIN_PASSWORD (12-72 bytes)")
return errors.New("first startup requires SERVICEPORT_ADMIN_USER and SERVICEPORT_ADMIN_PASSWORD (12-72 bytes)")
}
hash, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost)
if err != nil {
+3 -3
View File
@@ -1,8 +1,8 @@
package store
import (
"agentdb/internal/model"
"agentdb/internal/security"
"serviceport/internal/model"
"serviceport/internal/security"
"bytes"
"encoding/base64"
"os"
@@ -11,7 +11,7 @@ import (
)
func TestEncryptedPersistenceAndAdminReset(t *testing.T) {
path := filepath.Join(t.TempDir(), "agentdb.sqlite")
path := filepath.Join(t.TempDir(), "serviceport.sqlite")
vault, err := security.NewVault(base64.StdEncoding.EncodeToString(bytes.Repeat([]byte{1}, 32)))
if err != nil {
t.Fatal(err)
+5 -5
View File
@@ -2,8 +2,8 @@
package testutil
import (
"agentdb/internal/model"
"agentdb/internal/outbound"
"serviceport/internal/model"
"serviceport/internal/outbound"
"net"
"os"
"strconv"
@@ -15,7 +15,7 @@ import (
func FixtureDial(t testing.TB, p *model.Proxy) model.DialFunc {
t.Helper()
p.Outbound = model.Outbound{Mode: "direct"}
if address := os.Getenv("AGENTDB_TEST_SOCKS_ADDR"); address != "" {
if address := os.Getenv("SERVICEPORT_TEST_SOCKS_ADDR"); address != "" {
host, port, err := net.SplitHostPort(address)
if err != nil {
t.Fatal(err)
@@ -25,10 +25,10 @@ func FixtureDial(t testing.TB, p *model.Proxy) model.DialFunc {
t.Fatal(err)
}
p.Outbound = model.Outbound{Mode: "socks5", Host: host, Port: n, Auth: "none"}
if user := os.Getenv("AGENTDB_TEST_SOCKS_USER"); user != "" {
if user := os.Getenv("SERVICEPORT_TEST_SOCKS_USER"); user != "" {
p.Outbound.Auth = "password"
p.Outbound.Username = user
p.Outbound.Password = os.Getenv("AGENTDB_TEST_SOCKS_PASSWORD")
p.Outbound.Password = os.Getenv("SERVICEPORT_TEST_SOCKS_PASSWORD")
}
}
return outbound.Dialer(*p)
+13 -13
View File
@@ -1,14 +1,14 @@
# AgentDB Windows 双击运行包
# serviceport Windows 双击运行包
适用于 Windows x64,无需安装 Go 或 Node.js。网页界面已嵌入 `agentdb.exe`。
适用于 Windows x64,无需安装 Go 或 Node.js。网页界面已嵌入 `serviceport.exe`。
顶部版本入口及 `agentdb.exe --version --json` 显示当前版本、Git 提交与构建时间。Kubernetes 出口内置 port-forward,无需安装 kubectl;目标服务可配置 kubeconfig 路径、context、命名空间与 Pod/Service/Deployment,详见 [Kubernetes](docs/kubernetes.md)。
顶部版本入口及 `serviceport.exe --version --json` 显示当前版本、Git 提交与构建时间。Kubernetes 出口内置 port-forward,无需安装 kubectl;目标服务可配置 kubeconfig 路径、context、命名空间与 Pod/Service/Deployment,详见 [Kubernetes](docs/kubernetes.md)。
## 启动
「代理」菜单独立维护通用出口;目标服务通过下拉框选择代理或直接连接。多个目标可复用同一代理。代理被引用时禁止删除,修改前先停止所有关联网关;旧目标的内联出口自动迁移为独立加密配置。
1. 完整解压 ZIP,双击 `start-dev.cmd`(开发模式,数据保存到解压目录的 `data`)。也可以直接双击 `agentdb.exe` 使用下面说明的 `%APPDATA%\AgentDB` 数据目录。两种入口的数据目录不同,请固定使用同一种入口。
1. 完整解压 ZIP,双击 `start-dev.cmd`(开发模式,数据保存到解压目录的 `data`)。也可以直接双击 `serviceport.exe` 使用下面说明的 `%APPDATA%\serviceport` 数据目录。两种入口的数据目录不同,请固定使用同一种入口。
2. 浏览器自动打开;开发阶段默认免登录,直接进入控制台。
3. 先添加目标服务,再创建网关并选择关联目标;多个网关可复用一个目标。以后双击即用。右上角支持浅色、深色、海蓝和跟随系统主题。
@@ -16,13 +16,13 @@
## 管理员登录(可选)
默认 `AGENTDB_AUTH_ENABLED=false`,仅允许本机管理,无需创建管理员。要启用登录,在 PowerShell 设置 `$env:AGENTDB_AUTH_ENABLED = 'true'` 后运行 `.\agentdb.exe`;首次会打开管理员设置页面,已有账号则直接登录。开启认证后密码仍使用 bcrypt 保存。
默认 `SERVICEPORT_AUTH_ENABLED=false`,仅允许本机管理,无需创建管理员。要启用登录,在 PowerShell 设置 `$env:SERVICEPORT_AUTH_ENABLED = 'true'` 后运行 `.\serviceport.exe`;首次会打开管理员设置页面,已有账号则直接登录。开启认证后密码仍使用 bcrypt 保存。
## 数据和备份
本地数据固定保存在 `%APPDATA%\AgentDB`,不依赖解压目录或启动时的工作目录。更新前停止服务并备份整个目录。0.2.0 自动将旧代理记录拆成目标与网关引用,保留凭据、SSH 指纹和启用状态;迁移后不要使用旧版程序直接打开新数据库。
本地数据固定保存在 `%APPDATA%\serviceport`,不依赖解压目录或启动时的工作目录。更新前停止服务并备份整个目录。0.2.0 自动将旧代理记录拆成目标与网关引用,保留凭据、SSH 指纹和启用状态;迁移后不要使用旧版程序直接打开新数据库。
- `agentdb.sqlite`:可选管理员 bcrypt 密码、独立加密的目标/网关配置。
- `serviceport.sqlite`:可选管理员 bcrypt 密码、独立加密的目标/网关配置。
- `audit/YYYY-MM-DD.sqlite`:按 UTC 日期分库的命令审计与连接事件,升级时自动迁移旧日志。
- `master-key.dpapi`:Windows DPAPI 加密的主密钥,绑定当前 Windows 用户。
- `tls/`:自动生成的本机数据库网关证书和私钥。
@@ -30,26 +30,26 @@
停止服务后备份整个数据目录。不要单独删除密钥;密钥丢失后原代理配置无法解密。DPAPI 文件不能直接迁移给另一 Windows 用户或另一台电脑。跨机器部署请使用服务器模式,自行保管并备份主密钥。
管理员登录密码不可查看,通过在解压目录运行 `.\agentdb.exe reset-password` 交互重置(先停止服务)。目标服务、网关、SOCKS5 密码可分别查看;开启认证时需要再次输入管理员密码,开发模式直接查看。
管理员登录密码不可查看,通过在解压目录运行 `.\serviceport.exe reset-password` 交互重置(先停止服务)。目标服务、网关、SOCKS5 密码可分别查看;开启认证时需要再次输入管理员密码,开发模式直接查看。
## 数据库 TLS
双击模式自动生成有效期一年的本机自签名证书,覆盖 `localhost`、`127.0.0.1` 和 `::1`。MySQL/MongoDB 客户端使用 `%APPDATA%\AgentDB\tls\cert.pem` 作为 CA;SSH 使用自身主机密钥。
双击模式自动生成有效期一年的本机自签名证书,覆盖 `localhost`、`127.0.0.1` 和 `::1`。MySQL/MongoDB 客户端使用 `%APPDATA%\serviceport\tls\cert.pem` 作为 CA;SSH 使用自身主机密钥。
证书到期前,停止服务并备份、移走 `tls` 目录,下次启动会生成新证书,需要更新客户端 CA。局域网访问代理时,应配置覆盖实际网关域名的受信任证书,通过 `AGENTDB_TLS_CERT` 和 `AGENTDB_TLS_KEY` 指定。
证书到期前,停止服务并备份、移走 `tls` 目录,下次启动会生成新证书,需要更新客户端 CA。局域网访问代理时,应配置覆盖实际网关域名的受信任证书,通过 `SERVICEPORT_TLS_CERT` 和 `SERVICEPORT_TLS_KEY` 指定。
## 高级配置
设置 `AGENTDB_MASTER_KEY` 后使用手动密钥模式,不自动生成密钥、不自动打开浏览器,启用 `AGENTDB_AUTH_ENABLED=true` 时首次启动需提供 `AGENTDB_ADMIN_USER` / `AGENTDB_ADMIN_PASSWORD`。`agentdb.exe generate-key` 可生成主密钥,必须安全保存并在每次启动复用。
设置 `SERVICEPORT_MASTER_KEY` 后使用手动密钥模式,不自动生成密钥、不自动打开浏览器,启用 `SERVICEPORT_AUTH_ENABLED=true` 时首次启动需提供 `SERVICEPORT_ADMIN_USER` / `SERVICEPORT_ADMIN_PASSWORD`。`serviceport.exe generate-key` 可生成主密钥,必须安全保存并在每次启动复用。
程序不自动读取 `.env`;`.env.example` 是环境变量参考。本地模式可设置 `AGENTDB_DATA` 指定数据库路径,密钥与 TLS 目录跟随该数据库目录;`AGENTDB_HTTP_ADDR` 必须使用回环 IP,默认 `127.0.0.1:8080`;`AGENTDB_NO_BROWSER=1` 禁止自动打开浏览器。
程序不自动读取 `.env`;`.env.example` 是环境变量参考。本地模式可设置 `SERVICEPORT_DATA` 指定数据库路径,密钥与 TLS 目录跟随该数据库目录;`SERVICEPORT_HTTP_ADDR` 必须使用回环 IP,默认 `127.0.0.1:8080`;`SERVICEPORT_NO_BROWSER=1` 禁止自动打开浏览器。
## 使用说明
- 六类目标服务支持直连、SOCKS5 或 SSH 转发出口;网关引用目标,MySQL、MongoDB 网关默认读写,可选只读。
- SSH 保存配置后需探测主机指纹,通过可信渠道核对并确认后才能启动。
- 修改目标前先停止所有引用网关;被引用的目标不能删除。修改网关前先停止该网关,停止会断开已有客户端。
- 管理服务默认仅本机访问。远程管理必须设置 `AGENTDB_AUTH_ENABLED=true`,并配置 HTTPS 反向代理。
- 管理服务默认仅本机访问。远程管理必须设置 `SERVICEPORT_AUTH_ENABLED=true`,并配置 HTTPS 反向代理。
包内 `devcert.exe` 为独立开发证书工具(有效期 14 天),双击模式无需手动运行它。`SHA256SUMS.txt` 校验包内文件,发行包旁 `.zip.sha256` 校验整个压缩包。
+2 -2
View File
@@ -1,7 +1,7 @@
param([string]$Version)
$ErrorActionPreference = 'Stop'
. (Join-Path $PSScriptRoot 'version.ps1')
$buildInfo = Get-AgentDBBuildInfo -Version $Version
$buildInfo = Get-serviceportBuildInfo -Version $Version
Set-Location (Split-Path $PSScriptRoot -Parent)
$env:GOTOOLCHAIN = 'go1.25.0'
$env:GO111MODULE = 'on'
@@ -23,5 +23,5 @@ try {
& $goExecutable test ./... -count=1 -timeout=120s
if ($LASTEXITCODE -ne 0) { throw 'Go tests failed' }
New-Item -ItemType Directory -Force bin | Out-Null
& $goExecutable build -trimpath -ldflags $buildInfo.LDFlags -o bin/agentdb.exe ./cmd/agentdb
& $goExecutable build -trimpath -ldflags $buildInfo.LDFlags -o bin/serviceport.exe ./cmd/serviceport
if ($LASTEXITCODE -ne 0) { throw 'Go build failed' }
+1 -1
View File
@@ -15,4 +15,4 @@ npm --prefix web ci --no-audit --no-fund
npm --prefix web run build
go test ./...
mkdir -p bin
go build -trimpath -ldflags "-s -w -X agentdb/internal/buildinfo.Version=$version -X agentdb/internal/buildinfo.Commit=$commit -X agentdb/internal/buildinfo.BuildTime=$build_time" -o bin/agentdb ./cmd/agentdb
go build -trimpath -ldflags "-s -w -X serviceport/internal/buildinfo.Version=$version -X serviceport/internal/buildinfo.Commit=$commit -X serviceport/internal/buildinfo.BuildTime=$build_time" -o bin/serviceport ./cmd/serviceport
+6 -6
View File
@@ -7,7 +7,7 @@ function Write-PhaseTime([string]$Name) {
$phaseTimer.Restart()
}
. (Join-Path $PSScriptRoot 'version.ps1')
$buildInfo = Get-AgentDBBuildInfo -Version $Version
$buildInfo = Get-serviceportBuildInfo -Version $Version
$Version = $buildInfo.Version
$repo = Split-Path $PSScriptRoot -Parent
$goCommand = Get-Command go -ErrorAction SilentlyContinue
@@ -15,12 +15,12 @@ $goExecutable = if ($goCommand) { $goCommand.Source } else { 'C:\Program Files\G
if (!(Test-Path -LiteralPath $goExecutable)) { throw 'Go is required' }
if (!(Get-Command npm.cmd -ErrorAction SilentlyContinue) -or !(Get-Command node -ErrorAction SilentlyContinue)) { throw 'Node.js and npm are required' }
$stamp = Get-Date -Format 'yyyyMMdd-HHmmss'
$name = "agentdb-$Version-windows-amd64"
$name = "serviceport-$Version-windows-amd64"
$releaseRoot = Join-Path $repo "artifacts/releases/$stamp"
$stage = Join-Path $releaseRoot $name
$archive = Join-Path $releaseRoot "$name.zip"
$previous = @{}
foreach ($key in @('GOTOOLCHAIN','GO111MODULE','GOOS','GOARCH','CGO_ENABLED','AGENTDB_NO_BROWSER')) { $previous[$key] = [Environment]::GetEnvironmentVariable($key, 'Process') }
foreach ($key in @('GOTOOLCHAIN','GO111MODULE','GOOS','GOARCH','CGO_ENABLED','SERVICEPORT_NO_BROWSER')) { $previous[$key] = [Environment]::GetEnvironmentVariable($key, 'Process') }
$keepPath = Join-Path $repo 'web/dist/.gitkeep'
$hadKeep = Test-Path -LiteralPath $keepPath
$keepBytes = [byte[]]@()
@@ -29,7 +29,7 @@ Push-Location $repo
try {
$env:GOTOOLCHAIN = 'go1.25.0'
$env:GO111MODULE = 'on'
$env:AGENTDB_NO_BROWSER = '1'
$env:SERVICEPORT_NO_BROWSER = '1'
$env:GOOS = 'windows'
$env:GOARCH = 'amd64'
$env:CGO_ENABLED = '0'
@@ -39,8 +39,8 @@ try {
if ($LASTEXITCODE -ne 0) { throw 'Frontend build failed' }
Write-PhaseTime 'Frontend and dependencies'
New-Item -ItemType Directory -Path (Join-Path $stage 'docs') -Force | Out-Null
& $goExecutable build -trimpath -ldflags $buildInfo.LDFlags -o (Join-Path $stage 'agentdb.exe') ./cmd/agentdb
if ($LASTEXITCODE -ne 0) { throw 'AgentDB release build failed' }
& $goExecutable build -trimpath -ldflags $buildInfo.LDFlags -o (Join-Path $stage 'serviceport.exe') ./cmd/serviceport
if ($LASTEXITCODE -ne 0) { throw 'serviceport release build failed' }
& $goExecutable build -trimpath -ldflags '-s -w' -o (Join-Path $stage 'devcert.exe') ./cmd/devcert
if ($LASTEXITCODE -ne 0) { throw 'Certificate utility build failed' }
Write-PhaseTime 'Go build'
+18 -18
View File
@@ -1,20 +1,20 @@
$ErrorActionPreference = 'Stop'
$projectRoot = Split-Path -Parent $PSScriptRoot
$buildScript = Join-Path $PSScriptRoot 'build.ps1'
$binary = Join-Path $projectRoot 'agentdb.exe'
$binary = Join-Path $projectRoot 'serviceport.exe'
$sourceMode = Test-Path -LiteralPath $buildScript
if ($sourceMode) {
$binary = Join-Path $projectRoot 'bin\agentdb.exe'
$binary = Join-Path $projectRoot 'bin\serviceport.exe'
} elseif (-not (Test-Path -LiteralPath $binary)) {
throw 'agentdb.exe is missing. Extract the complete release ZIP again.'
throw 'serviceport.exe is missing. Extract the complete release ZIP again.'
}
Write-Host 'Stopping previous AgentDB service...'
$ownedPaths = @((Join-Path $projectRoot 'agentdb.exe'), (Join-Path $projectRoot 'bin\agentdb.exe'))
Get-CimInstance Win32_Process -Filter "Name = 'agentdb.exe'" | Where-Object { $_.ExecutablePath -in $ownedPaths } | ForEach-Object {
Write-Host 'Stopping previous serviceport service...'
$ownedPaths = @((Join-Path $projectRoot 'serviceport.exe'), (Join-Path $projectRoot 'bin\serviceport.exe'))
Get-CimInstance Win32_Process -Filter "Name = 'serviceport.exe'" | Where-Object { $_.ExecutablePath -in $ownedPaths } | ForEach-Object {
$previousProcess = Get-Process -Id $_.ProcessId -ErrorAction SilentlyContinue
if ($previousProcess) {
$previousProcess.Kill()
if (-not $previousProcess.WaitForExit(10000)) { throw 'Previous AgentDB process did not exit.' }
if (-not $previousProcess.WaitForExit(10000)) { throw 'Previous serviceport process did not exit.' }
}
}
$dataDirectory = Join-Path $projectRoot 'data'
@@ -25,23 +25,23 @@ if (Test-Path -LiteralPath $dataDirectory) {
Write-Host "Data backup: $backup"
}
if ($sourceMode) {
Write-Host 'Building latest AgentDB (Go and Node.js required)...'
Write-Host 'Building latest serviceport (Go and Node.js required)...'
& powershell.exe -NoProfile -ExecutionPolicy Bypass -File $buildScript
if ($LASTEXITCODE -ne 0 -or -not (Test-Path -LiteralPath $binary)) {
throw 'Build failed. See the error above.'
}
}
# Use the same database and Windows-account-protected key on every launch.
$env:AGENTDB_DATA = Join-Path $projectRoot 'data\agentdb.sqlite'
$env:AGENTDB_MASTER_KEY = ''
$env:AGENTDB_AUTH_ENABLED = 'false'
$env:AGENTDB_ADMIN_USER = ''
$env:AGENTDB_ADMIN_PASSWORD = ''
$env:AGENTDB_HTTP_ADDR = '127.0.0.1:8080'
$env:AGENTDB_SECURE_COOKIE = 'false'
$env:AGENTDB_TLS_CERT = ''
$env:AGENTDB_TLS_KEY = ''
Write-Host "SQLite: $env:AGENTDB_DATA"
$env:SERVICEPORT_DATA = Join-Path $projectRoot 'data\serviceport.sqlite'
$env:SERVICEPORT_MASTER_KEY = ''
$env:SERVICEPORT_AUTH_ENABLED = 'false'
$env:SERVICEPORT_ADMIN_USER = ''
$env:SERVICEPORT_ADMIN_PASSWORD = ''
$env:SERVICEPORT_HTTP_ADDR = '127.0.0.1:8080'
$env:SERVICEPORT_SECURE_COOKIE = 'false'
$env:SERVICEPORT_TLS_CERT = ''
$env:SERVICEPORT_TLS_KEY = ''
Write-Host "SQLite: $env:SERVICEPORT_DATA"
Set-Location -LiteralPath $projectRoot
& $binary
exit $LASTEXITCODE
+20 -20
View File
@@ -2,14 +2,14 @@ param([switch]$KeepFixtures)
$ErrorActionPreference = 'Stop'
$repo = Split-Path $PSScriptRoot -Parent
$compose = Join-Path $repo 'deploy/compose.test.yml'
$project = 'agentdb-matrix-' + [Guid]::NewGuid().ToString('N').Substring(0, 10)
$project = 'serviceport-matrix-' + [Guid]::NewGuid().ToString('N').Substring(0, 10)
$tempDir = Join-Path ([IO.Path]::GetTempPath()) $project
$caFile = Join-Path $tempDir 'ca.pem'
$goCommand = Get-Command go -ErrorAction SilentlyContinue
$goExecutable = if ($goCommand) { $goCommand.Source } else { 'C:\Program Files\Go\bin\go.exe' }
if (!(Test-Path -LiteralPath $goExecutable)) { throw 'Go is required' }
Get-Command docker -ErrorAction Stop | Out-Null
$names = @('GOTOOLCHAIN', 'AGENTDB_MYSQL_TEST_ADDR', 'AGENTDB_MYSQL_TEST_USER', 'AGENTDB_MYSQL_TEST_PASSWORD', 'AGENTDB_MYSQL_TEST_CA', 'AGENTDB_MONGO_TEST_ADDR', 'AGENTDB_MONGO_TEST_USER', 'AGENTDB_MONGO_TEST_PASSWORD', 'AGENTDB_MONGO_TEST_CA', 'AGENTDB_SSH_TEST_ADDR', 'AGENTDB_SSH_TEST_USER', 'AGENTDB_SSH_TEST_PASSWORD', 'AGENTDB_TEST_SOCKS_ADDR', 'AGENTDB_TEST_SOCKS_USER', 'AGENTDB_TEST_SOCKS_PASSWORD')
$names = @('GOTOOLCHAIN', 'SERVICEPORT_MYSQL_TEST_ADDR', 'SERVICEPORT_MYSQL_TEST_USER', 'SERVICEPORT_MYSQL_TEST_PASSWORD', 'SERVICEPORT_MYSQL_TEST_CA', 'SERVICEPORT_MONGO_TEST_ADDR', 'SERVICEPORT_MONGO_TEST_USER', 'SERVICEPORT_MONGO_TEST_PASSWORD', 'SERVICEPORT_MONGO_TEST_CA', 'SERVICEPORT_SSH_TEST_ADDR', 'SERVICEPORT_SSH_TEST_USER', 'SERVICEPORT_SSH_TEST_PASSWORD', 'SERVICEPORT_TEST_SOCKS_ADDR', 'SERVICEPORT_TEST_SOCKS_USER', 'SERVICEPORT_TEST_SOCKS_PASSWORD')
$previous = @{}
foreach ($name in $names) { $previous[$name] = [Environment]::GetEnvironmentVariable($name, 'Process') }
New-Item -ItemType Directory -Path $tempDir | Out-Null
@@ -22,39 +22,39 @@ try {
if ($LASTEXITCODE -ne 0) { throw 'MongoDB replica initialization failed' }
docker compose -f $compose -p $project cp 'cert-init:/certs/ca.pem' $caFile
if ($LASTEXITCODE -ne 0) { throw 'Could not export test CA' }
$env:AGENTDB_MYSQL_TEST_USER = 'root'
$env:AGENTDB_MYSQL_TEST_PASSWORD = 'agentdb-test-password'
$env:AGENTDB_MYSQL_TEST_CA = $caFile
$env:AGENTDB_MONGO_TEST_USER = 'root'
$env:AGENTDB_MONGO_TEST_PASSWORD = 'agentdb-test-password'
$env:AGENTDB_MONGO_TEST_CA = Get-Content -LiteralPath $caFile -Raw
$env:AGENTDB_SSH_TEST_USER = 'agentdb'
$env:AGENTDB_SSH_TEST_PASSWORD = 'agentdb-test-password'
$env:SERVICEPORT_MYSQL_TEST_USER = 'root'
$env:SERVICEPORT_MYSQL_TEST_PASSWORD = 'serviceport-test-password'
$env:SERVICEPORT_MYSQL_TEST_CA = $caFile
$env:SERVICEPORT_MONGO_TEST_USER = 'root'
$env:SERVICEPORT_MONGO_TEST_PASSWORD = 'serviceport-test-password'
$env:SERVICEPORT_MONGO_TEST_CA = Get-Content -LiteralPath $caFile -Raw
$env:SERVICEPORT_SSH_TEST_USER = 'serviceport'
$env:SERVICEPORT_SSH_TEST_PASSWORD = 'serviceport-test-password'
$mysqlTargets = @(@('mysql57', 13357), @('mysql80', 13380), @('mysql84', 13384))
$mongoTargets = @(@('mongo60', 27060), @('mongo70', 27070), @('mongo80', 27080))
foreach ($route in @('direct', 'socks-open', 'socks-auth')) {
$env:AGENTDB_TEST_SOCKS_ADDR = ''
$env:AGENTDB_TEST_SOCKS_USER = ''
$env:AGENTDB_TEST_SOCKS_PASSWORD = ''
if ($route -eq 'socks-open') { $env:AGENTDB_TEST_SOCKS_ADDR = '127.0.0.1:11080' }
$env:SERVICEPORT_TEST_SOCKS_ADDR = ''
$env:SERVICEPORT_TEST_SOCKS_USER = ''
$env:SERVICEPORT_TEST_SOCKS_PASSWORD = ''
if ($route -eq 'socks-open') { $env:SERVICEPORT_TEST_SOCKS_ADDR = '127.0.0.1:11080' }
if ($route -eq 'socks-auth') {
$env:AGENTDB_TEST_SOCKS_ADDR = '127.0.0.1:11081'
$env:AGENTDB_TEST_SOCKS_USER = 'socks'
$env:AGENTDB_TEST_SOCKS_PASSWORD = 'socks-test-password'
$env:SERVICEPORT_TEST_SOCKS_ADDR = '127.0.0.1:11081'
$env:SERVICEPORT_TEST_SOCKS_USER = 'socks'
$env:SERVICEPORT_TEST_SOCKS_PASSWORD = 'socks-test-password'
}
foreach ($target in $mysqlTargets) {
$env:AGENTDB_MYSQL_TEST_ADDR = if ($route -eq 'direct') { '127.0.0.1:' + $target[1] } else { $target[0] + ':3306' }
$env:SERVICEPORT_MYSQL_TEST_ADDR = if ($route -eq 'direct') { '127.0.0.1:' + $target[1] } else { $target[0] + ':3306' }
Write-Host "Testing MySQL $($target[0]) via $route"
& $goExecutable test -count=1 -timeout=120s ./internal/protocol/mysqlproxy -run '^TestRealMySQL$' -v
if ($LASTEXITCODE -ne 0) { throw "MySQL matrix failed: $($target[0]) / $route" }
}
foreach ($target in $mongoTargets) {
$env:AGENTDB_MONGO_TEST_ADDR = if ($route -eq 'direct') { '127.0.0.1:' + $target[1] } else { $target[0] + ':27017' }
$env:SERVICEPORT_MONGO_TEST_ADDR = if ($route -eq 'direct') { '127.0.0.1:' + $target[1] } else { $target[0] + ':27017' }
Write-Host "Testing MongoDB $($target[0]) via $route"
& $goExecutable test -count=1 -timeout=120s ./internal/protocol/mongoproxy -run '^TestRealMongo$' -v
if ($LASTEXITCODE -ne 0) { throw "MongoDB matrix failed: $($target[0]) / $route" }
}
$env:AGENTDB_SSH_TEST_ADDR = if ($route -eq 'direct') { '127.0.0.1:12222' } else { 'ssh:22' }
$env:SERVICEPORT_SSH_TEST_ADDR = if ($route -eq 'direct') { '127.0.0.1:12222' } else { 'ssh:22' }
Write-Host "Testing OpenSSH and SFTP via $route"
& $goExecutable test -count=1 -timeout=120s ./internal/protocol/sshproxy -run '^TestRealSSH$' -v
if ($LASTEXITCODE -ne 0) { throw "SSH matrix failed: $route" }
+15 -15
View File
@@ -5,7 +5,7 @@ command -v docker >/dev/null
command -v go >/dev/null
export GOTOOLCHAIN=go1.25.0
matrix_tmp="$(mktemp -d)"
matrix_project="agentdb-matrix-$(date +%s)-$$"
matrix_project="serviceport-matrix-$(date +%s)-$$"
compose=(docker compose -f deploy/compose.test.yml -p "$matrix_project")
cleanup() {
if [[ "${KEEP_FIXTURES:-0}" == 1 ]]; then
@@ -20,33 +20,33 @@ trap cleanup EXIT
"${compose[@]}" up -d --build --wait --wait-timeout 300
"${compose[@]}" run --rm --no-deps mongo80-init
"${compose[@]}" cp cert-init:/certs/ca.pem "$matrix_tmp/ca.pem"
export AGENTDB_MYSQL_TEST_USER=root
export AGENTDB_MYSQL_TEST_PASSWORD=agentdb-test-password
export AGENTDB_MYSQL_TEST_CA="$matrix_tmp/ca.pem"
export AGENTDB_MONGO_TEST_USER=root
export AGENTDB_MONGO_TEST_PASSWORD=agentdb-test-password
export AGENTDB_MONGO_TEST_CA="$(cat "$matrix_tmp/ca.pem")"
export AGENTDB_SSH_TEST_USER=agentdb
export AGENTDB_SSH_TEST_PASSWORD=agentdb-test-password
export SERVICEPORT_MYSQL_TEST_USER=root
export SERVICEPORT_MYSQL_TEST_PASSWORD=serviceport-test-password
export SERVICEPORT_MYSQL_TEST_CA="$matrix_tmp/ca.pem"
export SERVICEPORT_MONGO_TEST_USER=root
export SERVICEPORT_MONGO_TEST_PASSWORD=serviceport-test-password
export SERVICEPORT_MONGO_TEST_CA="$(cat "$matrix_tmp/ca.pem")"
export SERVICEPORT_SSH_TEST_USER=serviceport
export SERVICEPORT_SSH_TEST_PASSWORD=serviceport-test-password
for route in direct socks-open socks-auth; do
export AGENTDB_TEST_SOCKS_ADDR='' AGENTDB_TEST_SOCKS_USER='' AGENTDB_TEST_SOCKS_PASSWORD=''
if [[ "$route" == socks-open ]]; then export AGENTDB_TEST_SOCKS_ADDR=127.0.0.1:11080; fi
export SERVICEPORT_TEST_SOCKS_ADDR='' SERVICEPORT_TEST_SOCKS_USER='' SERVICEPORT_TEST_SOCKS_PASSWORD=''
if [[ "$route" == socks-open ]]; then export SERVICEPORT_TEST_SOCKS_ADDR=127.0.0.1:11080; fi
if [[ "$route" == socks-auth ]]; then
export AGENTDB_TEST_SOCKS_ADDR=127.0.0.1:11081 AGENTDB_TEST_SOCKS_USER=socks AGENTDB_TEST_SOCKS_PASSWORD=socks-test-password
export SERVICEPORT_TEST_SOCKS_ADDR=127.0.0.1:11081 SERVICEPORT_TEST_SOCKS_USER=socks SERVICEPORT_TEST_SOCKS_PASSWORD=socks-test-password
fi
for pair in mysql57:13357 mysql80:13380 mysql84:13384; do
service="${pair%%:*}"; port="${pair##*:}"
if [[ "$route" == direct ]]; then export AGENTDB_MYSQL_TEST_ADDR="127.0.0.1:$port"; else export AGENTDB_MYSQL_TEST_ADDR="$service:3306"; fi
if [[ "$route" == direct ]]; then export SERVICEPORT_MYSQL_TEST_ADDR="127.0.0.1:$port"; else export SERVICEPORT_MYSQL_TEST_ADDR="$service:3306"; fi
echo "Testing MySQL $service via $route"
go test -count=1 -timeout=120s ./internal/protocol/mysqlproxy -run '^TestRealMySQL$' -v
done
for pair in mongo60:27060 mongo70:27070 mongo80:27080; do
service="${pair%%:*}"; port="${pair##*:}"
if [[ "$route" == direct ]]; then export AGENTDB_MONGO_TEST_ADDR="127.0.0.1:$port"; else export AGENTDB_MONGO_TEST_ADDR="$service:27017"; fi
if [[ "$route" == direct ]]; then export SERVICEPORT_MONGO_TEST_ADDR="127.0.0.1:$port"; else export SERVICEPORT_MONGO_TEST_ADDR="$service:27017"; fi
echo "Testing MongoDB $service via $route"
go test -count=1 -timeout=120s ./internal/protocol/mongoproxy -run '^TestRealMongo$' -v
done
if [[ "$route" == direct ]]; then export AGENTDB_SSH_TEST_ADDR=127.0.0.1:12222; else export AGENTDB_SSH_TEST_ADDR=ssh:22; fi
if [[ "$route" == direct ]]; then export SERVICEPORT_SSH_TEST_ADDR=127.0.0.1:12222; else export SERVICEPORT_SSH_TEST_ADDR=ssh:22; fi
echo "Testing OpenSSH and SFTP via $route"
go test -count=1 -timeout=120s ./internal/protocol/sshproxy -run '^TestRealSSH$' -v
done
+8 -8
View File
@@ -9,11 +9,11 @@ import { resolve, join } from 'node:path'
import { setTimeout as delay } from 'node:timers/promises'
const require = createRequire(new URL('../web/package.json', import.meta.url))
const { chromium } = require('@playwright/test')
assert(process.argv[2], 'Provide packaged agentdb.exe')
assert(process.argv[2], 'Provide packaged serviceport.exe')
const binary = resolve(process.argv[2])
const temp = mkdtempSync(join(tmpdir(), 'agentdb-desktop-'))
const env = Object.fromEntries(Object.entries(process.env).filter(([k]) => !k.toUpperCase().startsWith('AGENTDB_') && k.toUpperCase() !== 'APPDATA'))
Object.assign(env, { APPDATA: temp, AGENTDB_NO_BROWSER: '1', AGENTDB_HTTP_ADDR: '127.0.0.1:0' })
const temp = mkdtempSync(join(tmpdir(), 'serviceport-desktop-'))
const env = Object.fromEntries(Object.entries(process.env).filter(([k]) => !k.toUpperCase().startsWith('SERVICEPORT_') && k.toUpperCase() !== 'APPDATA'))
Object.assign(env, { APPDATA: temp, SERVICEPORT_NO_BROWSER: '1', SERVICEPORT_HTTP_ADDR: '127.0.0.1:0' })
let child, browser
async function stop() {
if (child && child.exitCode === null) await new Promise(resolve => { child.once('exit', resolve); child.kill() })
@@ -39,17 +39,17 @@ try {
assert.equal(session.status, 200)
assert.equal((await session.json()).authEnabled, false)
await stop()
env.AGENTDB_AUTH_ENABLED = 'true'
env.SERVICEPORT_AUTH_ENABLED = 'true'
const url = await start()
assert(url.includes('/setup?token='), 'First launch must offer setup')
const keyPath = join(temp, 'AgentDB', 'master-key.dpapi')
const keyPath = join(temp, 'serviceport', 'master-key.dpapi')
const originalKey = readFileSync(keyPath)
const cert = new X509Certificate(readFileSync(join(temp, 'AgentDB', 'tls', 'cert.pem')))
const cert = new X509Certificate(readFileSync(join(temp, 'serviceport', 'tls', 'cert.pem')))
assert(cert.checkHost('localhost'))
browser = await chromium.launch({ channel: 'msedge', headless: true })
const page = await browser.newPage()
await page.goto(url)
await page.getByRole('heading', { name: '欢迎使用 AgentDB' }).waitFor()
await page.getByRole('heading', { name: '欢迎使用 serviceport' }).waitFor()
const password = randomBytes(24).toString('base64url')
await page.getByLabel('管理员用户名').fill('desktop-test')
await page.getByLabel('管理员密码', { exact: true }).fill(password)
+4 -4
View File
@@ -9,9 +9,9 @@ import { setTimeout as delay } from 'node:timers/promises'
const directory = resolve(process.argv[2] || '')
assert(process.argv[2], 'Provide an extracted package directory')
const binary = join(directory, 'agentdb.exe')
const binary = join(directory, 'serviceport.exe')
const certTool = join(directory, 'devcert.exe')
for (const name of ['agentdb.exe', 'devcert.exe', 'README.md', '.env.example', 'build-info.json', 'SHA256SUMS.txt']) assert(existsSync(join(directory, name)), `Missing ${name}`)
for (const name of ['serviceport.exe', 'devcert.exe', 'README.md', '.env.example', 'build-info.json', 'SHA256SUMS.txt']) assert(existsSync(join(directory, name)), `Missing ${name}`)
for (const line of readFileSync(join(directory, 'SHA256SUMS.txt'), 'utf8').trim().split(/\r?\n/)) {
const match = /^([a-f0-9]{64}) (.+)$/.exec(line)
assert(match, 'Invalid checksum entry')
@@ -22,7 +22,7 @@ for (const line of readFileSync(join(directory, 'SHA256SUMS.txt'), 'utf8').trim(
}
const key = execFileSync(binary, ['generate-key'], { encoding: 'utf8', windowsHide: true }).trim()
assert.equal(Buffer.from(key, 'base64').length, 32)
const temp = mkdtempSync(join(tmpdir(), 'agentdb-package-smoke-'))
const temp = mkdtempSync(join(tmpdir(), 'serviceport-package-smoke-'))
let child
try {
const tlsDir = join(temp, 'tls')
@@ -35,7 +35,7 @@ try {
const port = listener.address().port
await new Promise(resolve => listener.close(resolve))
const password = randomBytes(24).toString('base64url')
child = spawn(binary, [], { cwd: directory, windowsHide: true, stdio: ['ignore', 'ignore', 'pipe'], env: { ...process.env, AGENTDB_AUTH_ENABLED: 'true', AGENTDB_MASTER_KEY: key, AGENTDB_ADMIN_USER: 'package-test', AGENTDB_ADMIN_PASSWORD: password, AGENTDB_DATA: join(temp, 'test.sqlite'), AGENTDB_HTTP_ADDR: `127.0.0.1:${port}`, AGENTDB_SECURE_COOKIE: 'false', AGENTDB_TLS_CERT: join(tlsDir, 'cert.pem'), AGENTDB_TLS_KEY: join(tlsDir, 'key.pem') } })
child = spawn(binary, [], { cwd: directory, windowsHide: true, stdio: ['ignore', 'ignore', 'pipe'], env: { ...process.env, SERVICEPORT_AUTH_ENABLED: 'true', SERVICEPORT_MASTER_KEY: key, SERVICEPORT_ADMIN_USER: 'package-test', SERVICEPORT_ADMIN_PASSWORD: password, SERVICEPORT_DATA: join(temp, 'test.sqlite'), SERVICEPORT_HTTP_ADDR: `127.0.0.1:${port}`, SERVICEPORT_SECURE_COOKIE: 'false', SERVICEPORT_TLS_CERT: join(tlsDir, 'cert.pem'), SERVICEPORT_TLS_KEY: join(tlsDir, 'key.pem') } })
let startupError = ''
child.stderr.on('data', b => { startupError += b.toString() })
child.on('error', e => { startupError += e.message })
+2 -2
View File
@@ -1,4 +1,4 @@
function Get-AgentDBBuildInfo {
function Get-serviceportBuildInfo {
param([string]$Version)
$repoRoot = Split-Path $PSScriptRoot -Parent
if (!$Version) { $Version = (Get-Content -LiteralPath (Join-Path $repoRoot 'VERSION') -Raw).Trim() }
@@ -16,6 +16,6 @@ function Get-AgentDBBuildInfo {
$buildTime = [DateTime]::UtcNow.ToString('yyyy-MM-ddTHH:mm:ssZ')
return [PSCustomObject]@{
Version = $Version; Commit = $commit; Revision = $revision; Dirty = $dirty; BuildTime = $buildTime
LDFlags = "-s -w -X agentdb/internal/buildinfo.Version=$Version -X agentdb/internal/buildinfo.Commit=$commit -X agentdb/internal/buildinfo.BuildTime=$buildTime"
LDFlags = "-s -w -X serviceport/internal/buildinfo.Version=$Version -X serviceport/internal/buildinfo.Commit=$commit -X serviceport/internal/buildinfo.BuildTime=$buildTime"
}
}
+1 -1
View File
@@ -2,5 +2,5 @@
setlocal
powershell.exe -NoProfile -ExecutionPolicy Bypass -File "%~dp0scripts\start-dev.ps1"
set "startExit=%ERRORLEVEL%"
if not "%startExit%"=="0" if not "%AGENTDB_START_NO_PAUSE%"=="1" pause
if not "%startExit%"=="0" if not "%SERVICEPORT_START_NO_PAUSE%"=="1" pause
exit /b %startExit%
+1 -1
View File
@@ -1,4 +1,4 @@
package agentdb
package serviceport
import (
_ "embed"
+1 -1
View File
@@ -1 +1 @@
<!doctype html><html lang="zh-CN"><head><meta charset="UTF-8"><meta name="viewport" content="width=device-width,initial-scale=1"><meta name="theme-color" content="#132c2b"><title>AgentDB · 连接管理</title></head><body><div id="app"></div><script type="module" src="/src/main.ts"></script></body></html>
<!doctype html><html lang="zh-CN"><head><meta charset="UTF-8"><meta name="viewport" content="width=device-width,initial-scale=1"><meta name="theme-color" content="#132c2b"><title>serviceport · 连接管理</title></head><body><div id="app"></div><script type="module" src="/src/main.ts"></script></body></html>
+2 -2
View File
@@ -1,11 +1,11 @@
{
"name": "agentdb-web",
"name": "serviceport-web",
"version": "0.3.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "agentdb-web",
"name": "serviceport-web",
"version": "0.3.0",
"dependencies": {
"element-plus": "2.11.4",
+1 -1
View File
@@ -1,5 +1,5 @@
{
"name": "agentdb-web",
"name": "serviceport-web",
"private": true,
"version": "0.3.0",
"type": "module",
+1 -1
View File
@@ -6,7 +6,7 @@ export default defineConfig({
timeout: 45000,
use: {
baseURL: "http://127.0.0.1:18081",
channel: process.env.AGENTDB_BROWSER_CHANNEL || "msedge",
channel: process.env.SERVICEPORT_BROWSER_CHANNEL || "msedge",
viewport: { width: 1440, height: 1000 },
trace: "off",
screenshot: "only-on-failure",
+5 -5
View File
@@ -509,7 +509,7 @@ onUnmounted(() => {
<template>
<div class="theme-picker"><label for="theme-select">主题</label><select id="theme-select" v-model="theme" aria-label="主题"><option v-for="item in themes" :key="item.value" :value="item.value">{{ item.label }}</option></select></div>
<div v-if="!ready" class="loading">AgentDB · 正在连接…</div>
<div v-if="!ready" class="loading">serviceport · 正在连接…</div>
<div v-else-if="!user" class="login-shell">
<section class="login-story">
<div class="brand">
@@ -517,7 +517,7 @@ onUnmounted(() => {
a
<span>›</span>
</span>
AgentDB
serviceport
</div>
<div>
<div class="eyebrow">YOUR SERVICES. ONE GATEWAY.</div>
@@ -537,7 +537,7 @@ onUnmounted(() => {
<span>SSH</span>
</div>
</div>
<small>AGENTDB / CONNECTION WORKSPACE</small>
<small>SERVICEPORT / CONNECTION WORKSPACE</small>
</section>
<section class="login-panel">
<div class="login-card">
@@ -584,7 +584,7 @@ onUnmounted(() => {
a
<span>›</span>
</span>
AgentDB
serviceport
</div>
<span class="nav-caption">工作空间</span>
<nav>
@@ -1070,7 +1070,7 @@ onUnmounted(() => {
<div v-if="page==='events'" class="table-footer"><span>已加载 {{events.length}} 条;导出当前已应用条件的全部记录</span><el-button v-if="eventBefore" :loading="eventLoading" @click="loadEvents(true)">加载更早记录</el-button></div>
</section>
<footer class="workspace-footer">
<span>AgentDB</span>
<span>serviceport</span>
<span>统一连接 · 清晰掌控</span>
</footer>
</div>
+2 -2
View File
@@ -86,8 +86,8 @@ export function useAudit(api: AuditAPI, report: Report) {
} catch (error) { report(error); }
}
function exportEvents() { downloadAudit("events", appliedEventFilter.value, "agentdb-events.csv"); }
function exportCommands() { downloadAudit("commands", appliedCommandFilter.value, "agentdb-commands.csv"); }
function exportEvents() { downloadAudit("events", appliedEventFilter.value, "serviceport-events.csv"); }
function exportCommands() { downloadAudit("commands", appliedCommandFilter.value, "serviceport-commands.csv"); }
return {
events, eventFilter, eventBefore, eventLoading, eventExporting, loadEvents, applyEventFilters, exportEvents,
+2 -2
View File
@@ -7,7 +7,7 @@ export const themes = [
{ value: 'system', label: '跟随系统' },
];
function savedTheme() {
try { const saved = localStorage.getItem('agentdb-theme'); return themes.some(t => t.value === saved) ? saved! : 'system'; }
try { const saved = localStorage.getItem('serviceport-theme'); return themes.some(t => t.value === saved) ? saved! : 'system'; }
catch { return 'system'; }
}
export const theme = ref(savedTheme());
@@ -18,6 +18,6 @@ function apply() {
document.documentElement.classList.toggle('dark', actual === 'dark');
document.documentElement.style.colorScheme = actual === 'dark' ? 'dark' : 'light';
}
watch(theme, () => { apply(); try { localStorage.setItem('agentdb-theme', theme.value); } catch {} });
watch(theme, () => { apply(); try { localStorage.setItem('serviceport-theme', theme.value); } catch {} });
media.addEventListener('change', apply);
apply();
+1 -1
View File
@@ -12,7 +12,7 @@ test('内置 K8s 出口配置、保存与版本信息', async ({ page }) => {
const drawer = page.getByRole('dialog');
await drawer.locator('.el-form-item').filter({ hasText: /^代理名称/ }).getByRole('textbox').fill('K8s 测试代理');
await drawer.locator('.el-radio-button').filter({ hasText: 'K8s port-forward' }).click();
await drawer.getByPlaceholder('默认使用 KUBECONFIG 或 ~/.kube/config').fill('C:/agentdb-e2e-missing/kubeconfig');
await drawer.getByPlaceholder('默认使用 KUBECONFIG 或 ~/.kube/config').fill('C:/serviceport-e2e-missing/kubeconfig');
await drawer.getByPlaceholder('当前 context', { exact: true }).fill('test-cluster');
await drawer.getByPlaceholder('当前 context 的命名空间').fill('demo');
await drawer.getByPlaceholder('service/mysql 或 pod/mysql-0 或 deployment/mysql').fill('service/mysql');

Some files were not shown because too many files have changed in this diff Show More