Compare commits

...
1 Commits
6 changed files with 169 additions and 9 deletions

No files matched your search

+63
View File
@@ -39,6 +39,13 @@ ipw-cn/
├── ssrf/ # SSRF 防护模块
│ └── ssrf.go # 私有 IP 拦截、安全跳转校验
│
├── middleware-go/ # 独立转发中间件(Go Fiber v3,可单独部署)
│ ├── main.go # 中间件入口(/v1/* 与 /middleware/* 路由)
│ ├── setting.json # 中间件配置(节点列表、apiKeys、cors 等)
│ ├── setting.json.example # 中间件配置示例
│ ├── Dockerfile # 中间件 Docker 镜像
│ └── middleware-go-linux-arm64 # Linux ARM64 预编译二进制
│
├── frontend-ssr/ # Nuxt 4 SSR 前端(主版本,部署至 Cloudflare Workers)
│ ├── app/ # Nuxt 应用源码
│ │ └── pages/ # 页面组件(location/dns/tcping/ssl/speedtest/whois/dnssec/asn/screenshot)
@@ -85,6 +92,7 @@ ipw-cn/
| 组件 | 技术 |
|------|------|
| 后端(自托管) | Go 1.26 + Gin + Resty |
| 独立中间件 | Go 1.26 + Fiber v3(转发中间件,多节点候选) |
| 边缘后端(EdgeOne) | Go(边缘函数) |
| SSR 前端 | Nuxt 4 + Vue 3 + Element Plus + VueUse |
| Cloudflare Workers | Hono + TypeScript + Wrangler |
@@ -120,6 +128,25 @@ go run main.go
首次启动时会自动下载 IP 数据库文件(约 200MB),之后每 24 小时自动更新。
### 独立中间件(middleware-go)
转发中间件,供 SSR 前端在多个上游节点之间做候选重试,可独立部署。
```bash
cd middleware-go
# 构建(Linux ARM64 示例)
CGO_ENABLED=0 GOOS=linux GOARCH=arm64 go build -o middleware-go-linux-arm64 .
# 运行(需配置文件,可用 SETTING_FILE 指定路径)
./middleware-go-linux-arm64
# 查看版本
./middleware-go-linux-arm64 --version
```
所有配置均可通过环境变量覆盖,无需修改配置文件(详见下方「中间件配置」)。
### SSR 前端(部署至 Cloudflare Workers)
```bash
@@ -168,6 +195,7 @@ docker run -p 8080:8080 -v $(pwd)/setting.json:/app/setting.json lemon-ipw
本项目支持多平台部署,前端和后端可独立部署到不同平台:
- **自托管后端**:使用 Docker 或直接运行 Go 二进制,配合 `setting.json` 配置
- **独立转发中间件**:`middleware-go/` 使用 Go Fiber v3,提供 `/v1/*` 与 `/middleware/*` 转发路由,供前端做多节点候选重试
- **Cloudflare Workers 后端**:`lemon-getip/` 使用 Hono 框架部署到 Cloudflare Workers
- **EdgeOne 边缘后端**:`edgeone/` 和 `edgeone-getip/` 部署到腾讯 EdgeOne Pages
- **SSR 前端**:`frontend-ssr/` 使用 Nuxt 4 + Wrangler 部署到 Cloudflare Workers
@@ -191,6 +219,41 @@ docker run -p 8080:8080 -v $(pwd)/setting.json:/app/setting.json lemon-ipw
前端 API 地址和多节点配置位于各前端项目的 `config/` 目录中。
### 中间件配置(middleware-go)
配置来源优先级:**环境变量 > `setting.json` > 默认值**。`setting.json` 采用平铺结构(键名与前端 `config/index.ts` 一致),也可用环境变量覆盖任意配置项;数组/对象类配置项从环境变量读取 **JSON 字符串**解析。
```json
{
"port": "8091",
"httpTimeoutSeconds": 30,
"cors": "https://ipw.wsmdn.top",
"apiBaseUrls": [{ "label": "中国 江苏 移动", "id": "cn-jiangsu", "url": "https://cn-jiangsu.api-ipw.wsmdn.top/" }],
"IPLocationAPIs": [],
"TCPing": { "DualStack": [], "IPv4": [], "IPv6": [] },
"SpeedTest": { "DualStack": [], "IPv4": [], "IPv6": [] },
"NSLookup": [],
"apiKeys": { "cn-jiangsu": "token" }
}
```
| 配置项 | 环境变量 | 类型 | 说明 |
|--------|----------|------|------|
| `port` | `PORT` | 数字/字符串 | 监听端口 |
| `httpTimeoutSeconds` | `HTTP_TIMEOUT` | 数字 | 上游请求超时(秒) |
| `cors` | `CORS` | 字符串 | 逗号分隔的允许域名;为空则允许所有(`*`) |
| `apiBaseUrls` | `API_BASE_URLS` | JSON 数组 | whois/ssl/detail 上游节点 |
| `IPLocationAPIs` | `IP_LOCATION_APIS` | JSON 数组 | location/asn 上游节点 |
| `TCPing` | `TCPING` | JSON 对象 | tcping 节点(DualStack/IPv4/IPv6) |
| `SpeedTest` | `SPEED_TEST` | JSON 对象 | speed 节点(DualStack/IPv4/IPv6) |
| `NSLookup` | `NS_LOOKUP` | JSON 数组 | dns/dnssec 上游节点 |
| `apiKeys` | `APIKEYS` | JSON 对象 | `{"backendID": "token"}`,转发时加 `Authorization: Bearer <token>` |
| 配置文件路径 | `SETTING_FILE` | 字符串 | 指定 setting.json 路径(默认 `./setting.json` 或 `../setting.json`) |
**路由格式**:`/{prefix}/{backendID}/{apiType}/{raw...}`,其中 `prefix` 为 `/v1` 或 `/middleware`,`apiType` 支持 `whois/dns/location/ssl/asn/dnssec/detail/tcping/speed`。响应透传上游状态码与 body;上游网络不可达返回 502。
**版本信息**:通过 ldflags 注入 `main.VERSION / main.COMMIT / main.BUILD_TIME`,运行 `--version` 查看。
## 许可证
[GPL-3.0](LICENSE)
+4 -3
View File
@@ -10,6 +10,7 @@ const config = {
// 前端自带中间件始终放在候选列表最后一位兜底。
// 未配置或为空数组时,直接使用前端自带中间件。
Middleware: <string[]>[
"https://middleware-1.api-ipw.wsmdn.top/"
// "http://127.0.0.1:8091/",
// "https://middleware-2.wsmdn.top/",
],
@@ -21,9 +22,9 @@ const config = {
ICP: "苏ICP备2026012471号",
GongAn: "苏公网安备32132402000813号",
// Worker IP查询接口
v4OnlyAPI: "https://4.wsmdn.dpdns.org/",
v6OnlyAPI: "https://6.wsmdn.dpdns.org/",
DualStackAPI: "https://test.wsmdn.dpdns.org/",
v4OnlyAPI: "https://4.wsmdn.top",
v6OnlyAPI: "https://6.wsmdn.top",
DualStackAPI: "https://test.wsmdn.top",
apiBaseUrls: [
{
label: "中国 江苏 移动",
+8 -3
View File
@@ -24,9 +24,13 @@ function isConnectionError(e: any): boolean {
return !!e && typeof getErrorStatus(e) !== 'number'
}
// 需要切换节点重试的上游状态码:
// 401 未授权、403 被 CDN/WAF 风控拦截、418 被反爬/限流标记 —— 换一个节点往往就能绕过
const RETRY_STATUS_CODES = new Set([401, 403, 418])
// useMiddlewareFetch:在 Vue 页面中调用中间件(替代直接 useFetch('/middleware/...'))。
// 外部中间件与前端自带中间件同级:依次尝试 config.Middleware 中的外部节点,
// 仅当"节点无法连接"或上游返回 401 时重试下一个候选(最后一位兜底为前端自带中间件);
// 仅当"节点无法连接"或上游返回 401/403/418 时重试下一个候选(最后一位兜底为前端自带中间件);
// 其余状态码错误不重试,直接返回给页面展示。
export function useMiddlewareFetch<T = any>(url: MaybeRefOrGetter<string>, options?: any) {
const path = computed(() => toValue(url) || '')
@@ -37,11 +41,12 @@ export function useMiddlewareFetch<T = any>(url: MaybeRefOrGetter<string>, optio
const result = useFetch<T>(currentUrl, options)
// 仅"节点无法连接"或 401 时切换到下一个候选并重新请求
// 仅"节点无法连接"或命中 RETRY_STATUS_CODES 时切换到下一个候选并重新请求
watch(() => result.error.value, (err) => {
if (!err) return
if (idx.value >= candidates.value.length - 1) return
if (!isConnectionError(err) && getErrorStatus(err) !== 401) return
const status = getErrorStatus(err)
if (!isConnectionError(err) && !(typeof status === 'number' && RETRY_STATUS_CODES.has(status))) return
idx.value++
result.execute()
})
+28
View File
@@ -0,0 +1,28 @@
FROM golang:1.26-alpine AS build
ENV GO111MODULE=on \
CGO_ENABLED=0 \
GOOS=linux \
GOARCH=amd64
WORKDIR /app
COPY go.mod go.sum ./
RUN go mod download
COPY main.go ./
RUN go build -o main .
# 运行阶段
FROM alpine:latest
# 添加必要的证书
RUN apk --no-cache add ca-certificates
# 创建非root用户
RUN adduser -D appuser
USER appuser
WORKDIR /home/appuser
# 从构建阶段复制二进制文件
COPY --from=build /app/main .
EXPOSE 8080
CMD ["./main"]
+65 -3
View File
@@ -36,6 +36,7 @@ type stackConfig struct {
type middlewareConfig struct {
Port stringOrNumber `json:"port"`
HTTPTimeoutSeconds int `json:"httpTimeoutSeconds"`
Cors string `json:"cors"`
APIBaseURLs []apiInfo `json:"apiBaseUrls"`
IPLocationAPIs []apiInfo `json:"IPLocationAPIs"`
TCPing stackConfig `json:"TCPing"`
@@ -54,6 +55,8 @@ var (
BUILD_TIME string // 构建时间(构建时由 ldflags 注入)
PORT string // 监听端口
HTTP_TIMEOUT int // 上游请求超时(秒)
CORS string // CORS 配置原始字符串(逗号分隔的允许域名,可空)
ACCEPT_DOMAINS []string // 允许跨域的域名列表(由 CORS 拆分而来,空则允许所有)
API_BASE_URLS []apiInfo // whois/ssl/detail 上游列表
IP_LOCATION_APIS []apiInfo // location/asn 上游列表
TCPING stackConfig // tcping 上游列表(DualStack/IPv4/IPv6)
@@ -69,6 +72,19 @@ var HTTP_CLIENT = &http.Client{}
// ==================== 工具函数 ====================
// envJSON 从环境变量读取 JSON 字符串并反序列化到 target(用于数组/对象等映射表类配置项)。
// 环境变量为空时不做任何事;解析失败返回错误,避免静默使用空配置。
func envJSON(key string, target any) error {
raw := os.Getenv(key)
if raw == "" {
return nil
}
if err := json.Unmarshal([]byte(raw), target); err != nil {
return fmt.Errorf("parse env %s: %w", key, err)
}
return nil
}
func (s *stringOrNumber) UnmarshalJSON(data []byte) error {
var v any
if err := json.Unmarshal(data, &v); err != nil {
@@ -331,6 +347,41 @@ func readConfig() error {
return fmt.Errorf("parse %s: %w", path, err)
}
// 环境变量覆盖配置(优先级: 环境变量 > setting.json > 默认值):
// 标量直接读;映射表(数组/对象)从环境变量读 JSON 字符串解析。
// 部署时无需改动配置文件,用环境变量即可覆盖任意节点/密钥/端口等。
if v := os.Getenv("PORT"); v != "" {
mw.Port = stringOrNumber(v)
}
if v := os.Getenv("HTTP_TIMEOUT"); v != "" {
n, err := strconv.Atoi(v)
if err != nil {
return fmt.Errorf("parse env HTTP_TIMEOUT: %w", err)
}
mw.HTTPTimeoutSeconds = n
}
if v := os.Getenv("CORS"); v != "" {
mw.Cors = v
}
if err := envJSON("API_BASE_URLS", &mw.APIBaseURLs); err != nil {
return err
}
if err := envJSON("IP_LOCATION_APIS", &mw.IPLocationAPIs); err != nil {
return err
}
if err := envJSON("TCPING", &mw.TCPing); err != nil {
return err
}
if err := envJSON("SPEED_TEST", &mw.SpeedTest); err != nil {
return err
}
if err := envJSON("NS_LOOKUP", &mw.NSLookup); err != nil {
return err
}
if err := envJSON("APIKEYS", &mw.APIKeys); err != nil {
return err
}
// 校验配置存在(防止在错误的文件上静默空跑)
hasEndpoints := len(mw.APIBaseURLs) > 0 || len(mw.NSLookup) > 0 || len(mw.IPLocationAPIs) > 0 ||
len(mw.TCPing.DualStack) > 0 || len(mw.TCPing.IPv4) > 0 || len(mw.TCPing.IPv6) > 0 ||
@@ -342,6 +393,11 @@ func readConfig() error {
// 将配置内容写入全局变量
PORT = string(mw.Port)
HTTP_TIMEOUT = mw.HTTPTimeoutSeconds
CORS = mw.Cors
// 逗号分隔的 CORS 域名列表(对齐根 main.go 的 ACCEPT_DOMAINS 用法)
if CORS != "" {
ACCEPT_DOMAINS = strings.Split(CORS, ",")
}
API_BASE_URLS = mw.APIBaseURLs
IP_LOCATION_APIS = mw.IPLocationAPIs
TCPING = mw.TCPing
@@ -387,12 +443,18 @@ func main() {
// 中间件路由,对应前端 server/routes/middleware/[...slug].get.ts
// 同时兼容 /v1/... 与 /middleware/... 两种前缀
app.Use(cors.New(cors.Config{
AllowOrigins: []string{"*"},
// CORS: 配置了 cors 域名列表则仅允许这些来源,否则允许所有(对齐根 main.go 的 ACCEPT_DOMAINS 逻辑)
corsConfig := cors.Config{
AllowMethods: []string{"GET", "POST", "HEAD", "OPTIONS"},
AllowHeaders: []string{"Origin", "Content-Type", "Accept", "Authorization"},
MaxAge: 86400,
}))
}
if len(ACCEPT_DOMAINS) > 0 {
corsConfig.AllowOrigins = ACCEPT_DOMAINS
} else {
corsConfig.AllowOrigins = []string{"*"}
}
app.Use(cors.New(corsConfig))
app.Get("/v1/*", middlewareHandler)
app.Get("/middleware/*", middlewareHandler)
+1
View File
@@ -1,6 +1,7 @@
{
"port": "8091",
"httpTimeoutSeconds": 30,
"cors": "",
"apiBaseUrls": [
{
"label": "中国 江苏 移动",