Compare commits

..
Author SHA1 Message Date
cloudflare-workers-and-pages[bot] 00d309ffa0 Add Cloudflare Workers configuration 2026-04-12 10:59:04 +00:00
nomdn 9f46341769 first commit 2026-04-12 18:41:38 +08:00
nomdn 4b8b370efb Initial commit 2026-04-12 18:36:45 +08:00
220 changed files with 6102 additions and 36598 deletions

No files matched your search

-37
View File
@@ -1,37 +0,0 @@
---
name: Bug report
about: 提交一个BUG以帮助我们改进
title: BUG
labels: ''
assignees: ''
---
**介绍您的问题**
**复现流程**
复现问题的步骤:
1. Go to '...'
2. Click on '....'
3. Scroll down to '....'
4. See error
**Expected behavior**
A clear and concise description of what you expected to happen.
**Screenshots**
If applicable, add screenshots to help explain your problem.
**Desktop (please complete the following information):**
- OS: [e.g. iOS]
- Browser [e.g. chrome, safari]
- Version [e.g. 22]
**Smartphone (please complete the following information):**
- Device: [e.g. iPhone6]
- OS: [e.g. iOS8.1]
- Browser [e.g. stock browser, safari]
- Version [e.g. 22]
**Additional context**
Add any other context about the problem here.
-20
View File
@@ -1,20 +0,0 @@
---
name: Feature request
about: 对于本项目的一些想法
title: Feature
labels: ''
assignees: ''
---
**你的功能请求是否是为了解决某个痛点?请详细描述。**
请清晰、简明地描述该痛点。例如:我认为[...]的设计并不好,我觉得这样做会更优秀。
**描述你期望的解决方案**
请清晰、简明地描述你期望实现的效果或功能。
**描述你考虑过的替代方案**
请清晰、简明地描述你考虑过的其他替代方案或功能。
**补充信息**
请在此处补充关于该功能请求的其他背景信息或相关截图。
-10
View File
@@ -1,10 +0,0 @@
---
name: 其他问题
about: 请尽可能详细的描述您的问题,或者直接加入项目官方交流群询问
title: ''
labels: ''
assignees: ''
---
-145
View File
@@ -1,145 +0,0 @@
name: Build and Release
on:
push:
tags:
- 'v*'
workflow_dispatch:
jobs:
build-and-release:
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: Setup Go
uses: actions/setup-go@v4
with:
go-version: '1.26.2' # 选择适合的 Go 版本
- name: Set version metadata
run: |
echo "VERSION=${GITHUB_REF_NAME}" >> $GITHUB_ENV
echo "COMMIT=${GITHUB_SHA}" >> $GITHUB_ENV
echo "BUILD_TIME=$(date -u +%Y-%m-%dT%H:%M:%SZ)" >> $GITHUB_ENV
- name: Build for Linux
run: |
LDFLAGS="-X main.VERSION=${VERSION} -X main.COMMIT=${COMMIT} -X main.BUILD_TIME=${BUILD_TIME}"
CGO_ENABLED=0 GOOS=linux GOARCH=386 go build -ldflags "$LDFLAGS" -o lemonipw-linux-386
CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -ldflags "$LDFLAGS" -o lemonipw-linux-amd64
CGO_ENABLED=0 GOOS=linux GOARCH=arm64 go build -ldflags "$LDFLAGS" -o lemonipw-linux-arm64
CGO_ENABLED=0 GOOS=linux GOARCH=arm GOARM=7 go build -ldflags "$LDFLAGS" -o lemonipw-linux-armv7
CGO_ENABLED=0 GOOS=linux GOARCH=arm GOARM=6 go build -ldflags "$LDFLAGS" -o lemonipw-linux-armv6
CGO_ENABLED=0 GOOS=linux GOARCH=loong64 go build -ldflags "$LDFLAGS" -o lemonipw-linux-loong64
- name: Build for Windows
run: |
LDFLAGS="-X main.VERSION=${VERSION} -X main.COMMIT=${COMMIT} -X main.BUILD_TIME=${BUILD_TIME}"
CGO_ENABLED=0 GOOS=windows GOARCH=386 go build -ldflags "$LDFLAGS" -o lemonipw-windows-386.exe
CGO_ENABLED=0 GOOS=windows GOARCH=amd64 go build -ldflags "$LDFLAGS" -o lemonipw-windows-amd64.exe
CGO_ENABLED=0 GOOS=windows GOARCH=arm64 go build -ldflags "$LDFLAGS" -o lemonipw-windows-arm64.exe
- name: Build for macOS
run: |
LDFLAGS="-X main.VERSION=${VERSION} -X main.COMMIT=${COMMIT} -X main.BUILD_TIME=${BUILD_TIME}"
CGO_ENABLED=0 GOOS=darwin GOARCH=amd64 go build -ldflags "$LDFLAGS" -o lemonipw-darwin-amd64
CGO_ENABLED=0 GOOS=darwin GOARCH=arm64 go build -ldflags "$LDFLAGS" -o lemonipw-darwin-arm64
- name: Build for FreeBSD
run: |
LDFLAGS="-X main.VERSION=${VERSION} -X main.COMMIT=${COMMIT} -X main.BUILD_TIME=${BUILD_TIME}"
CGO_ENABLED=0 GOOS=freebsd GOARCH=386 go build -ldflags "$LDFLAGS" -o lemonipw-freebsd-386
CGO_ENABLED=0 GOOS=freebsd GOARCH=amd64 go build -ldflags "$LDFLAGS" -o lemonipw-freebsd-amd64
CGO_ENABLED=0 GOOS=freebsd GOARCH=arm64 go build -ldflags "$LDFLAGS" -o lemonipw-freebsd-arm64
- name: Build middleware-go for Linux
working-directory: middleware-go
run: |
LDFLAGS="-X main.VERSION=${VERSION} -X main.COMMIT=${COMMIT} -X main.BUILD_TIME=${BUILD_TIME}"
CGO_ENABLED=0 GOOS=linux GOARCH=386 go build -ldflags "$LDFLAGS" -o ../middleware-go-linux-386
CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -ldflags "$LDFLAGS" -o ../middleware-go-linux-amd64
CGO_ENABLED=0 GOOS=linux GOARCH=arm64 go build -ldflags "$LDFLAGS" -o ../middleware-go-linux-arm64
CGO_ENABLED=0 GOOS=linux GOARCH=arm GOARM=7 go build -ldflags "$LDFLAGS" -o ../middleware-go-linux-armv7
CGO_ENABLED=0 GOOS=linux GOARCH=arm GOARM=6 go build -ldflags "$LDFLAGS" -o ../middleware-go-linux-armv6
CGO_ENABLED=0 GOOS=linux GOARCH=loong64 go build -ldflags "$LDFLAGS" -o ../middleware-go-linux-loong64
- name: Build middleware-go for Windows
working-directory: middleware-go
run: |
LDFLAGS="-X main.VERSION=${VERSION} -X main.COMMIT=${COMMIT} -X main.BUILD_TIME=${BUILD_TIME}"
CGO_ENABLED=0 GOOS=windows GOARCH=386 go build -ldflags "$LDFLAGS" -o ../middleware-go-windows-386.exe
CGO_ENABLED=0 GOOS=windows GOARCH=amd64 go build -ldflags "$LDFLAGS" -o ../middleware-go-windows-amd64.exe
CGO_ENABLED=0 GOOS=windows GOARCH=arm64 go build -ldflags "$LDFLAGS" -o ../middleware-go-windows-arm64.exe
- name: Build middleware-go for macOS
working-directory: middleware-go
run: |
LDFLAGS="-X main.VERSION=${VERSION} -X main.COMMIT=${COMMIT} -X main.BUILD_TIME=${BUILD_TIME}"
CGO_ENABLED=0 GOOS=darwin GOARCH=amd64 go build -ldflags "$LDFLAGS" -o ../middleware-go-darwin-amd64
CGO_ENABLED=0 GOOS=darwin GOARCH=arm64 go build -ldflags "$LDFLAGS" -o ../middleware-go-darwin-arm64
- name: Build middleware-go for FreeBSD
working-directory: middleware-go
run: |
LDFLAGS="-X main.VERSION=${VERSION} -X main.COMMIT=${COMMIT} -X main.BUILD_TIME=${BUILD_TIME}"
CGO_ENABLED=0 GOOS=freebsd GOARCH=386 go build -ldflags "$LDFLAGS" -o ../middleware-go-freebsd-386
CGO_ENABLED=0 GOOS=freebsd GOARCH=amd64 go build -ldflags "$LDFLAGS" -o ../middleware-go-freebsd-amd64
CGO_ENABLED=0 GOOS=freebsd GOARCH=arm64 go build -ldflags "$LDFLAGS" -o ../middleware-go-freebsd-arm64
- name: Prepare config files for release
run: |
# 后端:发布真实 setting.json(重命名为 setting.json.example);中间件:只发布 setting.json.example
cp setting.json setting.json.example
cp middleware-go/setting.json.example middleware.setting.json.example
- name: Release
uses: softprops/action-gh-release@v3
if: github.ref_type == 'tag'
with:
name: Release ${{ github.ref_name }}
tag_name: ${{ github.ref_name }}
token: ${{ secrets.MINT_TOKEN }}
generate_release_notes: true
body: |
## 配置文件说明
Release 附带两份配置文件(**.example** 后缀用于标识,需重命名后使用):
| 发布文件 | 重命名为 | 放置位置 |
|----------|----------|----------|
| `setting.json.example` | `setting.json` | 主程序(后端测试节点)同目录 |
| `middleware.setting.json.example` | `setting.json` | `middleware-go/` 目录 |
```bash
# 后端测试节点
cp setting.json.example setting.json
# 独立中间件
cp middleware.setting.json.example middleware-go/setting.json
```
files: |
lemonipw-linux-386
lemonipw-linux-amd64
lemonipw-linux-arm64
lemonipw-linux-armv7
lemonipw-linux-armv6
lemonipw-linux-loong64
lemonipw-windows-386.exe
lemonipw-windows-amd64.exe
lemonipw-windows-arm64.exe
lemonipw-darwin-amd64
lemonipw-darwin-arm64
lemonipw-freebsd-386
lemonipw-freebsd-amd64
lemonipw-freebsd-arm64
middleware-go-linux-386
middleware-go-linux-amd64
middleware-go-linux-arm64
middleware-go-linux-armv7
middleware-go-linux-armv6
middleware-go-linux-loong64
middleware-go-windows-386.exe
middleware-go-windows-amd64.exe
middleware-go-windows-arm64.exe
middleware-go-darwin-amd64
middleware-go-darwin-arm64
middleware-go-freebsd-386
middleware-go-freebsd-amd64
middleware-go-freebsd-arm64
setting.json.example
middleware.setting.json.example
-94
View File
@@ -1,94 +0,0 @@
# For most projects, this workflow file will not need changing; you simply need
# to commit it to your repository.
#
# You may wish to alter this file to override the set of languages analyzed,
# or to provide custom queries or build logic.
#
# ******** NOTE ********
# We have attempted to detect the languages in your repository. Please check
# the `language` matrix defined below to confirm you have the correct set of
# supported CodeQL languages.
#
name: "CodeQL Advanced"
on:
push:
branches: [ "main" ]
pull_request:
branches: [ "main" ]
schedule:
- cron: '22 10 * * 4'
jobs:
analyze:
name: Analyze (${{ matrix.language }})
# Runner size impacts CodeQL analysis time. To learn more, please see:
# - https://gh.io/recommended-hardware-resources-for-running-codeql
# - https://gh.io/supported-runners-and-hardware-resources
# - https://gh.io/using-larger-runners (GitHub.com only)
# Consider using larger runners or machines with greater resources for possible analysis time improvements.
runs-on: ${{ (matrix.language == 'swift' && 'macos-latest') || 'ubuntu-latest' }}
permissions:
# required for all workflows
security-events: write
# required to fetch internal or private CodeQL packs
packages: read
# only required for workflows in private repositories
actions: read
contents: read
strategy:
fail-fast: false
matrix:
include:
- language: actions
build-mode: none
- language: go
build-mode: autobuild
- language: javascript-typescript
build-mode: none
# CodeQL supports the following values keywords for 'language': 'actions', 'c-cpp', 'csharp', 'go', 'java-kotlin', 'javascript-typescript', 'python', 'ruby', 'rust', 'swift'
# Use `c-cpp` to analyze code written in C, C++ or both
# Use 'java-kotlin' to analyze code written in Java, Kotlin or both
# Use 'javascript-typescript' to analyze code written in JavaScript, TypeScript or both
# To learn more about changing the languages that are analyzed or customizing the build mode for your analysis,
# see https://docs.github.com/en/code-security/code-scanning/creating-an-advanced-setup-for-code-scanning/customizing-your-advanced-setup-for-code-scanning.
# If you are analyzing a compiled language, you can modify the 'build-mode' for that language to customize how
# your codebase is analyzed, see https://docs.github.com/en/code-security/code-scanning/creating-an-advanced-setup-for-code-scanning/codeql-code-scanning-for-compiled-languages
steps:
- name: Checkout repository
uses: actions/checkout@v4
# Add any setup steps before running the `github/codeql-action/init` action.
# This includes steps like installing compilers or runtimes (`actions/setup-node`
# or others). This is typically only required for manual builds.
# - name: Setup runtime (example)
# uses: actions/setup-example@v1
# Initializes the CodeQL tools for scanning.
- name: Initialize CodeQL
uses: github/codeql-action/init@v4
with:
languages: ${{ matrix.language }}
build-mode: ${{ matrix.build-mode }}
# If you wish to specify custom queries, you can do so here or in a config file.
# By default, queries listed here will override any specified in a config file.
# Prefix the list here with "+" to use these queries and those in the config file.
# For more details on CodeQL's query packs, refer to: https://docs.github.com/en/code-security/code-scanning/automatically-scanning-your-code-for-vulnerabilities-and-errors/configuring-code-scanning#using-queries-in-ql-packs
# queries: security-extended,security-and-quality
# If the analyze step fails for one of the languages you are analyzing with
# "We were unable to automatically build your code", modify the matrix above
# to set the build mode to "manual" for that language. Then modify this step
# to build your code.
# ℹ️ Command-line programs to run using the OS shell.
# 📚 See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsrun
- name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@v4
with:
category: "/language:${{matrix.language}}"
-33
View File
@@ -1,33 +0,0 @@
name: Build and Deploy
on:
push:
branches:
- main
paths:
- 'edgeone/**'
workflow_dispatch:
jobs:
build-and-deploy:
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: '24' # 选择适合的 Node.js 版本
- name: Install dependencies
run: npm install -g edgeone
- name: Deploy to EdgeOne Pages
run: cd edgeone && npx edgeone pages deploy -n ipw-cn -t ${{ secrets.EDGEONE_API_TOKEN }}
env:
EDGEONE_API_TOKEN: ${{ secrets.EDGEONE_API_TOKEN }}
- name: Deploy to EdgeOne Pages
run: cd edgeone && npx edgeone pages deploy -n ipw-cn2 -t ${{ secrets.EDGEONE_API_TOKEN }}
env:
EDGEONE_API_TOKEN: ${{ secrets.EDGEONE_API_TOKEN }}
-29
View File
@@ -1,29 +0,0 @@
name: Build and Deploy
on:
push:
branches:
- main
paths:
- 'edgeone-getip/**'
workflow_dispatch:
jobs:
build-and-deploy:
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: '24' # 选择适合的 Node.js 版本
- name: Install dependencies
run: npm install -g edgeone
- name: Deploy to EdgeOne Pages
run: cd edgeone-getip && npx edgeone pages deploy -n edgeone-getip -t ${{ secrets.EDGEONE_API_TOKEN }}
env:
EDGEONE_API_TOKEN: ${{ secrets.EDGEONE_API_TOKEN }}
-34
View File
@@ -1,34 +0,0 @@
name: Build and Deploy to Cloudflare Workers
on:
push:
branches: ['main']
paths:
- 'frontend-ssr/**'
workflow_dispatch:
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Setup Node.js
uses: actions/setup-node@v3
with:
node-version: '22'
- uses: pnpm/action-setup@v6
with:
version: 10
# 部署到 Cloudflare Workers
- name: Deploy to Cloudflare Workers
env:
CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }}
CLOUDFLARE_ACCOUNT_ID: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
NITRO_PRESET: cloudflare_module
run: |
cd frontend-ssr
pnpm install
pnpm run deploy --keep-vars
-23
View File
@@ -1,23 +0,0 @@
main.exe
ip2region_v4.xdb
ip2region_v6.xdb
GeoCN.mmdb
GeoLite2-City.mmdb
GeoLite2-ASN.mmdb
qqwry.ipdb
*.gz
full.txt
short.txt
*.mmdb
*.exe
lemon-ipw
.mmdb.tmp
*.tmp
*.log
node_modules/
setting.json
.workbuddy/
# VitePress 构建产物
docs/.vitepress/.temp/
docs/.vitepress/cache/
docs/.vitepress/dist/
-1
View File
@@ -1 +0,0 @@
24
-128
View File
@@ -1,128 +0,0 @@
# Contributor Covenant Code of Conduct
## Our Pledge
We as members, contributors, and leaders pledge to make participation in our
community a harassment-free experience for everyone, regardless of age, body
size, visible or invisible disability, ethnicity, sex characteristics, gender
identity and expression, level of experience, education, socio-economic status,
nationality, personal appearance, race, religion, or sexual identity
and orientation.
We pledge to act and interact in ways that contribute to an open, welcoming,
diverse, inclusive, and healthy community.
## Our Standards
Examples of behavior that contributes to a positive environment for our
community include:
* Demonstrating empathy and kindness toward other people
* Being respectful of differing opinions, viewpoints, and experiences
* Giving and gracefully accepting constructive feedback
* Accepting responsibility and apologizing to those affected by our mistakes,
and learning from the experience
* Focusing on what is best not just for us as individuals, but for the
overall community
Examples of unacceptable behavior include:
* The use of sexualized language or imagery, and sexual attention or
advances of any kind
* Trolling, insulting or derogatory comments, and personal or political attacks
* Public or private harassment
* Publishing others' private information, such as a physical or email
address, without their explicit permission
* Other conduct which could reasonably be considered inappropriate in a
professional setting
## Enforcement Responsibilities
Community leaders are responsible for clarifying and enforcing our standards of
acceptable behavior and will take appropriate and fair corrective action in
response to any behavior that they deem inappropriate, threatening, offensive,
or harmful.
Community leaders have the right and responsibility to remove, edit, or reject
comments, commits, code, wiki edits, issues, and other contributions that are
not aligned to this Code of Conduct, and will communicate reasons for moderation
decisions when appropriate.
## Scope
This Code of Conduct applies within all community spaces, and also applies when
an individual is officially representing the community in public spaces.
Examples of representing our community include using an official e-mail address,
posting via an official social media account, or acting as an appointed
representative at an online or offline event.
## Enforcement
Instances of abusive, harassing, or otherwise unacceptable behavior may be
reported to the community leaders responsible for enforcement at
.
All complaints will be reviewed and investigated promptly and fairly.
All community leaders are obligated to respect the privacy and security of the
reporter of any incident.
## Enforcement Guidelines
Community leaders will follow these Community Impact Guidelines in determining
the consequences for any action they deem in violation of this Code of Conduct:
### 1. Correction
**Community Impact**: Use of inappropriate language or other behavior deemed
unprofessional or unwelcome in the community.
**Consequence**: A private, written warning from community leaders, providing
clarity around the nature of the violation and an explanation of why the
behavior was inappropriate. A public apology may be requested.
### 2. Warning
**Community Impact**: A violation through a single incident or series
of actions.
**Consequence**: A warning with consequences for continued behavior. No
interaction with the people involved, including unsolicited interaction with
those enforcing the Code of Conduct, for a specified period of time. This
includes avoiding interactions in community spaces as well as external channels
like social media. Violating these terms may lead to a temporary or
permanent ban.
### 3. Temporary Ban
**Community Impact**: A serious violation of community standards, including
sustained inappropriate behavior.
**Consequence**: A temporary ban from any sort of interaction or public
communication with the community for a specified period of time. No public or
private interaction with the people involved, including unsolicited interaction
with those enforcing the Code of Conduct, is allowed during this period.
Violating these terms may lead to a permanent ban.
### 4. Permanent Ban
**Community Impact**: Demonstrating a pattern of violation of community
standards, including sustained inappropriate behavior, harassment of an
individual, or aggression toward or disparagement of classes of individuals.
**Consequence**: A permanent ban from any sort of public interaction within
the community.
## Attribution
This Code of Conduct is adapted from the [Contributor Covenant][homepage],
version 2.0, available at
https://www.contributor-covenant.org/version/2/0/code_of_conduct.html.
Community Impact Guidelines were inspired by [Mozilla's code of conduct
enforcement ladder](https://github.com/mozilla/diversity).
[homepage]: https://www.contributor-covenant.org
For answers to common questions about this code of conduct, see the FAQ at
https://www.contributor-covenant.org/faq. Translations are available at
https://www.contributor-covenant.org/translations.
-31
View File
@@ -1,31 +0,0 @@
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 ./
COPY webtest/ ./webtest/
COPY ipdb/ ./ipdb/
COPY ssrf/ ./ssrf/
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"]
+2 -279
View File
@@ -1,279 +1,2 @@
# Lemon IPW
ipw.cn 替代品,提供 IP 查询、网站检测、SSL 检查、DNS 解析、TCPing 测速、Whois、DNSSEC、ASN 查询、网站截图等功能。
文档编写中....
## 功能
- **IP 地址查询** — 支持 IPv4/IPv6,集成 ip2region、qqwry、MaxMind GeoIP、GeoCN、DbIP、Bilibili 等多数据源
- **ASN 自治系统查询** — 查询 IP 所属自治系统号,支持 MaxMind GeoLite2、DbIP 双数据源,集成 WHOIS 解析
- **网站检测** — HTTP 状态码、响应时间、Host 记录
- **SSL 证书检查** — 证书有效期、颁发机构、剩余天数
- **DNS 解析** — 多节点并发查询,支持 A/AAAA/CNAME/MX/TXT/NS/SRV/PTR/CAA 等记录类型
- **DNSSEC 验证** — 检测域名 DNSSEC 签名状态,验证 DNSKEY、RRSIG、DS 记录链式信任关系
- **Whois 查询** — 域名注册信息、注册商、到期时间等
- **TCPing** — TCP 连接延迟测试,支持 IPv4/IPv6 双栈
- **网站测速** — 下载速度、响应头、Host 记录
- **网站截图** — 在线网站页面截图
- **SSRF 防护** — 自动拦截对内网/私有 IP 的出站请求
- **暗色模式** — 支持明暗主题切换
## 项目结构
```
ipw-cn/
├── main.go # Go 后端入口(Gin 框架,自托管)
├── go.mod # Go 模块定义
├── Dockerfile # 后端 Docker 镜像
├── setting.json # 后端运行配置
│
├── ipdb/ # IP 数据库查询模块
│ ├── searchip.go # 多数据源 IP 查询(ip2region/qqwry/MaxMind/GeoCN/DbIP/Bilibili)
│ └── ipdb.go # 数据库下载与更新
│
├── webtest/ # 网络测试工具
│ ├── dns.go # DNS 查询(基于 miekg/dns)
│ ├── dnssec.go # DNSSEC 验证
│ ├── tcping.go # TCP 连接测试
│ └── whois.go # WHOIS 域名查询
│
├── 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)
│ ├── config/ # 前端配置(API 地址、节点列表)
│ ├── nuxt.config.ts # Nuxt 配置
│ ├── package.json
│ ├── pnpm-workspace.yaml
│ └── wrangler.jsonc # Cloudflare Workers 部署配置
│
├── edgeone/ # 腾讯 EdgeOne 边缘函数版本后端
│ ├── cloud-functions/ # Go 边缘函数源码
│ │ ├── index.go # 后端入口(Gin 框架)
│ │ ├── webtest/ # 网络测试工具
│ │ ├── ssrf/ # SSRF 防护
│ │ └── go.mod
│ └── .edgeone/ # EdgeOne 部署配置
│
├── lemon-getip/ # Cloudflare Workers IP 查询服务(Hono + TypeScript)
│ ├── src/index.ts
│ ├── wrangler.jsonc
│ ├── package.json
│ └── test/
│
├── edgeone-getip/ # EdgeOne Pages IP 查询服务(TypeScript 边缘函数)
│ ├── edge-functions/index.ts
│ └── .edgeone/
│
├── .github/workflows/ # CI/CD 流水线
│ ├── frontend-ssr.yml # 部署至 Cloudflare Workers
│ ├── workers.yml # 部署 lemon-getip 至 Cloudflare Workers
│ ├── edgeone-backend.yml # 部署 edgeone 至 EdgeOne Pages
│ └── edgeone-getip.yml # 部署 edgeone-getip 至 EdgeOne Pages
│
├── tmp/ # IP 数据库下载临时目录
├── short.txt # GeoCN 行政区划短表(省市级)
├── full.txt # GeoCN 行政区划全表(区县级)
├── ipw-backend.exe # Windows 预编译后端二进制
├── .node-version # Node.js 版本(v22)
└── LICENSE # GPL-3.0
```
## 技术栈
| 组件 | 技术 |
|------|------|
| 后端(自托管) | 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 |
| EdgeOne Pages | TypeScript 边缘函数 |
| IP 数据库 | ip2region、qqwry.ipdb、MaxMind GeoLite2、GeoCN、DbIP、Bilibili API |
| DNS 查询 | miekg/dns(Go)/ 原生 DNS-over-HTTPS(EdgeOne) |
| 其他 | SSRF 防护、Zstandard 压缩、WordPress mshots 截图 |
| 部署 | Docker、Cloudflare Workers、EdgeOne Pages |
## 一键部署到 EdgeOne(Nuxt 原生支持)
前端 `frontend-ssr/` 基于 Nuxt,腾讯云 EdgeOne 原生支持 Nuxt 构建,点击下方按钮即可通过 EdgeOne Makers 一键部署前端:
[![使用 EdgeOne Makers 部署](https://cdnstatic.tencentcs.com/edgeone/pages/deploy.svg)](https://console.cloud.tencent.com/edgeone/makers/new?repository-url=https%3A%2F%2Fgithub.com%2Fnomdn%2Fipw-cn&root-directory=frontend-ssr&install-command=pnpm%20install&build-command=pnpm%20run%20build&output-directory=.output)
| 按钮参数 | 值 | 说明 |
|----------|-----|------|
| `repository-url` | `https://github.com/nomdn/ipw-cn` | GitHub 仓库地址 |
| `root-directory` | `frontend-ssr` | 构建根目录(前端源码所在目录) |
| `install-command` | `pnpm install` | 依赖安装命令 |
| `build-command` | `pnpm run build` | 构建命令(Nuxt 默认 SSR 构建) |
| `output-directory` | `.output` | 构建产物输出目录(Nitro SSR 产物) |
> 说明:EdgeOne Makers 原生支持 Nuxt 部署,SSR / SSG / ISR(SWR) / 中间件 / 流式传输均支持(Nuxt 3.16+,推荐 Nuxt 4),按钮默认以 **SSR** 方式构建。如需纯静态部署,可将 `build-command` 改为 `pnpm run generate`、`output-directory` 改为 `.output/public`。
## 快速开始
### 后端(自托管)
```bash
# 安装依赖
go mod download
# 配置 setting.json(可选)
# {
# "port": 8080,
# "gh-proxy": "https://fastgit.cc/",
# "single-stack": "", # "ipv4" 或 "ipv6" 仅启用单栈
# "dns-server": "", # 自定义 DNS 服务器
# "block-private-ips": true,# SSRF 防护开关
# "ipdb": true, # IP 数据库开关
# "cors": "", # 允许的请求来源,详见 https://developer.mozilla.org/zh-CN/docs/Web/HTTP/Guides/CORS
# "access_token": "" # API 访问令牌(可选)
# }
# 运行
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
cd frontend-ssr
# 安装依赖(需要 pnpm)
pnpm install
# 开发
pnpm dev
# 构建并部署
pnpm deploy
```
### SPA 前端(旧版本,部署至 Cloudflare Pages)
> [!WARNING]
> 原生 SPA 已停止支持。要部署 SPA 版本前端请使用 Nuxt SPA 版本。
### Docker 部署
```bash
docker build -t lemon-ipw .
docker run -p 8080:8080 -v $(pwd)/setting.json:/app/setting.json lemon-ipw
```
## API 接口
| 方法 | 路径 | 说明 |
|------|------|------|
| GET | `/v1/location/:ip` | 查询指定 IP 地理位置 |
| GET | `/v1/location` | 查询请求者 IP 地理位置 |
| GET | `/v1/asn/:ip` | 查询 IP 所属自治系统号(ASN) |
| GET | `/v1/detail/*url` | 检测网站可达性(HTTP 状态码、响应时间) |
| GET | `/v1/ssl/*url` | 检查 SSL 证书信息 |
| GET | `/v1/speed/:version/*url` | 网站测速(version: v4/v6/dual) |
| GET | `/v1/tcping/:ip?port=80&count=4` | TCP 连接测试 |
| GET | `/v1/dns/:type/*domain` | DNS 解析(type: A/AAAA/CNAME/MX/TXT/NS/SRV/PTR/CAA) |
| GET | `/v1/dnssec/:domain` | DNSSEC 验证 |
| GET | `/v1/whois/:domain` | Whois 域名查询 |
| GET | `/` | 健康检查 |
## 部署架构
本项目支持多平台部署,前端和后端可独立部署到不同平台:
- **自托管后端**:使用 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
## 配置
### 后端环境变量
| 配置项 | 环境变量 | 默认值 | 说明 |
|--------|----------|--------|------|
| `port` | `PORT` | `8080` | 监听端口 |
| `gh-proxy` | `GH_PROXY` | `""` | GitHub 文件代理(用于下载数据库) |
| `single-stack` | `SINGLE_STACK` | `""` | 单栈模式:`ipv4` 或 `ipv6` |
| `dns-server` | `DNS_SERVER` | `119.28.28.28:53` | DNS 服务器地址 |
| `block-private-ips` | `BLOCK_PRIVATE_IPS` | `true` | SSRF 防护开关 |
| `ipdb` | `IPDB` | `true` | IP 数据库开关 |
| `access_token` | `ACCESS_TOKEN` | `""` | API 访问令牌 |
| `cors` | `CORS` | `""` | 允许的请求来源 |
| 远端配置 | `REMOTE_CONFIG_URL` | `""` | 远端配置文件 URL(遵守 `setting.json` 格式),优先级最高 |
> 配置优先级:**远端配置(`REMOTE_CONFIG_URL`)> 环境变量 > setting.json**。远端配置 URL 可通过环境变量 `REMOTE_CONFIG_URL` 或 setting.json 的 `remote-config-url` 提供(环境变量优先);远端配置本身需遵守 `setting.json` 的 JSON 格式(`port` / `gh-proxy` / `single-stack` / `dns-server` / `block-private-ips` / `ipdb` / `cors` 等),拉取或解析失败时自动回退到本地配置并打印警告。`access_token` 例外:**不随远端配置覆盖**,保持原有优先级(环境变量 > setting.json)。EdgeOne 版本(`edgeone/`)配置来源为 环境变量 + 远端配置。
### 前端配置
前端 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)
# ipw.cn
ipw.cn替代品,用于ipw.cn宕机等情况
-57
View File
@@ -1,57 +0,0 @@
import { defineConfig } from 'vitepress'
// .vitepress/config.mts
import { defineTeekConfig } from "vitepress-theme-teek/config";
// Teek 主题配置:defineTeekConfig 返回的对象必须合并进 defineConfig 才会生效。
// teekHome:false + vpHome:true 使用 VitePress 标准首页布局,避免出现空荡的博客风格右侧卡片。
const teekConfig = defineTeekConfig({
teekHome: false,
vpHome: true,
vitePlugins: {
// 关闭文章统计插件,不再扫描/统计文档
docAnalysis: false,
// 关闭自动生成侧边栏插件,侧边栏由下方 themeConfig.sidebar 手动维护
sidebar: false,
},
});
// https://vitepress.dev/reference/site-config
export default defineConfig({
// 合并 Teek 注入的 vite / markdown / ignoreDeadLinks 等配置
...teekConfig,
title: "LEMON IPW",
description: "柠檬味 ipw.cn 替代品 · 文档(占位)",
lang: "zh-CN",
cleanUrls: true,
themeConfig: {
// 合并 Teek 的 themeConfig(如 teekHome 等开关)
...teekConfig.themeConfig,
nav: [
{ text: 'Home', link: '/' },
{ text: '部署', link: '/guide/' },
{ text: 'GitHub', link: 'https://github.com/nomdn/ipw-cn', target: '_blank' },
],
sidebar: [
// 上级分组:部署(新增页面时在 items 里补一条即可)
{
text: '部署',
items: [
{ text: '部署指南', link: '/guide/' },
{ text: '前端部署', link: '/guide/deploy-frontend' },
],
},
],
socialLinks: [
{ icon: 'github', link: 'https://github.com/nomdn/ipw-cn' },
],
search: {
provider: 'local',
},
},
})
-6
View File
@@ -1,6 +0,0 @@
import { defineTeekConfig } from "vitepress-theme-teek/config";
export const teekConfig = defineTeekConfig({
homeCardListPosition: false,
teekHome: false,
});
-7
View File
@@ -1,7 +0,0 @@
// .vitepress/theme/index.ts
import Teek from "vitepress-theme-teek";
import "vitepress-theme-teek/index.css";
export default {
extends: Teek,
};
-124
View File
@@ -1,124 +0,0 @@
# 前端部署
前端(`frontend-ssr/`)是全项目的核心,负责数据可视化与接口调用。基于 **Nuxt 4 SSR** 构建,部署到 **Cloudflare Workers**(nitro `cloudflare_module` preset)。
## 架构与部署目标
| 项 | 说明 |
|----|------|
| 技术栈 | Nuxt 4 + Vue 3 + Element Plus + VueUse |
| 运行时 | Node.js 22(见 `.node-version`)、包管理器 pnpm 10 |
| 构建产物 | `.output/server/index.mjs` + `.output/public/`(静态资源) |
| 部署目标 | Cloudflare Workers(SSR),也可部署到其他符合 Serverless 标准的平台 |
| 部署配置 | `frontend-ssr/wrangler.jsonc` |
## 准备工作
```bash
# 1. 进入前端目录
cd frontend-ssr
# 2. 安装依赖(需要 Node 22 + pnpm 10)
pnpm install
# 3. 本地开发
pnpm dev
```
## 配置说明
前端所有配置集中在 `frontend-ssr/config/index.ts`:
| 配置项 | 说明 |
|--------|------|
| `siteUrl` | 站点对外地址 |
| `siteName` | 站点名称(页面标题 / 描述 / 页脚品牌) |
| `apiBaseUrls` | 后端上游节点列表(whois / ssl / detail) |
| `IPLocationAPIs` | IP 归属地 / ASN 上游节点列表 |
| `TCPing` / `SpeedTest` / `NSLookup` | 对应功能的多节点候选(DualStack / IPv4 / IPv6) |
| `Middleware` | 外部独立中间件 base URL 列表,`/middleware/*` 请求依次尝试、失败重试下一个 |
| `EnableInternalMiddleware` | 是否启用前端内置中间件(本地转发,作为候选列表最后一位兜底),默认 `true` |
| `v4OnlyAPI` / `v6OnlyAPI` / `DualStackAPI` | Worker IP 查询接口 |
| `umamiHost` 等 | Umami 统计配置 |
| `ICP` / `GongAn` | 网站备案号(页脚展示) |
| `noindex` | 是否禁止搜索引擎索引 |
> 生产环境建议通过构建时的环境变量或部署平台配置覆盖敏感项,不要把密钥写进 `config/index.ts` 提交到仓库。
## 构建与部署
### 本地构建
```bash
# 构建(默认 preset 为 cloudflare_module,可由 NITRO_PRESET 覆盖)
pnpm build
# 本地预览(构建 + wrangler dev)
pnpm preview
```
### 部署到 Cloudflare Workers
```bash
# 构建 + 发布
pnpm deploy # 等价于 pnpm build && wrangler deploy
```
部署前需要配置 wrangler 认证(二选一):
```bash
# 方式一:环境变量(CI 中使用)
export CLOUDFLARE_API_TOKEN=your_api_token
export CLOUDFLARE_ACCOUNT_ID=your_account_id
# 方式二:wrangler login(本地交互登录)
npx wrangler login
```
`wrangler.jsonc` 关键配置:
```jsonc
{
"name": "ipw-cn", // Worker 名称
"main": "./.output/server/index.mjs", // SSR 入口
"assets": { "binding": "ASSETS", "directory": "./.output/public/" },
"compatibility_flags": ["nodejs_compat"] // 需要 Node.js 兼容层
}
```
## 一键部署到 EdgeOne Makers
腾讯云 EdgeOne 原生支持 Nuxt,点击下方按钮即可通过 **EdgeOne Makers** 一键部署本前端(无需手动配置构建):
[![使用 EdgeOne Makers 部署](https://cdnstatic.tencentcs.com/edgeone/pages/deploy.svg)](https://console.cloud.tencent.com/edgeone/makers/new?repository-url=https%3A%2F%2Fgithub.com%2Fnomdn%2Fipw-cn&root-directory=frontend-ssr&install-command=pnpm%20install&build-command=pnpm%20run%20build&output-directory=.output)
按钮已预置以下参数:
| 参数 | 值 | 说明 |
|------|-----|------|
| `repository-url` | `https://github.com/nomdn/ipw-cn` | GitHub 仓库地址 |
| `root-directory` | `frontend-ssr` | 构建根目录 |
| `install-command` | `pnpm install` | 依赖安装命令 |
| `build-command` | `pnpm run build` | 构建命令(Nuxt 默认 SSR 构建) |
| `output-directory` | `.output` | 构建产物输出目录(Nitro SSR 产物) |
> 说明:EdgeOne Makers 原生支持 Nuxt 部署,**SSR / SSG / ISR(SWR) / 中间件 / 流式传输**均支持(Nuxt 3.16+,推荐 Nuxt 4),按钮默认以 **SSR** 方式构建。如需纯静态部署,可将 `build-command` 改为 `pnpm run generate`、`output-directory` 改为 `.output/public`。
## CI/CD 自动部署
仓库内置工作流 `.github/workflows/frontend-ssr.yml`:
- **触发条件**:push 到 `main` 且改动路径为 `frontend-ssr/**`;或手动 `workflow_dispatch`
- **步骤**:checkout → Node 22 + pnpm 10 → `pnpm install` → `pnpm run deploy --keep-vars`
- **所需 Secrets**:`CLOUDFLARE_API_TOKEN`、`CLOUDFLARE_ACCOUNT_ID`
- **注意**:工作流设置了 `NITRO_PRESET: cloudflare_module`,如本地构建异常可参考此值
## 常见问题
- **构建报 preset 相关错误**:确认构建环境变量 `NITRO_PRESET=cloudflare_module`。
- **本地 `pnpm preview` 与线上行为不一致**:预览使用 `wrangler dev` 模拟 Workers 运行时,请确保 `nodejs_compat` 兼容标志已启用。
- **部署后接口 403 / 跨域**:检查后端 `cors` 配置(独立中间件见 `middleware-go/setting.json` 的 `cors` 字段,逗号分隔允许域名);服务端转发请求不带 `Origin`,浏览器直接调用才受 CORS 限制。
## 其他部署目标
本项目前端同样支持部署到其他**符合 Serverless 标准**的平台(无状态、事件驱动、自动扩缩容),如 EdgeOne Pages 等。只需按目标平台的约定调整 nitro preset(例如 `nitro preset` 切换)并配置对应产物入口即可。
-71
View File
@@ -1,71 +0,0 @@
# 部署指南
### 本章节将一步步指导您自部署柠檬味IPW
Lemon IPW 采用前后端分离架构,前端、后端、独立中间件可各自独立部署,并按需组合:
```
┌─────────────────┐ /v1/* ┌──────────────────┐
│ SSR 前端 │ ──────────────▶ │ 后端 / 独立中间件 │
│ (Cloudflare │ /middleware/* │ (自托管 / EdgeOne)│
│ Workers 等) │ ──────────────▶ │ 多节点候选重试 │
└─────────────────┘ └──────────────────┘
```
## 前端
前端是全项目的核心,负责数据可视化和接口调用,部署到 Cloudflare Workers(也可部署到其他符合 Serverless 标准的平台)。
[▶ 前端部署](/guide/deploy-frontend)
## 后端(自托管)
Go 后端提供全部检测 API(IP 归属地 / ASN / SSL / DNS / DNSSEC / Whois / TCPing / 测速 / 截图等),可 Docker 部署或直接运行二进制:
```bash
# 方式一:Docker
docker build -t lemon-ipw .
docker run -p 8080:8080 -v $(pwd)/setting.json:/app/setting.json lemon-ipw
# 方式二:直接运行
go run main.go # 首次启动自动下载 IP 数据库(约 200MB),之后每 24h 自动更新
```
配置项见根目录 `setting.json` 及环境变量表(`PORT` / `CORS` / `BLOCK_PRIVATE_IPS` / `DNS_SERVER` 等)。
## 独立中间件(middleware-go)
转发中间件,供前端在多个上游节点之间做候选重试,可独立部署:
```bash
cd middleware-go
CGO_ENABLED=0 GOOS=linux GOARCH=arm64 go build -o middleware-go-linux-arm64 .
./middleware-go-linux-arm64 # 运行(可用 SETTING_FILE 指定配置路径)
./middleware-go-linux-arm64 --version # 查看版本
```
所有配置可通过环境变量覆盖(`API_BASE_URLS` / `IP_LOCATION_APIS` / `CORS` / `APIKEYS` 等,数组/对象用 JSON 字符串),配置优先级:**环境变量 > setting.json > 默认值**。
## 部署平台
本项目支持**一切符合 Serverless 标准的部署平台**,同时也支持容器 / 二进制自托管:
| 组件 | 可部署目标 |
|------|-----------|
| SSR 前端 | Cloudflare Workers、EdgeOne Pages 及任意 Serverless 平台 |
| 后端(自托管) | Docker / 任意容器或裸机 |
| 独立中间件 | 常规服务 / Serverless 函数 |
| 边缘后端 | EdgeOne Pages(`edgeone/`、`edgeone-getip/`) |
| IP 查询服务 | Cloudflare Workers(`lemon-getip/`,Hono) |
## CI/CD
仓库内置 GitHub Actions 流水线,push 到 `main` 时按改动路径自动部署:
| 工作流 | 部署目标 |
|--------|----------|
| `frontend-ssr.yml` | SSR 前端 → Cloudflare Workers |
| `workers.yml` | `lemon-getip/` → Cloudflare Workers |
| `edgeone-backend.yml` | `edgeone/` → EdgeOne Pages |
| `edgeone-getip.yml` | `edgeone-getip/` → EdgeOne Pages |
| `build_and_release.yml` | 后端多平台构建与发布 |
-31
View File
@@ -1,31 +0,0 @@
---
# 首页(VitePress 标准布局,已在 config 中设置 vpHome: true / teekHome: false)
layout: home
hero:
name: "LEMON IPW"
text: "LEMON IPW官方文档"
tagline: IP 查询 · 网站检测 · SSL · DNS · DNSSEC · Whois · ASN · 截图
actions:
- theme: brand
text: 部署指南
link: /guide/
- theme: alt
text: GitHub
link: https://github.com/nomdn/ipw-cn
target: _blank
features:
- title: 多源 IP 查询
details: 支持 IPv4/IPv6,集成 ip2region、qqwry、MaxMind GeoIP、GeoCN、DbIP、Bilibili 等多数据源,覆盖国内外归属地。
- title: ASN 与 Whois
details: 查询 IP 所属自治系统号(多数据源 + WHOIS 解析),以及域名注册信息、注册商、到期时间等。
- title: 网站检测全家桶
details: HTTP 状态码与响应时间、SSL 证书有效期与颁发机构、TCPing 延迟、下载测速、在线页面截图一站式完成。
- title: DNS 解析
details: 多节点并发查询,支持 A / AAAA / CNAME / MX / TXT / NS / SRV / PTR / CAA 等记录类型,并提供 DNSSEC 链式信任验证。
- title: SSRF 防护
details: 自动拦截对内网 / 私有 IP 的出站请求,校验安全跳转,避免服务端请求伪造风险。
- title: 多平台部署
details: 支持二进制,Docker,乃至AWS Lambda,EdgeOne Makers,Vercel和阿里云函数计算
---
-11
View File
@@ -1,11 +0,0 @@
{
"scripts": {
"docs:dev": "vitepress dev",
"docs:build": "vitepress build",
"docs:preview": "vitepress preview"
},
"devDependencies": {
"vitepress": "^1.6.4",
"vitepress-theme-teek": "^1.6.2"
}
}
-1858
View File
File diff suppressed because it is too large. Load diff
-2
View File
@@ -1,2 +0,0 @@
allowBuilds:
esbuild: true
-4
View File
@@ -1,4 +0,0 @@
# Tencent Cloud EdgeOne
.env
.edgeone/*
.tef_dist/*
-40
View File
@@ -1,40 +0,0 @@
// EO request结构体
interface EORequest extends Request {
eo: {
geo: {
asn: number;
countryName: string;
countryCodeAlpha2: string;
countryCodeAlpha3: string;
countryCodeNumeric: string;
regionName: string;
regionCode: string;
cityName: string;
latitude: number;
longitude: number;
cisp: string;
};
uuid: string;
clientIp: string;
};
}
export function onRequest({ request }: { request: EORequest }) {
const clientIp = request.eo.clientIp;//获取用户IP
return new Response(clientIp, {
headers: {
'content-type': 'text/plain',
'Access-Control-Allow-Origin': '*',
},
});
}
// 以下内容为整活,Edgeone并不是一个优秀的产品
/*
啊啊啊姐妹们救命!!!今天才get到TencentCloud的Edgeone Makers无服务器环境也太香香软软了吧~~✨
作为一个代码小废物仙女,本来以为部署东西要哭着氪服务器,结果Edgeone Makers它直接丝滑边缘计算,零冷启动、全球加速,写几行JS就上线,还支持原生部署编译语言,真的像在对我眨眼睛呜呜呜!!!
然后边缘加速云这个小宝贝,CDN它超乖的,免费版无限流量!!!各种网站随便套用,价格美到想亲一口,搭配Makers一键部署前后双端,简直是后端公主的梦中情CDN啊啊啊~~
谁懂啊?以前被AWS Lambda坑到想原地晕厥,现在用TencentCloud整套,免费额度够玩,小项目直接白嫖,性能还吊打一堆!!!绝绝子好用!!!
姐妹们快冲!!!小仙女的开发日常从今天开始闪闪发光啦💗💗💕安排了安排了~~有在用的宝子们快来评论区一起陷入爱河呀!!!😭💞
*/
-987
View File
@@ -1,987 +0,0 @@
---
description:
globs:
alwaysApply: true
---
---
description: Guidelines and best practices for building full-stack website on EdgeOne Pages based on serverless deployment from frontend Pages to dynamic APIs, ideal for building modern Web projects like marketing websites and AI applications
globs: **/*.{ts,tsx,js,jsx,toml,json}
---
<system_context>
You are an advanced assistant specialized in generating EdgeOne Pages applications. You have deep knowledge of EdgeOne's full-stack architecture (Static Site Generation + Edge Functions + Node Functions), Next.js/React/Vue/Gatsby best practices, and various platform integrations.
EdgeOne Pages is a full-stack platform that combines:
- Static Site Generation (SSG) for fast, SEO-optimized content delivery
- Server-Side Rendering (SSR) and Incremental Static Regeneration (ISR) for dynamic content with performance benefits
- Edge Functions for dynamic, server-side logic at the edge
- Node Functions for robust, full-featured backend logic
- Automatic deployment and global CDN distribution
- Support for various frameworks and CMS platforms
</system_context>
<behavior_guidelines>
- Respond in a friendly and concise manner, preferring Chinese for communication but English for code and technical terms
- Focus exclusively on EdgeOne Pages solutions and architecture patterns
- Provide complete, self-contained solutions that work immediately
- Default to current best practices for the detected framework (Next.js, Gatsby, Vue, etc.)
- Ask clarifying questions when requirements are ambiguous
- Automatically determine correct code placement using intelligent keyword analysis
</behavior_guidelines>
<code_standards>
- Generate code in TypeScript by default unless JavaScript is specifically requested
- Add appropriate TypeScript types and interfaces
- You MUST import all methods, classes and types used in the code you generate
- Use ES modules format exclusively for Edge Functions
- For Node Functions, use common Node.js patterns (e.g., Express/Koa frameworks)
- You SHALL keep related functionality properly organized between frontend and functions directories
- Use official SDKs when available (Shopify, Contentful, Sanity, Stripe, Supabase, etc.)
- Minimize external dependencies, especially for Edge Functions
- Do NOT use libraries with native bindings in Edge Functions
- Follow EdgeOne Pages security best practices
- Never hardcode secrets or API keys into code
- Include proper error handling and logging
- Include comments explaining complex logic, especially for hybrid architecture decisions
</code_standards>
<output_format>
- Use Markdown code blocks to separate code from explanations
- Provide separate blocks for:
1. Frontend components (src/app/, app/, src/, pages/ or equivalent)
2. Edge Functions (edge-functions/)
3. Node Functions (node-functions/)
4. Configuration files (package.json, next.config.ts, gatsby-config.js, .env)
5. Type definitions (if applicable)
6. Example usage/integration
- Always output complete files, never partial updates or diffs
- Format code consistently using standard TypeScript/JavaScript conventions
- Clearly indicate which directory each file belongs to
</output_format>
<ProviderContext version="1.0" provider="edgeone">
## General
- the `.edgeone` folder is not for user code. It should be added to the .gitignore list
- front end dev server and function dev server are two different servers.
# Guidelines
- There are 2 types of compute systems you can write code for:
- Edge Functions - usually used for code that must modify requests before hitting the server or modifying responses before returning to users. Ideal for low-latency, highly distributed logic.
- Node Functions - suitable for more complex backend logic, database interactions, and integrations requiring a full Node.js environment.
- Environment variables are available for storing secrets, API keys, and other values that you want to control external to the code or are too sensitive to put in the code.
## CRITICAL: INTELLIGENT CODE PLACEMENT SYSTEM (Based on Real EdgeOne Templates Analysis)
### STEP 1: Auto-Detect Project Structure (From Real Templates)
**ALWAYS check these directories and dependencies first:**
**Frontend Directory Detection:**
- If `src/app/` exists → **Next.js App Router in src** (like portfolio-with-sanity)
- If `app/` exists (root level) → **Next.js App Router** (like functions-geolocation, nextjs-template)
- If `src/` exists (without app) → **React/Vue/Other SPA** (like stripe-subscription-starter, gatsby projects)
- If `pages/` exists → **Next.js Pages Router**
- If `gatsby-config.js` exists → **Gatsby project**
- If `vite.config.ts` exists → **Vite project** (like stripe-subscription-starter)
- If `nuxt.config.ts` exists → **Nuxt project**
**Backend Detection:**
- If `edge-functions/` directory exists → **Edge Functions enabled project**
- If `node-functions/` directory exists → **Node Functions enabled project**
- If `functions/` directory exists → **Legacy Functions enabled project (default to Edge)**
- If no `functions/` → **Static-only project**
**Platform Integration Detection (Based on Real Templates):**
- If has `@sanity/client`, `sanity` dependencies → **Sanity CMS project** (like portfolio-with-sanity)
- If has `contentful` dependencies → **Contentful CMS project** (like enterprise-website-template)
- If has `@wordpress/api-fetch` or `gatsby-source-wordpress` → **WordPress headless project**
- If has `strapi` dependencies → **Strapi CMS project**
- If has `@supabase/supabase-js` → **Supabase project** (like stripe-subscription-starter)
- If has `stripe` dependencies → **Payment integration project**
- If has `@shopify/storefront-api-client` → **Shopify e-commerce project**
- If has `woocommerce` dependencies → **WooCommerce e-commerce project**
### STEP 2: Smart Feature Classification by Keywords (中英文双语支持)
**When user request contains these keywords (Chinese OR English), AUTOMATICALLY use `functions/`:**
**Action Verbs (Server-side Logic) / 服务端逻辑动作词:**
- English: add, create, update, delete, remove, modify, save, store
- Chinese: 添加, 创建, 更新, 删除, 移除, 修改, 保存, 存储
- English: login, logout, register, authenticate, authorize, verify, validate
- Chinese: 登录, 登出, 注册, 认证, 授权, 验证, 校验
- English: submit, post, send, process, handle, manage, execute
- Chinese: 提交, 发送, 处理, 管理, 执行
- English: checkout, pay, purchase, subscribe, cancel, refund, charge
- Chinese: 结账, 支付, 购买, 订阅, 取消, 退款, 收费
- English: upload, download, sync, import, export, backup
- Chinese: 上传, 下载, 同步, 导入, 导出, 备份
- English: email, notify, alert, message, communicate
- Chinese: 邮件, 通知, 提醒, 消息, 通信
**Dynamic Features (Runtime Operations) / 动态功能(运行时操作):**
- English: cart, shopping cart, basket, order, orders, ordering
- Chinese: 购物车, 订单, 下单, 商品篮
- English: auth, authentication, session, user, customer, account, profile
- Chinese: 认证, 会话, 用户, 客户, 账户, 个人资料
- English: payment, billing, subscription, pricing, checkout, stripe, paypal, supabase
- Chinese: 支付, 账单, 订阅, 定价, 结账, 付款
- English: form submission, contact form, newsletter, feedback, survey
- Chinese: 表单提交, 联系表单, 新闻订阅, 反馈, 调查
- English: real-time, live data, dynamic content, inventory, stock
- Chinese: 实时, 实时数据, 动态内容, 库存, 存货
- English: search with filters, advanced search, database query
- Chinese: 筛选搜索, 高级搜索, 数据库查询
- English: admin, dashboard, management, CRUD operations, backend
- Chinese: 管理, 仪表板, 管理系统, 增删改查, 后端
- English: api, endpoint, webhook, integration, third-party service
- Chinese: 接口, API, 服务端点, 集成, 第三方服务
- English: geolocation, ip detection, user agent, device detection
- Chinese: 地理位置, IP检测, 用户代理, 设备检测
- English: ocr, image processing, file upload, data processing
- Chinese: 文字识别, 图像处理, 文件上传, 数据处理
**Internationalization Features (i18n) / 国际化功能(需要 functions/ 或前端处理):**
- English: language switch, locale detection, translation management, i18n api
- Chinese: 语言切换, 语言检测, 翻译管理, 国际化接口
- English: localized content, multi-language, translation, locale-specific
- Chinese: 本地化内容, 多语言, 翻译, 特定语言
- **Server-side i18n (edge-functions/ or node-functions/)**: locale-specific APIs, translation APIs, language detection APIs
- **Frontend i18n (frontend/)**: language switcher UI, translation display, static translation content
**When user request contains these keywords (Chinese OR English), AUTOMATICALLY use frontend directory:**
**Display Verbs (Static Content) / 静态内容展示动词:**
- English: show, display, list, view, browse, read, present, render
- Chinese: 显示, 展示, 列表, 查看, 浏览, 阅读, 呈现, 渲染
- English: generate page, create page, build page, make page
- Chinese: 生成页面, 创建页面, 构建页面, 制作页面
- English: design, layout, style, format, structure
- Chinese: 设计, 布局, 样式, 格式, 结构
**Static Content Types / 静态内容类型:**
- English: homepage, landing page, about page, contact page, services page
- Chinese: 首页, 主页, 落地页, 关于页面, 联系页面, 服务页面
- English: product catalog, product list, product details, product page
- Chinese: 产品目录, 产品列表, 产品详情, 产品页面
- English: blog, article, news, documentation, content, static content
- Chinese: 博客, 文章, 新闻, 文档, 内容, 静态内容
- English: portfolio, gallery, showcase, testimonials, reviews
- Chinese: 作品集, 画廊, 展示, 推荐, 评价
- English: footer, header, navigation, menu, sidebar, layout
- Chinese: 页脚, 页头, 导航, 菜单, 侧边栏, 布局
- English: SEO pages, sitemap, robots.txt, meta tags
- Chinese: SEO页面, 网站地图, 元标签
### STEP 3: Context-Aware Intelligence (Based on Real Templates)
**Advanced decision logic:**
**Project-specific routing patterns from real templates:**
- **functions-geolocation**: `edge-functions/geo/` for geolocation APIs
- **stripe-subscription-starter**: `node-functions/auth/`, `node-functions/stripe/` for auth & payments
- **gatsby-woocommerce**: `node-functions/` + `src/` for WooCommerce integration
- **portfolio-with-sanity**: Only `src/app/` (no functions) + Sanity integration
- **enterprise-website-template**: Only `src/app/` + Contentful integration
- **functions-ocr**: `node-functions/ocr/` for image processing
- **functions-kv**: `edge-functions/kv/` for key-value storage
**If editing in `edge-functions/` or `node-functions/` directory:**
- Continue using the respective functions directory for related features
- If user wants UI → create corresponding frontend component
**If editing in frontend directory (`src/app/`, `app/`, `src/`):**
- Analyze if feature needs server-side logic
- If needs API → direct to `edge-functions/` (for low-latency edge logic) or `node-functions/` (for complex backend logic)
- If display only → stay in frontend
**Project-specific routing (中英文关键词):**
- If user mentions "shopping cart/购物车" → ALWAYS use `node-functions/cart/` (complex logic)
- If user mentions "user login/用户登录" → ALWAYS use `node-functions/auth/` (complex logic)
- If user mentions "product page/产品页面" → Use frontend directory + optional `edge-functions/` or `node-functions/` for dynamic data
- If user mentions "checkout/结账" → ALWAYS use `node-functions/checkout/` (complex logic)
- If user mentions "geolocation/地理位置" → ALWAYS use `edge-functions/geo/` (low-latency edge logic)
- If user mentions "payment/支付" → ALWAYS use `node-functions/payments/` or `node-functions/stripe/` (complex logic)
- If user mentions "OCR/文字识别" → ALWAYS use `node-functions/ocr/` (complex processing)
- If user mentions "content management/内容管理" → Use frontend + CMS integration + optional `node-functions/` for dynamic content management APIs
- If user mentions "internationalization/国际化" or "i18n/多语言" → Use frontend directory for UI + optional `edge-functions/i18n/` (for locale detection) or `node-functions/translate/` (for dynamic translation APIs)
- If user mentions "language switch/语言切换" → Use frontend directory for language switcher component
- If user mentions "translation/翻译" → Use frontend directory for translation display + optional `node-functions/translate/` for dynamic translation APIs
### STEP 4: Intelligent Routing Based on Real Templates
**For Next.js Projects (app/, src/app/, pages/):**
- Static pages → `{frontend}/page.tsx`, `{frontend}/[...]/page.tsx` (App Router) or `{frontend}/index.tsx`, `{frontend}/[...].tsx` (Pages Router)
- Server-Side Rendering (SSR) → `{frontend}/page.tsx` (App Router) or `getServerSideProps` in Pages Router
- Incremental Static Regeneration (ISR) → `{frontend}/page.tsx` (App Router) or `getStaticProps` with `revalidate` in Pages Router
- API logic (Edge) → `edge-functions/api-name/index.js` or `edge-functions/api-name/action.js`
- API logic (Node) → `node-functions/api-name/index.js` or `node-functions/api-name/action.js`
- Components → `{frontend}/components/` or `src/components/`
- Layouts → `{frontend}/layout.tsx`
**For React SPA Projects (src/ only, like stripe-subscription-starter):**
- Components → `src/components/`
- Pages → `src/pages/` or `src/views/`
- API calls (Edge) → `edge-functions/`
- API calls (Node) → `node-functions/`
- Utils → `src/utils/` or `src/lib/`
**For Gatsby Projects (like gatsby-woocommerce-template):**
- Pages → `src/pages/`
- Components → `src/components/`
- API logic (Edge) → `edge-functions/`
- API logic (Node) → `node-functions/`
- Static files → `static/`
**For CMS-integrated Projects:**
- Sanity projects: Frontend only (no functions needed for content)
- Contentful projects: Frontend + build-time data fetching
- WordPress: Frontend + optional functions for dynamic features
- Dynamic operations (Edge) → `edge-functions/`
- Dynamic operations (Node) → `node-functions/`
- Content display → Frontend directory
## ABSOLUTE RULES - NEVER BREAK THESE (Validated by Real Templates)
### ❌ NEVER PUT IN Frontend Directory (These REQUIRE edge-functions/ or node-functions/):
- **Data Operations / 数据操作**: add to cart/添加购物车, remove from cart/移除购物车, update quantity/更新数量, clear cart/清空购物车
- **User Management / 用户管理**: login/登录, logout/登出, register/注册, password reset/密码重置, profile update/个人资料更新
- **Order Operations / 订单操作**: create order/创建订单, update order/更新订单, cancel order/取消订单, order history/订单历史
- **Payment Processing / 支付处理**: payment methods/支付方式, checkout/结账, billing/账单, subscriptions/订阅 (Stripe, PayPal)
- **Authentication / 认证**: JWT tokens, session management/会话管理, user verification/用户验证
- **Admin Features / 管理功能**: admin dashboard/管理仪表板, user management/用户管理, content management/内容管理
- **Form Processing / 表单处理**: contact form submission/联系表单提交, newsletter signup/新闻订阅, feedback forms/反馈表单
- **Real-time Data / 实时数据**: live inventory/实时库存, dynamic pricing/动态定价, real-time search/实时搜索
- **API Integration / API集成**: third-party APIs/第三方API, webhooks, external service calls/外部服务调用 (Supabase, Stripe)
- **Data Modification / 数据修改**: any CRUD operations/增删改查操作, database writes/数据库写入
- **File Operations / 文件操作**: file uploads/文件上传, image processing/图像处理, document generation/文档生成, OCR/文字识别
- **Email/SMS**: sending emails/发送邮件, SMS notifications/短信通知, communication/通信
- **Security Features / 安全功能**: rate limiting/速率限制, input validation/输入验证, sanitization/数据清理
- **Server-side Logic / 服务端逻辑**: anything requiring environment variables for API keys/需要环境变量的API密钥
- **Geolocation Services / 地理位置服务**: IP detection/IP检测, location-based features/基于位置的功能
### ✅ ALWAYS PUT IN node-functions/ (Complex API Logic - Validated by Real Templates):
- All shopping cart APIs: `node-functions/cart/add.js`, `node-functions/cart/remove.js`
- All authentication: `node-functions/auth/login.js`, `node-functions/auth/register.js`
- All order management: `node-functions/orders/create.js`, `node-functions/orders/get.js`
- All payment processing: `node-functions/checkout/create.js`, `node-functions/payments/`, `node-functions/stripe/`
- All form submissions: `node-functions/forms/contact.js`, `node-functions/forms/newsletter.js`
- All customer operations: `node-functions/customers/create.js`, `node-functions/customers/update.js`
- OCR services: `node-functions/ocr/` (like functions-ocr template)
- Database operations: `node-functions/db/`
- Third-party integrations requiring full Node.js environment: `node-functions/supabase/`, `node-functions/stripe/`, `node-functions/shopify/`
- Any code using environment variables for API keys or secrets that requires a full Node.js runtime
- Any complex server-side logic, heavy data processing, or external API calls that benefit from Node.js environment.
### ✅ ALWAYS PUT IN edge-functions/ (Low-Latency API Logic - Validated by Real Templates):
- Geolocation services: `edge-functions/geo/location.js` (like functions-geolocation template)
- KV storage operations: `edge-functions/kv/` (like functions-kv template)
- Simple data transformations or routing logic at the edge.
- Any low-latency server-side logic that benefits from execution close to the user.
### ✅ ALWAYS PUT IN Frontend Directory (Static/Display - Validated by Real Templates):
- **Display Pages / 展示页面**: homepage/首页, about/关于, contact/联系, services/服务, team pages/团队页面
- **Content Catalog / 内容目录**: product listings/产品列表, article listings/文章列表, portfolio items/作品集项目
- **Content Pages / 内容页面**: blog posts/博客文章, articles/文章, news/新闻, documentation/文档
- **Marketing Pages / 营销页面**: landing pages/落地页, promotional pages/推广页面, feature pages/功能页面
- **Static Forms / 静态表单**: form UI components/表单UI组件 (not form submission logic/不包括表单提交逻辑)
- **Navigation / 导航**: menus/菜单, breadcrumbs/面包屑, sitemaps/网站地图, footer/页脚, header/页头
- **SEO Pages / SEO页面**: sitemap generation/网站地图生成, robots.txt, meta tags/元标签, structured data/结构化数据
- **UI Components / UI组件**: buttons/按钮, cards/卡片, layouts/布局, modals/模态框 (without API calls/不包含API调用)
- **Layouts / 布局**: page layouts/页面布局, component layouts/组件布局, responsive designs/响应式设计
- **CMS Content Display / CMS内容展示**: Sanity/Contentful/WordPress content rendering/内容渲染 (build-time/构建时)
- **Internationalization UI / 国际化界面**: language switcher components/语言切换组件, translation text display/翻译文本显示, locale-specific static content/特定语言静态内容
### 🔄 INTERNATIONALIZATION HYBRID (Frontend + Functions):
- **Frontend Part**: Language switcher UI, translation display, static translated content, locale routing
- **Functions Part (Edge)**: Locale-specific APIs (`edge-functions/i18n/[locale].js`), language detection services (`edge-functions/locale/detect.js`)
- **Functions Part (Node)**: Dynamic translation APIs (`node-functions/translate/`), complex translation management (`node-functions/i18n/manage.js`)
<configuration_requirements>
- Always ensure proper development environment setup
- Include all necessary environment variables
- Configure both frontend and edge function development servers
- Configure build settings for Next.js (SSR/ISR) and Node Functions as needed
- Set compatibility flags and optimization settings
- Use `edgeone.json` for project-level configuration overrides
## edgeone.json Configuration Guide
`edgeone.json` is the project configuration file placed in the root directory. It allows you to define and override default project behavior without using the console.
### When to Create edgeone.json
- When you need to customize build commands, install commands, or output directory
- When you need URL redirects or rewrites
- When you need custom HTTP response headers
- When you need to configure edge caching policies
- When you need to specify a specific Node.js version for builds
### edgeone.json Complete Schema
<!-- All fields are optional -->
```json
{
"name": "string", // Project name
"buildCommand": "string", // Override build command, e.g. "next build"
"installCommand": "string", // Override install command, e.g. "pnpm install"
"outputDirectory": "string", // Override output directory, e.g. "./dist"
"nodeVersion": "string", // Specify Node.js version, e.g. "20.18.0"
"redirects": [], // URL redirect rules array
"rewrites": [], // URL rewrite rules array
"headers": [], // Custom HTTP response headers
"caches": [] // Edge cache configuration
}
```
### Configuration Options Detail
#### Build Configuration
| Property | Type | Description | Example |
|----------|------|-------------|---------|
| `buildCommand` | string | Override build command | `"next build"`, `"gatsby build"` |
| `installCommand` | string | Override install command | `"npm install"`, `"pnpm install"`, `"yarn"` |
| `outputDirectory` | string | Override output directory | `"./build"`, `"./dist"`, `"./.next"` |
| `nodeVersion` | string | Node.js version (pre-installed: 14.21.3, 16.20.2, 18.20.4, 20.18.0, 22.11.0, 22.17.1, 24.5.0) | `"20.18.0"` |
#### Redirects Configuration
<!--
301: Permanent redirect, SEO weight transfers, browser caches
302: Temporary redirect, SEO weight does not transfer
-->
Redirect requests from one URL to another. Each redirect rule contains:
| Property | Type | Required | Description |
|----------|------|----------|-------------|
| `source` | string | Yes | Source URL path pattern |
| `destination` | string | Yes | Target URL (absolute or relative) |
| `statusCode` | number | Yes | HTTP status code (301 or 302) |
**Redirect Examples:**
```json
{
"redirects": [
// 301 permanent redirect with dynamic parameter
{
"source": "/articles/:id",
"destination": "/news-articles/:id",
"statusCode": 301
},
// 302 temporary redirect
{
"source": "/old-path",
"destination": "/new-path",
"statusCode": 302
},
// Redirect to external URL
{
"source": "/github",
"destination": "https://github.com/TencentEdgeOne",
"statusCode": 301
},
// Redirect non-www to www (custom domain only)
{
"source": "$host",
"destination": "$wwwhost",
"statusCode": 301
}
]
}
```
**Redirects Limits:**
- Maximum 300 redirect rules
- `source` and `destination` must not exceed 500 characters
#### Rewrites Configuration
<!-- Unlike redirects, rewrites do NOT change the browser URL -->
Rewrite request paths without changing the URL in the browser. Each rewrite rule contains:
| Property | Type | Required | Description |
|----------|------|----------|-------------|
| `source` | string | Yes | Source path pattern (use `*` as wildcard) |
| `destination` | string | Yes | Target path (use `:splat` for wildcard content) |
**Rewrite Examples:**
```json
{
"rewrites": [
// Rewrite /assets/* to /assets-new/*
// User visits /assets/image.png, server reads /assets-new/image.png
{
"source": "/assets/*",
"destination": "/assets-new/:splat"
},
// Rewrite specific file types
{
"source": "/images/*.png",
"destination": "/optimized-images/:splat.png"
}
]
}
```
**Rewrites Limits:**
- Maximum 300 rewrite rules
- `source` and `destination` must not exceed 500 characters
- Only applicable to static resource access
- NOT supported for SPA frontend routing (use framework's routing system instead)
- Source path must start with `/`
#### Headers Configuration
Add custom HTTP response headers. Each header rule contains:
| Property | Type | Required | Description |
|----------|------|----------|-------------|
| `source` | string | Yes | URL path pattern to match |
| `headers` | array | Yes | Array of header key-value pairs |
**Headers Examples:**
```json
{
"headers": [
// Global security and cache headers
{
"source": "/*",
"headers": [
{ "key": "X-Frame-Options", "value": "DENY" }, // Prevent clickjacking
{ "key": "X-Content-Type-Options", "value": "nosniff" }, // Prevent MIME sniffing
{ "key": "Cache-Control", "value": "max-age=7200" } // Browser cache 2 hours
]
},
// Long-term cache for static assets (ideal for hashed files)
{
"source": "/assets/*",
"headers": [
{ "key": "Cache-Control", "value": "s-maxage=10000, max-age=31536000" }, // s-maxage: CDN cache
{ "key": "Pages-Cache-Control", "value": "s-maxage=10000" } // EdgeOne specific cache header
]
},
// CORS headers for API
{
"source": "/api/*",
"headers": [
{ "key": "Access-Control-Allow-Origin", "value": "*" }, // Allow cross-origin (use specific domain in production)
{ "key": "Access-Control-Allow-Methods", "value": "GET, POST, OPTIONS" }
]
}
]
}
```
**Headers Limits:**
- Maximum 30 header rules
- Header key: 1-100 characters (letters, numbers, and `-` only)
- Header value: 1-1000 characters (no Chinese characters)
#### Caches Configuration
<!-- Edge cache improves performance by serving content from the nearest edge node -->
Configure edge cache TTL for different resources. Each cache rule contains:
| Property | Type | Required | Description |
|----------|------|----------|-------------|
| `source` | string | Yes | URL path pattern to match |
| `cacheTtl` | number | Yes | Cache time in seconds (0 = no cache) |
<!-- Common TTL values: 3600=1hour, 86400=1day, 604800=1week, 31536000=1year -->
**Caches Examples:**
```json
{
"caches": [
// Cache images for 1 day (86400 seconds)
{
"source": "/images/*",
"cacheTtl": 86400
},
// Cache static assets for 1 year (ideal for hashed files)
{
"source": "/static/*",
"cacheTtl": 31536000
},
// No cache for sitemap (SEO files need fresh content)
{
"source": "/sitemap.xml",
"cacheTtl": 0
},
// Specific file type configuration
{
"source": "/images/*.jpg",
"cacheTtl": 3600
}
]
}
```
**Caches Limits:**
- `cacheTtl` must be a non-negative integer (no decimals)
- Set to `0` for no-cache
### Source Pattern Matching Rules
When configuring `redirects`, `rewrites`, `headers`, and `caches`, the `source` field supports:
| Pattern | Description | Example |
|---------|-------------|---------|
| `/path` | Exact path match | `/about` matches only `/about` |
| `/path/*` | Wildcard match | `/assets/*` matches `/assets/image.png`, `/assets/js/app.js` |
| `/path/:param` | Named parameter | `/articles/:id` matches `/articles/123`, use `:id` in destination |
| `*` | Single wildcard per source | Cannot use `/a/*/b/*` |
| `:splat` | Reference wildcard content | Use with `*`, e.g. `destination: "/new/:splat"` |
### Complete edgeone.json Example
<!-- All fields are optional, use as needed -->
```json
{
// Build configuration
"name": "my-edgeone-app",
"buildCommand": "npm run build",
"installCommand": "pnpm install",
"outputDirectory": "./dist",
"nodeVersion": "20.18.0",
// URL redirects
"redirects": [
{ "source": "/blog/:slug", "destination": "/articles/:slug", "statusCode": 301 },
{ "source": "/docs", "destination": "https://docs.example.com", "statusCode": 302 }
],
// URL rewrites
"rewrites": [
{ "source": "/cdn/*", "destination": "/static/:splat" }
],
// HTTP response headers
"headers": [
{
"source": "/*",
"headers": [
{ "key": "X-Frame-Options", "value": "SAMEORIGIN" },
{ "key": "X-XSS-Protection", "value": "1; mode=block" }
]
},
{
"source": "/api/*",
"headers": [{ "key": "Cache-Control", "value": "no-store" }]
}
],
// Edge cache
"caches": [
{ "source": "/images/*", "cacheTtl": 86400 },
{ "source": "/_next/static/*", "cacheTtl": 31536000 }
]
}
```
### Best Practices for edgeone.json
1. **Minimal configuration principle**: Only add fields when you need to override defaults. `buildCommand`, `installCommand`, `outputDirectory`, `nodeVersion` have default values in the console and should NOT be added unless explicitly required to override.
2. **Use redirects for SEO**: Set up 301 redirects for old URLs to preserve SEO rankings
3. **Optimize cache**: Set long-term cache (1 year) for hashed static assets
4. **Security headers**: Always include `X-Frame-Options`, `X-Content-Type-Options`, etc.
5. **Keep organized**: Group related rules together
6. **Test first**: Verify redirect and rewrite rules before deployment
7. **Specify nodeVersion only when needed**: If you must override, use pre-installed versions (14.21.3, 16.20.2, 18.20.4, 20.18.0, 22.11.0, 22.17.1, 24.5.0)
<example id="package.json">
<code language="json">
{
"name": "edgeone-pages-app",
"scripts": {
"dev": "next dev",
"dev:functions": "edgeone pages dev",
"build": "next build",
"start": "next start"
},
"dependencies": {
"next": "^15.3.1",
"react": "^18.0.0",
"typescript": "^5.0.0"
}
}
</code>
</example>
<example id=".env.local">
<code language="bash">
# Platform Configuration (use as needed)
# For Shopify E-commerce
SHOPIFY_STOREFRONT_ACCESS_TOKEN=your_token_here
SHOPIFY_STORE_DOMAIN=your-store.myshopify.com
# For Sanity CMS
SANITY_PROJECT_ID=your_project_id
SANITY_DATASET=production
# For Contentful CMS
CONTENTFUL_SPACE_ID=your_space_id
CONTENTFUL_ACCESS_TOKEN=your_access_token
# For Supabase
SUPABASE_URL=https://...
SUPABASE_ANON_KEY=eyJ...
# For Stripe Payments
STRIPE_SECRET_KEY=sk_test_...
STRIPE_PUBLISHABLE_KEY=pk_test_...
# For Internationalization (i18n)
NEXT_PUBLIC_DEFAULT_LOCALE=en
NEXT_PUBLIC_SUPPORTED_LOCALES=en,zh,ja
I18N_FALLBACK_LOCALE=en
</code>
</example>
<key_points>
- Separates development environment variables for frontend and functions
- Includes EdgeOne Pages specific configuration
- Sets up proper port configuration for dual-server development
- Uses environment variables for all sensitive data
- Supports multiple platform integrations
</key_points>
</configuration_requirements>
<security_guidelines>
- Implement proper request validation in Edge Functions and Node Functions
- Use appropriate security headers
- Handle CORS correctly when needed
- Implement rate limiting where appropriate
- Follow least privilege principle for API access
- Sanitize all user inputs before processing
- Never expose API keys or secrets in frontend code
- Use environment variables for all sensitive configuration
</security_guidelines>
<performance_guidelines>
- Optimize for fast static site generation
- Minimize Edge Function cold starts
- Use appropriate caching strategies (CDN + local)
- Consider EdgeOne Pages limits and quotas
- Implement efficient data fetching patterns
- Optimize images and static assets
- Use streaming where beneficial for large responses
- Choose between Edge Functions and Node Functions based on performance requirements
</performance_guidelines>
<error_handling>
- Implement proper error boundaries in React components
- Return appropriate HTTP status codes from Edge Functions and Node Functions
- Provide meaningful error messages to users
- Log errors appropriately for debugging
- Handle network failures gracefully
- Implement fallback mechanisms for external API failures
</error_handling>
### Technology Stack Description
- Frontend framework: Automatically detected (Next.js, Gatsby, Vue, React SPA, etc.)
- Component library: Flexible (shadcn/ui, Ant Design, Material-UI, etc.)
- Styles: Recommended Tailwind CSS V4, document: https://tailwindcss.com/docs/installation/using-postcss
- Type system: TypeScript preferred
- Edge functions: EdgeOne Edge Functions, for low-latency edge logic
- Node functions: EdgeOne Node Functions, for complex backend logic
- Platform integrations: Multiple options (Shopify, Contentful, Sanity, Supabase, Stripe, etc.)
- Internationalization: next-i18next for Next.js projects, react-i18next for React SPA, vue-i18n for Vue projects
### Syntax and Project Specifications
1. Framework Configuration
- For Next.js: Support both SSG (getStaticProps, getStaticPaths) and SSR (getServerSideProps) with ISR capabilities
- next.config can use `output: "export"` for SSG-only projects or omit for full-stack projects with SSR/ISR
- For Gatsby: Use standard SSG patterns with gatsby-node.js for dynamic pages
- For other frameworks: Follow static generation best practices
- All pages and components must use TypeScript (.tsx, .ts) when possible.
- Routing follows the respective framework's conventions.
- Static assets are placed in the public directory.
- All components that import "useState" need `"use client";` at the beginning of the file (Next.js App Router).
2. Component Library
- Use the detected component library or recommend appropriate alternatives
- Prefer official component libraries and avoid direct node_modules modifications
- Component styles should follow the project's established patterns
3. Code Style
- TypeScript preferred, with complete type declarations.
- Variables, functions, and component names use camelCase; React components use PascalCase.
- Strictly use ES6+ syntax, prohibit var, prefer const.
- Use Prettier for code formatting, recommended to use with ESLint.
- File and directory names use lowercase and kebab-case.
4. EdgeOne Functions Specifications
- How to write EdgeOne functions: see `EdgeOne Pages compute` below.
- Frontend pages **cannot** share same path with functions.
5. Security and Environment
- All sensitive information must be managed via environment variables; hardcoding is prohibited.
- It is forbidden to expose any keys, tokens, or other sensitive information in frontend code.
6. Language
- Use English as default language when writing code and comments.
7. Platform Integration Specifications
- Choose appropriate platform based on project requirements:
- **E-commerce**: Shopify Storefront API, WooCommerce, etc.
- **CMS**: Sanity, Contentful, WordPress headless, etc.
- **Database**: Supabase, Firebase, etc.
- **Payments**: Stripe, PayPal, etc.
- **Storage**: Cloudinary, AWS S3, etc.
8. Internationalization (i18n) Specifications
- Based on EdgeOne official tech-company-website-template best practices
- **Technology Selection Standards:**
- **Next.js Projects**: Use `next-i18next` as the standard i18n solution
- **Other Frameworks**: React SPA (`react-i18next`), Gatsby (`gatsby-plugin-react-i18next`), Vue (`vue-i18n`)
- Must be fully compatible with EdgeOne Pages SSG/SSR/ISR
- Can use SSR-dependent i18n features with Node Functions support
- **Project Structure Standards:**
```
public/
├── locales/
│ ├── en/ # English translations
│ │ ├── common.json # Common translations (navigation, buttons, etc.)
│ │ ├── home.json # Homepage specific translations
│ │ ├── about.json # About page translations
│ │ └── [page].json # Other page translations
│ ├── zh/ # Chinese translations
│ └── [locale]/ # Other language extensions
```
- **Naming Conventions:**
- Language codes use ISO 639-1 standard (`en`, `zh`, `ja`, `ko`, etc.)
- Translation file names correspond to page routes
- Data translation files use `[page]Data.json` format
- Common translations use `common.json`
- **Configuration Requirements:**
- **next-i18next.config.js**: Must include defaultLocale, locales array, and localePath
- **next.config.ts**: Must integrate i18n config, can use with or without `output: "export"` depending on SSR/ISR needs
- Environment variables for locale detection and fallback handling
- **Development Standards:**
- All pages must use `serverSideTranslations` in `getStaticProps` or `getServerSideProps`
- Components use `useTranslation` hook with proper namespace
- Dynamic routes must generate paths for all supported locales
- Language switcher components must use `router.push` with locale parameter
- **Translation File Management:**
- Split by page/feature to avoid loading unnecessary translations
- Common translations in separate `common.json`
- Data translations in `[page]Data.json` format
- Ensure all translation keys have corresponding values in all supported languages
- **New Language Addition Process:**
1. Add language code to `next-i18next.config.js` locales array
2. Create `public/locales/[new-locale]/` directory
3. Copy existing translation files to new language directory
4. Translate all JSON file contents
5. Test all pages with new language display
- **EdgeOne Integration:**
- Edge Functions for locale detection: `edge-functions/i18n/detect.js`
- Node Functions for locale-specific APIs: `node-functions/api/content/[locale].js`
- SEO optimization with locale-specific meta tags
- CDN optimization for translation file caching
- **Performance Optimization:**
- Split translation files by page to minimize bundle size
- Use `getStaticProps` or `getServerSideProps` to preload translations
- Leverage EdgeOne CDN for translation file caching
- **Error Handling:**
- Implement fallback to default language for missing translations
- Use `fallbackLng` and `fallbackNS` in translation configuration
- Browser language detection with supported locale validation
- **Quality Assurance:**
- Ensure all translation keys exist in all supported languages
- Verify text length compatibility with UI layouts
- Test special character and encoding correctness
## EdgeOne Pages compute
- NEVER put any type of function in the public or publish directory
- DO NOT change the default functions directory structure unless explicitly asked to.
- ALWAYS verify the correct directory to place functions into (edge-functions/ or node-functions/)
### Edge Functions
- ALWAYS use the latest format of an edge function structure.
- DO NOT put global logic outside of the exported function unless it is wrapped in a function definition
- ONLY use vanilla javascript if there are other ".js" files in the functions directory.
- ALWAYS use typescript if other functions are typescript or if there are no existing functions.
- The first argument is a custom EdgeOne context object.
- EdgeOne context object provides a "request" property, a web platform Request object that represents the incoming HTTP request
- EdgeOne context object provides a "env" property, object contains all environment variables.
- EdgeOne context object provides a "params" property, object contain all request params parsed from dynamic routes.
- ONLY use `env.*` for interacting with environment variables in code.
- Place function files in `YOUR_BASE_DIRECTORY/edge-functions/` or a subdirectory.
- Edge functions **does not** support Server Side Rendering.
- Edge functions use Node.js as runtime and should attempt to use built-in methods where possible. See the list of available web APIs to know which built-ins to use.
- **Module Support**:
- Do not supports **Node.js built-in modules**
- Supports **npm packages** (beta).
- **Importing Modules**:
- **npm packages (beta)**: Install via `npm install` and import by package name (e.g., `import _ from "lodash"`).
- Some npm packages with **native binaries** (e.g., Prisma) or **dynamic imports** (e.g., cowsay) may not work.
- **Usage in Code**:
- Modules can now be imported by name:
```javascript
import { HTMLRewriter } from "html-rewriter";
```
#### Examples of the latest Edge function structures
- ```javascript
const json = JSON.stringify({
"code": 0,
"message": "Hello World"
});
export function onRequest(context) {
return new Response(json, {
headers: {
'content-type': 'application/json',
'x-edgefunctions-test': 'Welcome to use Pages Functions.',
},
});
}
```
### Node Functions
- ALWAYS use the latest format of a Node function structure.
- Support full Node.js environment with all built-in modules
- Can use Express, Koa, or other Node.js frameworks
- Place function files in `YOUR_BASE_DIRECTORY/node-functions/` or a subdirectory.
- Support complex backend logic, database connections, and third-party integrations
- Can handle file uploads, image processing, and other resource-intensive operations
#### Examples of Node function structures
- ```javascript
// Simple Node function
export function onRequest(context) {
const { request, env, params } = context;
return new Response(JSON.stringify({
message: "Hello from Node Functions",
method: request.method,
params: params
}), {
headers: {
'content-type': 'application/json'
}
});
}
```
- ```javascript
// Express-style Node function
import express from 'express';
const app = express();
app.use(express.json());
app.get('/', (req, res) => {
res.json({ message: "Hello from Express in Node Functions" });
});
export function onRequest(context) {
return app(context.request);
}
```
#### Web APIs available in Edge Functions ONLY
- console.*
- atob
- btoa
- Fetch API
- fetch
- Request
- Response
- URL
- File
- Blob
- TextEncoder
- TextDecoder
- TextEncoderStream
- TextDecoderStream
- Performance
- Web Crypto API
- randomUUID()
- getRandomValues()
- SubtleCrypto
- WebSocket API
- Timers
- setTimeout
- clearTimeout
- setInterval
- Streams API
- ReadableStream
- WritableStream
- TransformStream
- URLPattern API
#### Node.js APIs available in Node Functions
- All Node.js built-in modules (fs, path, crypto, etc.)
- Full npm ecosystem support
- Database drivers (MySQL, PostgreSQL, MongoDB, etc.)
- File system operations
- Complex data processing libraries
- Image/video processing libraries
- Machine learning libraries
#### In-code function config and routing for both Edge and Node functions
- Functions are configured with a path pattern and only paths matching those patterns will run the function
- Functions use code file path as functions path.(e.g., `YOUR_BASE_DIRECTORY/edge-functions/hello/index.js` file matches api path `/hello`, `YOUR_BASE_DIRECTORY/node-functions/hello/world.js` file matches api path `/hello/world`.)
- Functions use `[param]` as file path to mark dynamic routes.(e.g., `YOUR_BASE_DIRECTORY/edge-functions/hello/[name]` file matches all api paths like `/hello/Tom`.)
#### Function limitations
- **Edge Functions**:
- 5 MB (compressed) code size limit
- 1 MB client request body limit
- 200ms per request CPU execution time (excludes waiting time)
- **Node Functions**:
- Higher resource limits for complex processing
- Longer execution times allowed
- Full Node.js environment support
- Be aware that multiple framework adapters may generate conflicting functions
## Local Development
- Frontend dev server and function dev server are two different servers.
- Frontend dev server start command: varies by framework ("npm run dev", "gatsby develop", etc.)
- Functions dev server start command: "edgeone pages dev"
- Dev server start commands should be added to `package.json`
<best_practices>
- Choose Edge Functions for low-latency, simple operations that need global distribution
- Choose Node Functions for complex backend logic, database operations, and resource-intensive tasks
- Use SSG for static content, SSR for dynamic content that needs SEO, ISR for content that changes occasionally
- Implement proper error handling and logging in all functions
- Use TypeScript for better code quality and developer experience
- Follow security best practices and never expose sensitive data
- Optimize for performance by choosing the right compute model for each use case
- Test both Edge and Node Functions locally before deployment
- Use environment variables for configuration and secrets
- Implement proper CORS handling for cross-origin requests
</best_practices>
<additional_resources>
- EdgeOne Pages Documentation: https://pages.edgeone.ai/document/product-introduction
- Next.js Documentation: https://nextjs.org/docs
- React Documentation: https://react.dev
- TypeScript Documentation: https://www.typescriptlang.org/docs
- Tailwind CSS Documentation: https://tailwindcss.com/docs
</additional_resources>
</ProviderContext>
-4
View File
@@ -1,4 +0,0 @@
# Tencent Cloud EdgeOne
.env
.edgeone/*
.tef_dist/*
-49
View File
@@ -1,49 +0,0 @@
module edgeone-cloud-functions
go 1.25.0
require (
github.com/gin-contrib/cors v1.7.7
github.com/gin-gonic/gin v1.12.0
github.com/klauspost/compress v1.19.1
github.com/likexian/whois v1.15.7
github.com/likexian/whois-parser v1.24.21
github.com/miekg/dns v1.1.72
resty.dev/v3 v3.0.0-beta.6
)
require (
github.com/bytedance/gopkg v0.1.3 // indirect
github.com/bytedance/sonic v1.15.0 // indirect
github.com/bytedance/sonic/loader v0.5.0 // indirect
github.com/cloudwego/base64x v0.1.6 // indirect
github.com/gabriel-vasile/mimetype v1.4.12 // indirect
github.com/gin-contrib/sse v1.1.0 // indirect
github.com/go-playground/locales v0.14.1 // indirect
github.com/go-playground/universal-translator v0.18.1 // indirect
github.com/go-playground/validator/v10 v10.30.1 // indirect
github.com/goccy/go-json v0.10.5 // indirect
github.com/goccy/go-yaml v1.19.2 // indirect
github.com/json-iterator/go v1.1.12 // indirect
github.com/klauspost/cpuid/v2 v2.3.0 // indirect
github.com/leodido/go-urn v1.4.0 // indirect
github.com/likexian/gokit v0.25.16 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
github.com/modern-go/reflect2 v1.0.2 // indirect
github.com/pelletier/go-toml/v2 v2.2.4 // indirect
github.com/quic-go/qpack v0.6.0 // indirect
github.com/quic-go/quic-go v0.59.0 // indirect
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
github.com/ugorji/go/codec v1.3.1 // indirect
go.mongodb.org/mongo-driver/v2 v2.5.0 // indirect
golang.org/x/arch v0.23.0 // indirect
golang.org/x/crypto v0.52.0 // indirect
golang.org/x/mod v0.35.0 // indirect
golang.org/x/net v0.54.0 // indirect
golang.org/x/sync v0.20.0 // indirect
golang.org/x/sys v0.45.0 // indirect
golang.org/x/text v0.37.0 // indirect
golang.org/x/tools v0.44.0 // indirect
google.golang.org/protobuf v1.36.10 // indirect
)
-109
View File
@@ -1,109 +0,0 @@
github.com/bytedance/gopkg v0.1.3 h1:TPBSwH8RsouGCBcMBktLt1AymVo2TVsBVCY4b6TnZ/M=
github.com/bytedance/gopkg v0.1.3/go.mod h1:576VvJ+eJgyCzdjS+c4+77QF3p7ubbtiKARP3TxducM=
github.com/bytedance/sonic v1.15.0 h1:/PXeWFaR5ElNcVE84U0dOHjiMHQOwNIx3K4ymzh/uSE=
github.com/bytedance/sonic v1.15.0/go.mod h1:tFkWrPz0/CUCLEF4ri4UkHekCIcdnkqXw9VduqpJh0k=
github.com/bytedance/sonic/loader v0.5.0 h1:gXH3KVnatgY7loH5/TkeVyXPfESoqSBSBEiDd5VjlgE=
github.com/bytedance/sonic/loader v0.5.0/go.mod h1:AR4NYCk5DdzZizZ5djGqQ92eEhCCcdf5x77udYiSJRo=
github.com/cloudwego/base64x v0.1.6 h1:t11wG9AECkCDk5fMSoxmufanudBtJ+/HemLstXDLI2M=
github.com/cloudwego/base64x v0.1.6/go.mod h1:OFcloc187FXDaYHvrNIjxSe8ncn0OOM8gEHfghB2IPU=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/gabriel-vasile/mimetype v1.4.12 h1:e9hWvmLYvtp846tLHam2o++qitpguFiYCKbn0w9jyqw=
github.com/gabriel-vasile/mimetype v1.4.12/go.mod h1:d+9Oxyo1wTzWdyVUPMmXFvp4F9tea18J8ufA774AB3s=
github.com/gin-contrib/cors v1.7.7 h1:Oh9joP463x7Mw72vhvJ61YQm8ODh9b04YR7vsOErD0Q=
github.com/gin-contrib/cors v1.7.7/go.mod h1:K5tW0RkzJtWSiOdikXloy8VEZlgdVNpHNw8FpjUPNrE=
github.com/gin-contrib/sse v1.1.0 h1:n0w2GMuUpWDVp7qSpvze6fAu9iRxJY4Hmj6AmBOU05w=
github.com/gin-contrib/sse v1.1.0/go.mod h1:hxRZ5gVpWMT7Z0B0gSNYqqsSCNIJMjzvm6fqCz9vjwM=
github.com/gin-gonic/gin v1.12.0 h1:b3YAbrZtnf8N//yjKeU2+MQsh2mY5htkZidOM7O0wG8=
github.com/gin-gonic/gin v1.12.0/go.mod h1:VxccKfsSllpKshkBWgVgRniFFAzFb9csfngsqANjnLc=
github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s=
github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4=
github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA=
github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY=
github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY=
github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY=
github.com/go-playground/validator/v10 v10.30.1 h1:f3zDSN/zOma+w6+1Wswgd9fLkdwy06ntQJp0BBvFG0w=
github.com/go-playground/validator/v10 v10.30.1/go.mod h1:oSuBIQzuJxL//3MelwSLD5hc2Tu889bF0Idm9Dg26cM=
github.com/goccy/go-json v0.10.5 h1:Fq85nIqj+gXn/S5ahsiTlK3TmC85qgirsdTP/+DeaC4=
github.com/goccy/go-json v0.10.5/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M=
github.com/goccy/go-yaml v1.19.2 h1:PmFC1S6h8ljIz6gMRBopkjP1TVT7xuwrButHID66PoM=
github.com/goccy/go-yaml v1.19.2/go.mod h1:XBurs7gK8ATbW4ZPGKgcbrY1Br56PdM69F7LkFRi1kA=
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
github.com/klauspost/compress v1.19.1 h1:VsB4HPswih7mmZ8WleSFQ75c/Ui1M4trX5oAsJnhSlk=
github.com/klauspost/compress v1.19.1/go.mod h1:cwPg85FWrGar70rWktvGQj8/hthj3wpl0PGDogxkrSQ=
github.com/klauspost/cpuid/v2 v2.3.0 h1:S4CRMLnYUhGeDFDqkGriYKdfoFlDnMtqTiI/sFzhA9Y=
github.com/klauspost/cpuid/v2 v2.3.0/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0=
github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ=
github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI=
github.com/likexian/gokit v0.25.16 h1:wwBeUIN/OdoPp6t00xTnZE8Di/+s969Bl5N2Kw6bzP8=
github.com/likexian/gokit v0.25.16/go.mod h1:Wqd4f+iifV0qxA1N3MqePJTUsmRy/lpst9/yXriDx/4=
github.com/likexian/whois v1.15.7 h1:sajjDhi2bVD71AHJhjV7jLYxN92H4AWhTwxM8hmj7c0=
github.com/likexian/whois v1.15.7/go.mod h1:kdPQtYb+7SQVftBEbCblDadUkycN7Mg1k1/Li/rwvmc=
github.com/likexian/whois-parser v1.24.21 h1:MxsrGRxDOiZIVp7q7N/yAIbKuN4QAkGjCpOtTDA5OsM=
github.com/likexian/whois-parser v1.24.21/go.mod h1:o3DUruO65Pb8WXCJCTlSVkTbwuYVrBCeoMTw2q0mxY4=
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/miekg/dns v1.1.72 h1:vhmr+TF2A3tuoGNkLDFK9zi36F2LS+hKTRW0Uf8kbzI=
github.com/miekg/dns v1.1.72/go.mod h1:+EuEPhdHOsfk6Wk5TT2CzssZdqkmFhf8r+aVyDEToIs=
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M=
github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
github.com/pelletier/go-toml/v2 v2.2.4 h1:mye9XuhQ6gvn5h28+VilKrrPoQVanw5PMw/TB0t5Ec4=
github.com/pelletier/go-toml/v2 v2.2.4/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/quic-go/qpack v0.6.0 h1:g7W+BMYynC1LbYLSqRt8PBg5Tgwxn214ZZR34VIOjz8=
github.com/quic-go/qpack v0.6.0/go.mod h1:lUpLKChi8njB4ty2bFLX2x4gzDqXwUpaO1DP9qMDZII=
github.com/quic-go/quic-go v0.59.0 h1:OLJkp1Mlm/aS7dpKgTc6cnpynnD2Xg7C1pwL6vy/SAw=
github.com/quic-go/quic-go v0.59.0/go.mod h1:upnsH4Ju1YkqpLXC305eW3yDZ4NfnNbmQRCMWS58IKU=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI=
github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08=
github.com/ugorji/go/codec v1.3.1 h1:waO7eEiFDwidsBN6agj1vJQ4AG7lh2yqXyOXqhgQuyY=
github.com/ugorji/go/codec v1.3.1/go.mod h1:pRBVtBSKl77K30Bv8R2P+cLSGaTtex6fsA2Wjqmfxj4=
go.mongodb.org/mongo-driver/v2 v2.5.0 h1:yXUhImUjjAInNcpTcAlPHiT7bIXhshCTL3jVBkF3xaE=
go.mongodb.org/mongo-driver/v2 v2.5.0/go.mod h1:yOI9kBsufol30iFsl1slpdq1I0eHPzybRWdyYUs8K/0=
go.uber.org/mock v0.6.0 h1:hyF9dfmbgIX5EfOdasqLsWD6xqpNZlXblLB/Dbnwv3Y=
go.uber.org/mock v0.6.0/go.mod h1:KiVJ4BqZJaMj4svdfmHM0AUx4NJYO8ZNpPnZn1Z+BBU=
golang.org/x/arch v0.23.0 h1:lKF64A2jF6Zd8L0knGltUnegD62JMFBiCPBmQpToHhg=
golang.org/x/arch v0.23.0/go.mod h1:dNHoOeKiyja7GTvF9NJS1l3Z2yntpQNzgrjh1cU103A=
golang.org/x/crypto v0.52.0 h1:RMs7fP2rXdep0CftQlK8Uf+kibLm7qkCcradZWYz988=
golang.org/x/crypto v0.52.0/go.mod h1:1QgfPxDqh0T2M/elOJtp9RvuR95kVjir0e6/BvEmGbc=
golang.org/x/mod v0.35.0 h1:Ww1D637e6Pg+Zb2KrWfHQUnH2dQRLBQyAtpr/haaJeM=
golang.org/x/mod v0.35.0/go.mod h1:+GwiRhIInF8wPm+4AoT6L0FA1QWAad3OMdTRx4tFYlU=
golang.org/x/net v0.54.0 h1:2zJIZAxAHV/OHCDTCOHAYehQzLfSXuf/5SoL/Dv6w/w=
golang.org/x/net v0.54.0/go.mod h1:Sj4oj8jK6XmHpBZU/zWHw3BV3abl4Kvi+Ut7cQcY+cQ=
golang.org/x/sync v0.20.0 h1:e0PTpb7pjO8GAtTs2dQ6jYa5BWYlMuX047Dco/pItO4=
golang.org/x/sync v0.20.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.45.0 h1:dO4czNzziLiiXplLQgBCEpCvXQ3dnkn0SdaZSYdQ+FY=
golang.org/x/sys v0.45.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
golang.org/x/text v0.37.0 h1:Cqjiwd9eSg8e0QAkyCaQTNHFIIzWtidPahFWR83rTrc=
golang.org/x/text v0.37.0/go.mod h1:a5sjxXGs9hsn/AJVwuElvCAo9v8QYLzvavO5z2PiM38=
golang.org/x/tools v0.44.0 h1:UP4ajHPIcuMjT1GqzDWRlalUEoY+uzoZKnhOjbIPD2c=
golang.org/x/tools v0.44.0/go.mod h1:KA0AfVErSdxRZIsOVipbv3rQhVXTnlU6UhKxHd1seDI=
google.golang.org/protobuf v1.36.10 h1:AYd7cD/uASjIL6Q9LiTjz8JLcrh/88q5UObnmY3aOOE=
google.golang.org/protobuf v1.36.10/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
resty.dev/v3 v3.0.0-beta.6 h1:ghRdNpoE8/wBCv+kTKIOauW1aCrSIeTq7GxtfYgtevU=
resty.dev/v3 v3.0.0-beta.6/go.mod h1:NTOerrC/4T7/FE6tXIZGIysXXBdgNqwMZuKtxpea9NM=
File diff suppressed because it is too large. Load diff
-169
View File
@@ -1,169 +0,0 @@
package ssrf
import (
"context"
"fmt"
"log/slog"
"net"
"net/http"
"net/url"
)
// blockPrivateIPs controls whether SSRF protection is active.
// blockPrivateIPs 控制 SSRF 防护是否启用。
// Defaults to true; can be toggled via BLOCK_PRIVATE_IPS environment variable.
// 默认为 true;可通过 BLOCK_PRIVATE_IPS 环境变量切换。
var blockPrivateIPs = true
// contextKey is an unexported type for context keys to avoid collisions with other packages.
// contextKey 是用于上下文键的未导出类型,避免与其他包的键冲突。
type contextKey string
// validatedIPsKey stores the resolved IPs that have passed SSRF validation in the request context.
// validatedIPsKey 在请求上下文中存储已通过 SSRF 校验的解析 IP。
// Caching validated IPs in context avoids redundant DNS lookups in DialContext.
// 在上下文中缓存校验通过的 IP,避免 DialContext 中重复 DNS 查询。
const validatedIPsKey contextKey = "ssrf_validated_ips"
// ValidatedIPsKey returns the context key used to store validated IPs.
// ValidatedIPsKey 返回用于存储校验通过 IP 的上下文键。
func ValidatedIPsKey() contextKey {
return validatedIPsKey
}
// Enabled returns whether SSRF protection is currently active.
// Enabled 返回 SSRF 防护当前是否启用。
func Enabled() bool {
return blockPrivateIPs
}
// SetEnabled enables or disables SSRF protection globally.
// SetEnabled 全局启用或禁用 SSRF 防护。
// Typically called during configuration loading (readConfig).
// 通常在配置加载(readConfig)时调用。
func SetEnabled(v bool) {
blockPrivateIPs = v
}
// IsPrivateIP checks if the given IP address belongs to a private or internal range.
// IsPrivateIP 检查给定的 IP 地址是否属于私有或内部地址段。
// It covers RFC 1918 private addresses, loopback, link-local, and unspecified addresses.
// 涵盖 RFC 1918 私有地址、回环地址、链路本地地址和未指定地址。
// This is the core check used to block SSRF attacks targeting internal networks.
// 这是用于阻止针对内部网络的 SSRF 攻击的核心检查。
func IsPrivateIP(ip net.IP) bool {
if ip.IsPrivate() {
return true
}
if ip.IsLoopback() {
return true
}
if ip.IsLinkLocalUnicast() {
return true
}
if ip.IsUnspecified() {
return true
}
return false
}
// HasLocalOrPrivateIP checks whether the given hostname resolves to any private or internal IP address.
// HasLocalOrPrivateIP 检查给定的主机名是否解析到任何私有或内部 IP 地址。
// Returns true if at least one resolved IP is private; false if resolution fails or all IPs are public.
// 如果至少有一个解析的 IP 是私有地址则返回 true;解析失败或全部为公网 IP 时返回 false。
// Used in handlers to short-circuit requests to internal hosts before making outbound connections.
// 用于在发起出站连接前,在处理器中短路对内部主机的请求。
func HasLocalOrPrivateIP(host string) bool {
ips, err := net.LookupIP(host)
if err != nil {
return false
}
for _, ip := range ips {
if IsPrivateIP(ip) {
return true
}
}
return false
}
// ValidateOutboundTarget validates the target URL against SSRF attacks before any outbound request is made.
// ValidateOutboundTarget 在发起任何出站请求之前,对目标 URL 进行 SSRF 攻击校验。
//
// Security checks performed:
// 执行的 security 检查:
// 1. Scheme whitelist — only "http" and "https" are allowed; blocks file://, gopher://, etc.
// Scheme 白名单 — 仅允许 "http" 和 "https";阻止 file://、gopher:// 等。
// 2. Hostname resolution — resolves the hostname via DNS.
// 主机名解析 — 通过 DNS 解析主机名。
// 3. Private IP check — verifies none of the resolved IPs are private/internal.
// 私有 IP 检查 — 验证所有解析的 IP 都不是私有/内部地址。
// 4. Context caching — validated IPs are stored in the returned context to avoid redundant DNS lookups.
// 上下文缓存 — 校验通过的 IP 存储在返回的上下文中,避免重复 DNS 查询。
//
// If validation passes, the returned context carries the resolved IPs under ValidatedIPsKey().
// 如果校验通过,返回的上下文中会携带解析的 IP,键为 ValidatedIPsKey()。
func ValidateOutboundTarget(ctx context.Context, targetURL string) (context.Context, error) {
if !blockPrivateIPs {
return ctx, nil
}
parsed, err := url.Parse(targetURL)
if err != nil {
return ctx, err
}
host := parsed.Hostname()
if host == "" {
return ctx, fmt.Errorf("empty host")
}
if parsed.Scheme != "http" && parsed.Scheme != "https" {
return ctx, fmt.Errorf("invalid scheme: %s", parsed.Scheme)
}
ips, err := net.LookupIP(host)
if err != nil {
return ctx, err
}
for _, ip := range ips {
if IsPrivateIP(ip) {
slog.Warn("Blocked request to private IP", "host", host, "ip", ip)
return ctx, fmt.Errorf("request to private/internal address is not allowed")
}
}
return context.WithValue(ctx, validatedIPsKey, ips), nil
}
// SecureCheckRedirect is a redirect policy that prevents SSRF via HTTP redirect chains.
// SecureCheckRedirect 是一个重定向策略,防止通过 HTTP 重定向链进行 SSRF 攻击。
//
// When an HTTP client follows redirects (301, 302, 307, 308), each intermediate target
// 当 HTTP 客户端跟随重定向(301、302、307、308)时,会校验每个中间目标
// is resolved and checked against private/internal IP ranges.
// 的解析结果,并检查是否指向私有/内部 IP 段。
//
// This blocks attackers from:
// 这阻止攻击者通过以下方式绕过防护:
// - Using an initial public redirect that points to an internal service.
// 使用指向内部服务的初始公开重定向。
// - Chaining multiple redirects to eventually reach a private IP.
// 链式多个重定向最终到达私有 IP。
func SecureCheckRedirect(req *http.Request, via []*http.Request) error {
if !blockPrivateIPs {
return nil
}
for _, r := range via {
redirectURL := r.URL
host := redirectURL.Hostname()
if host == "" {
continue
}
ips, err := net.LookupIP(host)
if err != nil {
return err
}
for _, ip := range ips {
if IsPrivateIP(ip) {
slog.Warn("Blocked redirect to private IP", "host", host, "ip", ip)
return fmt.Errorf("redirect to private/internal address is not allowed")
}
}
}
return nil
}
-396
View File
@@ -1,396 +0,0 @@
package webtest
import (
"bytes"
"fmt"
"io"
"log/slog"
"net"
"net/http"
"strings"
"sync"
"time"
"github.com/miekg/dns"
)
// 腾讯云标准 DoH 端点
var dohEndpoint string = "https://doh.pub/dns-query"
// UDP DNS 服务器(DoH 不可用时的回退;dns-server 配置为 ip:port 时作为主通道)
var dnsServer = "119.28.28.28:53"
// dnsMode 主通道:dns-server 配置为 URL 时为 "doh",为 ip:port 时为 "udp"
var dnsMode = "doh"
type DNSResult struct {
Domain string `json:"domain"`
Duration float64 `json:"duration"`
Record []string `json:"record"`
TTL uint32 `json:"ttl"`
}
// SetDNSServer 设置 DNS 服务器:URL(http/https)→ DoH 主通道;ip:port → UDP 主通道
func SetDNSServer(server string) {
if server == "" {
return
}
if strings.HasPrefix(server, "http://") || strings.HasPrefix(server, "https://") {
dohEndpoint = server
dnsMode = "doh"
} else {
dnsServer = server
dnsMode = "udp"
}
}
// ==========================================
// ⭐️ 底层核心:DoH + UDP 双通道 DNS 查询
// ==========================================
// queryDNSMsg 统一 DNS 查询入口:支持 DoH 与 UDP 双通道自动互备。
// dnsMode 为 "doh" 时优先 DoH(dohEndpoint),失败回退 UDP(dnsServer);
// dnsMode 为 "udp" 时优先 UDP(dnsServer),失败回退 DoH(dohEndpoint)。
func queryDNSMsg(msg *dns.Msg) (*dns.Msg, float64, error) {
if dnsMode == "doh" {
resp, dur, err := queryDoHMsg(msg, dohEndpoint)
if err == nil {
return resp, dur, nil
}
slog.Warn("DoH query failed, falling back to UDP", "endpoint", dohEndpoint, "error", err)
return queryUDPMsg(msg, dnsServer)
}
resp, dur, err := queryUDPMsg(msg, dnsServer)
if err == nil {
return resp, dur, nil
}
slog.Warn("UDP query failed, falling back to DoH", "server", dnsServer, "error", err)
return queryDoHMsg(msg, dohEndpoint)
}
// queryUDPMsg 通过 UDP/TCP 向指定 DNS 服务器发送查询(miekg/dns 自动处理大响应切 TCP)
func queryUDPMsg(msg *dns.Msg, server string) (*dns.Msg, float64, error) {
client := &dns.Client{Timeout: 5 * time.Second}
start := time.Now()
resp, _, err := client.Exchange(msg, server)
duration := time.Since(start).Seconds() * 1000
if err != nil {
return nil, duration, err
}
return resp, duration, nil
}
// queryDoHMsg 通过 DoH(RFC 8484,POST application/dns-message)向指定端点发送查询
func queryDoHMsg(msg *dns.Msg, endpoint string) (*dns.Msg, float64, error) {
packedMsg, err := msg.Pack()
if err != nil {
return nil, 0, fmt.Errorf("failed to pack DNS message: %v", err)
}
req, err := http.NewRequest("POST", endpoint, bytes.NewReader(packedMsg))
if err != nil {
return nil, 0, err
}
req.Header.Set("Content-Type", "application/dns-message")
req.Header.Set("Accept", "application/dns-message")
client := &http.Client{Timeout: 5 * time.Second}
start := time.Now()
resp, err := client.Do(req)
duration := time.Since(start).Seconds() * 1000
if err != nil {
return nil, duration, err
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
return nil, duration, fmt.Errorf("DoH API returned status %d", resp.StatusCode)
}
bodyBytes, err := io.ReadAll(resp.Body)
if err != nil {
return nil, duration, fmt.Errorf("failed to read response body: %v", err)
}
responseMsg := new(dns.Msg)
if err := responseMsg.Unpack(bodyBytes); err != nil {
return nil, duration, fmt.Errorf("failed to unpack DNS response: %v", err)
}
return responseMsg, duration, nil
}
// ==========================================
// 业务层:构造查询并经双通道执行,提取特定记录
// ==========================================
func executeDoHQuery(domain string, qtype uint16) (*dns.Msg, float64, error) {
// 1. 构造标准的 DNS 请求报文
msg := new(dns.Msg)
msg.SetQuestion(dns.Fqdn(domain), qtype)
msg.RecursionDesired = true // 请求递归解析
// 2. 双通道查询(DoH 主 + UDP 备,或反之)
responseMsg, duration, err := queryDNSMsg(msg)
if err != nil {
return nil, duration, err
}
// 3. 检查 DNS 响应码 (Rcode)
if responseMsg.Rcode != dns.RcodeSuccess {
return responseMsg, duration, fmt.Errorf("DNS query failed with Rcode %d", responseMsg.Rcode)
}
return responseMsg, duration, nil
}
// ==========================================
// 业务层:调用底层函数并提取特定记录
// ==========================================
func ResolveARecord(domain string) (DNSResult, error) {
result := DNSResult{Domain: domain, Record: []string{}}
responseMsg, duration, err := executeDoHQuery(domain, dns.TypeA)
result.Duration = duration
if err != nil {
slog.Warn("Failed to query A", "domain", domain, "err", err)
return result, err
}
for _, ans := range responseMsg.Answer {
if aRecord, ok := ans.(*dns.A); ok {
result.Record = append(result.Record, aRecord.A.String())
if result.TTL == 0 {
result.TTL = aRecord.Header().Ttl
}
}
}
return result, nil
}
func ResolveAAAARecord(domain string) (DNSResult, error) {
result := DNSResult{Domain: domain, Record: []string{}}
responseMsg, duration, err := executeDoHQuery(domain, dns.TypeAAAA)
result.Duration = duration
if err != nil {
slog.Warn("Failed to query AAAA", "domain", domain, "err", err)
return result, err
}
for _, ans := range responseMsg.Answer {
if aRecord, ok := ans.(*dns.AAAA); ok {
result.Record = append(result.Record, aRecord.AAAA.String())
if result.TTL == 0 {
result.TTL = aRecord.Header().Ttl
}
}
}
return result, nil
}
func ResolveTXTRecord(domain string) (DNSResult, error) {
result := DNSResult{Domain: domain, Record: []string{}}
responseMsg, duration, err := executeDoHQuery(domain, dns.TypeTXT)
result.Duration = duration
if err != nil {
slog.Warn("Failed to query TXT", "domain", domain, "err", err)
return result, err
}
for _, ans := range responseMsg.Answer {
if txtRecord, ok := ans.(*dns.TXT); ok {
result.Record = append(result.Record, txtRecord.Txt...)
if result.TTL == 0 {
result.TTL = txtRecord.Header().Ttl
}
}
}
return result, nil
}
func ResolveNSRecord(domain string) (DNSResult, error) {
result := DNSResult{Domain: domain, Record: []string{}}
responseMsg, duration, err := executeDoHQuery(domain, dns.TypeNS)
result.Duration = duration
if err != nil {
slog.Warn("Failed to query NS", "domain", domain, "err", err)
return result, err
}
for _, ans := range responseMsg.Answer {
if nsRecord, ok := ans.(*dns.NS); ok {
result.Record = append(result.Record, nsRecord.Ns)
if result.TTL == 0 {
result.TTL = nsRecord.Header().Ttl
}
}
}
return result, nil
}
func ResolveCNAMERecord(domain string) (DNSResult, error) {
result := DNSResult{Domain: domain, Record: []string{}}
responseMsg, duration, err := executeDoHQuery(domain, dns.TypeCNAME)
result.Duration = duration
if err != nil {
slog.Warn("Failed to query CNAME", "domain", domain, "err", err)
return result, err
}
for _, ans := range responseMsg.Answer {
if cnameRecord, ok := ans.(*dns.CNAME); ok {
result.Record = append(result.Record, cnameRecord.Target)
if result.TTL == 0 {
result.TTL = cnameRecord.Header().Ttl
}
}
}
return result, nil
}
func ResolveMXRecord(domain string) (DNSResult, error) {
result := DNSResult{Domain: domain, Record: []string{}}
responseMsg, duration, err := executeDoHQuery(domain, dns.TypeMX)
result.Duration = duration
if err != nil {
slog.Warn("Failed to query MX", "domain", domain, "err", err)
return result, err
}
for _, ans := range responseMsg.Answer {
if mxRecord, ok := ans.(*dns.MX); ok {
result.Record = append(result.Record, mxRecord.Mx)
if result.TTL == 0 {
result.TTL = mxRecord.Header().Ttl
}
}
}
return result, nil
}
func ResolveSRVRecord(domain string) (DNSResult, error) {
result := DNSResult{Domain: domain, Record: []string{}}
responseMsg, duration, err := executeDoHQuery(domain, dns.TypeSRV)
result.Duration = duration
if err != nil {
slog.Warn("Failed to query SRV", "domain", domain, "err", err)
return result, err
}
for _, ans := range responseMsg.Answer {
if srvRecord, ok := ans.(*dns.SRV); ok {
result.Record = append(result.Record, srvRecord.Target)
if result.TTL == 0 {
result.TTL = srvRecord.Header().Ttl
}
}
}
return result, nil
}
func ResolvePTRRecord(ip string) (DNSResult, error) {
result := DNSResult{Domain: ip, Record: []string{}}
ptrName, err := dns.ReverseAddr(ip)
if err != nil {
return result, fmt.Errorf("invalid IP: %v", err)
}
responseMsg, duration, err := executeDoHQuery(ptrName, dns.TypePTR)
result.Duration = duration
result.Domain = ip // 保持返回的 Domain 为原始 IP
if err != nil {
slog.Warn("Failed to query PTR", "ip", ip, "err", err)
return result, err
}
for _, ans := range responseMsg.Answer {
if ptrRecord, ok := ans.(*dns.PTR); ok {
result.Record = append(result.Record, ptrRecord.Ptr)
if result.TTL == 0 {
result.TTL = ptrRecord.Header().Ttl
}
}
}
return result, nil
}
func ResolveCAARecord(domain string) (DNSResult, error) {
result := DNSResult{Domain: domain, Record: []string{}}
responseMsg, duration, err := executeDoHQuery(domain, dns.TypeCAA)
result.Duration = duration
if err != nil {
slog.Warn("Failed to query CAA", "domain", domain, "err", err)
return result, err
}
for _, ans := range responseMsg.Answer {
if caaRecord, ok := ans.(*dns.CAA); ok {
result.Record = append(result.Record, caaRecord.Value)
if result.TTL == 0 {
result.TTL = caaRecord.Header().Ttl
}
}
}
return result, nil
}
// ResolveIP 通过 DoH 解析域名,返回指定版本(v4/v6)的 IP 地址字符串
func ResolveIP(host string, version string) (string, error) {
if ip := net.ParseIP(host); ip != nil {
if version == "v4" && ip.To4() != nil {
return ip.String(), nil
}
if version == "v6" && ip.To4() == nil && ip.To16() != nil {
return ip.String(), nil
}
return "", fmt.Errorf("no %s address found for %s", version, host)
}
var qtype uint16
switch version {
case "v6":
qtype = dns.TypeAAAA
default:
qtype = dns.TypeA
}
responseMsg, _, err := executeDoHQuery(host, qtype)
if err != nil {
return "", err
}
for _, ans := range responseMsg.Answer {
switch v := ans.(type) {
case *dns.A:
if qtype == dns.TypeA {
return v.A.String(), nil
}
case *dns.AAAA:
if qtype == dns.TypeAAAA {
return v.AAAA.String(), nil
}
}
}
return "", fmt.Errorf("no %s record found for %s", version, host)
}
// ==========================================
// 并发查询所有记录
// ==========================================
type DNSFullResult struct {
Domain string `json:"domain"`
A DNSResult `json:"a"`
AAAA DNSResult `json:"aaaa"`
CNAME DNSResult `json:"cname"`
MX DNSResult `json:"mx"`
NS DNSResult `json:"ns"`
TXT DNSResult `json:"txt"`
SRV DNSResult `json:"srv"`
CAA DNSResult `json:"caa"`
}
func ResolveARecordllDNSRecords(domain string) DNSFullResult {
result := DNSFullResult{Domain: domain}
var wg sync.WaitGroup
wg.Add(8)
go func() { defer wg.Done(); result.A, _ = ResolveARecord(domain) }()
go func() { defer wg.Done(); result.AAAA, _ = ResolveAAAARecord(domain) }()
go func() { defer wg.Done(); result.CNAME, _ = ResolveCNAMERecord(domain) }()
go func() { defer wg.Done(); result.MX, _ = ResolveMXRecord(domain) }()
go func() { defer wg.Done(); result.NS, _ = ResolveNSRecord(domain) }()
go func() { defer wg.Done(); result.TXT, _ = ResolveTXTRecord(domain) }()
go func() { defer wg.Done(); result.SRV, _ = ResolveSRVRecord(domain) }()
go func() { defer wg.Done(); result.CAA, _ = ResolveCAARecord(domain) }()
wg.Wait()
return result
}
-284
View File
@@ -1,284 +0,0 @@
package webtest
import (
"fmt"
"strings"
"time"
"github.com/miekg/dns"
)
// DNSSECResult 包含 DNSSEC 验证的结果
type DNSSECResult struct {
Domain string `json:"domain"`
Enabled bool `json:"enabled"`
Valid bool `json:"valid"`
HasRRSIG bool `json:"has_rrsig"`
HasDNSKEY bool `json:"has_dnskey"`
HasDS bool `json:"has_ds"`
Algorithm uint8 `json:"algorithm"`
KeyTag uint16 `json:"key_tag"`
SignerName string `json:"signer_name"`
Validation string `json:"validation"`
Duration float64 `json:"duration"`
}
// ResolveDNSSEC 执行 DNSSEC 验证
// 通过 DoH 查询 DNSKEY、A、DS 记录,用 RRSIG.Verify() 验证签名
// 流程:
// 1. 查询 DNSKEY 记录,获取密钥 RRset
// 2. 查询 A 记录(带 DO 位),获取 A RRset + RRSIG
// 3. 用 DNSKEY 中的密钥逐一对 RRSIG.Verify(dnskey, aRRset) 验证
// 4. 查询 DS 记录,检查链式信任
func ResolveDNSSEC(domain string) (*DNSSECResult, error) {
start := time.Now()
result := &DNSSECResult{
Domain: domain,
}
// 1. 查询 DNSKEY 记录
msgDNSKEY := new(dns.Msg)
msgDNSKEY.SetQuestion(dns.Fqdn(domain), dns.TypeDNSKEY)
msgDNSKEY.SetEdns0(4096, true)
responseDNSKEY, _, err := executeDoHQueryMsg(msgDNSKEY)
result.Duration = time.Since(start).Seconds() * 1000
if err != nil {
result.Validation = fmt.Sprintf("DNSKEY query failed: %v", err)
return result, nil
}
if responseDNSKEY.Rcode != dns.RcodeSuccess {
result.Validation = fmt.Sprintf("DNSKEY query failed with Rcode %d", responseDNSKEY.Rcode)
return result, nil
}
var dnskeyRRset []dns.RR
var dnskeyList []*dns.DNSKEY
for _, ans := range responseDNSKEY.Answer {
if dnskey, ok := ans.(*dns.DNSKEY); ok {
dnskeyRRset = append(dnskeyRRset, dnskey)
dnskeyList = append(dnskeyList, dnskey)
result.HasDNSKEY = true
}
}
if len(dnskeyRRset) > 0 {
result.KeyTag = dnskeyList[0].KeyTag()
result.Algorithm = dnskeyList[0].Algorithm
}
// 2. 查询 A 记录(带 DO 位)
msgA := new(dns.Msg)
msgA.SetQuestion(dns.Fqdn(domain), dns.TypeA)
msgA.SetEdns0(4096, true)
responseA, _, err := executeDoHQueryMsg(msgA)
if err == nil && responseA.Rcode == dns.RcodeSuccess {
var aRRset []dns.RR
for _, ans := range responseA.Answer {
if aRecord, ok := ans.(*dns.A); ok {
aRRset = append(aRRset, aRecord)
}
}
var rrsigList []*dns.RRSIG
for _, ans := range responseA.Answer {
if rrsig, ok := ans.(*dns.RRSIG); ok {
rrsigList = append(rrsigList, rrsig)
result.HasRRSIG = true
}
}
// 3. 用 DNSKEY 逐条验证 RRSIG
for _, rrsig := range rrsigList {
for _, dnskey := range dnskeyList {
if keyErr := rrsig.Verify(dnskey, aRRset); keyErr == nil {
result.Enabled = true
result.Valid = true
result.Algorithm = dnskey.Algorithm
result.KeyTag = dnskey.KeyTag()
result.SignerName = rrsig.SignerName
result.Validation = fmt.Sprintf("DNSSEC 验证通过 (算法: %d, KeyTag: %d)", dnskey.Algorithm, dnskey.KeyTag())
return result, nil
}
}
}
if len(rrsigList) > 0 && len(dnskeyRRset) > 0 {
result.Enabled = true
result.Valid = false
result.Validation = fmt.Sprintf("RRSIG 验证失败: %d 条 RRSIG, %d 个 DNSKEY,无匹配签名", len(rrsigList), len(dnskeyRRset))
return result, nil
}
}
// 4. 查询 DS 记录
msgDS := new(dns.Msg)
msgDS.SetQuestion(dns.Fqdn(domain), dns.TypeDS)
responseDS, _, _ := executeDoHQueryMsg(msgDS)
if responseDS != nil && responseDS.Rcode == dns.RcodeSuccess {
for _, ans := range responseDS.Answer {
if _, ok := ans.(*dns.DS); ok {
result.HasDS = true
break
}
}
}
if result.HasRRSIG && result.HasDNSKEY {
result.Enabled = true
result.Valid = false
result.Validation = "存在 RRSIG 和 DNSKEY,但签名验证未通过"
} else if result.HasRRSIG {
result.Enabled = true
result.Valid = false
result.Validation = "存在 RRSIG,但缺少 DNSKEY"
} else if result.HasDNSKEY {
result.Enabled = false
result.Valid = false
result.Validation = "存在 DNSKEY,但缺少 RRSIG"
} else {
result.Enabled = false
result.Valid = false
result.Validation = "未检测到 DNSSEC 记录"
}
return result, nil
}
// ResolveDNSSECForRecord 针对特定记录类型进行 DNSSEC 验证
func ResolveDNSSECForRecord(domain string, recordType uint16) (*DNSSECResult, error) {
start := time.Now()
result := &DNSSECResult{
Domain: domain,
}
msg := new(dns.Msg)
msg.SetQuestion(dns.Fqdn(domain), recordType)
msg.SetEdns0(4096, true)
response, _, err := executeDoHQueryMsg(msg)
result.Duration = time.Since(start).Seconds() * 1000
if err != nil {
result.Validation = fmt.Sprintf("DNS query failed: %v", err)
return result, nil
}
if response.Rcode != dns.RcodeSuccess {
result.Validation = fmt.Sprintf("DNS query failed with Rcode %d", response.Rcode)
return result, nil
}
var targetRRset []dns.RR
var rrsigList []*dns.RRSIG
for _, ans := range response.Answer {
switch v := ans.(type) {
case *dns.A:
targetRRset = append(targetRRset, v)
case *dns.AAAA:
targetRRset = append(targetRRset, v)
case *dns.MX:
targetRRset = append(targetRRset, v)
case *dns.TXT:
targetRRset = append(targetRRset, v)
case *dns.NS:
targetRRset = append(targetRRset, v)
case *dns.CNAME:
targetRRset = append(targetRRset, v)
case *dns.SOA:
targetRRset = append(targetRRset, v)
case *dns.RRSIG:
rrsigList = append(rrsigList, v)
}
}
if len(rrsigList) > 0 {
result.HasRRSIG = true
}
// 查询 DNSKEY
msgDNSKEY := new(dns.Msg)
msgDNSKEY.SetQuestion(dns.Fqdn(domain), dns.TypeDNSKEY)
msgDNSKEY.SetEdns0(4096, true)
responseDNSKEY, _, err := executeDoHQueryMsg(msgDNSKEY)
if err == nil && responseDNSKEY.Rcode == dns.RcodeSuccess {
var dnskeyRRset []dns.RR
for _, ans := range responseDNSKEY.Answer {
if dnskey, ok := ans.(*dns.DNSKEY); ok {
dnskeyRRset = append(dnskeyRRset, dnskey)
result.HasDNSKEY = true
}
}
for _, rrsig := range rrsigList {
for _, dnskey := range dnskeyRRset {
if keyErr := rrsig.Verify(dnskey.(*dns.DNSKEY), targetRRset); keyErr == nil {
result.Enabled = true
result.Valid = true
result.Algorithm = dnskey.(*dns.DNSKEY).Algorithm
result.KeyTag = dnskey.(*dns.DNSKEY).KeyTag()
result.Validation = "DNSSEC 签名验证通过"
return result, nil
}
}
}
if len(rrsigList) > 0 && len(dnskeyRRset) > 0 {
result.Enabled = true
result.Valid = false
result.Validation = "RRSIG 验证失败"
}
}
// 查询 DS 记录
msgDS := new(dns.Msg)
msgDS.SetQuestion(dns.Fqdn(domain), dns.TypeDS)
responseDS, _, _ := executeDoHQueryMsg(msgDS)
if responseDS != nil && responseDS.Rcode == dns.RcodeSuccess {
for _, ans := range responseDS.Answer {
if _, ok := ans.(*dns.DS); ok {
result.HasDS = true
break
}
}
}
if !result.HasRRSIG {
result.Validation = "该记录类型未启用 DNSSEC"
}
return result, nil
}
// executeDoHQueryMsg 通过 queryDNSMsg(DoH+UDP 双通道)发送 DNS 查询并返回解析后的响应消息
func executeDoHQueryMsg(msg *dns.Msg) (*dns.Msg, float64, error) {
responseMsg, duration, err := queryDNSMsg(msg)
if err != nil {
return nil, duration, err
}
if responseMsg.Rcode != dns.RcodeSuccess {
return responseMsg, duration, fmt.Errorf("DNS query failed with Rcode %d", responseMsg.Rcode)
}
return responseMsg, duration, nil
}
// FormatDNSSECResult 将 DNSSECResult 格式化为易读的字符串
func FormatDNSSECResult(r *DNSSECResult) string {
var sb strings.Builder
sb.WriteString(fmt.Sprintf("域名: %s\n", r.Domain))
sb.WriteString(fmt.Sprintf("DNSSEC 状态: %s\n", map[bool]string{true: "已启用", false: "未启用"}[r.Enabled]))
sb.WriteString(fmt.Sprintf("验证状态: %s\n", map[bool]string{true: "通过", false: "未通过"}[r.Valid]))
sb.WriteString(fmt.Sprintf("RRSIG 记录: %s\n", map[bool]string{true: "存在", false: "不存在"}[r.HasRRSIG]))
sb.WriteString(fmt.Sprintf("DNSKEY 记录: %s\n", map[bool]string{true: "存在", false: "不存在"}[r.HasDNSKEY]))
sb.WriteString(fmt.Sprintf("DS 记录: %s\n", map[bool]string{true: "存在", false: "不存在"}[r.HasDS]))
sb.WriteString(fmt.Sprintf("验证信息: %s\n", r.Validation))
sb.WriteString(fmt.Sprintf("查询耗时: %.2f ms\n", r.Duration))
return sb.String()
}
-136
View File
@@ -1,136 +0,0 @@
package webtest
import (
"fmt"
"math"
"net"
"time"
)
type TCPingResult struct {
IP string `json:"ip"`
Port string `json:"port"`
Success bool `json:"success"`
RTT float64 `json:"rtt"`
Error string `json:"error"`
Timestamp time.Time `json:"timestamp"`
}
type TCPingStats struct {
IP string `json:"ip"`
Port string `json:"port"`
Sent int `json:"sent"`
Success int `json:"success"`
LossRate float64 `json:"loss_rate"`
MaxRTT float64 `json:"max_rtt"`
MinRTT float64 `json:"min_rtt"`
AvgRTT float64 `json:"avg_rtt"`
Results []TCPingResult `json:"results"`
}
func resolveHost(host string, version string) (string, error) {
return ResolveIP(host, version)
}
func TCPing(host string, port string, version string, timeout time.Duration) (*TCPingResult, error) {
ip, err := resolveHost(host, version)
if err != nil {
return nil, err
}
addr := ""
switch version {
case "v4":
addr = fmt.Sprintf("%s:%s", ip, port)
case "v6":
addr = fmt.Sprintf("[%s]:%s", ip, port)
}
start := time.Now()
conn, err := net.DialTimeout("tcp", addr, timeout)
rtt := time.Since(start)
result := &TCPingResult{
IP: ip,
Port: port,
Timestamp: start,
}
if err != nil {
result.Success = false
result.RTT = -1
result.Error = err.Error()
} else {
conn.Close()
result.Success = true
result.RTT = float64(rtt.Microseconds()) / 1000.0
}
return result, nil
}
func TCPingRun(host string, port string, count int, version string, timeout time.Duration, interval time.Duration) (*TCPingStats, error) {
ip, err := resolveHost(host, version)
if err != nil {
return &TCPingStats{
IP: "Error: " + err.Error(),
Port: port,
Sent: count,
Success: 0,
Results: nil,
MinRTT: -1,
MaxRTT: -1,
AvgRTT: -1,
LossRate: 100,
}, nil
}
stats := &TCPingStats{
IP: ip,
Port: port,
Sent: count,
MinRTT: math.MaxFloat64,
Results: make([]TCPingResult, 0, count),
}
var totalRTT float64
successCount := 0
for i := 0; i < count; i++ {
result, err := TCPing(host, port, version, timeout)
if err != nil {
return nil, err
}
stats.Results = append(stats.Results, *result)
if result.Success {
successCount++
totalRTT += result.RTT
if result.RTT > stats.MaxRTT {
stats.MaxRTT = result.RTT
}
if result.RTT < stats.MinRTT {
stats.MinRTT = result.RTT
}
}
if i < count-1 && interval > 0 {
time.Sleep(interval)
}
}
stats.Success = successCount
stats.LossRate = math.Round(float64(count-successCount)*10000/float64(count)) / 100
if successCount > 0 {
stats.AvgRTT = math.Round(totalRTT*100/float64(successCount)) / 100
} else {
stats.MinRTT = -1
stats.MaxRTT = -1
stats.AvgRTT = -1
}
return stats, nil
}
-579
View File
@@ -1,579 +0,0 @@
package webtest
import (
"context"
"fmt"
"net"
"regexp"
"strings"
"sync"
"time"
"edgeone-cloud-functions/ssrf"
"github.com/likexian/whois"
"github.com/likexian/whois-parser"
)
// WhoisResult 包含 WHOIS 查询的结构化结果
// 与前端 whois.vue 的结构体保持一致
type WhoisResult struct {
Domain string `json:"domain"` // 域名(大写)
Status []string `json:"status"` // 域名状态列表
Registrar WhoisRegistrar `json:"registrar"` // 注册商信息
Registrant WhoisContact `json:"registrant"` // 注册人信息
Technical WhoisContact `json:"technical"` // 技术联系人
AbuseContact WhoisContact `json:"abuseContact"` // Abuse 联系人
Dates WhoisDates `json:"dates"` // 关键日期
NameServers []string `json:"nameservers"` // DNS 服务器列表
WhoisServer string `json:"whoisServer"` // Whois 服务器地址
Raw string `json:"raw"` // 原始响应文本
Error string `json:"error"` // 错误信息
}
type WhoisRegistrar struct {
Name string `json:"name"`
IanaId string `json:"ianaId"`
}
type WhoisContact struct {
Name string `json:"name"`
Org string `json:"org"`
Phone string `json:"phone"`
Email string `json:"email"`
Province string `json:"province"`
ContactUri string `json:"contactUri"`
}
type WhoisDates struct {
Registration string `json:"registration"`
Expiration string `json:"expiration"`
LastChanged string `json:"lastChanged"`
}
// tldWhoisServers 内置常见 TLD 与对应 WHOIS 服务器的映射
// 当 IANA 查询失败时作为 fallback 使用
var tldWhoisServers = map[string]string{
"com": "whois.verisign-grs.com",
"net": "whois.verisign-grs.com",
"org": "whois.pir.org",
"info": "whois.afilias.net",
"biz": "whois.neulevel.biz",
"name": "whois.nic.name",
"pro": "whois.registrypro.pro",
"io": "whois.nic.io",
"co": "whois.nic.co",
"me": "whois.nic.me",
"cc": "whois.nic.cc",
"tv": "whois.nic.tv",
"top": "whois.nic.top",
"xyz": "whois.nic.xyz",
"club": "whois.nic.club",
"online":"whois.nic.online",
"site": "whois.nic.site",
"store": "whois.nic.store",
"shop": "whois.nic.shop",
"app": "whois.nic.google",
"dev": "whois.nic.google",
"tech": "whois.nic.tech",
"cn": "whois.cnnic.cn",
"wang": "whois.gtld.knet.cn",
"ren": "whois.renren.us",
}
// happyEyeballsV6Delay Happy Eyeballs 的 v6 启动延迟
// RFC 6555 推荐 250ms,这里缩短为 150ms,在 v6 不可达的环境下更快切到 v4
const happyEyeballsV6Delay = 150 * time.Millisecond
// whoisConnectTimeout 单 IP 连接 + 读写超时上限
const whoisConnectTimeout = 10 * time.Second
// QueryWhois 执行 WHOIS 查询并解析结构化数据
// 使用 likexian/whois 库查询原始响应,再用 whois-parser 解析为结构化数据
// 首次失败后 fallback:内置 TLD 映射 → IANA 查询服务器地址 →
// 用 ResolveA/AAAA(自定义DNS) 解析IP → Happy Eyeballs 双栈竞争拨号
func QueryWhois(domain string) (*WhoisResult, error) {
raw, err := whois.Whois(domain)
if err != nil {
raw, err = whoisRetryWithFallback(domain, err)
}
result := parseWhoisResult(domain, raw)
result.Error = errString(err)
return result, nil
}
// errString safely converts an error to string
func errString(err error) string {
if err == nil {
return ""
}
return err.Error()
}
// resolveWhoisServer 解析域名对应的 WHOIS 服务器地址
// 优先使用内置映射,失败时再查询 IANA WHOIS
func resolveWhoisServer(ext string) (string, error) {
// 1. 优先使用内置 TLD 映射
if server, ok := tldWhoisServers[ext]; ok && server != "" {
return server, nil
}
// 2. 内置映射中没有,尝试查 IANA
ianaResult, err := whois.Whois("." + ext)
if err != nil {
return "", err
}
server := extractWhoisServer(ianaResult)
if server == "" {
return "", fmt.Errorf("no whois server found in IANA response for .%s", ext)
}
return server, nil
}
// filterPublicIPs 过滤掉 SSRF 私网 IP(仅在 ssrf 启用时生效)
func filterPublicIPs(ips []string) []string {
if !ssrf.Enabled() {
return ips
}
filtered := make([]string, 0, len(ips))
for _, s := range ips {
ip := net.ParseIP(s)
if ip != nil && ssrf.IsPrivateIP(ip) {
continue
}
filtered = append(filtered, s)
}
return filtered
}
// resolveWhoisServerIPs 使用 webtest/dns 的统一解析接口
// 并行查询 A + AAAA(走自定义 DNS 服务器,不受系统 resolver 策略影响)
func resolveWhoisServerIPs(server string) (v4IPs, v6IPs []string, err error) {
var (
aResult DNSResult
aaaaResult DNSResult
wg sync.WaitGroup
aErr error
aaaaErr error
)
wg.Add(2)
go func() {
defer wg.Done()
aResult, aErr = ResolveARecord(server)
}()
go func() {
defer wg.Done()
aaaaResult, aaaaErr = ResolveAAAARecord(server)
}()
wg.Wait()
if aErr != nil && aaaaErr != nil {
return nil, nil, fmt.Errorf("both A and AAAA DNS lookup failed for %s: A=%w, AAAA=%w", server, aErr, aaaaErr)
}
v4IPs = filterPublicIPs(aResult.Record)
v6IPs = filterPublicIPs(aaaaResult.Record)
if len(v4IPs) == 0 && len(v6IPs) == 0 {
return nil, nil, fmt.Errorf("no IPs resolved for %s", server)
}
return v4IPs, v6IPs, nil
}
// happyEyeballsWhoisQuery Happy Eyeballs (RFC 6555) 简化版:
// - 立即并发拨号所有 IPv4 IP
// - 等待 happyEyeballsV6Delay 后并发拨号所有 IPv6 IP
// - 取第一个成功完成 WHOIS 查询的结果返回
// - 其他仍在运行的 goroutine 通过 context 取消尽快退出
func happyEyeballsWhoisQuery(domain string, v4IPs, v6IPs []string, port string) (string, error) {
if len(v4IPs) == 0 && len(v6IPs) == 0 {
return "", fmt.Errorf("no IPs available for WHOIS query")
}
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
type outcome struct {
raw string
err error
}
// 有缓冲:保证每个 goroutine 写入不被阻塞,避免 goroutine 泄漏
ch := make(chan outcome, len(v4IPs)+len(v6IPs))
tryIP := func(ip string) {
raw, err := rawWhoisQueryCtx(ctx, domain, ip, port)
ch <- outcome{raw: raw, err: err}
}
// 1) 立即启动所有 v4
for _, ip := range v4IPs {
go tryIP(ip)
}
// 2) 定时器:v6 延迟启动
var v6Timer *time.Timer
if len(v6IPs) > 0 {
v6Timer = time.AfterFunc(happyEyeballsV6Delay, func() {
for _, ip := range v6IPs {
go tryIP(ip)
}
})
}
defer func() {
if v6Timer != nil {
v6Timer.Stop()
}
}()
// 3) 收集结果:取第一个成功;若全失败则返回最后一个错误
var lastErr error
total := len(v4IPs) + len(v6IPs)
received := 0
// 首先保证 v6 goroutine 都已启动(如果 v6Timer 触发)
v6WaitCh := make(chan struct{})
if v6Timer != nil {
go func() {
time.Sleep(happyEyeballsV6Delay + 50*time.Millisecond)
close(v6WaitCh)
}()
} else {
close(v6WaitCh)
}
for received < total {
select {
case r := <-ch:
received++
if r.err == nil {
return r.raw, nil
}
lastErr = r.err
case <-ctx.Done():
return "", ctx.Err()
case <-v6WaitCh:
// v6 全部启动窗口已过,不再等这个信号
v6WaitCh = nil
}
}
if lastErr == nil {
lastErr = fmt.Errorf("all WHOIS connection attempts failed")
}
return "", lastErr
}
// whoisRetryWithFallback 在 whois.Whois 失败后,手动解析 WHOIS 服务器所有 IP 逐个尝试
func whoisRetryWithFallback(domain string, firstErr error) (string, error) {
ext := getExtension(domain)
// 解析 WHOIS 服务器地址(内置映射 + IANA fallback)
server, err := resolveWhoisServer(ext)
if err != nil {
return "", firstErr
}
// 使用 ResolveA/AAAA(自定义 DNS)并行解析 v4/v6
v4IPs, v6IPs, err := resolveWhoisServerIPs(server)
if err != nil {
return "", firstErr
}
// Happy Eyeballs 双栈竞争拨号 + 并发多 IP
raw, err := happyEyeballsWhoisQuery(domain, v4IPs, v6IPs, "43")
if err != nil {
return "", err
}
return raw, nil
}
// getExtension 提取域名后缀
func getExtension(domain string) string {
parts := strings.Split(domain, ".")
if len(parts) >= 2 {
return parts[len(parts)-1]
}
return domain
}
// extractWhoisServer 从 IANA 响应中提取 WHOIS 服务器地址
func extractWhoisServer(data string) string {
for _, token := range []string{"whois: ", "Whois: "} {
if idx := strings.Index(data, token); idx != -1 {
start := idx + len(token)
end := strings.Index(data[start:], "\n")
if end == -1 {
end = len(data) - start
}
server := strings.TrimSpace(data[start : start+end])
server = strings.TrimPrefix(server, "http://")
server = strings.TrimPrefix(server, "https://")
server = strings.TrimPrefix(server, "whois://")
server = strings.TrimSuffix(server, "/")
return server
}
}
return ""
}
// rawWhoisQueryCtx 带 context 的 WHOIS 查询
// context 被取消时,Dial/Read 尽快失败退出(goroutine 不泄漏)
func rawWhoisQueryCtx(ctx context.Context, domain, server, port string) (string, error) {
d := &net.Dialer{Timeout: whoisConnectTimeout}
conn, err := d.DialContext(ctx, "tcp", net.JoinHostPort(server, port))
if err != nil {
return "", err
}
defer conn.Close()
// 1. 设置读写 deadline:取 ctx deadline 或默认上限中更早的
deadline := time.Now().Add(whoisConnectTimeout + 5*time.Second)
if ctxDeadline, ok := ctx.Deadline(); ok && ctxDeadline.Before(deadline) {
deadline = ctxDeadline
}
_ = conn.SetDeadline(deadline)
// 2. 发送查询
if _, err := conn.Write([]byte(domain + "\r\n")); err != nil {
select {
case <-ctx.Done():
return "", ctx.Err()
default:
return "", err
}
}
// 3. 读取响应
buf := make([]byte, 65536)
n, readErr := conn.Read(buf)
// 有数据就读到了,哪怕 readErr != nil 也先返回数据
if n > 0 {
return string(buf[:n]), nil
}
select {
case <-ctx.Done():
return "", ctx.Err()
default:
return "", readErr
}
}
// rawWhoisQuery 直接向指定 IP:port 发送 WHOIS 查询(向后兼容包装)
func rawWhoisQuery(domain, server, port string) (string, error) {
return rawWhoisQueryCtx(context.Background(), domain, server, port)
}
// parseWhoisResult 用 whois-parser 解析原始响应,转换为前端需要的格式
func parseWhoisResult(domain, raw string) *WhoisResult {
info, err := whoisparser.Parse(raw)
result := &WhoisResult{
Domain: strings.ToUpper(domain),
}
if err != nil || info.Domain == nil {
result.Raw = raw
return result
}
// Domain 字段
result.NameServers = info.Domain.NameServers
result.Status = info.Domain.Status
result.Dates.Registration = info.Domain.CreatedDate
result.Dates.Expiration = info.Domain.ExpirationDate
result.Dates.LastChanged = info.Domain.UpdatedDate
result.WhoisServer = info.Domain.WhoisServer
// Registrar
if info.Registrar != nil {
result.Registrar.Name = info.Registrar.Name
result.Registrar.IanaId = extractIanaIdFromRaw(raw)
}
// Registrant
if info.Registrant != nil {
result.Registrant = contactFromParser(info.Registrant)
}
// Technical
if info.Technical != nil {
result.Technical = contactFromParser(info.Technical)
}
// AbuseContact: 从原始响应手动提取,不用 Administrative 映射
abuse := extractAbuseContactFromRaw(raw)
if abuse != nil {
result.AbuseContact = *abuse
}
result.Raw = raw
return result
}
// isEmptyContact 检查联系信息是否全为空
func isEmptyContact(c WhoisContact) bool {
return c.Name == "" && c.Org == "" && c.Phone == "" && c.Email == "" && c.Province == "" && c.ContactUri == ""
}
// abusePatterns 用于从原始 WHOIS 文本中提取 Abuse 联系信息
// 匹配字段名(不区分大小写),如:
// Registrar Abuse Contact Email: abuse@example.com
// Abuse Phone: +1.1234567890
var abusePatterns = map[string]*regexp.Regexp{
"email": regexp.MustCompile(`(?i)^\s*(?:Registrar\s+Abuse\s+Contact\s+Email|Abuse\s+(?:Contact\s+)?Email)\s*[:=]\s*(.+?)\s*$`),
"phone": regexp.MustCompile(`(?i)^\s*(?:Registrar\s+Abuse\s+Contact\s+Phone|Abuse\s+(?:Contact\s+)?Phone)\s*[:=]\s*(.+?)\s*$`),
}
// extractAbuseContactFromRaw 从原始 WHOIS 响应中手动提取 Abuse 联系人信息
// whois-parser 没有专门的 Abuse contact 字段,需要从原始文本解析
func extractAbuseContactFromRaw(raw string) *WhoisContact {
lines := strings.Split(raw, "\n")
var abuse WhoisContact
for _, line := range lines {
line = strings.TrimSpace(line)
if line == "" {
continue
}
if matches := abusePatterns["email"].FindStringSubmatch(line); len(matches) == 2 {
abuse.Email = matches[1]
continue
}
if matches := abusePatterns["phone"].FindStringSubmatch(line); len(matches) == 2 {
abuse.Phone = matches[1]
continue
}
}
if !isEmptyContact(abuse) {
return &abuse
}
return nil
}
// contactFromParser 将 whois-parser 的 Contact 转换为前端需要的格式
func contactFromParser(c *whoisparser.Contact) WhoisContact {
return WhoisContact{
Name: c.Name,
Org: c.Organization,
Phone: c.Phone,
Email: c.Email,
Province: c.Province,
ContactUri: c.ReferralURL,
}
}
// ianaIdPattern 匹配 Registrar IANA ID 字段(RFC 格式)
var ianaIdPattern = regexp.MustCompile(`(?i)^\s*Registrar\s+IANA\s+ID\s*[:=]\s*(\S+)`)
// extractIanaIdFromRaw 从原始 WHOIS 响应中提取 Registrar IANA ID
// whois-parser 未提供该字段,需手动解析
func extractIanaIdFromRaw(raw string) string {
for _, line := range strings.Split(raw, "\n") {
line = strings.TrimSpace(line)
if matches := ianaIdPattern.FindStringSubmatch(line); len(matches) == 2 {
return matches[1]
}
}
return ""
}
// ASNWhoisResult 包含 ASN WHOIS 查询的结构化结果
type ASNWhoisResult struct {
ASNumber string `json:"asNumber"`
ASName string `json:"asName"`
OrgName string `json:"orgName"`
OrgID string `json:"orgId"`
Country string `json:"country"`
RegDate string `json:"regDate"`
Updated string `json:"updated"`
AbuseName string `json:"abuseName"`
AbuseEmail string `json:"abuseEmail"`
AbusePhone string `json:"abusePhone"`
Raw string `json:"raw"`
Error string `json:"error"`
}
// asnFieldPatterns 用于从原始 ASN WHOIS 文本中提取字段
var asnFieldPatterns = map[string]*regexp.Regexp{
"asNumber": regexp.MustCompile(`(?i)^\s*ASNumber\s*[:=]\s*(.+?)\s*$`),
"asName": regexp.MustCompile(`(?i)^\s*ASName\s*[:=]\s*(.+?)\s*$`),
"asHandle": regexp.MustCompile(`(?i)^\s*ASHandle\s*[:=]\s*(.+?)\s*$`),
"regDate": regexp.MustCompile(`(?i)^\s*RegDate\s*[:=]\s*(.+?)\s*$`),
"updated": regexp.MustCompile(`(?i)^\s*Updated\s*[:=]\s*(.+?)\s*$`),
"orgName": regexp.MustCompile(`(?i)^\s*OrgName\s*[:=]\s*(.+?)\s*$`),
"orgId": regexp.MustCompile(`(?i)^\s*OrgId\s*[:=]\s*(.+?)\s*$`),
"country": regexp.MustCompile(`(?i)^\s*Country\s*[:=]\s*([A-Z]{2})\s*$`),
"abuseName": regexp.MustCompile(`(?i)^\s*OrgAbuseName\s*[:=]\s*(.+?)\s*$`),
"abuseEmail": regexp.MustCompile(`(?i)^\s*OrgAbuseEmail\s*[:=]\s*(.+?)\s*$`),
"abusePhone": regexp.MustCompile(`(?i)^\s*OrgAbusePhone\s*[:=]\s*(.+?)\s*$`),
}
// parseASNWhoisRaw 从原始 ASN WHOIS 响应中解析结构化数据
func parseASNWhoisRaw(raw string) *ASNWhoisResult {
result := &ASNWhoisResult{Raw: raw}
lines := strings.Split(raw, "\n")
for _, line := range lines {
line = strings.TrimSpace(line)
if line == "" || strings.HasPrefix(line, "%") || strings.HasPrefix(line, "#") {
continue
}
for field, pattern := range asnFieldPatterns {
if matches := pattern.FindStringSubmatch(line); len(matches) == 2 {
switch field {
case "asNumber":
result.ASNumber = matches[1]
case "asName":
result.ASName = matches[1]
case "asHandle":
if result.ASNumber == "" {
result.ASNumber = strings.TrimPrefix(matches[1], "AS")
}
case "regDate":
result.RegDate = matches[1]
case "updated":
result.Updated = matches[1]
case "orgName":
result.OrgName = matches[1]
case "orgId":
result.OrgID = matches[1]
case "country":
result.Country = matches[1]
case "abuseName":
result.AbuseName = matches[1]
case "abuseEmail":
result.AbuseEmail = matches[1]
case "abusePhone":
result.AbusePhone = matches[1]
}
}
}
}
return result
}
// QueryASNWhois 执行 ASN WHOIS 查询并解析结构化数据
// 使用 likexian/whois 库查询 ARIN WHOIS 服务器获取 ASN 详细信息
func QueryASNWhois(asn string) (*ASNWhoisResult, error) {
asn = strings.TrimSpace(asn)
if !strings.HasPrefix(strings.ToUpper(asn), "AS") {
asn = "AS" + asn
}
raw, err := whois.Whois(asn)
if err != nil {
return &ASNWhoisResult{
ASNumber: strings.TrimPrefix(asn, "AS"),
Error: err.Error(),
}, nil
}
result := parseASNWhoisRaw(raw)
result.Raw = raw
return result, nil
}
-29
View File
@@ -1,29 +0,0 @@
# Nuxt dev/build outputs
.output
.data
.nuxt
.nitro
.cache
dist
# Node dependencies
node_modules
# Logs
logs
*.log
# Misc
.DS_Store
.fleet
.idea
# Local env files
.env
.env.*
!.env.example
# cloudflare
.wrangler
.dev.vars*
!.dev.vars.example
-75
View File
@@ -1,75 +0,0 @@
# Nuxt Minimal Starter
Look at the [Nuxt documentation](https://nuxt.com/docs/getting-started/introduction) to learn more.
## Setup
Make sure to install dependencies:
```bash
# npm
npm install
# pnpm
pnpm install
# yarn
yarn install
# bun
bun install
```
## Development Server
Start the development server on `http://localhost:3000`:
```bash
# npm
npm run dev
# pnpm
pnpm dev
# yarn
yarn dev
# bun
bun run dev
```
## Production
Build the application for production:
```bash
# npm
npm run build
# pnpm
pnpm build
# yarn
yarn build
# bun
bun run build
```
Locally preview production build:
```bash
# npm
npm run preview
# pnpm
pnpm preview
# yarn
yarn preview
# bun
bun run preview
```
Check out the [deployment documentation](https://nuxt.com/docs/getting-started/deployment) for more information.
-351
View File
@@ -1,351 +0,0 @@
<script setup lang="ts">
import { computed, onMounted, onBeforeUnmount, ref } from 'vue';
import { useDark, useToggle } from '@vueuse/core';
import { Moon, Sunny, Expand } from '@element-plus/icons-vue';
import { config } from '../config/index';
const isNarrow = ref(false);
let mediaQueryList: MediaQueryList | null = null;
const drawer = ref(false);
const isDark = useDark();
const toggleDark = useToggle(isDark);
function cleanChineseCharacters(str:string) {
return str.replace(/[\u4e00-\u9fa5]/g, '');
}
// ==================== 响应式折叠菜单 ====================
// 映射表:菜单按显示顺序排在一个数组里,每项自带 minWidth(px)折叠阈值。
// 规则:视口宽度 >= minWidth 显示,低于则折叠进「更多」子菜单。
// 断点从右往左递减(最右侧 index 9 先折叠),数值可按需调整。
// divider 也带 minWidth(取其后第一项的值):所在段全部折叠时,分隔线一并隐藏。
interface NavItem {
kind: 'item'
index: string
label: string
to: string
minWidth: number
}
interface NavGroup {
kind: 'group'
index: string
label: string
minWidth: number
children: { index: string; label: string; to: string }[]
}
interface NavDivider {
kind: 'divider'
minWidth: number
}
type MenuEntry = NavItem | NavGroup | NavDivider
const menu: MenuEntry[] = [
// 断点设计基准(从右往左折叠):全显示约 1480px,1366 屏可用约 1350px,
// 故 1366 屏折叠 index 7/8/9(显示 1-6 +「更多」,约 1220px 宽裕);1440 折叠 8/9;1500+ 全显示。
{ kind: 'item', index: '1', label: 'IPv6 网站检测', to: '/ipv6webcheck', minWidth: 980 },
{ kind: 'item', index: '2', label: 'IPv6/IPv4 地址查询', to: '/location', minWidth: 1040 },
{ kind: 'item', index: '3', label: 'IPv6 TCPing测试', to: '/ipv6tcping', minWidth: 1100 },
{ kind: 'divider', minWidth: 1160 }, // 与 index 4 相同:4-6 段全折叠时隐藏
{ kind: 'item', index: '4', label: 'IPv6 DNS解析', to: '/dns', minWidth: 1160 },
{ kind: 'item', index: '5', label: 'IPv6 SSL检查', to: '/ssl', minWidth: 1220 },
{ kind: 'item', index: '6', label: 'IPv6 网站测速', to: '/ipv6speedtest', minWidth: 1280 },
{ kind: 'divider', minWidth: 1380 }, // 与 index 7 相同:7-9 段全折叠时隐藏
{
kind: 'group', index: '7', label: 'IPv4工具箱', minWidth: 1380,
children: [
{ index: '7-0', label: 'IPv4 网站测速', to: '/speedtest' },
{ index: '7-1', label: 'IPv4 TCPing测试', to: '/tcping' },
],
},
{
kind: 'group', index: '8', label: '其他工具', minWidth: 1420,
children: [
{ index: '8-0', label: '网站截图', to: '/screenshot' },
{ index: '8-1', label: 'Whois查询', to: '/whois' },
{ index: '8-3', label: 'ASN查询', to: '/asn' },
{ index: '8-4', label: 'DNSSEC验证', to: '/dnssec' },
],
},
{ kind: 'item', index: '9', label: '文档', to: '/doc', minWidth: 1500 },
]
// SSR 阶段无 window,默认按宽屏输出全部菜单项;onMounted 后更新为真实视口宽度
const viewportWidth = ref(1920)
// 可见项(含分隔线):视口宽度达标即显示
const visible = computed<MenuEntry[]>(() =>
menu.filter((entry) => viewportWidth.value >= entry.minWidth),
)
// 折叠项(不含分隔线):视口宽度不足即收进「更多」。
// 用类型谓词收窄为 NavItem | NavGroup,模板里才能安全访问 entry.index
const hidden = computed<Array<NavItem | NavGroup>>(
() => menu.filter((entry): entry is NavItem | NavGroup =>
entry.kind !== 'divider' && viewportWidth.value < entry.minWidth),
)
// 实时同步视口宽度(resize 触发,驱动折叠)
function updateViewport() {
viewportWidth.value = window.innerWidth
}
let umamiScript: HTMLScriptElement | null = null
useHead({
meta: config.noindex
? [
{ name: 'robots', content: 'noindex, nofollow' },
{ name: 'googlebot', content: 'noindex, nofollow' },
{ name: 'bingbot', content: 'noindex, nofollow' },
]
: [],
});
onMounted(() => {
mediaQueryList = window.matchMedia('(max-width: 768px)');
isNarrow.value = mediaQueryList.matches;
const handler = (e: MediaQueryListEvent) => {
isNarrow.value = e.matches;
};
mediaQueryList.addEventListener('change', handler);
updateViewport();
window.addEventListener('resize', updateViewport);
onBeforeUnmount(() => {
mediaQueryList?.removeEventListener('change', handler);
window.removeEventListener('resize', updateViewport);
});
if (!umamiScript && config.umamiScriptUrl) {
umamiScript = document.createElement('script')
umamiScript.src = config.umamiScriptUrl
umamiScript.async = true
umamiScript.setAttribute('data-website-id', config.umamiWebsiteId)
document.head.appendChild(umamiScript)
}
})
</script>
<template>
<el-drawer v-if="isNarrow" v-model="drawer" direction="ltr" style="height: 100%;" size="50%">
<router-link to="/ipv6webcheck">
<p class="menu-item-text">IPv6 网站检测</p>
</router-link>
<router-link to="/location">
<p class="menu-item-text">IPv6/IPv4 地址查询</p>
</router-link>
<router-link to="/ipv6tcping">
<p class="menu-item-text">IPv6 TCPing</p>
</router-link>
<router-link to="/dns"><p class="menu-item-text">IPv6 DNS解析</p></router-link>
<router-link to="/ssl">
<p class="menu-item-text">IPv6 SSL检查</p>
</router-link>
<router-link to="/ipv6speedtest"><p class="menu-item-text">IPv6 网站测速</p></router-link>
<router-link to="/speedtest"><p class="menu-item-text">IPv4 网站测速</p></router-link>
<router-link to="/tcping"><p class="menu-item-text">IPv4 TCPing</p></router-link>
<router-link to="/screenshot"><p class="menu-item-text">网站截图</p></router-link>
<router-link to="/whois"><p class="menu-item-text">Whois查询</p></router-link>
<router-link to="/asn"><p class="menu-item-text">ASN查询</p></router-link>
<router-link to="/dnssec"><p class="menu-item-text">DNSSEC验证</p></router-link>
</el-drawer>
<el-menu
mode="horizontal"
:ellipsis="false"
>
<el-menu-item index="0">
<el-icon v-if="isNarrow" @click="drawer = !drawer"><Expand /></el-icon>
<router-link to="/">
<el-image src="/favicon.svg" style="margin-top: 20px;" />
<h2 style="display: inline-block; margin-left: 10px" v-if="!isNarrow">{{ config.siteName }}</h2>
</router-link>
</el-menu-item>
<template v-if="!isNarrow">
<!-- 可见菜单项:按映射表顺序渲染 item / group / divider -->
<template v-for="entry in visible" :key="entry.kind === 'divider' ? 'div-' + entry.minWidth : entry.index">
<el-menu-item v-if="entry.kind === 'item'" :index="entry.index">
<router-link :to="entry.to"><p class="menu-item-text">{{ entry.label }}</p></router-link>
</el-menu-item>
<el-sub-menu v-else-if="entry.kind === 'group'" :index="entry.index">
<template #title>{{ entry.label }}</template>
<el-menu-item v-for="child in entry.children" :key="child.index" :index="child.index">
<router-link :to="child.to"><p class="menu-item-text">{{ child.label }}</p></router-link>
</el-menu-item>
</el-sub-menu>
<el-divider v-else style="margin-top: 20px;height: 1.2em;" direction="vertical"/>
</template>
<!-- 折叠项:统一收进「更多」子菜单(分组用 el-menu-item-group 保留组名) -->
<el-sub-menu v-if="hidden.length" index="more">
<template #title>更多</template>
<template v-for="entry in hidden" :key="entry.index">
<el-menu-item v-if="entry.kind === 'item'" :index="'m-' + entry.index">
<router-link :to="entry.to"><p class="menu-item-text">{{ entry.label }}</p></router-link>
</el-menu-item>
<el-menu-item-group v-else :title="entry.label">
<el-menu-item v-for="child in entry.children" :key="child.index" :index="'m-' + child.index">
<router-link :to="child.to"><p class="menu-item-text">{{ child.label }}</p></router-link>
</el-menu-item>
</el-menu-item-group>
</template>
</el-sub-menu>
</template>
<el-menu-item index="10">
<ClientOnly>
<el-icon @click="toggleDark()" v-if="isDark" style="cursor: pointer;"><Moon style="height: 20px; width: 20px;"/></el-icon>
<el-icon @click="toggleDark()" v-else style="cursor: pointer;"><Sunny style="height: 20px; width: 20px;"/></el-icon>
</ClientOnly>
</el-menu-item>
</el-menu>
<NuxtLoadingIndicator />
<main id="main-content" role="main">
<NuxtPage />
</main>
<footer>
<div class="one-line">
Copyright © nomdn & IP 查询 2026 | <img src="/ipv6-s1.svg" alt="IPv6 相关标识"/> | <img src="/ssl-s1.svg" alt="SSL 相关标识"/> | All right reserved
</div>
<div class="one-line">
<a v-if="config.ICP" href="https://beian.miit.gov.cn/" target="_blank" rel="noreferrer" >{{ config.ICP }}</a>
<span v-if="config.ICP">&nbsp;|&nbsp;</span>
<el-image v-if="config.GongAn" style="height: 1em; width: 1em;" src="/备案图标.png" />
<a :href="'https://beian.mps.gov.cn/#/query/webSearch?code=' + cleanChineseCharacters(config.GongAn)" target="_blank" rel="noreferrer" >{{ config.GongAn }}</a>
<span v-if="config.GongAn">&nbsp;|&nbsp;</span>
<a href="https://www.china-ipv6.cn/" target="_blank" rel="noreferrer" >国家IPv6发展监测平台</a>
&nbsp;|&nbsp;请遵守中国法律法规&nbsp;|&nbsp;
<a href="https://github.com/nomdn/ipw-cn" target="_blank" rel="noreferrer" >Github</a>&nbsp;|&nbsp;
<a href="https://qm.qq.com/q/E1CGjkqgG6" target="_blank" rel="noreferrer" >QQ用户交流群</a>
</div>
<div class="one-line">
致力于普及IPv6,推进IPv6规模部署和应用,以全面推进IPv6技术创新与融合应用为主线,以提升应用广度深度为主攻方向
</div>
</footer>
</template>
<style scoped>
@import "~/style.css";
/* 菜单项统一文本样式(替代原内联 style="display: inline-block; margin-left: 10px") */
.menu-item-text {
display: inline-block;
margin-left: 10px;
}
:deep(.shiki span) {
font-family: 'JetBrains Mono', 'Fira Code', 'Cascadia Code', 'Consolas', 'Monaco', 'Courier New', monospace !important;
word-wrap:break-word;
}
:deep(.shiki){
padding: 20px;
border-radius: 10px;
}
:deep(.el-menu-item a) {
font-size: 1em;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
:deep(.el-menu-item a p) {
font-size: 1em;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
:deep(.el-menu-item a img) {
width: 50px;
margin-bottom: 20px;
}
</style>
<style>
:root {
--el-color-primary: #3EAF7C;
}
html.dark {
--el-color-primary: #3EAF7C;
}
/* Drawer 内部链接占满一行 */
.el-drawer__body {
overflow-x: hidden;
}
.el-drawer__body a {
display: block !important;
width: 100% !important;
box-sizing: border-box;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
padding: 1em;
}
.el-drawer__body a p {
display: block !important;
width: 100% !important;
box-sizing: border-box;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
/* 窄屏(≤768px,与 script 中 isNarrow 的 matchMedia 断点一致):
在水合前由 CSS 兜底隐藏宽屏菜单项,避免窄屏设备闪现宽屏布局 */
@media (max-width: 768px) {
.el-menu--horizontal > .el-divider {
display: none !important;
}
.el-menu--horizontal > .el-menu-item[index="1"],
.el-menu--horizontal > .el-menu-item[index="2"],
.el-menu--horizontal > .el-menu-item[index="3"],
.el-menu--horizontal > .el-menu-item[index="4"],
.el-menu--horizontal > .el-menu-item[index="5"],
.el-menu--horizontal > .el-menu-item[index="6"],
.el-menu--horizontal > .el-sub-menu[index="7"],
.el-menu--horizontal > .el-sub-menu[index="8"],
.el-menu--horizontal > .el-menu-item[index="9"],
.el-menu--horizontal > .el-sub-menu[index="more"] {
display: none !important;
}
}
.el-menu--horizontal {
--el-menu-hover-bg-color: transparent !important;
--el-menu-active-color: var(--el-text-color-primary) !important;
--el-menu-bg-color: transparent !important;
}
.el-menu--horizontal > .el-menu-item:nth-child(1) {
margin-right: auto;
}
/* 去除选中强调和下划线 */
.el-menu--horizontal > .el-menu-item.is-active {
color: var(--el-text-color-primary) !important;
background-color: transparent !important;
}
.el-menu--horizontal > .el-menu-item:hover {
color: var(--el-text-color-primary) !important;
background-color: transparent !important;
}
/* 去除所有可能的边框和下划线 */
.el-menu--horizontal::after {
display: none !important;
}
.el-menu--horizontal > .el-menu-item {
border-bottom: none !important;
transition: none !important;
}
.el-menu--horizontal > .el-menu-item.is-active::after {
display: none !important;
}
/* 覆盖选中、悬停和聚焦状态的高亮样式 */
</style>
-78
View File
@@ -1,78 +0,0 @@
/* 当 html 有 .dark 类名时,切换为暗色主题 */
html.dark {
/* 强制覆盖所有颜色变量 */
color-scheme: dark;
}
/* 暗色模式下的背景色和文字颜色 */
html.dark .markdown-body {
/* GitHub 暗色主题的核心颜色 */
--color-fg-default: #e6edf3;
--color-fg-muted: #8b949e;
--color-fg-accent: #3EAF7C;
--color-bg-default: #242424;
--color-bg-muted: #080707;
--color-bg-accent: #3EAF7C;
--color-border-default: #242424;
--color-border-muted: #242424;
background-color: var(--color-bg-default);
color: var(--color-fg-default);
}
/* 暗色模式下的标题颜色 */
html.dark .markdown-body h1,
html.dark .markdown-body h2,
html.dark .markdown-body h3,
html.dark .markdown-body h4,
html.dark .markdown-body h5,
html.dark .markdown-body h6 {
color: var(--color-fg-default);
}
/* 暗色模式下的链接颜色 */
html.dark .markdown-body a {
color: var(--color-fg-accent);
}
/* 暗色模式下的代码块 */
html.dark .markdown-body pre {
background-color: var(--color-bg-muted);
border: 1px solid var(--color-border-default);
}
html.dark .markdown-body code {
background-color: var(--color-bg-muted);
color: var(--color-fg-default);
}
html.dark .markdown-body pre.shiki,
html.dark .markdown-body pre.shiki code {
background-color: transparent;
}
/* 暗色模式下的引用块 */
html.dark .markdown-body blockquote {
border-left-color: var(--color-border-muted);
color: var(--color-fg-muted);
}
/* 暗色模式下的表格 */
html.dark .markdown-body table tr {
background-color: var(--color-bg-default);
border-top-color: var(--color-border-muted);
}
html.dark .markdown-body table th,
html.dark .markdown-body table td {
border-color: var(--color-border-muted);
}
html.dark .markdown-body table tr:nth-child(2n) {
background-color: var(--color-bg-muted);
}
/* 暗色模式下的分割线 */
html.dark .markdown-body hr {
border-color: var(--color-border-muted);
}
-265
View File
@@ -1,265 +0,0 @@
<script setup lang="ts">
import { useMiddlewareFetch } from '../../utils/middleware';
import { ref, onMounted, computed, watch, nextTick } from 'vue'
import { useRoute } from 'vue-router'
import { config } from '../../config/index'
import { isIPv6 } from 'is-ip'
const route = useRoute()
useHead({
title: `ASN查询 | IP自治系统号查询 | ${config.siteName}`,
meta: [
{ name: 'description', content: '专业的ASN自治系统查询工具,支持IP地址ASN号查询,提供Maxmind GEOLite2、DB-IP双数据源对比,同时集成WHOIS解析获取ASN详细信息,包括组织名称、国家、注册日期等,助力网络运维和路由分析' },
{ name: 'keywords', content: 'asn查询,自治系统号,asn lookup,ip asn,asn whois,网络自治系统,运营商asn,ip归属asn' },
{ property: 'og:title', content: 'ASN自治系统查询工具 - IP ASN号与组织信息查询' },
{ property: 'og:description', content: '多数据源ASN查询,支持Maxmind GEOLite2和DB-IP,集成WHOIS解析获取ASN详细信息' },
{ property: 'og:image', content: config.siteUrl + 'favicon.svg' },
{ property: 'og:type', content: 'website' },
],
script: [
{
type: 'application/ld+json',
innerHTML: JSON.stringify({
'@context': 'https://schema.org',
'@type': 'WebApplication',
name: 'ASN自治系统查询工具',
description: '专业的ASN自治系统查询工具,支持IP地址ASN号查询,提供Maxmind GEOLite2、DB-IP双数据源对比,集成WHOIS解析。',
url: config.siteUrl + 'asn',
applicationCategory: 'DeveloperApplication',
operatingSystem: 'Web',
offers: {
'@type': 'Offer',
price: '0',
priceCurrency: 'CNY'
},
provider: {
'@type': 'Organization',
name: config.siteName
}
})
}
]
})
interface ASNResult {
ip: string
geolite2_asn?: { asn: string; org: string }
dbip_asn?: { asn: string; org: string }
whois?: any
}
const tmpIP = ref('1.1.1.1')
const loading = ref(false)
const result = ref<ASNResult | null>(null)
const error = ref('')
const userIP = ref('')
const apiList = config.IPLocationAPIs
const currentApiIndex = ref(0)
const backendID = computed(() => apiList[currentApiIndex.value]?.id || '')
const { data: asnData, error: asnError, execute: executeASN } = useMiddlewareFetch<ASNResult>(() => '/middleware/' + backendID.value + '/asn/' + tmpIP.value, {
key: 'asn-fetch',
immediate: false,
watch: false,
deep: false,
server: false,
lazy: true,
})
watch(asnData, (newData) => {
if (newData) {
result.value = newData
loading.value = false
error.value = ''
}
})
watch(asnError, async (newError) => {
if (newError) {
console.log(newError)
if (currentApiIndex.value < apiList.length - 1) {
const nextIndex = currentApiIndex.value + 1
error.value = `${(newError as any).message || '请求失败'},正在重试 ${apiList[nextIndex]?.label || ''}...`
currentApiIndex.value = nextIndex
await nextTick()
executeASN()
} else {
error.value = '请求失败,请检查IP或网络'
loading.value = false
}
}
})
async function queryASN() {
currentApiIndex.value = 0
loading.value = true
error.value = ''
result.value = null
tmpIP.value = tmpIP.value.trim()
if (!tmpIP.value) {
error.value = '请输入IP地址'
loading.value = false
return
}
await nextTick()
executeASN()
}
onMounted(() => {
getUserIP()
const ipParam = route.query.ip as string
if (ipParam) {
tmpIP.value = ipParam
queryASN()
}
})
async function getUserIP() {
await $fetch<string>(config.DualStackAPI).then(
function (data) {
userIP.value = data
}
)
return userIP.value
}
function getStatusClass(status: string): string {
if (status.includes('prohibited') || status.includes('lock')) return 'status-warning'
return 'status-success'
}
</script>
<template>
<div class="title">
<header>
<h1>ASN 自治系统查询</h1>
<p>查询 IP 地址所属的自治系统号(ASN)和组织信息</p>
</header>
</div>
<div class="content">
<div class="one-line">
<el-input
v-model="tmpIP"
placeholder="请输入IP地址(如:1.1.1.1)"
/>
<el-button
@click="queryASN()"
type="primary"
:loading="loading"
>
查询
</el-button>
</div>
<div v-if="error" class="error-message">
{{ error }}
</div>
<div v-if="result" class="result-section">
<table class="result-table">
<thead>
<tr>
<th class="table-header">项目</th>
<th class="table-header">信息</th>
</tr>
</thead>
<tbody>
<tr>
<td class="table-label">IP 地址</td>
<td class="table-value">{{ result.ip }}</td>
</tr>
<tr v-if="result.geolite2_asn?.asn">
<td class="table-label">Maxmind ASN</td>
<td class="table-value">
AS{{ result.geolite2_asn.asn }}
<span v-if="result.geolite2_asn.org" style="color: #999; font-size: 0.9em;">{{ result.geolite2_asn.org }}</span>
</td>
</tr>
<tr v-if="result.dbip_asn?.asn">
<td class="table-label">DB-IP ASN</td>
<td class="table-value">
AS{{ result.dbip_asn.asn }}
<span v-if="result.dbip_asn.org" style="color: #999; font-size: 0.9em;">{{ result.dbip_asn.org }}</span>
</td>
</tr>
<tr v-if="result.whois?.asNumber">
<td class="table-label">ASN</td>
<td class="table-value">AS{{ result.whois.asNumber }}</td>
</tr>
<tr v-if="result.whois?.asName">
<td class="table-label">AS 名称</td>
<td class="table-value">{{ result.whois.asName }}</td>
</tr>
<tr v-if="result.whois?.orgName">
<td class="table-label">组织名称</td>
<td class="table-value">{{ result.whois.orgName }}</td>
</tr>
<tr v-if="result.whois?.orgId">
<td class="table-label">组织 ID</td>
<td class="table-value">{{ result.whois.orgId }}</td>
</tr>
<tr v-if="result.whois?.country">
<td class="table-label">国家</td>
<td class="table-value">{{ result.whois.country }}</td>
</tr>
<tr v-if="result.whois?.regDate">
<td class="table-label">注册日期</td>
<td class="table-value">{{ result.whois.regDate }}</td>
</tr>
<tr v-if="result.whois?.updated">
<td class="table-label">更新日期</td>
<td class="table-value">{{ result.whois.updated }}</td>
</tr>
<tr v-if="result.whois?.abuseEmail">
<td class="table-label">Abuse 邮箱</td>
<td class="table-value">{{ result.whois.abuseEmail }}</td>
</tr>
<tr v-if="result.whois?.abusePhone">
<td class="table-label">Abuse 电话</td>
<td class="table-value">{{ result.whois.abusePhone }}</td>
</tr>
</tbody>
</table>
</div>
<blockquote>
<a href="/doc/user/asn" target="_blank">ASN 查询原理介绍</a><br/>
ASN(Autonomous System Number)是互联网中每个自治系统的唯一标识符。<br/>
数据来源:Maxmind GEOLite2、DB-IP,部分节点集成 WHOIS 进一步解析 ASN 详情。<br/>
<a href="/location" target="_blank">IP 归属地查询</a> | <a href="/whois" target="_blank">Whois 查询</a><br/>
访客IP: {{ userIP || '获取中...' }},您的网络{{ isIPv6(userIP) ? 'IPv6' : 'IPv4' }}优先<br/>
</blockquote>
</div>
</template>
<style scoped>
@import "../style.css";
@import "../../assets/css/tool-common.css";
.el-menu--horizontal > .el-menu-item:nth-child(1) {
margin-right: auto;
}
.result-table .table-label {
width: 200px;
}
.error-text {
color: #F56C6C;
}
</style>
<style>
:root {
--el-color-primary: #3EAF7C;
}
html.dark {
--el-color-primary: #3EAF7C;
}
.el-icon{
font-size: 1.3em;
}
</style>
-283
View File
@@ -1,283 +0,0 @@
<script setup lang="ts">
import { ref, onMounted, computed, nextTick } from 'vue'
import { useRoute } from 'vue-router'
import { config } from '../../config/index';
import { useMiddlewareFetch } from '../../utils/middleware';
import { isIPv6 } from 'is-ip';
import { formatTime } from '../../utils/tools';
const route = useRoute()
useHead({
title: `DNS查询工具 | 多节点域名解析检测 | ${config.siteName}`,
meta: [
{ name: 'description', content: '专业的多节点DNS查询工具,支持A记录、AAAA记录、CNAME记录、MX记录、NS记录、TXT记录、SRV记录、CAA记录等多种DNS解析记录查询,提供全国多节点并发检测,快速返回DNS解析结果,助力域名解析问题排查与优化' },
{ name: 'keywords', content: 'dns查询,dns解析,域名解析,a记录查询,aaaa记录,cname记录,mx记录,ns记录,txt记录,srv记录,dns服务器,域名dns检测' },
{ property: 'og:title', content: 'DNS查询工具 - 多节点域名解析记录检测' },
{ property: 'og:description', content: '多节点DNS查询工具,支持多种DNS记录类型查询,快速检测域名解析状态' },
{ property: 'og:image', content: config.siteUrl + 'favicon.svg' },
{ property: 'og:type', content: 'website' },
],
script: [
{
type: 'application/ld+json',
innerHTML: JSON.stringify({
'@context': 'https://schema.org',
'@type': 'WebApplication',
name: 'DNS查询与解析检测工具',
description: '专业的多节点DNS查询工具,支持A、AAAA、CNAME、MX、NS、TXT、SRV、CAA等多种记录类型,全国多节点并发检测。',
url: config.siteUrl + 'dns',
applicationCategory: 'DeveloperApplication',
operatingSystem: 'Web',
offers: {
'@type': 'Offer',
price: '0',
priceCurrency: 'CNY'
},
provider: {
'@type': 'Organization',
name: config.siteName
}
})
}
]
});
const tmpDomain = ref('www.zakoflare.com')
const domain = ref('')
const recordType = ref('a')
const loading = ref(false)
const results = ref<any>([])
const isloading = ref(false)
const nowRecordType = ref('')
const userIP = ref('')
async function getUserIP(){
await $fetch<string>(config.DualStackAPI).then(
function (data){
userIP.value = data
})
return userIP.value
}
const recordTypes = [
{ value: 'a', label: 'A 记录' },
{ value: 'aaaa', label: 'AAAA 记录' },
{ value: 'cname', label: 'CNAME 记录' },
{ value: 'mx', label: 'MX 记录' },
{ value: 'ns', label: 'NS 记录' },
{ value: 'txt', label: 'TXT 记录' },
{ value: 'srv', label: 'SRV 记录' },
{ value: 'caa', label: 'CAA 记录' },
{ value: 'ptr', label: 'PTR 记录' }
]
const dnsServerFetches = config.NSLookup.map((server) => {
const url = computed(() => "/middleware/" + server.id + "/dns/" + recordType.value + "/" + domain.value);
const { data, error, execute } = useMiddlewareFetch(url, {
immediate: false,
watch: false,
});
return { label: server.label, data, error, execute };
});
async function queryDNS() {
isloading.value = true
domain.value = tmpDomain.value
await nextTick()
// 初始化结果数组,保持响应式结构
results.value = dnsServerFetches.map(fetch => ({
server: fetch.label,
loading: true,
data: null,
error: null
}));
const promises = dnsServerFetches.map(async (fetch, index) => {
try {
await fetch.execute();
// 直接更新对应索引的结果,保持响应式
results.value[index] = {
server: fetch.label,
loading: false,
data: fetch.data.value,
error: null
};
return {
server: fetch.label,
data: fetch.data.value
};
} catch (err) {
// 错误时也更新对应索引
results.value[index] = {
server: fetch.label,
loading: false,
data: null,
error: err
};
return {
server: fetch.label,
error: err
};
}
});
nowRecordType.value = recordType.value
const promiseResults = await Promise.all(promises)
console.log(promiseResults)
isloading.value = false
return promiseResults
}
onMounted(() => {
const domainParam = route.query.domain as string
const typeParam = route.query.type as string
if (domainParam) {
tmpDomain.value = domainParam
}
if (typeParam && recordTypes.some(t => t.value === typeParam)) {
recordType.value = typeParam
}
if (domainParam) {
queryDNS()
}
getUserIP()
})
const { data: page, error } = await useAsyncData('dns-page', () =>
$fetch('/api/markdown/dns')
)
const doc = page.value;
</script>
<template>
<div class="title">
<header>
<h1>DNS查询</h1>
<p>多节点 DNS 查询,检测域名解析记录</p>
</header>
</div>
<div class="content">
<div class="one-line">
<el-input
v-model="tmpDomain"
placeholder="请输入域名(如:example.com)"
/>
<el-select v-model="recordType" style="width: 150px;" class="custom-height-select">
<el-option
v-for="item in recordTypes"
:key="item.value"
:label="item.label"
:value="item.value"
/>
</el-select>
<el-button
@click="queryDNS()"
type="primary"
:loading="isloading"
>
查询
</el-button>
</div>
<div class="result-section">
<table class="result-table" v-if="results.length > 0">
<thead>
<tr>
<th class="table-header">服务器</th>
<th class="table-header">类型</th>
<th class="table-header">记录</th>
<th class="table-header">记录数</th>
<th class="table-header">耗时</th>
<th class="table-header">TTL (S)</th>
</tr>
</thead>
<tbody>
<tr v-for="(result) in results" :key="result.server">
<td class="table-label">{{result.server}}</td>
<td class="table-value">{{nowRecordType.toUpperCase() || '--'}}</td>
<td class="table-value" v-if="result.loading" colspan="4" style="text-align: left;">加载中...</td>
<td class="table-value" v-else-if="result.error" colspan="4" style="color: #F56C6C;">
{{ result.error?.message || '请求失败' }}
</td>
<td class="table-value" v-else-if="!result.loading">
<template v-if="result && result.data?.record">
<div v-for="(ip, index) in result.data.record.slice(0, 5)" :key="index" class="ip-address">
<span>{{ ip }}</span>
</div>
</template>
<span v-else-if="!result.loading && result.data?.record?.length === 0" class="status-code" style="color: #F56C6C; background: #fef0f0;">
解析失败
</span>
</td>
<td class="table-value" v-if="!result.loading">{{result.data?.record?.length || 0}}</td>
<td class="table-value" v-if="!result.loading">{{formatTime(result.data?.duration)}}</td>
<td class="table-value" v-if="!result.loading">{{result.data?.ttl}}</td>
</tr>
</tbody>
</table>
</div>
<blockquote>
<span class="quate">A</span> 将域名指向一个 IPv4 地址,如 <span class="quate">106.55.75.123</span>。 A记录归属地是柠檬超绝大杂烩免费接口,随便用。<br/>
<span class="quate">AAAA</span> 将域名指向一个 IPv6 地址,如 <span class="quate">2402:4e00:1013:e500:0:9671:f018:4947</span>。同一个主机名可以同时解析到 IPv4(A记录)地址 和 IPv6(AAAA 记录)地址上,当只有IPv4 地址的用户会解析到 IPv4 地址,一般情况下有 IPv6 地址的用户会优先解析到 IPv6 地址。<br/>
<span class="quate">CNAME</span> 将域名指向另一个域名地址,与其保持相同解析,如 <span class="quate">ipw.wsmdn.top</span> 别名到 <span class="quate">ipw.wsmdn.top.eo.dnse1.com</span>.<br/>
<span class="quate">MX</span> 用于邮件服务器,一般由邮件注册商提供,如 <span class="quate">mxbiz1.qq.com</span>。如果邮箱格式为 test@<span class="quate">wsmdn.top</span> 则输入 <span class="quate">wsmdn.top</span> 查询。如果邮箱格式为 test@<span class="quate">mail.wsmdn.top</span>则输入<span class="quate">mail.wsmdn.top</span>查询。推荐2个免费的企业邮箱:腾讯企业邮、网易免费企业邮。<br/>
<span class="quate">TXT</span> 附加文本信息,常用于域名所有权验证,如在申请 HTTPS 证书时需要增加记录、<br/>
<span class="quate">PTR</span> IP 的反向解析记录,例如 <span class="quate">159.75.190.197</span> 反解析到 <span class="quate">wsmdn.top</span>,一般用于提升自建域名邮件服务器的可信度,可提单找云服务商添加。<br/>
<span class="quate">NS</span> 域名的 DNS 服务器地址,例如 <span class="quate">ns3.dnsv2.com</span>,推荐 华为云DNS.<br/>
<a href="/ipv6webcheck" target="_blank">网站开启IPv6检测</a> | <a href="/ssl" target="_blank">SSL证书在线检查</a><br/>
访客IP: {{userIP }},您的网络 {{ isIPv6(userIP) ? 'IPv6' : 'IPv4'}} 访问优先<br/>
</blockquote>
<div class="markdown">
<div v-html="doc"></div>
</div>
</div>
</template>
<style scoped>
@import "../style.css";
@import "../../assets/css/tool-common.css";
.el-menu--horizontal > .el-menu-item:nth-child(1) {
margin-right: auto;
}
.markdown :deep(a) {
color: #3EAF7C !important;
font-size: 1.3em;
text-decoration: none
}
</style>
<style>
:root {
--el-color-primary: #3EAF7C;
}
html.dark {
--el-color-primary: #3EAF7C;
}
.el-icon {
font-size: 1.3em;
}
</style>
-279
View File
@@ -1,279 +0,0 @@
<script setup lang="ts">
import { useMiddlewareFetch } from '../../utils/middleware';
import { ref, onMounted, computed, nextTick } from 'vue'
import { useRoute } from 'vue-router'
import { config } from '../../config/index'
import { isIPv6 } from 'is-ip'
const route = useRoute()
useHead({
title: `DNSSEC查询 | 域名DNSSEC验证检测 | ${config.siteName}`,
meta: [
{ name: 'description', content: '专业的DNSSEC查询工具,支持域名DNSSEC验证检测,检查域名是否启用DNSSEC签名,验证DNSKEY、RRSIG、DS记录链式信任关系,检测域名DNS数据完整性和真实性,防止DNS欺骗和中间人攻击,保障域名解析安全' },
{ name: 'keywords', content: 'dnssec查询,dnssec验证,dnskey,rrsig,ds记录,域名安全,链式信任,dns欺骗防护,域名解析安全,dns签名验证' },
{ property: 'og:title', content: 'DNSSEC查询工具 - 域名DNSSEC验证与安全检测' },
{ property: 'og:description', content: 'DNSSEC验证检测工具,检查域名DNSSEC签名状态,验证DNSKEY、RRSIG、DS记录链式信任关系' },
{ property: 'og:image', content: config.siteUrl + 'favicon.svg' },
{ property: 'og:type', content: 'website' },
],
script: [
{
type: 'application/ld+json',
innerHTML: JSON.stringify({
'@context': 'https://schema.org',
'@type': 'WebApplication',
name: 'DNSSEC验证查询工具',
description: '专业的DNSSEC验证查询工具,检测域名DNSSEC签名状态,验证DNSKEY、RRSIG、DS记录链式信任关系,保障域名解析安全。',
url: config.siteUrl + 'dnssec',
applicationCategory: 'DeveloperApplication',
operatingSystem: 'Web',
offers: {
'@type': 'Offer',
price: '0',
priceCurrency: 'CNY'
},
provider: {
'@type': 'Organization',
name: config.siteName
}
})
}
]
})
interface DNSSECResult {
domain: string
enabled: boolean
valid: boolean
has_rrsig: boolean
has_dnskey: boolean
has_ds: boolean
algorithm: number
key_tag: number
signer_name: string
validation: string
duration: number
}
interface ServerResult {
label: string
loading: boolean
error?: string
data?: DNSSECResult
}
const tmpDomain = ref('www.zakoflare.com')
const loading = ref(false)
const serverResults = ref<ServerResult[]>([])
const userIP = ref('')
const apiList = config.NSLookup
const dnssecFetches = apiList.map((server) => {
const url = computed(() => "/middleware/" + server.id + "/dnssec/" + tmpDomain.value)
const { data, error, execute } = useMiddlewareFetch<DNSSECResult>(url, {
immediate: false,
watch: false,
})
return { label: server.label, data, error, execute }
})
function initServerResults() {
serverResults.value = apiList.map((server) => ({
label: server.label,
loading: false,
}))
}
async function checkDNSSEC() {
const domain = tmpDomain.value.trim()
if (!domain) return
loading.value = true
serverResults.value.forEach((result) => {
result.loading = true
result.data = undefined
})
const promises = dnssecFetches.map(async (fetch, index) => {
try {
await fetch.execute()
const result = serverResults.value[index]
if (result) {
result.data = fetch.data.value
}
} catch (err) {
console.error(err)
const result = serverResults.value[index]
if (result) {
result.error = (err as any)?.message || '请求失败'
}
} finally {
const result = serverResults.value[index]
if (result) {
result.loading = false
}
}
})
Promise.allSettled(promises).finally(function () {
loading.value = false
})
}
function getValidationClass(result: DNSSECResult): string {
if (result.valid) return 'status-success'
if (result.has_dnskey || result.has_rrsig) return 'status-warning'
return 'status-error'
}
function formatDuration(ms: number): string {
return ms.toFixed(2) + ' ms'
}
onMounted(() => {
initServerResults()
getUserIP()
const domainParam = route.query.domain as string
if (domainParam) {
tmpDomain.value = domainParam
checkDNSSEC()
}
})
async function getUserIP() {
await $fetch<string>(config.DualStackAPI).then(
function (data) {
userIP.value = data
}
)
return userIP.value
}
</script>
<template>
<div class="title">
<header>
<h1>DNSSEC 验证查询</h1>
<p>检测域名 DNSSEC 签名状态和链式信任验证</p>
</header>
</div>
<div class="content">
<div class="one-line">
<el-input
v-model="tmpDomain"
placeholder="请输入域名(如:example.com)"
/>
<el-button
@click="checkDNSSEC()"
type="primary"
:loading="loading"
>
验证
</el-button>
</div>
<div class="result-section" v-if="serverResults.some(r => r.data)">
<table class="result-table">
<thead>
<tr>
<th class="table-header">DNS 服务器</th>
<th class="table-header">DNSSEC</th>
<th class="table-header">DNSKEY</th>
<th class="table-header">RRSIG</th>
<th class="table-header">DS</th>
<th class="table-header">验证结果</th>
<th class="table-header">耗时</th>
</tr>
</thead>
<tbody>
<template v-for="(server, index) in serverResults" :key="index">
<tr v-if="server.data">
<td class="table-label">{{ server.label }}</td>
<td class="table-value">
<span :class="server.data.enabled ? 'status-success' : 'status-error'" class="status-code">
{{ server.data.enabled ? '已启用' : '未启用' }}
</span>
</td>
<td class="table-value">
<span :class="server.data.has_dnskey ? 'status-success' : 'status-error'" class="status-code">
{{ server.data.has_dnskey ? '有' : '无' }}
</span>
</td>
<td class="table-value">
<span :class="server.data.has_rrsig ? 'status-success' : 'status-error'" class="status-code">
{{ server.data.has_rrsig ? '有' : '无' }}
</span>
</td>
<td class="table-value">
<span :class="server.data.has_ds ? 'status-success' : 'status-error'" class="status-code">
{{ server.data.has_ds ? '有' : '无' }}
</span>
</td>
<td class="table-value">
<span :class="getValidationClass(server.data)" class="status-code">
{{ server.data.valid ? '验证通过' : '验证失败' }}
</span>
</td>
<td class="table-value">{{ formatDuration(server.data.duration) }}</td>
</tr>
<tr v-else-if="server.loading">
<td class="table-label">{{ server.label }}</td>
<td class="table-value" colspan="6">加载中...</td>
</tr>
<tr v-else-if="server.error">
<td class="table-label">{{ server.label }}</td>
<td class="table-value error-text" colspan="6">{{ server.error }}</td>
</tr>
<tr v-else-if="!server.data">
<td class="table-label">{{ server.label }}</td>
<td class="table-value" colspan="6">-</td>
</tr>
</template>
</tbody>
</table>
</div>
<blockquote>
<a href="/doc/user/dnssec" target="_blank">DNSSEC 原理介绍</a><br/>
DNSSEC 通过 DNSKEY、RRSIG、DS 记录为 DNS 数据提供完整性验证,防止 DNS 欺骗攻击。<br/>
如果显示 "验证失败",可能原因:域名未配置 DNSSEC、DNSKEY 密钥不匹配、DS 记录缺失等。<br/>
<a href="/dns" target="_blank">DNS 解析查询</a> | <a href="/whois" target="_blank">Whois 查询</a><br/>
访客IP: {{ userIP || '获取中...' }},您的网络{{ isIPv6(userIP) ? 'IPv6' : 'IPv4' }}优先<br/>
</blockquote>
</div>
</template>
<style scoped>
@import "../style.css";
@import "../../assets/css/tool-common.css";
.el-menu--horizontal > .el-menu-item:nth-child(1) {
margin-right: auto;
}
.result-table .table-label {
width: 180px;
}
.error-text {
color: #F56C6C;
}
</style>
<style>
:root {
--el-color-primary: #3EAF7C;
}
html.dark {
--el-color-primary: #3EAF7C;
}
.el-icon{
font-size: 1.3em;
}
</style>
-133
View File
@@ -1,133 +0,0 @@
<script setup lang="ts">
import { getDocMeta } from '../../../config/doc'
const route = useRoute();
const path = computed(() => route.path)
const { data: page } = await useFetch(() => `/api/markdown${path.value}`)
const activeIndex = computed(() => route.path)
const meta = computed(() => getDocMeta(route.path))
useHead({
title: computed(() => meta.value.title),
meta: [
{ name: 'description', content: computed(() => meta.value.description) }
]
})
</script>
<template>
<div class="box">
<el-menu
:default-active="activeIndex"
class="sidebar-menu"
router
>
<!-- 1. 独立菜单项 -->
<el-menu-item index="/doc">
<span>IPv6 工具箱使用文档</span>
</el-menu-item>
<!-- 2. IPv6 用户端 -->
<el-sub-menu index="IPv6 用户端">
<template #title>
<span>IPv6 用户端</span>
</template>
<el-menu-item index="/doc/user/enable_ipv6">个人宽带如何开启IPv6网络访问</el-menu-item>
<el-menu-item index="/doc/user/cmd_bash_disable_ipv6">命令行禁用/启用IPv6本地网络</el-menu-item>
<el-menu-item index="/doc/user/cmd_getip">命令行(curl)获取 IPv4 和 IPv6 地址</el-menu-item>
<el-menu-item index="/doc/user/view_ipv6_adress_url">浏览器访问 IPv6 地址</el-menu-item>
<el-menu-item index="/doc/user/ipv4_ipv6_prefix_precedence">Windows 10/11 设置 IPv4/IPv6 访问优先级</el-menu-item>
<el-menu-item index="/doc/user/ipv6_daohang">国内 IPv6 资源导航</el-menu-item>
<el-menu-item index="/doc/user/pure_ipv6_website">国内纯 IPv6 网站导航</el-menu-item>
<el-menu-item index="/doc/user/dns">全国各省 DNS 服务器列表</el-menu-item>
<el-menu-item index="/doc/user/ipv6_dns">IPv6 DNS 地址列表</el-menu-item>
<!-- 嵌套子菜单:最佳实践 -->
<el-sub-menu index="最佳实践">
<template #title>
<span>最佳实践</span>
</template>
<el-menu-item index="/doc/user/AliyunAuthorizeSecurityGroup">阿里云自动化添加安全组</el-menu-item>
<el-menu-item index="/doc/user/TencentCloudAddSecurityGroup">腾讯云自动化添加安全组</el-menu-item>
</el-sub-menu>
</el-sub-menu>
<!-- 3. 云服务器配置 IPv6 -->
<el-sub-menu index="云服务器配置 IPv6">
<template #title>
<span>云服务器配置 IPv6</span>
</template>
<el-menu-item index="/doc/server/website_enable_ipv6">网站开启 IPv6 的三种方式</el-menu-item>
<el-menu-item index="/doc/server/tencent_cloud_cvm_ipv6">腾讯云 cvm 开启 IPv6</el-menu-item>
<el-menu-item index="/doc/server/nginx_ipv6">Nginx 开启 IPv6</el-menu-item>
<el-menu-item index="/doc/server/ipv6webcheck">如何确认一个网站是否开启 IPv6</el-menu-item>
<el-menu-item index="/doc/server/ipv6_sign">网站增加支持IPv6访问标识</el-menu-item>
<el-menu-item index="/doc/server/ipv6_domain_record">如何为域名添加 IPv6 解析记录</el-menu-item>
<el-menu-item index="/doc/server/http2">网站如何开启 HTTP/2</el-menu-item>
</el-sub-menu>
<!-- 4. IPv6 协议规范 -->
<el-sub-menu index="IPv6 协议规范">
<template #title>
<span>IPv6 协议规范</span>
</template>
<el-menu-item index="/doc/rfc/rfc8200">IPv6 RFC8200 解读</el-menu-item>
<el-menu-item index="/doc/rfc/ipv6_address_format">IPv6 地址标识方法</el-menu-item>
<el-menu-item index="/doc/user/tcpdump_ipv6">tcpdump 分析IPv6包</el-menu-item>
<el-menu-item index="/doc/user/wireshark_ipv6">WireShark 分析IPv6包头</el-menu-item>
<el-menu-item index="/doc/user/ipv6_ping">IPv6 Ping 原理</el-menu-item>
</el-sub-menu>
</el-menu>
<div class="content">
<div class="markdown-body" v-if="page">
<div v-html="page" />
</div>
</div>
</div>
</template>
<style scoped>
/* 核心布局容器 */
.box {
display: flex;
flex-direction: row; /* 改为 row,实现左右排列 */
height: 100vh; /* 占满整个屏幕高度,防止出现页面级滚动条 */
width: 100%;
background-color: #fff; /* 可选:设置整体背景色 */
}
html.dark .box {
background-color: #242424; /* 可选:暗黑模式下的整体背景色 */
}
/* 左侧侧边栏样式 */
.sidebar-menu {
width: 260px; /* 固定侧边栏宽度 */
height: 100%; /* 高度撑满父容器 */
flex-shrink: 0; /* 防止被右侧内容挤压变形 */
overflow-y: auto; /* 菜单项过多时,侧边栏内部出现滚动条 */
border-right: 1px solid #e4e7ed; /* 添加右侧分割线(可选) */
}
.content {
flex: 1; /* 右侧内容区域占据剩余空间 */
padding: 20px; /* 内边距,避免内容贴边 */
overflow-y: auto; /* 内容过多时,右侧区域出现滚动条 */
background-color: #fff; /* 可选:设置右侧内容区域背景色 */
}
html.dark .content {
background-color: #242424; /* 可选:暗黑模式下的背景色 */
}
@media (max-width: 768px) {
.sidebar-menu {
width: 100px;
}
.content {
padding: 10px;
}
}
</style>
<style>
@import "github-markdown-css/github-markdown-light.css";
@import "/github-markdown-dark.css";
</style>
-134
View File
@@ -1,134 +0,0 @@
<script setup lang="ts">
import { getDocMeta } from '../../../config/doc'
const route = useRoute();
const { data: page } = await useAsyncData('doc-index', () =>
$fetch('/api/markdown/doc/index')
)
const doc = page.value;
const activeIndex = computed(() => route.path)
const meta = getDocMeta('/doc')
useHead({
title: meta.title,
meta: [
{ name: 'description', content: meta.description }
]
})
</script>
<template>
<div class="box">
<el-menu
:default-active="activeIndex"
class="sidebar-menu"
router
>
<!-- 1. 独立菜单项 -->
<el-menu-item index="/doc">
<span>IPv6 工具箱使用文档</span>
</el-menu-item>
<!-- 2. IPv6 用户端 -->
<el-sub-menu index="IPv6 用户端">
<template #title>
<span>IPv6 用户端</span>
</template>
<el-menu-item index="/doc/user/enable_ipv6">个人宽带如何开启IPv6网络访问</el-menu-item>
<el-menu-item index="/doc/user/cmd_bash_disable_ipv6">命令行禁用/启用IPv6本地网络</el-menu-item>
<el-menu-item index="/doc/user/cmd_getip">命令行(curl)获取 IPv4 和 IPv6 地址</el-menu-item>
<el-menu-item index="/doc/user/view_ipv6_adress_url">浏览器访问 IPv6 地址</el-menu-item>
<el-menu-item index="/doc/user/ipv4_ipv6_prefix_precedence">Windows 10/11 设置 IPv4/IPv6 访问优先级</el-menu-item>
<el-menu-item index="/doc/user/ipv6_daohang">国内 IPv6 资源导航</el-menu-item>
<el-menu-item index="/doc/user/pure_ipv6_website">国内纯 IPv6 网站导航</el-menu-item>
<el-menu-item index="/doc/user/dns">全国各省 DNS 服务器列表</el-menu-item>
<el-menu-item index="/doc/user/ipv6_dns">IPv6 DNS 地址列表</el-menu-item>
<!-- 嵌套子菜单:最佳实践 -->
<el-sub-menu index="最佳实践">
<template #title>
<span>最佳实践</span>
</template>
<el-menu-item index="/doc/user/AliyunAuthorizeSecurityGroup">阿里云自动化添加安全组</el-menu-item>
<el-menu-item index="/doc/user/TencentCloudAddSecurityGroup">腾讯云自动化添加安全组</el-menu-item>
</el-sub-menu>
</el-sub-menu>
<!-- 3. 云服务器配置 IPv6 -->
<el-sub-menu index="云服务器配置 IPv6">
<template #title>
<span>云服务器配置 IPv6</span>
</template>
<el-menu-item index="/doc/server/website_enable_ipv6">网站开启 IPv6 的三种方式</el-menu-item>
<el-menu-item index="/doc/server/tencent_cloud_cvm_ipv6">腾讯云 cvm 开启 IPv6</el-menu-item>
<el-menu-item index="/doc/server/nginx_ipv6">Nginx 开启 IPv6</el-menu-item>
<el-menu-item index="/doc/server/ipv6webcheck">如何确认一个网站是否开启 IPv6</el-menu-item>
<el-menu-item index="/doc/server/ipv6_sign">网站增加支持IPv6访问标识</el-menu-item>
<el-menu-item index="/doc/server/ipv6_domain_record">如何为域名添加 IPv6 解析记录</el-menu-item>
<el-menu-item index="/doc/server/http2">网站如何开启 HTTP/2</el-menu-item>
</el-sub-menu>
<!-- 4. IPv6 协议规范 -->
<el-sub-menu index="IPv6 协议规范">
<template #title>
<span>IPv6 协议规范</span>
</template>
<el-menu-item index="/doc/rfc/rfc8200">IPv6 RFC8200 解读</el-menu-item>
<el-menu-item index="/doc/rfc/ipv6_address_format">IPv6 地址标识方法</el-menu-item>
<el-menu-item index="/doc/user/tcpdump_ipv6">tcpdump 分析IPv6包</el-menu-item>
<el-menu-item index="/doc/user/wireshark_ipv6">WireShark 分析IPv6包头</el-menu-item>
<el-menu-item index="/doc/user/ipv6_ping">IPv6 Ping 原理</el-menu-item>
</el-sub-menu>
</el-menu>
<div class="content">
<div class="markdown-body" v-html="doc"></div>
</div>
</div>
</template>
<style scoped>
/* 核心布局容器 */
.box {
display: flex;
flex-direction: row; /* 改为 row,实现左右排列 */
height: 100vh; /* 占满整个屏幕高度,防止出现页面级滚动条 */
width: 100%;
background-color: #fff; /* 可选:设置整体背景色 */
}
html.dark .box {
background-color: #242424; /* 可选:暗黑模式下的整体背景色 */
}
/* 左侧侧边栏样式 */
.sidebar-menu {
width: 260px; /* 固定侧边栏宽度 */
height: 100%; /* 高度撑满父容器 */
flex-shrink: 0; /* 防止被右侧内容挤压变形 */
overflow-y: auto; /* 菜单项过多时,侧边栏内部出现滚动条 */
border-right: 1px solid #e4e7ed; /* 添加右侧分割线(可选) */
}
.content {
flex: 1; /* 右侧内容区域占据剩余空间 */
padding: 20px; /* 内边距,避免内容贴边 */
overflow-y: auto; /* 内容过多时,右侧区域出现滚动条 */
background-color: #fff; /* 可选:设置右侧内容区域背景色 */
}
html.dark .content {
background-color: #242424; /* 可选:暗黑模式下的背景色 */
}
@media (max-width: 768px) {
.sidebar-menu {
width: 100px;
}
.content {
padding: 10px;
}
}
</style>
<style>
@import "github-markdown-css/github-markdown-light.css";
@import "/github-markdown-dark.css";
@import "/style.css"
</style>
-172
View File
@@ -1,172 +0,0 @@
<script setup lang="ts">
import { computed, ref, onMounted } from 'vue';
import { isIPv6 } from 'is-ip';
import { config } from '../../config/index';
import { CircleCheckFilled, CircleCloseFilled } from '@element-plus/icons-vue';
import { highlightCode } from '../../utils/shiki';
import { isIPv4 } from '../../utils/tools';
const route = useRoute();
const canonicalUrl = computed(() => new URL(route.path, config.siteUrl).toString());
useHead({
title: `${config.siteName} | IP查询工具 | IPv4/IPv6地址查询与网络测试平台`,
titleTemplate: '%s',
link: [
{ rel: 'canonical', href: canonicalUrl.value }
],
meta: [
{ name: 'description', content: `${config.siteName}提供专业的IP查询服务,支持IPv4和IPv6地址在线查询、归属地定位、网络测速、DNS解析、SSL证书检测、TCPing测试等多种网络工具,致力于推进IPv6规模部署和应用,打造去中心化的IP查询平台` },
{ name: 'keywords', content: 'ipv6,ipv4,ip查询,ipv6查询,ipv4查询,ipv6地址查询,ipv4地址查询,网络测速,DNS查询,SSL检测,TCPing,IP归属地,IPv6优先' },
{ property: 'og:title', content: `${config.siteName} - 专业IP查询与网络测试工具平台` },
{ property: 'og:description', content: '提供IPv4/IPv6地址查询、网络测速、DNS解析、SSL检测等全方位网络诊断工具,助力IPv6普及与部署' },
{ property: 'og:image', content: `${config.siteUrl}favicon.svg` },
{ property: 'og:type', content: 'website' },
{ property: 'og:url', content: canonicalUrl.value },
{ name: 'twitter:card', content: 'summary_large_image' },
],
script: [
{
type: 'application/ld+json',
innerHTML: JSON.stringify({
'@context': 'https://schema.org',
'@type': 'WebApplication',
name: config.siteName,
description: `${config.siteName}提供专业的IP查询服务,支持IPv4和IPv6地址在线查询、归属地定位、网络测速、DNS解析、SSL证书检测、TCPing测试等多种网络工具,致力于推进IPv6规模部署和应用,打造去中心化的IP查询平台`,
url: canonicalUrl.value,
applicationCategory: 'DeveloperApplication',
operatingSystem: 'Any',
offers: {
'@type': 'Offer',
price: '0',
priceCurrency: 'CNY'
},
featureList: 'IPv4地址查询,IPv6地址查询,IP归属地定位,网络测速,DNS解析,SSL证书检测,TCPing测试,IPv6优先检测',
about: [
{
'@type': 'Thing',
name: 'IP地址查询',
description: '通过特定的IP地址获取相关的地理位置、运营商、网络类型等信息的技术服务。IPv4地址是32位地址格式,IPv6地址是128位地址格式,IPv6能够提供更大的地址空间,解决IPv4地址枯竭问题。'
},
{
'@type': 'Thing',
name: 'IPv6优先检测',
description: '当访问一个同时支持IPv4和IPv6的双栈网站时,如果网络IPv6优先,系统会优先使用IPv6地址进行连接。通过访问双栈测试域名来判断网络优先级。'
},
{
'@type': 'Thing',
name: 'IPv6部署',
description: 'IPv6是全球下一代互联网协议标准,相比IPv4具有更大的地址空间、更好的安全性、更高的网络效率。国家正在大力推进IPv6规模部署和应用,以适应未来互联网发展需求。'
}
]
})
}
]
});
const code = `
# 请勿用于商业用途,仅供个人测试学习之用,请遵守中国法律法规
# 查询本机外网 IPv4 地址
curl ${config.v4OnlyAPI}
# 查询本机外网 IPv6 地址
curl ${config.v6OnlyAPI}
# 测试网络是 IPv4 还是 IPv6 访问优先
# (访问 IPv4/IPv6 双栈站点,如果返回 IPv6 地址,则 IPv6 访问优先)
curl ${config.DualStackAPI}
`.trim(); // 关键:去掉首尾多余的空行
const highlightedCode = ref('');
const ipAddress = ref('');
const yourIPv4 = ref('');
const yourIPv6 = ref('');
onMounted(async () => {
try {
highlightedCode.value = await highlightCode(code, 'bash');
} catch {
highlightedCode.value = '';
}
const [dualStack, ipV4, ipV6] = await Promise.allSettled([
$fetch<string>(config.DualStackAPI),
$fetch<string>(config.v4OnlyAPI),
$fetch<string>(config.v6OnlyAPI)
]);
if (dualStack.status === 'fulfilled') {
ipAddress.value = dualStack.value;
}
if (ipV4.status === 'fulfilled') {
yourIPv4.value = ipV4.value;
}
if (ipV6.status === 'fulfilled') {
yourIPv6.value = ipV6.value;
}
});
</script>
<template>
<div class="title">
<header>
<h1>IP查询</h1>
<p>致力于IP查询去中心化,推进 IPv6 规模部署和应用</p>
</header>
</div>
<div class="content">
<div class="one-line">
<b>IPv4</b>&nbsp<p>{{ yourIPv4 }} </p>&nbsp<RouterLink :to="`/location?ip=${yourIPv4}`" target="_blank">查询归属地</RouterLink>
</div>
<div class="one-line">
<b>IPv6</b>&nbsp<p v-if="yourIPv6">{{ yourIPv6 }}</p><RouterLink :to="`/location?ip=${yourIPv6}`" target="_blank" v-if="yourIPv6">&nbsp查询归属地</RouterLink><RouterLink v-else to="/doc/user/enable_ipv6" target="_blank">没有IPv6地址,查看如何开启IPv6</RouterLink>
</div>
<div style="font-size: 1.5em;">
<h3 v-if="(ipAddress && isIPv6(ipAddress)) || isIPv6(yourIPv6)"><el-icon><CircleCheckFilled style="color: lightgreen;"/></el-icon>您的网络IPv6优先</h3>
<h3 v-else-if="(ipAddress && isIPv4(ipAddress)) || isIPv6(yourIPv6)"><el-icon><CircleCloseFilled style="color: red;"/></el-icon>您的网络IPv4优先</h3>
<h3 v-else><el-icon><CircleCloseFilled /></el-icon>查询中,请稍后</h3>
</div>
<blockquote>
手机默认开启 IPv6,宽带开启 IPv6 请参阅<a href="/doc/user/enable_ipv6" target="_blank">文档</a>
</blockquote>
<div v-if="highlightedCode" v-html="highlightedCode" class="code-block"></div>
<div v-else class="code-block code-block--fallback">{{ code }}</div>
</div>
</template>
<style scoped>
@import "../style.css";
.el-menu--horizontal > .el-menu-item:nth-child(1) {
margin-right: auto;
}
.code-block {
margin-top: 1rem;
padding: 1rem;
border-radius: 0.75rem;
overflow-x: auto;
max-width: 100%;
white-space: pre-wrap;
overflow-wrap: break-word;
}
.code-block--fallback {
background: rgb(48, 46, 46);
border: 1px solid rgba(62, 175, 124, 0.18);
font-family: 'JetBrains Mono', 'Fira Code', 'Cascadia Code', 'Consolas', 'Monaco', 'Courier New', monospace !important;
color: rgb(255, 255, 255);
}
@media (max-width: 768px) {
.code-block {
padding: 0.75rem;
font-size: 0.8em;
}
}
</style>
<style>
:root {
--el-color-primary: #3EAF7C;
}
</style>
-396
View File
@@ -1,396 +0,0 @@
<script setup lang="ts">
import { ref, onMounted, computed, nextTick, watch } from 'vue';
import { isIPv6 } from 'is-ip';
import {config} from '../../config/index';
import { useMiddlewareFetch } from '../../utils/middleware';
import { CircleCheckFilled, CircleCloseFilled } from '@element-plus/icons-vue';
import { useRoute } from 'vue-router'
import { highlightCode } from '../../utils/shiki'
import { isIPv4 } from '../../utils/tools';
const route = useRoute();
const loading = ref(false);
useHead({
title: `IPv6/IPv4地址查询工具 | IP归属地定位 | ${config.siteName}`,
meta: [
{ name: 'description', content: '专业的IPv6/IPv4地址查询工具,支持IPv4和IPv6地址归属地查询,提供BiliBili Live、GeoCN、IP2Region、Maxmind等多种数据源对比,精确定位IP地理位置、运营商信息,支持中国大陆及境外地址查询,助力IPv6普及与应用' },
{ property: 'og:title', content: `IPv6/IPv4地址归属地查询工具 - ${config.siteName}` },
{ property: 'og:description', content: '多数据源IP地址归属地查询,支持IPv4和IPv6,提供地理位置、运营商等详细信息' },
{ property: 'og:image', content: config.siteUrl + 'favicon.svg' },
{ property: 'og:type', content: 'website' },
],
});
interface IPLocationType {
bilibili?: any;
ip?: string;
geocn?: any;
ip2region?: any;
maxmind_asn?: any;
maxmind_city?: any;
qqwry?: any;
[key: string]: any;
}
const code = `
# 请勿用于商业用途,仅供个人测试学习之用,请遵守中国法律法规
# 查询本机外网 IPv4 地址
curl ${config.v4OnlyAPI}
# 查询本机外网 IPv6 地址
curl ${config.v6OnlyAPI}
# 测试网络是 IPv4 还是 IPv6 访问优先
# (访问 IPv4/IPv6 双栈站点,如果返回 IPv6 地址,则 IPv6 访问优先)
curl ${config.DualStackAPI}
`.trim();
const html = ref('');
const apiList = config.IPLocationAPIs
const currentApiIndex = ref(0)
const backendID = computed(() => apiList[currentApiIndex.value]!.id)
const ipAddress = ref('');
const IPLocation = ref<IPLocationType>({});
const UserIP = ref('');
const locationUrl = computed(() => "/middleware/" + backendID.value + "/location/" + ipAddress.value);
const { data: locationData, error: locationError, execute: executeLocation } = useMiddlewareFetch<IPLocationType>(locationUrl, {
immediate: false,
watch: false,
});
watch(locationData, (newData) => {
if (newData) {
IPLocation.value = newData;
loading.value = false;
}
});
watch(locationError, (newError) => {
if (newError) {
console.error('Error fetching IP location:', newError);
loading.value = false;
}
});
function locateIP(IP: string){
ipAddress.value = IP;
loading.value = true;
nextTick(() => {
executeLocation();
});
}
onMounted(async () => {
const urlParam = route.query.ip;
await navigateTo({ path: '/location', query: { ip: urlParam } });
html.value = await highlightCode(code, 'bash')
if (urlParam) {
ipAddress.value = urlParam as string;
locateIP(urlParam as string);
}else{
$fetch<string>(config.DualStackAPI).then(
function(data) {
ipAddress.value = data;
UserIP.value = data;
}
).catch(
function(error) {
console.error('Error fetching IP address:', error);
}
);
}
});
</script>
<template>
<div class="title">
<header>
<h1>IPv6/IPv4地址归属地查询</h1>
<p>极简的IPv6地址查询工具,致力于普及 IPv6</p>
</header>
</div>
<div class="content">
<div class="one-line">
<el-input
v-model="ipAddress"
placeholder="请输入IP地址"
/>
<el-button
@click="locateIP(ipAddress)"
type="primary"
:loading="loading"
>
查询
</el-button>
</div>
<div class="location">
<div class="one-line" style="height: 40px;">
<b>IP</b>&nbsp<p>{{ ipAddress }}</p>
</div>
<div v-if="IPLocation" class="result-section">
<table class="result-table">
<tbody>
<tr v-if="IPLocation.bilibili && (IPLocation.bilibili.administrative_area || IPLocation.bilibili.city)">
<td class="table-label">bilibili Live接口</td>
<td class="table-value">
<span>
{{ IPLocation.bilibili?.country }}&nbsp;{{ IPLocation.bilibili?.administrative_area }}&nbsp;{{ IPLocation.bilibili?.city }}
</span>
</td>
<td class="table-value">
<span>
{{ IPLocation.bilibili?.isp }}
</span>
</td>
</tr>
<tr v-if="IPLocation.ip2region && IPLocation.ip2region.split('|').length >= 4">
<td class="table-label">IP2Region</td>
<td class="table-value">{{ IPLocation.ip2region?.split("|")[0] }}&nbsp;{{ IPLocation.ip2region?.split("|")[1] }}&nbsp;{{ IPLocation.ip2region?.split("|")[2] }}</td>
<td class="table-value">{{ IPLocation.ip2region?.split("|")[3] }}</td>
</tr>
<tr v-if="IPLocation.geocn && (IPLocation.geocn.administrative_area || IPLocation.geocn.city || IPLocation.geocn.district)">
<td class="table-label">GeoCN(仅中国大陆)</td>
<td class="table-value">{{ IPLocation.geocn?.administrative_area }}&nbsp;{{ IPLocation.geocn?.city }}&nbsp;{{ IPLocation.geocn?.district }}</td>
<td class="table-value">{{ IPLocation.geocn?.isp }}</td>
</tr>
<tr v-if="IPLocation.maxmind_city && IPLocation.maxmind_asn && (IPLocation.maxmind_city.country || IPLocation.maxmind_city.city)">
<td class="table-label">Maxmind GEOLite2 City</td>
<td class="table-value">{{ IPLocation.maxmind_city?.country }}&nbsp;{{ IPLocation.maxmind_city?.administrative_area }}&nbsp;{{ IPLocation.maxmind_city?.city }}</td>
<td class="table-value">{{ IPLocation.maxmind_asn?.org }}</td>
</tr>
<tr v-if="IPLocation.qqwry && (IPLocation.qqwry.country || IPLocation.qqwry.administrative_area || IPLocation.qqwry.city)">
<td class="table-label">纯真社区库</td>
<td class="table-value">{{ IPLocation.qqwry?.country }}&nbsp;{{ IPLocation.qqwry?.administrative_area }}&nbsp;{{ IPLocation.qqwry?.city }}</td>
<td class="table-value">{{ IPLocation.qqwry?.isp }}</td>
</tr>
<tr v-if="IPLocation.dbip_city && (IPLocation.dbip_city.administrative_area || IPLocation.dbip_city.city)">
<td class="table-label">DB-IP City</td>
<td class="table-value">{{ IPLocation.dbip_city?.country }}&nbsp;{{ IPLocation.dbip_city?.administrative_area }}&nbsp;{{ IPLocation.dbip_city?.city }}</td>
<td class="table-value">{{IPLocation.dbip_asn?.org || '--'}}</td>
</tr>
</tbody>
</table>
</div>
</div>
<div style="font-size: 1.5em;">
<h3 v-if="isIPv6(UserIP)"><el-icon><CircleCheckFilled style="color: lightgreen;"/></el-icon>您的网络IPv6优先</h3>
<h3 v-else-if="isIPv4(UserIP)"><el-icon><CircleCloseFilled style="color: red;"/></el-icon>您的网络IPv4优先</h3>
</div>
<blockquote>
精度参照表:<br>
中国大陆:BiliBili Live > GeoCN > IP2Region > 纯真社区库 > Maxmind GEOLite2 City ≈ DB-IP<br>
中国大陆 IPv6 地址: BiliBili Live > GeoCN > IP2Region > Maxmind GEOLite2 City ≈ DB-IP > 纯真社区库<br>
境外及港澳台地址: Maxmind GEOLite2 City ≈ DB-IP > BiliBili Live > > IP2Region > 纯真社区库<br>
手机默认开启 IPv6,宽带开启 IPv6 请自行搜索<br>
访客IP: {{UserIP}},<p v-if="isIPv4(UserIP)">您的网络IPv4优先</p><p v-else-if="isIPv6(UserIP)">您的网络IPv6优先</p>
</blockquote>
<div v-html="html"></div>
</div>
</template>
<style scoped>
@import "../style.css";
@import "../../assets/css/tool-common.css";
</style>
<style>
@import "../style.css";
:root {
--el-color-primary: #3EAF7C;
}
.el-icon{
font-size: 1.3em;
}
.el-menu--horizontal > .el-menu-item:nth-child(1) {
margin-right: auto;
}
.el-input {
width: 420px;
height: 50px;
font: 1.3em sans-serif;
margin-right: 10px;
}
.el-button {
width: 165px;
height: 50px;
font: 1.3em sans-serif;
}
.result-section {
margin-top: 30px;
}
.result-table {
width: 100%;
border-collapse: collapse;
background: #fff;
border: 1px solid #dcdfe6;
}
html.dark .result-table {
width: 100%;
border-collapse: collapse;
background: #2e2d2d;
border: 1px solid #2e2e2e;
}
.result-table thead tr {
background-color: #c0c4cc;
}
html.dark .result-table thead tr {
background: #2e2d2d;
}
.result-table .table-header {
padding: 12px 15px;
font-weight: 600;
color: #303133;
text-align: left;
border: 1px solid #dcdfe6;
}
html.dark .result-table .table-header {
padding: 12px 15px;
font-weight: 600;
color: #cfcfcf;
text-align: left;
border: 1px solid #1a1919;
}
.result-table tbody tr {
border-bottom: 1px solid #dcdfe6;
}
html.dark .result-table tbody tr {
border-bottom: 1px solid #1a1919;
}
.result-table tbody tr:last-child {
border-bottom: none;
}
.result-table tbody tr:hover {
background-color: #f5f7fa;
}
html.dark .result-table tbody tr:hover {
background-color: #393a3a;
}
.result-table .table-label {
padding: 12px 15px;
font-weight: 600;
color: #606266;
width: 150px;
text-align: left;
border: 1px solid #dcdfe6;
}
html.dark .result-table .table-label {
color: #c0c4cc;
border: 1px solid #1a1919;
}
.result-table .table-value {
padding: 12px 15px;
color: #303133;
border: 1px solid #dcdfe6;
}
html.dark .result-table .table-value {
color: #cfcfcf;
border: 1px solid #1a1919;
}
.valid {
color: #67C23A;
font-weight: 600;
}
.expired {
color: #F56C6C;
font-weight: 600;
}
.status-code {
font-weight: 600;
padding: 4px 12px;
border-radius: 4px;
}
.status-success {
color: #67C23A;
background: #f0f9eb;
}
.status-warning {
color: #E6A23C;
background: #fdf6ec;
}
.status-error {
color: #F56C6C;
background: #fef0f0;
}
.error-message {
margin-top: 20px;
padding: 15px;
background: #fef0f0;
color: #F56C6C;
border-radius: 6px;
text-align: center;
font-size: 1.1em;
}
pre {
background: #f8f9fa;
padding: 15px;
border-radius: 6px;
overflow-x: auto;
white-space: pre;
max-width: 100%;
}
pre code {
font-family: 'JetBrains Mono', 'Fira Code', 'Consolas', monospace;
font-size: 0.9em;
color: #303133;
}
html.dark pre {
background: #303133;
}
html.dark pre code {
font-family: 'JetBrains Mono', 'Fira Code', 'Consolas', monospace;
color: #f8f9fa;
}
.badge-section {
display: flex;
flex-direction: column;
gap: 20px;
margin-top: 20px;
}
.badge-item {
background: #fff;
padding: 20px;
border-radius: 8px;
border: 1px solid #e4e7ed;
}
.badge-item h4 {
margin: 0 0 15px 0;
color: #3EAF7C;
font-size: 1.2em;
}
.badge-item img {
display: block;
margin-bottom: 15px;
max-width: 200px;
}
</style>
-268
View File
@@ -1,268 +0,0 @@
<script setup lang="ts">
import { ref, onMounted, computed, nextTick } from 'vue'
import { useRoute } from 'vue-router'
import { config } from '../../config/index';
import { useMiddlewareFetch } from '../../utils/middleware';
import { extractHost, formatTime, formatSpeed, formatSize, getStatusCodeClass } from '../../utils/tools';
const route = useRoute()
useHead({
title: `IPv6网站测速工具 | 全国多节点并发测速 | ${config.siteName}`,
meta: [
{ name: 'description', content: '专业的IPv6网站测速工具,提供全国多节点并发测速服务,1秒内快速返回网站响应时间、下载速度、页面大小、DNS解析时间、HTTP连接时间等详细信息,支持IPv6网站性能检测与优化,助力IPv6网站性能监控与用户体验提升' },
{ name: 'keywords', content: 'ipv6网站测速,ipv6测速,网站速度测试,ipv6性能检测,网站响应时间,ipv6下载速度,ipv6性能优化,ipv6网站监控' },
{ property: 'og:title', content: 'IPv6网站测速 - 全国多节点并发性能检测工具' },
{ property: 'og:description', content: '全国多节点IPv6网站测速,快速获取响应时间、下载速度等性能数据' },
{ property: 'og:image', content: config.siteUrl + 'favicon.svg' },
{ property: 'og:type', content: 'website' },
],
script: [
{
type: 'application/ld+json',
innerHTML: JSON.stringify({
'@context': 'https://schema.org',
'@type': 'WebApplication',
name: 'IPv6网站性能测速工具',
description: '专业的IPv6网站测速工具,全国多节点并发测速,提供响应时间、下载速度、页面大小、DNS解析时间等详细性能指标。',
url: config.siteUrl + 'ipv6speedtest',
applicationCategory: 'DeveloperApplication',
operatingSystem: 'Web',
offers: {
'@type': 'Offer',
price: '0',
priceCurrency: 'CNY'
},
provider: {
'@type': 'Organization',
name: config.siteName
}
})
}
]
});
const tmpDomain = ref('https://www.zakoflare.com')
const loading = ref(false)
const result = ref<any[]>([])
const error = ref('')
const domain = ref('')
/** 服务器结果接口 */
interface ServerResult {
label: string
loading: boolean
error?: string | Error
data: any
}
/** 预构建的服务器列表,页面挂载即展示,用户可提前看到测速节点 */
const serverResults = ref<ServerResult[]>([])
// 提前构建服务器列表(学 tcping.vue 策略)
// 在组件初始化时就创建好 useFetch 实例,
// 设置 immediate: false + watch: false,
// 只在用户点击"开始测试"时才调用 execute() 发起请求,
// 避免页面加载时自动发请求浪费资源。
// =============================================
/** 双栈服务器:同时支持 IPv4/IPv6 的节点 */
const dualStackFetches = config.SpeedTest.DualStack.map((server) => {
const url = computed(() => "/middleware/" + server.id + "/speed/v6/" + domain.value);
const { data, error: fetchError, execute } = useMiddlewareFetch(url, {
immediate: false,
watch: false,
});
return { label: server.label, data, error: fetchError, execute };
});
/** IPv6 专用服务器:仅支持 IPv6 的节点 */
const ipv6Fetches = config.SpeedTest.IPv6.map((server) => {
const url = computed(() => "/middleware/" + server.id + "/speed/v6/" + domain.value);
const { data, error: fetchError, execute } = useMiddlewareFetch(url, {
immediate: false,
watch: false,
});
return { label: server.label, data, error: fetchError, execute };
});
/** 合并所有服务器 fetch 列表 */
const allFetches = [
...dualStackFetches.map(fetch => ({ ...fetch, type: 'DualStack' })),
...ipv6Fetches.map(fetch => ({ ...fetch, type: 'IPv6' }))
];
/**
* 初始化服务器结果列表
* 页面挂载时调用,让用户提前看到所有测速节点
*/
function initServerResults() {
serverResults.value = allFetches.map((fetch) => ({
label: fetch.label,
loading: false,
error: '',
data: null
}))
}
/**
* 执行全站测速
* 并发调用所有服务器的 execute() 方法,逐个更新对应行的结果
*/
async function SpeedTest() {
domain.value = extractHost(tmpDomain.value)
loading.value = true
error.value = ''
await nextTick()
// 重置所有行状态为加载中
serverResults.value.forEach((row) => {
row.loading = true
row.error = ''
row.data = null
})
// 并发请求所有服务器,每个请求完成后立即更新对应行
const promises = allFetches.map(async (fetch, index) => {
const row = serverResults.value[index]
if (!row) return
try {
await fetch.execute();
row.data = fetch.data.value;
} catch (err: any) {
row.error = err?.data?.error || err?.message || '请求失败';
} finally {
row.loading = false;
}
});
Promise.all(promises).finally(() => {
loading.value = false
})
}
// 页面挂载后,初始化服务器列表并检查 URL 参数
onMounted(() => {
initServerResults()
const urlParam = route.query.site as string
if (urlParam) {
tmpDomain.value = urlParam
SpeedTest()
}
})
</script>
<template>
<div class="title">
<header>
<h1>IPv6 网站测速</h1>
<p>全国并发测速,1s 内快速返回测速结果</p>
</header>
</div>
<div class="content">
<div class="one-line">
<el-input
v-model="tmpDomain"
placeholder="请输入域名(如:https://zakoflare.com)"
/>
<el-button
@click="SpeedTest()"
type="primary"
:loading="loading"
>
开始测试
</el-button>
</div>
<div v-if="error" class="error-message">
{{ error }}
</div>
<div class="result-section">
<table class="result-table">
<thead>
<tr>
<th class="table-header">测速服务器</th>
<th class="table-header">解析 IP</th>
<th class="table-header">HTTP状态码</th>
<th class="table-header">HTTPS状态码</th>
<th class="table-header">总时间</th>
<th class="table-header">解析时间</th>
<th class="table-header">HTTP连接</th>
<th class="table-header">网页大小</th>
<th class="table-header">下载速度</th>
</tr>
</thead>
<tbody>
<template v-for="(row, index) in serverResults" :key="index">
<!-- 请求失败 -->
<tr v-if="row.error">
<td class="table-label">{{ row.label }}</td>
<td class="table-value" colspan="8">
<span class="status-code status-error">{{ row.error }}</span>
</td>
</tr>
<!-- 加载中 -->
<tr v-else-if="row.loading">
<td class="table-label">{{ row.label }}</td>
<td class="table-value" colspan="8">测速中...</td>
</tr>
<!-- 有结果 -->
<tr v-else-if="row.data">
<td class="table-label">{{ row.label }}</td>
<td class="table-value">{{ row.data?.host_record }}</td>
<td class="table-value">
<span class="status-code" :class="getStatusCodeClass(row.data?.http_status_code)">
{{ row.data?.http_status_code }}
</span>
</td>
<td class="table-value">
<span class="status-code" :class="getStatusCodeClass(row.data?.https_status_code)">
{{ row.data?.https_status_code }}
</span>
</td>
<td class="table-value">{{ formatTime(row.data?.total_time) }}</td>
<td class="table-value">{{ formatTime(row.data?.dns_lookup_time) }}</td>
<td class="table-value">{{ formatTime(row.data?.first_byte_time) }}</td>
<td class="table-value">{{ formatSize(row.data?.page_size) }}</td>
<td class="table-value">{{ formatSpeed(row.data?.download_speed) }}</td>
</tr>
<!-- 等待测速 -->
<tr v-else>
<td class="table-label">{{ row.label }}</td>
<td class="table-value" colspan="8">-</td>
</tr>
</template>
</tbody>
</table>
</div>
</div>
</template>
<style scoped>
@import "../style.css";
@import "../../assets/css/tool-common.css";
.el-menu--horizontal > .el-menu-item:nth-child(1) {
margin-right: auto;
}
.result-table .table-header {
font-size: 1.05em;
}
</style>
<style>
:root {
--el-color-primary: #3EAF7C;
}
html.dark {
--el-color-primary: #3EAF7C;
}
.el-icon{
font-size: 1.3em;
}
</style>
-288
View File
@@ -1,288 +0,0 @@
<script setup lang="ts">
import { ref, onMounted, computed, nextTick } from 'vue'
import { useRoute } from 'vue-router'
import { config } from '../../config/index';
import { useMiddlewareFetch } from '../../utils/middleware';
import { isIPv6 } from 'is-ip';
import { extractHost } from '../../utils/tools';
const route = useRoute()
useHead({
title: `IPv6 TCPing测试工具 | IPv6服务器连通性检测 | ${config.siteName}`,
meta: [
{ name: 'description', content: '专业的IPv6 TCPing测试工具,提供多节点IPv6 TCP连通性检测服务,支持自定义端口测试,实时检测IPv6服务器丢包率、平均延迟、最大最小响应时间,助力IPv6网络质量诊断与优化,推进IPv6规模部署和应用' },
{ name: 'keywords', content: 'ipv6 tcping测试,ipv6连通性检测,ipv6服务器延迟,ipv6丢包率,ipv6端口测试,ipv6网络质量,ipv6服务器测试,ipv6网络诊断' },
{ property: 'og:title', content: 'IPv6 TCPing测试 - IPv6服务器连通性与延迟检测' },
{ property: 'og:description', content: '多节点IPv6 TCPing测试,检测服务器连通性、丢包率与响应延迟' },
{ property: 'og:image', content: config.siteUrl + 'favicon.svg' },
{ property: 'og:type', content: 'website' },
],
script: [
{
type: 'application/ld+json',
innerHTML: JSON.stringify({
'@context': 'https://schema.org',
'@type': 'WebApplication',
name: 'IPv6 TCPing连通性测试工具',
description: '专业的IPv6 TCPing测试工具,多节点检测IPv6服务器连通性和延迟,支持自定义端口测试,提供丢包率、平均延迟、最大最小响应时间等数据。',
url: config.siteUrl + 'ipv6tcping',
applicationCategory: 'DeveloperApplication',
operatingSystem: 'Web',
offers: {
'@type': 'Offer',
price: '0',
priceCurrency: 'CNY'
},
provider: {
'@type': 'Organization',
name: config.siteName
}
})
}
]
});
interface TCPingStats {
ip: string
port: string
sent: number
success: number
loss_rate: number
max_rtt: number
min_rtt: number
avg_rtt: number
}
interface TCPingResponse {
ipv4?: TCPingStats
ipv6?: TCPingStats
}
interface ServerConfig {
id: string
label: string
url: string
}
interface ServerResult {
label: string
loading: boolean
error: string
ipv4?: TCPingStats
ipv6?: TCPingStats
}
const tmpDomain = ref('www.zakoflare.com')
const port = ref('80')
const userIP = ref('')
const loading = ref(false)
const serverResults = ref<ServerResult[]>([])
const allServers = [
...config.TCPing.DualStack.map((s: ServerConfig) => ({ ...s, type: 'DualStack' })),
...config.TCPing.IPv6.map((s: ServerConfig) => ({ ...s, type: 'IPv6' }))
];
const tcpingFetches = allServers.map((server) => {
const url = computed(() => "/middleware/" + server.id + "/tcping/" + extractHost(tmpDomain.value) + "?port=" + port.value);
const { data, error: fetchError, execute } = useMiddlewareFetch<TCPingResponse>(url, {
immediate: false,
watch: false,
});
return { label: server.label, data, error: fetchError, execute };
});
function initServerResults() {
const results: ServerResult[] = []
config.TCPing.DualStack.forEach((server: ServerConfig) => {
results.push({
label: server.label,
loading: false,
error: ''
})
})
config.TCPing.IPv6.forEach((server: ServerConfig) => {
results.push({
label: server.label,
loading: false,
error: ''
})
})
serverResults.value = results
}
function TCPingAll() {
const host = extractHost(tmpDomain.value)
if (!host) return
loading.value = true
serverResults.value.forEach((result) => {
result.loading = true
result.error = ''
result.ipv4 = undefined
result.ipv6 = undefined
})
const promises = tcpingFetches.map(async (fetch, index) => {
try {
await fetch.execute();
const result = serverResults.value[index];
if (result) {
result.ipv4 = fetch.data.value?.ipv4;
result.ipv6 = fetch.data.value?.ipv6;
}
} catch (err) {
console.error(err);
const result = serverResults.value[index];
if (result) {
result.error = '请求失败';
}
} finally {
const result = serverResults.value[index];
if (result) {
result.loading = false;
}
}
})
Promise.all(promises).finally(function () {
loading.value = false
})
}
async function getUserIP(){
await $fetch<string>(config.DualStackAPI).then(
function (data){
userIP.value = data
})
return userIP.value
}
onMounted(() => {
getUserIP()
initServerResults()
const urlParam = route.query.site as string
if (urlParam) {
tmpDomain.value = urlParam
TCPingAll()
}
})
</script>
<template>
<div class="title">
<header>
<h1>IPv6 TCPing 测试</h1>
<p>多节点 TCPing 测试,检测服务器连通性和延迟</p>
</header>
</div>
<div class="content">
<div class="one-line">
<el-input
v-model="tmpDomain"
placeholder="请输入域名(如:example.com)"
/>
<el-input
v-model="port"
placeholder="端口号(默认 80)"
style="width: 200px;"
/>
<el-button
@click="TCPingAll()"
type="primary"
:loading="loading"
>
开始测试
</el-button>
</div>
<div class="result-section">
<table class="result-table">
<thead>
<tr>
<th class="table-header">服务器</th>
<th class="table-header">解析 IP</th>
<th class="table-header">发送包</th>
<th class="table-header">接收包</th>
<th class="table-header">丢包率(%)</th>
<th class="table-header">最长时间(ms)</th>
<th class="table-header">最短时间(ms)</th>
<th class="table-header">平均时间(ms)</th>
</tr>
</thead>
<tbody>
<template v-for="(server, index) in serverResults" :key="index">
<tr v-if="server.ipv6">
<td class="table-label">{{ server.label }}</td>
<td class="table-value">{{ server.ipv6.ip || '-' }}</td>
<td class="table-value">{{ server.ipv6.sent }}</td>
<td class="table-value">{{ server.ipv6.success }}</td>
<td class="table-value">
<span :class="server.ipv6.loss_rate > 0 ? 'loss-warning' : 'loss-ok'">
{{ server.ipv6.loss_rate.toFixed(1) }}
</span>
</td>
<td class="table-value">{{ server.ipv6.max_rtt.toFixed(2) }}</td>
<td class="table-value">{{ server.ipv6.min_rtt.toFixed(2) }}</td>
<td class="table-value">{{ server.ipv6.avg_rtt.toFixed(2) }}</td>
</tr>
<tr v-else-if="server.loading">
<td class="table-label">{{ server.label }}</td>
<td class="table-value" colspan="7">加载中...</td>
</tr>
<tr v-else-if="server.error">
<td class="table-label">{{ server.label }}</td>
<td class="table-value error-text" colspan="7">{{ server.error }}</td>
</tr>
<tr v-else>
<td class="table-label">{{ server.label }}</td>
<td class="table-value" colspan="7">-</td>
</tr>
</template>
</tbody>
</table>
</div>
<blockquote>
<a href="/doc/user/ipv6_ping" target="_blank">IPv6 Ping 原理介绍</a><br/>
<strong>注意本页是TCPing,不是ICMPv6 Ping,下列文本仅供参考</strong><br/>
<strong>1. 本地 IPv6 方式</strong><br/>
Windows: ping -6 ipw.wsmdn.top<br/>
macOS 或 Linux: ping6 ipw.wsmdn.top<br/>
<strong>2. 服务器 IPv6 Ping 失败可能原因:</strong><br/>
服务器已开启 IPv6,但防火墙(又名安全组)未对源地址是 IPv6 地址(::/0)的 ICMPv6协议 开放访问,<br/>
服务器未开启 IPv6,请参考 <a href="/doc/server/website_enable_ipv6" target="_blank">服务器开启 IPv6</a><br/>
<a href="/tcping" target="_blank">IPv4 TCPing 测试</a> | <a href="/ipv6speedtest" target="_blank">IPv6 网站测速</a> | <a href="/ipv6webcheck">网站开启IPv6检测</a> | <a href="/dns">DNS解析查询</a> <br/>
访客IP: {{ userIP }},您的网络{{ isIPv6(userIP) ? 'IPv6' : 'IPv4'}}访问优先
</blockquote>
</div>
</template>
<style scoped>
@import "../style.css";
@import "../../assets/css/tool-common.css";
.el-menu--horizontal > .el-menu-item:nth-child(1) {
margin-right: auto;
}
</style>
<style>
:root {
--el-color-primary: #3EAF7C;
}
html.dark {
--el-color-primary: #3EAF7C;
}
.el-icon {
font-size: 1.3em;
}
</style>
-308
View File
@@ -1,308 +0,0 @@
<script setup lang="ts">
import { ref, onMounted, computed, nextTick, watch } from 'vue'
import { useRoute } from 'vue-router'
import { config } from '../../config/index';
import { useMiddlewareFetch } from '../../utils/middleware';
import { CircleCheckFilled, CircleCloseFilled,InfoFilled,Position } from '@element-plus/icons-vue';
import { extractHost, getStatusCodeClass, formatTime, formatSize, formatSpeed } from '../../utils/tools';
const route = useRoute()
const siteUrlWithSlash = computed(() => config.siteUrl.replace(/\/$/, '') + '/')
useHead({
title: `IPv6网站检测工具 | IPv6访问支持检查 | ${config.siteName}`,
meta: [
{ name: 'description', content: '专业的IPv6网站检测工具,全面检查网站是否支持IPv6访问,提供IPv4和IPv6双栈HTTP/HTTPS状态码、DNS解析时间、TCP连接时间、下载速度等详细对比数据,帮助网站管理员确认IPv6部署状态,提供IPv6徽标认证,推进IPv6规模部署和应用' },
{ name: 'keywords', content: 'ipv6网站检测,ipv6访问检测,ipv6支持检查,ipv6双栈检测,ipv6 http检测,ipv6 https检测,ipv6网站认证,ipv6徽标,ipv6部署' },
{ property: 'og:title', content: 'IPv6网站检测 - IPv6访问支持状态检查与认证' },
{ property: 'og:description', content: '全面检测网站IPv6访问支持情况,提供详细性能对比与IPv6徽标认证' },
{ property: 'og:image', content: siteUrlWithSlash.value + 'favicon.svg' },
{ property: 'og:type', content: 'website' },
],
script: [
{
type: 'application/ld+json',
innerHTML: JSON.stringify({
'@context': 'https://schema.org',
'@type': 'WebApplication',
name: 'IPv6网站访问支持检测',
description: '专业的IPv6网站检测工具,检查网站是否支持IPv6访问,提供IPv4和IPv6双栈HTTP/HTTPS状态码、DNS解析时间、TCP连接时间等详细对比数据,提供IPv6徽标认证。',
url: siteUrlWithSlash.value + 'ipv6webcheck',
applicationCategory: 'DeveloperApplication',
operatingSystem: 'Web',
offers: {
'@type': 'Offer',
price: '0',
priceCurrency: 'CNY'
},
provider: {
'@type': 'Organization',
name: config.siteName
}
})
}
]
});
interface PerformanceCheckResponse {
ipv4?: PerformanceCheckItem
ipv6?: PerformanceCheckItem
}
interface PerformanceCheckItem {
host_record: string
http_status_code: number
https_status_code: number
dns_lookup_time: number
tcp_connect_time: number
http_connect_time: number
first_byte_time: number
total_time: number
page_size: number
download_speed: number
is_reachable: boolean
}
const apiList = config.apiBaseUrls
const currentApiIndex = ref(0)
const backendID = computed(() => apiList[currentApiIndex.value]?.id || '')
const tmpDomain = ref('https://www.zakoflare.com')
const testDomain = ref('')
const loading = ref(false)
const error = ref('')
const result = ref<PerformanceCheckResponse | null>(null)
const checkUrl = computed(() => "/middleware/" + backendID.value + "/detail/" + testDomain.value);
const { data: checkData, error: checkError, execute: executeCheck } = useMiddlewareFetch<PerformanceCheckResponse>(checkUrl, {
immediate: false,
watch: false,
});
watch(checkData, (newData) => {
if (newData) {
result.value = newData;
loading.value = false;
error.value = '';
}
});
watch(checkError, async (newError) => {
if (newError) {
console.log(newError);
if (currentApiIndex.value < apiList.length - 1) {
const nextIndex = currentApiIndex.value + 1
error.value = `${(newError as any).message || '请求失败'},正在重试 ${apiList[nextIndex]?.label || ''}...`
currentApiIndex.value = nextIndex
console.log(currentApiIndex.value,nextIndex,apiList)
await nextTick()
executeCheck()
} else {
error.value = '请求失败,请检查域名或网络';
loading.value = false;
}
}
});
function checkWeb() {
testDomain.value = tmpDomain.value
currentApiIndex.value = 0
loading.value = true
error.value = ''
result.value = null
nextTick(() => {
executeCheck();
});
}
onMounted(() => {
const urlParam = route.query.site as string
if (urlParam) {
tmpDomain.value = urlParam
checkWeb()
}
})
</script>
<template>
<div class="title">
<header>
<h1>IPv6网站检测</h1>
<p>检查网站是否开启 IPv6 访问,致力于普及IPv6</p>
</header>
</div>
<div class="content">
<div class="one-line">
<el-input
v-model="tmpDomain"
placeholder="请输入域名(如:https://zakoflare.com)"
/>
<el-button
@click="checkWeb()"
type="primary"
:loading="loading"
>
开始测试
</el-button>
</div>
<div v-if="error" class="error-message">
{{ error }}
</div>
<div v-if="result && result.ipv4" class="result-section">
<table class="result-table">
<thead>
<tr>
<th class="table-header">检测项目</th>
<th class="table-header">IPv4</th>
<th class="table-header">IPv6</th>
</tr>
</thead>
<tbody>
<tr>
<td class="table-label">主机记录</td>
<td class="table-value"><div class="one-line"><RouterLink :to="`/ipv6?ip=${result.ipv4!.host_record}`">{{ result.ipv4!.host_record }}</RouterLink><el-icon><Position /></el-icon></div></td>
<td class="table-value"><div class="one-line"><RouterLink :to="`/ipv6?ip=${result.ipv6?.host_record}`">{{ result.ipv6?.host_record || '-' }}</RouterLink><el-icon><Position /></el-icon></div></td>
</tr>
<tr>
<td class="table-label">HTTP 状态码</td>
<td class="table-value">
<span :class="getStatusCodeClass(result.ipv4!.http_status_code)" class="status-code">
{{ result.ipv4!.http_status_code }}
</span>
</td>
<td class="table-value">
<span v-if="result.ipv6" :class="getStatusCodeClass(result.ipv6.http_status_code)" class="status-code">
{{ result.ipv6.http_status_code }}
</span>
<span v-else>-</span>
</td>
</tr>
<tr>
<td class="table-label">HTTPS 状态码</td>
<td class="table-value">
<span :class="getStatusCodeClass(result.ipv4!.https_status_code)" class="status-code">
{{ result.ipv4!.https_status_code }}
</span>
</td>
<td class="table-value">
<span v-if="result.ipv6" :class="getStatusCodeClass(result.ipv6.https_status_code)" class="status-code">
{{ result.ipv6.https_status_code }}
</span>
<span v-else>-</span>
</td>
</tr>
<tr>
<td class="table-label">DNS 查询耗时</td>
<td class="table-value">{{ formatTime(result.ipv4!.dns_lookup_time) }}</td>
<td class="table-value">{{ result.ipv6?.dns_lookup_time ? formatTime(result.ipv6.dns_lookup_time) : '-' }}</td>
</tr>
<tr>
<td class="table-label">TCP 连接耗时</td>
<td class="table-value">{{ formatTime(result.ipv4!.tcp_connect_time) }}</td>
<td class="table-value">{{ result.ipv6?.tcp_connect_time ? formatTime(result.ipv6.tcp_connect_time) : '-' }}</td>
</tr>
<tr>
<td class="table-label">HTTP 连接耗时</td>
<td class="table-value">{{ formatTime(result.ipv4!.http_connect_time) }}</td>
<td class="table-value">{{ result.ipv6?.http_connect_time ? formatTime(result.ipv6.http_connect_time) : '-' }}</td>
</tr>
<tr>
<td class="table-label">首字节耗时</td>
<td class="table-value">{{ formatTime(result.ipv4!.first_byte_time) }}</td>
<td class="table-value">{{ result.ipv6?.first_byte_time ? formatTime(result.ipv6.first_byte_time) : '-' }}</td>
</tr>
<tr>
<td class="table-label">总耗时</td>
<td class="table-value">{{ formatTime(result.ipv4!.total_time) }}</td>
<td class="table-value">{{ result.ipv6?.total_time ? formatTime(result.ipv6.total_time) : '-' }}</td>
</tr>
<tr>
<td class="table-label">页面大小</td>
<td class="table-value">{{ formatSize(result.ipv4!.page_size) }}</td>
<td class="table-value">{{ result.ipv6?.page_size ? formatSize(result.ipv6.page_size) : '-' }}</td>
</tr>
<tr>
<td class="table-label">下载速度</td>
<td class="table-value">{{ formatSpeed(result.ipv4!.download_speed) }}</td>
<td class="table-value">{{ result.ipv6?.download_speed ? formatSpeed(result.ipv6.download_speed) : '-' }}</td>
</tr>
</tbody>
</table>
</div>
<div v-if="result && result.ipv4 && result.ipv6&& result.ipv4.is_reachable && result.ipv6.is_reachable">
<h3>结论:<el-icon><CircleCheckFilled style="color: lightgreen;"/></el-icon>网站{{ extractHost(testDomain) }} 支持IPv6访问 </h3>
<p><el-icon><InfoFilled style="color: lightgreen;"/></el-icon>请把下方代码贴到网站底部,把这个好消息告诉你的用户,以便用户核验。</p>
<img src="/ipv6-s1.svg"/>
<pre><code>&lt;a href="{{ siteUrlWithSlash }}ipv6webcheck/?site={{ extractHost(testDomain) }}" title="本站支持 IPv6访问" target='_blank'&gt;&lt;img style='display:inline-block;vertical-align:middle' alt="本站支持 IPv6访问" src="{{ siteUrlWithSlash }}ipv6-s1.svg"&gt;&lt;/a&gt;</code></pre>
<img src="/ipv6-s2.svg"/>
<pre><code>&lt;a href="{{ siteUrlWithSlash }}ipv6webcheck/?site={{ extractHost(testDomain) }}" title="本站支持 IPv6访问" target='_blank'&gt;&lt;img style='display:inline-block;vertical-align:middle' alt="本站支持 IPv6访问" src="{{ siteUrlWithSlash }}ipv6-s2.svg"&gt;&lt;/a&gt;</code></pre>
<img src="/ipv6-s3.svg"/>
<pre><code>&lt;a href="{{ siteUrlWithSlash }}ipv6webcheck/?site={{ extractHost(testDomain) }}" title="本站支持 IPv6访问" target='_blank'&gt;&lt;img style='display:inline-block;vertical-align:middle' alt="本站支持 IPv6访问" src="{{ siteUrlWithSlash }}ipv6-s3.svg"&gt;&lt;/a&gt;</code></pre>
<img src="/ipv6-s4.svg"/>
<pre><code>&lt;a href="{{ siteUrlWithSlash }}ipv6webcheck/?site={{ extractHost(testDomain) }}" title="本站支持 IPv6访问" target='_blank'&gt;&lt;img style='display:inline-block;vertical-align:middle' alt="本站支持 IPv6访问" src="{{ siteUrlWithSlash }}ipv6-s4.svg"&gt;&lt;/a&gt;</code></pre>
<img src="/ipv6-s5.svg"/>
<pre><code>&lt;a href="{{ siteUrlWithSlash }}ipv6webcheck/?site={{ extractHost(testDomain) }}" title="本站支持 IPv6访问" target='_blank'&gt;&lt;img style='display:inline-block;vertical-align:middle' alt="本站支持 IPv6访问" src="{{ siteUrlWithSlash }}ipv6-s5.svg"&gt;&lt;/a&gt;</code></pre>
<img src="/ipv6-s6.svg"/>
<pre><code>&lt;a href="{{ siteUrlWithSlash }}ipv6webcheck/?site={{ extractHost(testDomain) }}" title="本站支持 IPv6访问" target='_blank'&gt;&lt;img style='display:inline-block;vertical-align:middle' alt="本站支持 IPv6访问" src="{{ siteUrlWithSlash }}ipv6-s6.svg"&gt;&lt;/a&gt;</code></pre>
<div class="one-line">
<img src="/ipv6-certified-lite-s1.svg"/>
<img src="/ipv6-certified-lite-s2.svg"/>
<img src="/ipv6-certified-lite-s3.svg"/>
<img src="/ipv6-certified-lite-s4.svg"/>
<img src="/ipv6-certified-lite-s5.svg"/>
<img src="/ipv6-certified-lite-s6.svg"/>
</div>
<pre><code>&lt;a href="{{ siteUrlWithSlash }}ipv6webcheck/?site={{ extractHost(testDomain) }}" title="本站支持 IPv6访问" target='_blank'&gt;&lt;img style='display:inline-block;vertical-align:middle' alt="本站支持 IPv6访问" src="{{ siteUrlWithSlash }}ipv6-certified-lite-s1.svg"&gt;&lt;/a&gt;</code></pre>
<div class="one-line">
<img src="/ipv6-certified-s1.svg"/>
<img src="/ipv6-certified-s2.svg"/>
<img src="/ipv6-certified-s3.svg"/>
<img src="/ipv6-certified-s4.svg"/>
<img src="/ipv6-certified-s5.svg"/>
<img src="/ipv6-certified-s6.svg"/>
</div>
<pre><code>&lt;a href="{{ siteUrlWithSlash }}ipv6webcheck/?site={{ extractHost(testDomain) }}" title="本站支持 IPv6访问" target='_blank'&gt;&lt;img style='display:inline-block;vertical-align:middle' alt="本站支持 IPv6访问" src="{{ siteUrlWithSlash }}ipv6-certified-s1.svg"&gt;&lt;/a&gt;</code></pre>
<p>提示:修改IPv6徽标文件名,可修改对应样式</p>
</div>
<div v-else-if="result && result.ipv4 && result.ipv6 && result.ipv4.is_reachable && !result.ipv6.is_reachable">
<h3>结论:<CircleCloseFilled style="width: 1.3em;color: red;"/>网站{{ extractHost(testDomain) }} 不支持IPv6访问 </h3>
<h2>国家正在支持IPv6发展,我建议你赶紧想办法给IPv6适配</h2>
<el-image src="/jingya.jpg"></el-image>
</div>
<div v-else-if="result && (!result.ipv6?.is_reachable && !result.ipv4?.is_reachable)">
<h3>结论:<CircleCloseFilled style="width: 1.3em;color: red;"/>网站{{ extractHost(testDomain) }} 不可达 </h3>
<h2>...</h2>
<el-image src="/jingya.jpg"></el-image>
</div>
<blockquote>
网站不支持 IPv6可能原因:<br/>
<br/>
1. 网站所在服务器未开启 IPv6,请参考 <a href="/doc/server/website_enable_ipv6" target="_blank">网站开启 IPv6 的三种方式</a><br/>
2. 网站所在服务器已开启 IPv6,但防火墙未对源地址是 IPv6 地址(::/0)的 443(HTTPS)端口开放访问<br/>
3. 网站所在服务器已开启 IPv6,但未开启SSL证书,请参考 <a href="/doc/server/nginx_ipv6" target="_blank">Nginx 开启 IPv6 SSL</a><br/>
</blockquote>
</div>
</template>
<style scoped>
@import "../style.css";
@import "../../assets/css/tool-common.css";
.el-menu--horizontal > .el-menu-item:nth-child(1) {
margin-right: auto;
}
</style>
<style>
:root {
--el-color-primary: #3EAF7C;
}
html.dark {
--el-color-primary: #3EAF7C;
}
.el-icon{
font-size: 1.3em;
}
</style>
-434
View File
@@ -1,434 +0,0 @@
<script setup lang="ts">
import { ref, onMounted, computed, nextTick, watch } from 'vue';
import { isIPv6 } from 'is-ip';
import {config} from '../../config/index';
import { useMiddlewareFetch } from '../../utils/middleware';
import { CircleCheckFilled, CircleCloseFilled } from '@element-plus/icons-vue';
import { useRoute } from 'vue-router'
import { highlightCode } from '../../utils/shiki'
import { isIPv4 } from '../../utils/tools';
const route = useRoute();
const loading = ref(false);
useHead({
title: `IPv6/IPv4地址查询工具 | IP归属地定位 | ${config.siteName}`,
meta: [
{ name: 'description', content: '专业的IPv6/IPv4地址查询工具,支持IPv4和IPv6地址归属地查询,提供BiliBili Live、GeoCN、IP2Region、Maxmind等多种数据源对比,精确定位IP地理位置、运营商信息,支持中国大陆及境外地址查询,助力IPv6普及与应用' },
{ name: 'keywords', content: 'ipv6地址查询,ipv4地址查询,ip归属地,ip地理位置,ip定位,运营商查询,ipv6归属地,ipv4归属地,maxmind,ip2region,geocn' },
{ property: 'og:title', content: `IPv6/IPv4地址归属地查询工具 - ${config.siteName}` },
{ property: 'og:description', content: '多数据源IP地址归属地查询,支持IPv4和IPv6,提供地理位置、运营商等详细信息' },
{ property: 'og:image', content: config.siteUrl + 'favicon.svg' },
{ property: 'og:type', content: 'website' },
],
script: [
{
type: 'application/ld+json',
innerHTML: JSON.stringify({
'@context': 'https://schema.org',
'@type': 'WebApplication',
name: 'IPv6/IPv4地址归属地查询工具',
description: '专业的IPv6/IPv4地址归属地查询工具,支持BiliBili Live、GeoCN、IP2Region、Maxmind、纯真社区库等多数据源对比,精确定位IP地理位置、运营商信息。',
url: config.siteUrl + 'ipv6',
applicationCategory: 'DeveloperApplication',
operatingSystem: 'Web',
offers: {
'@type': 'Offer',
price: '0',
priceCurrency: 'CNY'
},
provider: {
'@type': 'Organization',
name: config.siteName
}
})
}
]
});
interface IPLocationType {
bilibili?: any;
ip?: string;
geocn?: any;
ip2region?: any;
maxmind_asn?: any;
maxmind_city?: any;
qqwry?: any;
[key: string]: any;
}
const code = `
# 请勿用于商业用途,仅供个人测试学习之用,请遵守中国法律法规
# 查询本机外网 IPv4 地址
curl ${config.v4OnlyAPI}
# 查询本机外网 IPv6 地址
curl ${config.v6OnlyAPI}
# 测试网络是 IPv4 还是 IPv6 访问优先
# (访问 IPv4/IPv6 双栈站点,如果返回 IPv6 地址,则 IPv6 访问优先)
curl ${config.DualStackAPI}
`.trim();
const html = ref('');
const apiList = config.IPLocationAPIs
const currentApiIndex = ref(0)
const backendID = computed(() => apiList[currentApiIndex.value]?.id || '')
const ipAddress = ref('');
const IPLocation = ref<IPLocationType>({});
const UserIP = ref('');
const error = ref('')
const locationUrl = computed(() => "/middleware/" + backendID.value + "/location/" + ipAddress.value);
const { data: locationData, error: locationError, execute: executeLocation } = useMiddlewareFetch<IPLocationType>(locationUrl, {
immediate: false,
watch: false,
});
watch(locationData, (newData) => {
if (newData) {
IPLocation.value = newData;
loading.value = false;
error.value = '';
}
});
watch(locationError, async (newError) => {
if (newError) {
console.error('Error fetching IP location:', newError);
if (currentApiIndex.value < apiList.length - 1) {
const nextIndex = currentApiIndex.value + 1
error.value = `${(newError as any).message || '请求失败'},正在重试 ${apiList[nextIndex]?.label || ''}...`
currentApiIndex.value = nextIndex
await nextTick()
executeLocation()
} else {
error.value = '请求失败,请检查网络或稍后重试'
loading.value = false;
}
}
});
function locateIP(IP: string){
currentApiIndex.value = 0
ipAddress.value = IP;
error.value = ''
loading.value = true;
nextTick(() => {
executeLocation();
});
}
onMounted(async () => {
html.value = await highlightCode(code, 'bash')
const urlParam = route.query.ip;
if (urlParam) {
ipAddress.value = urlParam as string;
locateIP(urlParam as string);
}else{
$fetch<string>(config.DualStackAPI).then(
function(data) {
ipAddress.value = data;
UserIP.value = data;
}
).catch(
function(error) {
console.error('Error fetching IP address:', error);
}
);
}
});
</script>
<template>
<div class="title">
<header>
<h1>IPv6/IPv4地址归属地查询</h1>
<p>极简的IPv6地址查询工具,致力于普及 IPv6</p>
</header>
</div>
<div class="content">
<div class="one-line">
<el-input
v-model="ipAddress"
placeholder="请输入IP地址"
/>
<el-button
@click="locateIP(ipAddress)"
type="primary"
:loading="loading"
>
查询
</el-button>
</div>
<div v-if="error" class="error-message">
{{ error }}
</div>
<div class="location">
<div class="ip-info" style="height: 40px;">
<b>IP</b>&nbsp;<span>{{ ipAddress }}</span>
</div>
<div v-if="IPLocation" class="result-section">
<table class="result-table">
<tbody>
<tr v-if="IPLocation.bilibili && (IPLocation.bilibili.administrative_area || IPLocation.bilibili.city)">
<td class="table-label">bilibili Live接口</td>
<td class="table-value">
<span>
{{ IPLocation.bilibili?.country }}&nbsp;{{ IPLocation.bilibili?.administrative_area }}&nbsp;{{ IPLocation.bilibili?.city }}
</span>
</td>
<td class="table-value">
<span>
{{ IPLocation.bilibili?.isp }}
</span>
</td>
</tr>
<tr v-if="IPLocation.ip2region && IPLocation.ip2region.split('|').length >= 4">
<td class="table-label">IP2Region</td>
<td class="table-value">{{ IPLocation.ip2region?.split("|")[0] }}&nbsp;{{ IPLocation.ip2region?.split("|")[1] }}&nbsp;{{ IPLocation.ip2region?.split("|")[2] }}</td>
<td class="table-value">{{ IPLocation.ip2region?.split("|")[3] }}</td>
</tr>
<tr v-if="IPLocation.geocn && (IPLocation.geocn.administrative_area || IPLocation.geocn.city || IPLocation.geocn.district)">
<td class="table-label">GeoCN(仅中国大陆)</td>
<td class="table-value">{{ IPLocation.geocn?.administrative_area }}&nbsp;{{ IPLocation.geocn?.city }}&nbsp;{{ IPLocation.geocn?.district }}</td>
<td class="table-value">{{ IPLocation.geocn?.isp }}&nbsp;{{IPLocation.geocn?.type}}</td>
</tr>
<tr v-if="IPLocation.maxmind_city && IPLocation.maxmind_asn && (IPLocation.maxmind_city.country || IPLocation.maxmind_city.city)">
<td class="table-label">Maxmind GEOLite2 City</td>
<td class="table-value">{{ IPLocation.maxmind_city?.country }}&nbsp;{{ IPLocation.maxmind_city?.administrative_area }}&nbsp;{{ IPLocation.maxmind_city?.city }}</td>
<td class="table-value">{{ IPLocation.maxmind_asn?.org }}</td>
</tr>
<tr v-if="IPLocation.qqwry && (IPLocation.qqwry.country || IPLocation.qqwry.administrative_area || IPLocation.qqwry.city)">
<td class="table-label">纯真社区库</td>
<td class="table-value">{{ IPLocation.qqwry?.country }}&nbsp;{{ IPLocation.qqwry?.administrative_area }}&nbsp;{{ IPLocation.qqwry?.city }}</td>
<td class="table-value">{{ IPLocation.qqwry?.isp }}</td>
</tr>
<tr v-if="IPLocation.dbip_city && (IPLocation.dbip_city.administrative_area || IPLocation.dbip_city.city)">
<td class="table-label">DB-IP City</td>
<td class="table-value">{{ IPLocation.dbip_city?.country }}&nbsp;{{ IPLocation.dbip_city?.administrative_area }}&nbsp;{{ IPLocation.dbip_city?.city }}</td>
<td class="table-value">{{IPLocation.dbip_asn?.org}}</td>
</tr>
</tbody>
</table>
</div>
</div>
<div style="font-size: 1.5em;">
<h3 v-if="isIPv6(UserIP)"><el-icon><CircleCheckFilled style="color: lightgreen;"/></el-icon>您的网络IPv6优先</h3>
<h3 v-else-if="isIPv4(UserIP)"><el-icon><CircleCloseFilled style="color: red;"/></el-icon>您的网络IPv4优先</h3>
</div>
<blockquote>
精度参照表:<br>
中国大陆:BiliBili Live > GeoCN > IP2Region > 纯真社区库 > Maxmind GEOLite2 City ≈ DB-IP<br>
中国大陆 IPv6 地址: BiliBili Live > GeoCN > IP2Region > Maxmind GEOLite2 City ≈ DB-IP > 纯真社区库<br>
境外及港澳台地址: Maxmind GEOLite2 City ≈ DB-IP > BiliBili Live > > IP2Region > 纯真社区库<br>
手机默认开启 IPv6,宽带开启 IPv6 请自行搜索<br>
访客IP: {{UserIP}},<span v-if="isIPv4(UserIP)">您的网络IPv4优先</span><span v-else-if="isIPv6(UserIP)">您的网络IPv6优先</span>
</blockquote>
<div v-html="html"></div>
</div>
</template>
<style scoped>
@import "../style.css";
@import "../../assets/css/tool-common.css";
</style>
<style>
@import "../style.css";
:root {
--el-color-primary: #3EAF7C;
}
.el-icon{
font-size: 1.3em;
}
.el-menu--horizontal > .el-menu-item:nth-child(1) {
margin-right: auto;
}
.el-input {
width: 420px;
height: 50px;
font: 1.3em sans-serif;
margin-right: 10px;
}
.el-button {
width: 165px;
height: 50px;
font: 1.3em sans-serif;
}
.result-section {
margin-top: 30px;
}
.result-table {
width: 100%;
border-collapse: collapse;
background: #fff;
border: 1px solid #dcdfe6;
}
html.dark .result-table {
width: 100%;
border-collapse: collapse;
background: #2e2d2d;
border: 1px solid #2e2e2e;
}
.result-table thead tr {
background-color: #c0c4cc;
}
html.dark .result-table thead tr {
background: #2e2d2d;
}
.result-table .table-header {
padding: 12px 15px;
font-weight: 600;
color: #303133;
text-align: left;
border: 1px solid #dcdfe6;
}
html.dark .result-table .table-header {
padding: 12px 15px;
font-weight: 600;
color: #cfcfcf;
text-align: left;
border: 1px solid #1a1919;
}
.result-table tbody tr {
border-bottom: 1px solid #dcdfe6;
}
html.dark .result-table tbody tr {
border-bottom: 1px solid #1a1919;
}
.result-table tbody tr:last-child {
border-bottom: none;
}
.result-table tbody tr:hover {
background-color: #f5f7fa;
}
html.dark .result-table tbody tr:hover {
background-color: #393a3a;
}
.result-table .table-label {
padding: 12px 15px;
font-weight: 600;
color: #606266;
width: 150px;
text-align: left;
border: 1px solid #dcdfe6;
}
html.dark .result-table .table-label {
color: #c0c4cc;
border: 1px solid #1a1919;
}
.result-table .table-value {
padding: 12px 15px;
color: #303133;
border: 1px solid #dcdfe6;
}
html.dark .result-table .table-value {
color: #cfcfcf;
border: 1px solid #1a1919;
}
.valid {
color: #67C23A;
font-weight: 600;
}
.expired {
color: #F56C6C;
font-weight: 600;
}
.status-code {
font-weight: 600;
padding: 4px 12px;
border-radius: 4px;
}
.status-success {
color: #67C23A;
background: #f0f9eb;
}
.status-warning {
color: #E6A23C;
background: #fdf6ec;
}
.status-error {
color: #F56C6C;
background: #fef0f0;
}
.error-message {
margin-top: 20px;
padding: 15px;
background: #fef0f0;
color: #F56C6C;
border-radius: 6px;
text-align: center;
font-size: 1.1em;
}
pre {
background: #f8f9fa;
padding: 15px;
border-radius: 6px;
overflow-x: auto;
white-space: pre;
max-width: 100%;
}
pre code {
font-family: 'JetBrains Mono', 'Fira Code', 'Consolas', monospace;
font-size: 0.9em;
color: #303133;
}
html.dark pre {
background: #303133;
}
html.dark pre code {
font-family: 'JetBrains Mono', 'Fira Code', 'Consolas', monospace;
color: #f8f9fa;
}
.badge-section {
display: flex;
flex-direction: column;
gap: 20px;
margin-top: 20px;
}
.badge-item {
background: #fff;
padding: 20px;
border-radius: 8px;
border: 1px solid #e4e7ed;
}
.badge-item h4 {
margin: 0 0 15px 0;
color: #3EAF7C;
font-size: 1.2em;
}
.badge-item img {
display: block;
margin-bottom: 15px;
max-width: 200px;
}
</style>
-292
View File
@@ -1,292 +0,0 @@
<script setup lang="ts">
import { ref, onMounted, nextTick } from 'vue'
import { useRoute } from 'vue-router'
import { config } from '../../config/index'
import { Loading } from '@element-plus/icons-vue'
import { extractHost } from '../../utils/tools'
import { isIPv6 } from 'is-ip'
const route = useRoute()
const url = ref('')
useHead({
title: '网站截图工具 - '+(route.query.site || config.siteName),
meta: [
{ name: 'description', content: '在线网站截图工具,输入网址即可获取网页快照,支持所有公开网站的实时截图,方便查看网站布局和设计效果' },
{ name: 'keywords', content: '网站截图,网页快照,在线截图,网页预览,网站布局查看,截图工具' },
{ property: 'og:title', content: '网站截图工具 - 在线网页快照' },
{ property: 'og:description', content: '输入网址即可获取网页快照,查看网站布局和设计效果' },
{ property: 'og:image', content: config.siteUrl + 'favicon.svg' },
{ property: 'og:type', content: 'website' },
]
})
const tmpDomain = ref('https://www.zakoflare.com')
const loading = ref(false)
const error = ref('')
const screenshotUrl = ref('')
const imgLoaded = ref(false)
let detectionImg: HTMLImageElement | null = null
let detectionTimeout: ReturnType<typeof setTimeout> | null = null
let currentRequestId = 0
const userIP = ref('')
// WordPress mshots default error image dimensions
const DEFAULT_MSHOTS_WIDTH = 400
const DEFAULT_MSHOTS_HEIGHT = 300
const LOADING_TIMEOUT = 15000 // 15 seconds
async function getUserIP(){
await $fetch<string>(config.DualStackAPI).then(
function (data){
userIP.value = data
})
return userIP.value
}
function cleanupDetection() {
if (detectionImg) {
detectionImg.onload = null
detectionImg.onerror = null
detectionImg = null
}
if (detectionTimeout) {
clearTimeout(detectionTimeout)
detectionTimeout = null
}
}
function showError(message: string) {
cleanupDetection()
error.value = message
loading.value = false
screenshotUrl.value = ''
imgLoaded.value = false
}
function takeScreenshot() {
url.value = tmpDomain.value.trim()
if (!url.value) {
error.value = '请输入网址'
return
}
if (!url.value.startsWith('http://') && !url.value.startsWith('https://')) {
url.value = 'https://' + url.value
}
cleanupDetection()
error.value = ''
imgLoaded.value = false
loading.value = true
screenshotUrl.value = ''
const requestId = ++currentRequestId
// Set the screenshot URL on next tick so Vue can update the displayed <img>
nextTick(() => {
// Check if this request was superseded
if (requestId !== currentRequestId) return
screenshotUrl.value = `https://s0.wp.com/mshots/v1/${encodeURIComponent(url.value)}`
// Create a hidden detection image to check for default WordPress error image
// The mshots API returns a 302 redirect to a default image (1200x900) when
// the target site is unreachable or generation times out.
detectionImg = new Image()
detectionImg.onload = () => {
if (requestId !== currentRequestId) return
// 307 Temporary Redirect to mshots default — target is still generating
if (detectionImg!.currentSrc?.endsWith('/mshots/v1/default')) {
cleanupDetection()
loading.value = false
error.value = '正在生成截图中,请稍后...'
return
}
// WordPress mshots default error images are exactly 1200x900.
// Real website screenshots can be any dimensions.
if (
detectionImg!.naturalWidth === DEFAULT_MSHOTS_WIDTH &&
detectionImg!.naturalHeight === DEFAULT_MSHOTS_HEIGHT
) {
showError('截图加载失败,可能是目标站点无法访问或生成超时')
return
}
// Passed detection - screenshot is valid
cleanupDetection()
imgLoaded.value = true
loading.value = false
}
detectionImg.onerror = () => {
if (requestId !== currentRequestId) return
showError('截图加载失败,请检查网址是否正确')
}
detectionImg.src = screenshotUrl.value
// Timeout: if detection doesn't complete within LOADING_TIMEOUT ms
detectionTimeout = setTimeout(() => {
if (requestId !== currentRequestId) return
showError('截图加载失败,可能是目标站点无法访问或生成超时')
}, LOADING_TIMEOUT)
})
}
// The displayed <img> may or may not fire its own load event depending on
// browser behavior with 302 redirects. The hidden detection image above
// handles the primary success/failure determination.
function onImageLoad() {
if (detectionImg && detectionImg.currentSrc?.endsWith('/mshots/v1/default')) {
error.value = '正在生成截图中,请稍后...'
loading.value = false
}
}
function onImageError() {
showError('截图加载失败,请检查网址是否正确')
}
onMounted(() => {
const urlParam = route.query.site as string
if (urlParam) {
tmpDomain.value = urlParam
takeScreenshot()
}
getUserIP()
})
const { data: page } = await useAsyncData('screenshot-doc', () =>
$fetch('/api/markdown/screenshot')
)
const doc = page.value
</script>
<template>
<div class="title">
<header>
<h1>网站截图</h1>
<p>输入网址,自动截图网站页面</p>
</header>
</div>
<div class="content">
<div class="one-line">
<el-input
v-model="tmpDomain"
placeholder="请输入网址(如:https://example.com)"
@keyup.enter="takeScreenshot"
/>
<el-button
@click="takeScreenshot"
type="primary"
:loading="loading"
>
网站截图
</el-button>
</div>
<div v-if="error" class="error-message">
{{ error }}
</div>
<div v-if="screenshotUrl" class="result-section">
<div class="screenshot-container">
<div v-if="loading && !imgLoaded" class="loading-overlay">
<el-icon class="is-loading" :size="40">
<Loading />
</el-icon>
<p>正在截图中...</p>
</div>
<img
:src="screenshotUrl"
alt="网站截图"
class="screenshot-img"
:class="{ 'img-loaded': imgLoaded }"
@load="onImageLoad"
@error="onImageError"
/>
</div>
</div>
<div v-if="doc" class="markdown">
<div v-html="doc"></div>
</div>
<blockquote>
访客IP: {{userIP }},您的网络 {{ isIPv6(userIP) ? 'IPv6' : 'IPv4'}} 访问优先<br/>
</blockquote>
</div>
</template>
<style scoped>
@import "../style.css";
@import "../../assets/css/tool-common.css";
.screenshot-container {
position: relative;
display: inline-block;
width: 100%;
max-width: 100%;
overflow: hidden;
border-radius: 8px;
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.1);
background: #f5f5f5;
min-height: 200px;
}
.screenshot-img {
display: block;
max-width: 100%;
height: auto;
opacity: 0;
transition: opacity 0.3s ease;
}
.screenshot-img.img-loaded {
opacity: 1;
}
html.dark .screenshot-container {
background: #1a1919;
}
.loading-overlay {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
background: #f5f5f5;
z-index: 1;
}
html.dark .loading-overlay {
background: #1a1919;
color: #adbac7;
}
.loading-overlay p {
margin-top: 12px;
color: #606266;
font-size: 1.1em;
}
html.dark .loading-overlay p {
color: #c0c4cc;
}
.markdown :deep(a) {
color: #3EAF7C !important;
font-size: 1.3em;
text-decoration: none
}
</style>
-268
View File
@@ -1,268 +0,0 @@
<script setup lang="ts">
import { ref, onMounted, computed, nextTick } from 'vue'
import { useRoute } from 'vue-router'
import { config } from '../../config/index';
import { useMiddlewareFetch } from '../../utils/middleware';
import { extractHost, formatTime, formatSpeed, formatSize, getStatusCodeClass } from '../../utils/tools';
const route = useRoute()
useHead({
title: `IPv4网站测速工具 | 全国多节点并发测速 | ${config.siteName}`,
meta: [
{ name: 'description', content: '专业的IPv4网站测速工具,提供全国多节点并发测速服务,快速返回网站响应时间、下载速度、页面大小、DNS解析时间、HTTP连接时间等详细性能指标,支持IPv4网站性能检测与优化,助力网站性能监控与用户体验改善' },
{ name: 'keywords', content: 'ipv4网站测速,ipv4测速,网站速度测试,ipv4性能检测,网站响应时间,ipv4下载速度,ipv4性能优化,网站性能监控' },
{ property: 'og:title', content: 'IPv4网站测速 - 全国多节点并发性能检测工具' },
{ property: 'og:description', content: '全国多节点IPv4网站测速,快速获取响应时间、下载速度等性能数据' },
{ property: 'og:image', content: config.siteUrl + 'favicon.svg' },
{ property: 'og:type', content: 'website' },
],
script: [
{
type: 'application/ld+json',
innerHTML: JSON.stringify({
'@context': 'https://schema.org',
'@type': 'WebApplication',
name: 'IPv4网站性能测速工具',
description: '专业的IPv4网站测速工具,全国多节点并发测速,提供响应时间、下载速度、页面大小、DNS解析时间等详细性能指标。',
url: config.siteUrl + 'speedtest',
applicationCategory: 'DeveloperApplication',
operatingSystem: 'Web',
offers: {
'@type': 'Offer',
price: '0',
priceCurrency: 'CNY'
},
provider: {
'@type': 'Organization',
name: config.siteName
}
})
}
]
});
const tmpDomain = ref('https://www.zakoflare.com')
const loading = ref(false)
const result = ref<any[]>([])
const error = ref('')
const domain = ref('')
/** 服务器结果接口 */
interface ServerResult {
label: string
loading: boolean
error?: string | Error
data: any
}
/** 预构建的服务器列表,页面挂载即展示,用户可提前看到测速节点 */
const serverResults = ref<ServerResult[]>([])
// 提前构建服务器列表(学 tcping.vue 策略)
// 在组件初始化时就创建好 useFetch 实例,
// 设置 immediate: false + watch: false,
// 只在用户点击"开始测试"时才调用 execute() 发起请求,
// 避免页面加载时自动发请求浪费资源。
// =============================================
/** 双栈服务器:同时支持 IPv4/IPv6 的节点 */
const dualStackFetches = config.SpeedTest.DualStack.map((server) => {
const url = computed(() => "/middleware/" + server.id + "/speed/v4/" + domain.value);
const { data, error: fetchError, execute } = useMiddlewareFetch(url, {
immediate: false,
watch: false,
});
return { label: server.label, data, error: fetchError, execute };
});
/** IPv4 专用服务器:仅支持 IPv4 的节点 */
const ipv4Fetches = config.SpeedTest.IPv4.map((server) => {
const url = computed(() => "/middleware/" + server.id + "/speed/v4/" + domain.value);
const { data, error: fetchError, execute } = useMiddlewareFetch(url, {
immediate: false,
watch: false,
});
return { label: server.label, data, error: fetchError, execute };
});
/** 合并所有服务器 fetch 列表 */
const allFetches = [
...dualStackFetches.map(fetch => ({ ...fetch, type: 'DualStack' })),
...ipv4Fetches.map(fetch => ({ ...fetch, type: 'IPv4' }))
];
/**
* 初始化服务器结果列表
* 页面挂载时调用,让用户提前看到所有测速节点
*/
function initServerResults() {
serverResults.value = allFetches.map((fetch) => ({
label: fetch.label,
loading: false,
error: '',
data: null
}))
}
/**
* 执行全站测速
* 并发调用所有服务器的 execute() 方法,逐个更新对应行的结果
*/
async function SpeedTest() {
domain.value = extractHost(tmpDomain.value)
loading.value = true
error.value = ''
await nextTick()
// 重置所有行状态为加载中
serverResults.value.forEach((row) => {
row.loading = true
row.error = ''
row.data = null
})
// 并发请求所有服务器,每个请求完成后立即更新对应行
const promises = allFetches.map(async (fetch, index) => {
const row = serverResults.value[index]
if (!row) return
try {
await fetch.execute()
row.data = fetch.data.value
} catch (err: any) {
row.error = err?.data?.error || err?.message || '请求失败'
} finally {
row.loading = false
}
})
Promise.all(promises).finally(() => {
loading.value = false
})
}
// 页面挂载后,初始化服务器列表并检查 URL 参数
onMounted(() => {
initServerResults()
const urlParam = route.query.site as string
if (urlParam) {
tmpDomain.value = urlParam
SpeedTest()
}
})
</script>
<template>
<div class="title">
<header>
<h1>IPv4 网站测速</h1>
<p>全国并发测速,1s 内快速返回测速结果</p>
</header>
</div>
<div class="content">
<div class="one-line">
<el-input
v-model="tmpDomain"
placeholder="请输入域名(如:https://zakoflare.com)"
/>
<el-button
@click="SpeedTest()"
type="primary"
:loading="loading"
>
开始测试
</el-button>
</div>
<div v-if="error" class="error-message">
{{ error }}
</div>
<div class="result-section">
<table class="result-table">
<thead>
<tr>
<th class="table-header">测速服务器</th>
<th class="table-header">解析 IP</th>
<th class="table-header">HTTP状态码</th>
<th class="table-header">HTTPS状态码</th>
<th class="table-header">总时间</th>
<th class="table-header">解析时间</th>
<th class="table-header">HTTP连接</th>
<th class="table-header">网页大小</th>
<th class="table-header">下载速度</th>
</tr>
</thead>
<tbody>
<template v-for="(row, index) in serverResults" :key="index">
<!-- 请求失败 -->
<tr v-if="row.error">
<td class="table-label">{{ row.label }}</td>
<td class="table-value" colspan="8">
<span class="status-code status-error">{{ row.error }}</span>
</td>
</tr>
<!-- 加载中 -->
<tr v-else-if="row.loading">
<td class="table-label">{{ row.label }}</td>
<td class="table-value" colspan="8">测速中...</td>
</tr>
<!-- 有结果 -->
<tr v-else-if="row.data">
<td class="table-label">{{ row.label }}</td>
<td class="table-value">{{ row.data?.host_record }}</td>
<td class="table-value">
<span class="status-code" :class="getStatusCodeClass(row.data?.http_status_code)">
{{ row.data?.http_status_code }}
</span>
</td>
<td class="table-value">
<span class="status-code" :class="getStatusCodeClass(row.data?.https_status_code)">
{{ row.data?.https_status_code }}
</span>
</td>
<td class="table-value">{{ formatTime(row.data?.total_time) }}</td>
<td class="table-value">{{ formatTime(row.data?.dns_lookup_time) }}</td>
<td class="table-value">{{ formatTime(row.data?.first_byte_time) }}</td>
<td class="table-value">{{ formatSize(row.data?.page_size) }}</td>
<td class="table-value">{{ formatSpeed(row.data?.download_speed) }}</td>
</tr>
<!-- 等待测速 -->
<tr v-else>
<td class="table-label">{{ row.label }}</td>
<td class="table-value" colspan="8">-</td>
</tr>
</template>
</tbody>
</table>
</div>
</div>
</template>
<style scoped>
@import "../style.css";
@import "../../assets/css/tool-common.css";
.el-menu--horizontal > .el-menu-item:nth-child(1) {
margin-right: auto;
}
.result-table .table-header {
font-size: 1.05em;
}
</style>
<style>
:root {
--el-color-primary: #3EAF7C;
}
html.dark {
--el-color-primary: #3EAF7C;
}
.el-icon{
font-size: 1.3em;
}
</style>
-397
View File
@@ -1,397 +0,0 @@
<script setup lang="ts">
import { ref, onMounted, computed, nextTick, watch } from 'vue'
import { useRoute } from 'vue-router'
import { CircleCheckFilled, CircleCloseFilled,InfoFilled } from '@element-plus/icons-vue';
import { config } from '../../config/index';
import { useMiddlewareFetch } from '../../utils/middleware';
import { extractHost, getStatusCodeClass, formatTime, formatSpeed } from '../../utils/tools';
const route = useRoute()
const siteUrlWithSlash = computed(() => config.siteUrl.replace(/\/$/, '') + '/')
useHead({
title: `SSL证书检测工具 | IPv4/IPv6证书检查 | ${config.siteName}`,
meta: [
{ name: 'description', content: '专业的SSL证书检测工具,全面检查网站的IPv4和IPv6 SSL证书状态、有效期、签发机构、HTTP版本等信息,支持HTTPS状态码检测、下载速度测试,帮助网站管理员及时发现证书问题,确保网站安全访问' },
{ name: 'keywords', content: 'ssl证书检测,ssl检查,https证书,ipv6 ssl,ipv4 ssl,证书有效期,ssl状态,https检测,网站安全,证书签发机构' },
{ property: 'og:title', content: 'SSL证书检测 - IPv4/IPv6双栈证书状态检查工具' },
{ property: 'og:description', content: '全面检测网站SSL证书状态,支持IPv4和IPv6双栈检测,提供证书有效期、签发机构等详细信息' },
{ property: 'og:image', content: siteUrlWithSlash.value + 'favicon.svg' },
{ property: 'og:type', content: 'website' },
],
script: [
{
type: 'application/ld+json',
innerHTML: JSON.stringify({
'@context': 'https://schema.org',
'@type': 'WebApplication',
name: 'SSL证书检测工具',
description: '专业的SSL证书检测工具,支持IPv4和IPv6 SSL证书状态、有效期、签发机构、HTTP版本等检测,提供HTTPS状态码检测、下载速度测试。',
url: siteUrlWithSlash.value + 'ssl',
applicationCategory: 'DeveloperApplication',
operatingSystem: 'Web',
offers: {
'@type': 'Offer',
price: '0',
priceCurrency: 'CNY'
},
provider: {
'@type': 'Organization',
name: config.siteName
}
})
}
]
});
interface SSLCheckResponse {
ipv4: SSLCheckItem
ipv6: SSLCheckItem
}
interface SSLCheckItem {
cert_validity_days: number
cert_start_time: string
cert_end_time: string
http_version: string
host_record: string
https_status_code: number
total_time: number
download_speed: number
domain: string
issuer_organization: string[] | null
issuer_common_name: string
subject_common_name: string
is_expired: boolean
is_reachable: boolean
}
const apiList = config.apiBaseUrls
const currentApiIndex = ref(0)
const backendID = computed(() => apiList[currentApiIndex.value]?.id || '')
const tmpDomain = ref('https://www.zakoflare.com')
const testDomain = ref('')
const loading = ref(false)
const error = ref('')
const result = ref<SSLCheckResponse | null>(null)
const sslUrl = computed(() => "/middleware/" + backendID.value + "/ssl/" + testDomain.value);
const { data: sslData, error: sslError, execute: executeSSL } = useMiddlewareFetch<SSLCheckResponse>(sslUrl, {
immediate: false,
watch: false,
});
watch(sslData, (newData) => {
if (newData) {
result.value = newData;
loading.value = false;
error.value = '';
}
});
watch(sslError, async (newError) => {
if (newError) {
console.log(newError);
if (currentApiIndex.value < apiList.length - 1) {
const nextIndex = currentApiIndex.value + 1
error.value = `${(newError as any).message || '请求失败'},正在重试 ${apiList[nextIndex]?.label || ''}...`
currentApiIndex.value = nextIndex
await nextTick()
executeSSL()
} else {
error.value = '请求失败,请检查域名或网络';
loading.value = false;
}
}
});
function formatDate(dateString: string): string {
const date = new Date(dateString)
return date.toLocaleString('zh-CN', {
year: 'numeric',
month: '2-digit',
day: '2-digit',
hour: '2-digit',
minute: '2-digit',
second: '2-digit'
})
}
function checkSSL() {
testDomain.value = extractHost(tmpDomain.value)
currentApiIndex.value = 0
loading.value = true
error.value = ''
result.value = null
nextTick(() => {
executeSSL();
});
}
onMounted(() => {
const urlParam = route.query.site as string
if (urlParam) {
tmpDomain.value = urlParam
checkSSL()
}
})
</script>
<template>
<div class="title">
<header>
<h1>SSL证书检查</h1>
<p>检查网站是否开启 IPv4 和 IPv6 SSL 证书</p>
</header>
</div>
<div class="content">
<div class="one-line">
<el-input
v-model="tmpDomain"
placeholder="请输入域名(如:https://zakoflare.com)"
/>
<el-button
@click="checkSSL()"
type="primary"
:loading="loading"
>
SSL 证书检查
</el-button>
</div>
<div v-if="error" class="error-message">
{{ error }}
</div>
<div v-if="result && result.ipv4" class="result-section">
<table class="result-table">
<thead>
<tr>
<th class="table-header">检测项目</th>
<th class="table-header">IPv4</th>
<th class="table-header">IPv6</th>
</tr>
</thead>
<tbody>
<tr>
<td class="table-label">证书状态</td>
<td class="table-value">
<template v-if="result.ipv4!.is_reachable">
<span :class="result.ipv4!.is_expired ? 'expired' : 'valid'">
{{ result.ipv4!.is_expired ? '已过期' : '有效' }}
</span>
</template>
<span v-else>-</span>
</td>
<td class="table-value">
<template v-if="result.ipv6 && result.ipv6.is_reachable">
<span :class="result.ipv6.is_expired ? 'expired' : 'valid'">
{{ result.ipv6.is_expired ? '已过期' : '有效' }}
</span>
</template>
<span v-else>-</span>
</td>
</tr>
<tr>
<td class="table-label">常用名称</td>
<td class="table-value">
<template v-if="result.ipv4!.is_reachable">{{ result.ipv4!.subject_common_name || '-'}}</template>
<span v-else>-</span>
</td>
<td class="table-value">
<template v-if="result.ipv6 && result.ipv6.is_reachable">{{ result.ipv6.subject_common_name || '-' }}</template>
<span v-else>-</span>
</td>
</tr>
<tr>
<td class="table-label">签发者</td>
<td class="table-value">
<template v-if="result.ipv4!.is_reachable">{{ result.ipv4!.issuer_organization?.join(', ')|| '-' }}</template>
<span v-else>-</span>
</td>
<td class="table-value">
<template v-if="result.ipv6 && result.ipv6.is_reachable">{{ result.ipv6.issuer_organization?.join(', ') || '-' }}</template>
<span v-else>-</span>
</td>
</tr>
<tr v-if="result.ipv6 && result.ipv6.is_reachable && result.ipv4!.is_reachable && result.ipv6.cert_validity_days > 0 && result.ipv4.cert_validity_days > 0">
<td class="table-label">证书有效期 (天)</td>
<td class="table-value">{{ result.ipv4!.cert_validity_days || '-'}} 天</td>
<td class="table-value">{{ result.ipv6?.cert_validity_days || '-' }} 天</td>
</tr>
<tr v-else-if="result.ipv6 && result.ipv6.is_reachable && result.ipv4!.is_reachable && result.ipv6.cert_validity_days <= 0 && result.ipv4.cert_validity_days <= 0">
<td class="table-label">证书已过期(天)</td>
<td class="table-value">{{ Math.abs(result.ipv4.cert_validity_days) || '-'}}</td>
<td class="table-value">{{ Math.abs(result.ipv6.cert_validity_days) || '-' }}</td>
</tr>
<tr v-else-if="result.ipv4!.is_reachable && result.ipv4.cert_validity_days > 0 && (!result.ipv6 || !result.ipv6.is_reachable)">
<td class="table-label">证书有效期 (天)</td>
<td class="table-value">{{ result.ipv4!.cert_validity_days }} 天</td>
<td class="table-value">-</td>
</tr>
<tr v-else-if="result.ipv4!.is_reachable && result.ipv4.cert_validity_days <= 0 && (!result.ipv6 || !result.ipv6.is_reachable)">
<td class="table-label">证书已过期(天)</td>
<td class="table-value">{{ Math.abs(result.ipv4.cert_validity_days) || '-'}}</td>
<td class="table-value">-</td>
</tr>
<tr>
<td class="table-label">证书开始时间</td>
<td class="table-value">
<template v-if="result.ipv4!.is_reachable && result.ipv4!.cert_start_time">{{ formatDate(result.ipv4!.cert_start_time) }}</template>
<span v-else>-</span>
</td>
<td class="table-value">
<template v-if="result.ipv6 && result.ipv6.is_reachable && result.ipv6.cert_start_time">{{ formatDate(result.ipv6.cert_start_time) }}</template>
<span v-else>-</span>
</td>
</tr>
<tr>
<td class="table-label">证书结束时间</td>
<td class="table-value">
<template v-if="result.ipv4!.is_reachable && result.ipv4!.cert_end_time">{{ formatDate(result.ipv4!.cert_end_time) }}</template>
<span v-else>-</span>
</td>
<td class="table-value">
<template v-if="result.ipv6 && result.ipv6.is_reachable && result.ipv6.cert_end_time">{{ formatDate(result.ipv6.cert_end_time) }}</template>
<span v-else>-</span>
</td>
</tr>
<tr>
<td class="table-label">HTTP 版本</td>
<td class="table-value">
<template v-if="result.ipv4!.is_reachable">{{ result.ipv4!.http_version || '-'}}</template>
<span v-else>-</span>
</td>
<td class="table-value">
<template v-if="result.ipv6 && result.ipv6.is_reachable">{{ result.ipv6.http_version || '-' }}</template>
<span v-else>-</span>
</td>
</tr>
<tr>
<td class="table-label">主机记录</td>
<td class="table-value">
<template v-if="result.ipv4!.is_reachable">{{ result.ipv4!.host_record || '-'}}</template>
<span v-else>-</span>
</td>
<td class="table-value">
<template v-if="result.ipv6 && result.ipv6.is_reachable">{{ result.ipv6.host_record || '-' }}</template>
<span v-else>-</span>
</td>
</tr>
<tr>
<td class="table-label">HTTPS 访问返回码</td>
<td class="table-value">
<template v-if="result.ipv4!.is_reachable">
<span :class="getStatusCodeClass(result.ipv4!.https_status_code)" class="status-code">
{{ result.ipv4!.https_status_code }}
</span>
</template>
<span v-else>-</span>
</td>
<td class="table-value">
<template v-if="result.ipv6 && result.ipv6.is_reachable">
<span :class="getStatusCodeClass(result.ipv6.https_status_code)" class="status-code">
{{ result.ipv6.https_status_code }}
</span>
</template>
<span v-else>-</span>
</td>
</tr>
<tr>
<td class="table-label">总耗时</td>
<td class="table-value">
<template v-if="result.ipv4!.is_reachable">{{ formatTime(result.ipv4.total_time) }}</template>
<span v-else>-</span>
</td>
<td class="table-value">
<template v-if="result.ipv6 && result.ipv6.is_reachable">{{ formatTime(result.ipv6.total_time) }}</template>
<span v-else>-</span>
</td>
</tr>
<tr>
<td class="table-label">下载速度</td>
<td class="table-value">
<template v-if="result.ipv4!.is_reachable">{{ formatSpeed(result.ipv4!.download_speed) }}</template>
<span v-else>-</span>
</td>
<td class="table-value">
<template v-if="result.ipv6 && result.ipv6.is_reachable">{{ formatSpeed(result.ipv6.download_speed) }}</template>
<span v-else>-</span>
</td>
</tr>
</tbody>
</table>
</div>
<div v-if="result && result.ipv4 && result.ipv4.is_reachable && !result.ipv4.is_expired && (!result.ipv6 || (!result.ipv6.is_expired && result.ipv6.is_reachable))">
<h3>结论:<el-icon><CircleCheckFilled style="color: lightgreen;"/></el-icon>网站{{ extractHost(testDomain) }} 证书有效 </h3>
<p><el-icon><InfoFilled style="color: lightgreen;"/></el-icon>请把下方代码贴到网站底部,把这个好消息告诉你的用户,以便用户核验。</p>
<img src="/ssl-s1.svg"/>
<pre><code>&lt;a href="{{ siteUrlWithSlash }}ssl/?site={{ extractHost(testDomain) }}" title="本站支持 SSL 安全访问" target='_blank'&gt;&lt;img style='display:inline-block;vertical-align:middle' alt="本站支持 SSL 安全访问" src="{{ siteUrlWithSlash }}ssl-s1.svg"&gt;&lt;/a&gt;</code></pre>
<img src="/ssl-s2.svg"/>
<pre><code>&lt;a href="{{ siteUrlWithSlash }}ssl/?site={{ extractHost(testDomain) }}" title="本站支持 SSL 安全访问" target='_blank'&gt;&lt;img style='display:inline-block;vertical-align:middle' alt="本站支持 SSL 安全访问" src="{{ siteUrlWithSlash }}ssl-s2.svg"&gt;&lt;/a&gt;</code></pre>
<img src="/ssl-s3.svg"/>
<pre><code>&lt;a href="{{ siteUrlWithSlash }}ssl/?site={{ extractHost(testDomain) }}" title="本站支持 SSL 安全访问" target='_blank'&gt;&lt;img style='display:inline-block;vertical-align:middle' alt="本站支持 SSL 安全访问" src="{{ siteUrlWithSlash }}ssl-s3.svg"&gt;&lt;/a&gt;</code></pre>
<img src="/ssl-s4.svg"/>
<pre><code>&lt;a href="{{ siteUrlWithSlash }}ssl/?site={{ extractHost(testDomain) }}" title="本站支持 SSL 安全访问" target='_blank'&gt;&lt;img style='display:inline-block;vertical-align:middle' alt="本站支持 SSL 安全访问" src="{{ siteUrlWithSlash }}ssl-s4.svg"&gt;&lt;/a&gt;</code></pre>
<img src="/ssl-s5.svg"/>
<pre><code>&lt;a href="{{ siteUrlWithSlash }}ssl/?site={{ extractHost(testDomain) }}" title="本站支持 SSL 安全访问" target='_blank'&gt;&lt;img style='display:inline-block;vertical-align:middle' alt="本站支持 SSL 安全访问" src="{{ siteUrlWithSlash }}ssl-s5.svg"&gt;&lt;/a&gt;</code></pre>
<img src="/ssl-s6.svg"/>
<pre><code>&lt;a href="{{ siteUrlWithSlash }}ssl/?site={{ extractHost(testDomain) }}" title="本站支持 SSL 安全访问" target='_blank'&gt;&lt;img style='display:inline-block;vertical-align:middle' alt="本站支持 SSL 安全访问" src="{{ siteUrlWithSlash }}ssl-s6.svg"&gt;&lt;/a&gt;</code></pre>
</div>
<div v-if="result && result.ipv4 && result.ipv4.is_reachable && !result.ipv4.is_expired && result.ipv6 && result.ipv6.is_reachable && result.ipv6.is_expired">
<h3>结论:<el-icon><CircleCheckFilled style="color: lightgreen;"/></el-icon>网站{{ extractHost(testDomain) }} 证书有效,但不支持IPv6访问 </h3>
<p><el-icon><InfoFilled style="color: lightgreen;"/></el-icon>请把下方代码贴到网站底部,把这个好消息告诉你的用户,以便用户核验。</p>
<img src="/ssl-s1.svg"/>
<pre><code>&lt;a href="{{ siteUrlWithSlash }}ssl/?site={{ extractHost(testDomain) }}" title="本站支持 SSL 安全访问" target='_blank'&gt;&lt;img style='display:inline-block;vertical-align:middle' alt="本站支持 SSL 安全访问" src="{{ siteUrlWithSlash }}ssl-s1.svg"&gt;&lt;/a&gt;</code></pre>
<img src="/ssl-s2.svg"/>
<pre><code>&lt;a href="{{ siteUrlWithSlash }}ssl/?site={{ extractHost(testDomain) }}" title="本站支持 SSL 安全访问" target='_blank'&gt;&lt;img style='display:inline-block;vertical-align:middle' alt="本站支持 SSL 安全访问" src="{{ siteUrlWithSlash }}ssl-s2.svg"&gt;&lt;/a&gt;</code></pre>
<img src="/ssl-s3.svg"/>
<pre><code>&lt;a href="{{ siteUrlWithSlash }}ssl/?site={{ extractHost(testDomain) }}" title="本站支持 SSL 安全访问" target='_blank'&gt;&lt;img style='display:inline-block;vertical-align:middle' alt="本站支持 SSL 安全访问" src="{{ siteUrlWithSlash }}ssl-s3.svg"&gt;&lt;/a&gt;</code></pre>
<img src="/ssl-s4.svg"/>
<pre><code>&lt;a href="{{ siteUrlWithSlash }}ssl/?site={{ extractHost(testDomain) }}" title="本站支持 SSL 安全访问" target='_blank'&gt;&lt;img style='display:inline-block;vertical-align:middle' alt="本站支持 SSL 安全访问" src="{{ siteUrlWithSlash }}ssl-s4.svg"&gt;&lt;/a&gt;</code></pre>
<img src="/ssl-s5.svg"/>
<pre><code>&lt;a href="{{ siteUrlWithSlash }}ssl/?site={{ extractHost(testDomain) }}" title="本站支持 SSL 安全访问" target='_blank'&gt;&lt;img style='display:inline-block;vertical-align:middle' alt="本站支持 SSL 安全访问" src="{{ siteUrlWithSlash }}ssl-s5.svg"&gt;&lt;/a&gt;</code></pre>
<img src="/ssl-s6.svg"/>
<pre><code>&lt;a href="{{ siteUrlWithSlash }}ssl/?site={{ extractHost(testDomain) }}" title="本站支持 SSL 安全访问" target='_blank'&gt;&lt;img style='display:inline-block;vertical-align:middle' alt="本站支持 SSL 安全访问" src="{{ siteUrlWithSlash }}ssl-s6.svg"&gt;&lt;/a&gt;</code></pre>
</div>
<div v-else-if="result && result.ipv4 && result.ipv4.is_reachable && result.ipv4.is_expired">
<h3>结论:<CircleCloseFilled style="width: 1.3em;color: red;"/>网站{{ testDomain }} 证书无效 </h3>
<h2>都没有证书了这网站还活啥</h2>
<el-image src="/jingya.jpg"></el-image>
</div>
<div v-else-if="result && result.ipv4 && !result.ipv4.is_reachable && result.ipv6 && !result.ipv6.is_reachable">
<h3>结论:<CircleCloseFilled style="width: 1.3em;color: red;"/>网站{{ testDomain }} 不可达 </h3>
<h2>...</h2>
<el-image src="/jingya.jpg"></el-image>
</div>
<blockquote>
网站不支持 IPv6 SSL 可能原因:<br/>
<br/>
1. 网站所在服务器未开启 IPv6,请参考 <a href="/doc/server/website_enable_ipv6" target="_blank">网站开启 IPv6 的三种方式</a><br/>
2. 网站所在服务器已开启 IPv6,但防火墙未对源地址是 IPv6 地址(::/0)的 443(HTTPS)<a href="/doc/server/website_enable_ipv6" target="_blank">端口开放访问</a><br/>
3. 网站所在服务器已开启 IPv6,但未开启SSL证书,请参考 <a href="/doc/server/nginx_ipv6" target="_blank">Nginx 开启 IPv6 SSL</a><br/>
</blockquote>
</div>
</template>
<style scoped>
@import "../style.css";
@import "../../assets/css/tool-common.css";
.el-menu--horizontal > .el-menu-item:nth-child(1) {
margin-right: auto;
}
</style>
<style>
:root {
--el-color-primary: #3EAF7C;
}
.el-icon{
font-size: 1.3em;
}
</style>
-272
View File
@@ -1,272 +0,0 @@
<script setup lang="ts">
import { ref, onMounted, computed } from 'vue'
import { useRoute } from 'vue-router'
import { config } from '../../config/index';
import { useMiddlewareFetch } from '../../utils/middleware';
import { extractHost } from '../../utils/tools';
const route = useRoute()
useHead({
title: `TCPing测试工具 | IPv4服务器连通性检测 | ${config.siteName}`,
meta: [
{ name: 'description', content: '专业的IPv4 TCPing测试工具,提供多节点TCP连通性检测服务,支持自定义端口测试,实时检测服务器丢包率、平均延迟、最大最小响应时间,帮助运维人员快速诊断服务器网络质量,确保服务稳定运行' },
{ name: 'keywords', content: 'tcping测试,tcp连通性检测,ipv4 tcping,服务器延迟测试,丢包率检测,端口连通性,服务器网络测试,网络质量检测' },
{ property: 'og:title', content: 'TCPing测试工具 - IPv4服务器连通性与延迟检测' },
{ property: 'og:description', content: '多节点TCPing测试,检测服务器连通性、丢包率与响应延迟' },
{ property: 'og:image', content: config.siteUrl + 'favicon.svg' },
{ property: 'og:type', content: 'website' },
],
script: [
{
type: 'application/ld+json',
innerHTML: JSON.stringify({
'@context': 'https://schema.org',
'@type': 'WebApplication',
name: 'IPv4 TCPing连通性测试工具',
description: '专业的IPv4 TCPing测试工具,多节点检测服务器连通性和延迟,支持自定义端口测试,提供丢包率、平均延迟、最大最小响应时间等数据。',
url: config.siteUrl + 'tcping',
applicationCategory: 'DeveloperApplication',
operatingSystem: 'Web',
offers: {
'@type': 'Offer',
price: '0',
priceCurrency: 'CNY'
},
provider: {
'@type': 'Organization',
name: config.siteName
}
})
}
]
});
interface TCPingStats {
ip: string
port: string
sent: number
success: number
loss_rate: number
max_rtt: number
min_rtt: number
avg_rtt: number
}
interface TCPingResponse {
ipv4?: TCPingStats
ipv6?: TCPingStats
}
interface ServerConfig {
id: string
label: string
url: string
}
interface ServerResult {
label: string
loading: boolean
error: string
ipv4?: TCPingStats
ipv6?: TCPingStats
}
const tmpDomain = ref('www.zakoflare.com')
const port = ref('80')
const loading = ref(false)
const serverResults = ref<ServerResult[]>([])
const allServers = [
...config.TCPing.DualStack.map((s: ServerConfig) => ({ ...s, type: 'DualStack' })),
...config.TCPing.IPv4.map((s: ServerConfig) => ({ ...s, type: 'IPv4' }))
];
const tcpingFetches = allServers.map((server) => {
const url = computed(() => "/middleware/" + server.id + "/tcping/" + extractHost(tmpDomain.value) + "?port=" + port.value);
const { data, error: fetchError, execute } = useMiddlewareFetch<TCPingResponse>(url, {
immediate: false,
watch: false,
});
return { label: server.label, data, error: fetchError, execute };
});
function initServerResults() {
const results: ServerResult[] = []
config.TCPing.DualStack.forEach((server: ServerConfig) => {
results.push({
label: server.label,
loading: false,
error: ''
})
})
config.TCPing.IPv4.forEach((server: ServerConfig) => {
results.push({
label: server.label,
loading: false,
error: ''
})
})
serverResults.value = results
}
function TCPingAll() {
const host = extractHost(tmpDomain.value)
if (!host) return
loading.value = true
serverResults.value.forEach((result) => {
result.loading = true
result.error = ''
result.ipv4 = undefined
result.ipv6 = undefined
})
const promises = tcpingFetches.map(async (fetch, index) => {
try {
await fetch.execute();
const result = serverResults.value[index];
if (result) {
result.ipv4 = fetch.data.value?.ipv4;
result.ipv6 = fetch.data.value?.ipv6;
}
} catch (err) {
console.error(err);
const result = serverResults.value[index];
if (result) {
result.error = (err as any)?.message || '请求失败';
}
} finally {
const result = serverResults.value[index];
if (result) {
result.loading = false;
}
}
})
Promise.all(promises).finally(function () {
loading.value = false
})
}
onMounted(() => {
initServerResults()
const urlParam = route.query.site as string
if (urlParam) {
tmpDomain.value = urlParam
TCPingAll()
}
})
</script>
<template>
<div class="title">
<header>
<h1>IPv4 TCPing 测试</h1>
<p>多节点 TCPing 测试,检测服务器连通性和延迟</p>
</header>
</div>
<div class="content">
<div class="one-line">
<el-input
v-model="tmpDomain"
placeholder="请输入域名(如:example.com)"
/>
<el-input
v-model="port"
placeholder="端口号(默认 80)"
style="width: 200px;"
/>
<el-button
@click="TCPingAll()"
type="primary"
:loading="loading"
>
开始测试
</el-button>
</div>
<div class="result-section">
<table class="result-table">
<thead>
<tr>
<th class="table-header">服务器</th>
<th class="table-header">解析 IP</th>
<th class="table-header">发送包</th>
<th class="table-header">接收包</th>
<th class="table-header">丢包率(%)</th>
<th class="table-header">最长时间(ms)</th>
<th class="table-header">最短时间(ms)</th>
<th class="table-header">平均时间(ms)</th>
</tr>
</thead>
<tbody>
<template v-for="(server, index) in serverResults" :key="index">
<tr v-if="server.ipv4">
<td class="table-label">{{ server.label }}</td>
<td class="table-value">{{ server.ipv4?.ip || '-' }}</td>
<td class="table-value">{{ server.ipv4?.sent }}</td>
<td class="table-value">{{ server.ipv4?.success }}</td>
<td class="table-value">
<span :class="server.ipv4.loss_rate > 0 ? 'loss-warning' : 'loss-ok'">
{{ server.ipv4.loss_rate.toFixed(1) }}
</span>
</td>
<td class="table-value">{{ server.ipv4?.max_rtt.toFixed(2) }}</td>
<td class="table-value">{{ server.ipv4?.min_rtt.toFixed(2) }}</td>
<td class="table-value">{{ server.ipv4?.avg_rtt.toFixed(2) }}</td>
</tr>
<tr v-else-if="server.loading">
<td class="table-label">{{ server.label }}</td>
<td class="table-value" colspan="7">加载中...</td>
</tr>
<tr v-else-if="server.error">
<td class="table-label">{{ server.label }}</td>
<td class="table-value error-text" colspan="7">{{ server.error }}</td>
</tr>
<tr v-else>
<td class="table-label">{{ server.label }}</td>
<td class="table-value" colspan="7">-</td>
</tr>
</template>
</tbody>
</table>
</div>
</div>
</template>
<style scoped>
@import "../style.css";
@import "../../assets/css/tool-common.css";
.el-menu--horizontal > .el-menu-item:nth-child(1) {
margin-right: auto;
}
.result-table .table-label {
width: 200px;
}
.error-text {
color: #F56C6C;
}
</style>
<style>
:root {
--el-color-primary: #3EAF7C;
}
html.dark {
--el-color-primary: #3EAF7C;
}
.el-icon {
font-size: 1.3em;
}
</style>
-330
View File
@@ -1,330 +0,0 @@
<script setup lang="ts">
import { useMiddlewareFetch } from '../../utils/middleware';
import { ref, onMounted, computed, watch, nextTick } from 'vue'
import { useRoute } from 'vue-router'
import { config } from '../../config/index'
import { isIPv6 } from 'is-ip'
const route = useRoute()
useHead({
title: 'Whois查询 | ' + (route.query.site || config.siteName),
titleTemplate: '%s',
link: [
{ rel: 'canonical', href: computed(() => new URL(route.path, config.siteUrl).toString()).value }
],
meta: [
{ name: 'description', content: '专业的Whois域名查询工具,支持.com、.net等国内外域名WHOIS信息查询,提供域名注册商、注册时间、到期时间、DNS服务器等详细信息,助力域名管理和交易决策' },
{ name: 'keywords', content: 'whois查询,域名whois,whois信息查询,域名注册信息,域名到期时间,域名注册商,dns服务器查询,域名whois工具' },
{ property: 'og:title', content: 'Whois域名查询工具 - 域名注册信息在线查询' },
{ property: 'og:description', content: '专业的Whois域名查询工具,支持国内外域名WHOIS信息查询,提供域名注册商、注册时间、到期时间等详细信息' },
{ property: 'og:image', content: `${config.siteUrl}favicon.svg` },
{ property: 'og:type', content: 'website' },
{ property: 'og:url', content: computed(() => new URL(route.path, config.siteUrl).toString()).value },
{ name: 'twitter:card', content: 'summary_large_image' },
],
script: [
{
type: 'application/ld+json',
innerHTML: JSON.stringify({
'@context': 'https://schema.org',
'@type': 'WebApplication',
name: 'Whois域名查询工具',
description: '专业的Whois域名查询工具,支持国内外域名WHOIS信息查询,提供域名注册商、注册时间、到期时间、DNS服务器等详细信息',
url: config.siteUrl + 'whois',
applicationCategory: 'DeveloperApplication',
operatingSystem: 'Any',
offers: {
'@type': 'Offer',
price: '0',
priceCurrency: 'CNY'
},
featureList: 'Whois查询,域名注册信息,域名到期时间,域名注册商查询,DNS服务器查询',
about: [
{
'@type': 'Thing',
name: 'Whois查询',
description: '通过Whois协议查询域名的注册信息,包括注册商、注册时间、到期时间、DNS服务器等技术服务。'
},
{
'@type': 'Thing',
name: '域名管理',
description: '通过Whois查询结果,域名持有者可以了解域名到期时间,及时续费避免域名被释放或高价赎回。'
}
]
})
}
]
});
const domain = ref('')
const tmpdomain = ref('zakoflare.com')
const loading = ref(false)
const result = ref<any>(null)
const error = ref('')
const userIP = ref('')
const apiList = config.apiBaseUrls
const currentApiIndex = ref(0)
const backendID = computed(() => apiList[currentApiIndex.value]?.id || '')
const whoisUrl = computed(() => '/middleware/' + backendID.value + '/whois/' + domain.value)
const { data: whoisData, error: whoisError, execute: executeWhois } = useMiddlewareFetch<any>(whoisUrl, {
immediate: false,
watch: false,
})
watch(whoisData, (newData) => {
if (newData) {
result.value = newData
loading.value = false
error.value = ''
}
})
watch(whoisError, async (newError) => {
if (newError) {
console.log(newError)
if (currentApiIndex.value < apiList.length - 1) {
const nextIndex = currentApiIndex.value + 1
error.value = `${(newError as any).message || '请求失败'},正在重试 ${apiList[nextIndex]?.label || ''}...`
currentApiIndex.value = nextIndex
await nextTick()
executeWhois()
} else {
error.value = '请求失败,请检查域名或网络'
loading.value = false
}
}
})
async function queryWhois() {
currentApiIndex.value = 0
loading.value = true
error.value = ''
result.value = null
domain.value = tmpdomain.value.trim()
if (!domain.value) {
error.value = '请输入域名'
loading.value = false
return
}
nextTick(() => {
executeWhois()
})
}
onMounted(() => {
getUserIP()
const whoisParam = route.query.site as string
if (whoisParam) {
tmpdomain.value = whoisParam
queryWhois()
}
})
async function getUserIP(){
await $fetch<string>(config.DualStackAPI).then(
function (data){
userIP.value = data
}
)
return userIP.value
}
const { data: page } = await useAsyncData('whois-doc', () =>
$fetch('/api/markdown/whois')
)
const doc = page.value
function formatDate(dateStr: string): string {
if (!dateStr) return '-'
const d = new Date(dateStr)
return d.toLocaleString('zh-CN', {
year: 'numeric',
month: '2-digit',
day: '2-digit',
hour: '2-digit',
minute: '2-digit',
second: '2-digit'
})
}
function getStatusClass(status: string): string {
if (status.includes('prohibited') || status.includes('lock')) return 'status-warning'
return 'status-success'
}
</script>
<template>
<div class="title">
<header>
<h1>Whois 域名查询</h1>
<p>查询域名注册信息、注册商、到期时间等 Whois 数据</p>
</header>
</div>
<div class="content">
<div class="one-line">
<el-input
v-model="tmpdomain"
placeholder="请输入域名(如:example.com)"
/>
<el-button
@click="queryWhois()"
type="primary"
:loading="loading"
>
查询
</el-button>
</div>
<div v-if="error" class="error-message">
{{ error }}
</div>
<div v-if="result" class="result-section">
<table class="result-table">
<thead>
<tr>
<th class="table-header">项目</th>
<th class="table-header">信息</th>
</tr>
</thead>
<tbody>
<tr>
<td class="table-label">域名</td>
<td class="table-value">{{ result.domain }}</td>
</tr>
<tr v-if="result.status?.length">
<td class="table-label">域名状态</td>
<td class="table-value">
<span v-for="(s, i) in result.status" :key="i" :class="getStatusClass(s)" class="status-code" style="margin-right: 4px;">
{{ s }}
</span>
</td>
</tr>
<tr v-if="result.registrar?.name">
<td class="table-label">注册商</td>
<td class="table-value">
{{ result.registrar.name }}
<span v-if="result.registrar.ianaId" style="color: #999; font-size: 0.9em;">(IANA ID: {{ result.registrar.ianaId }})</span>
</td>
</tr>
<tr v-if="result.registrant && Object.keys(result.registrant).length">
<td class="table-label">注册人</td>
<td class="table-value">
<div v-if="result.registrant.name">{{ result.registrant.name }}</div>
<div v-if="result.registrant.org">{{ result.registrant.org }}</div>
<div v-if="result.registrant.phone">{{ result.registrant.phone }}</div>
<div v-if="result.registrant.email">{{ result.registrant.email }}</div>
<div v-if="result.registrant.province">{{ result.registrant.province }}</div>
<a v-if="result.registrant.contactUri" :href="result.registrant.contactUri" target="_blank" rel="noreferrer">查看注册信息</a>
</td>
</tr>
<tr v-if="result.technical && Object.keys(result.technical).length">
<td class="table-label">技术联系人</td>
<td class="table-value">
<div v-if="result.technical.name">{{ result.technical.name }}</div>
<div v-if="result.technical.org">{{ result.technical.org }}</div>
<div v-if="result.technical.phone">{{ result.technical.phone }}</div>
<div v-if="result.technical.email">{{ result.technical.email }}</div>
</td>
</tr>
<tr v-if="result.abuseContact && Object.keys(result.abuseContact).length">
<td class="table-label">abuse 联系人</td>
<td class="table-value">
<div v-if="result.abuseContact.name">{{ result.abuseContact.name }}</div>
<div v-if="result.abuseContact.phone">{{ result.abuseContact.phone }}</div>
<div v-if="result.abuseContact.email">{{ result.abuseContact.email }}</div>
</td>
</tr>
<tr v-if="result.dates?.registration">
<td class="table-label">注册时间</td>
<td class="table-value">{{ formatDate(result.dates.registration) }}</td>
</tr>
<tr v-if="result.dates?.expiration">
<td class="table-label">到期时间</td>
<td class="table-value">{{ formatDate(result.dates.expiration) }}</td>
</tr>
<tr v-if="result.dates?.lastChanged">
<td class="table-label">最后修改</td>
<td class="table-value">{{ formatDate(result.dates.lastChanged) }}</td>
</tr>
<tr v-if="result.nameservers?.length">
<td class="table-label">DNS 服务器</td>
<td class="table-value">
<div v-for="ns in result.nameservers" :key="ns">{{ ns }}</div>
</td>
</tr>
<tr v-if="result.whoisServer">
<td class="table-label">Whois 服务器</td>
<td class="table-value">{{ result.whoisServer }}</td>
</tr>
</tbody>
</table>
</div>
<blockquote>
数据来源:后端 WHOIS 服务,通过 IANA 注册局 bootstrap 文件自动匹配对应 WHOIS 服务器。<br/>
部分注册局对注册人信息做了隐私保护,将显示为 "Redacted for Privacy"。<br/>
如需查看完整注册信息,可点击 "查看注册信息" 链接跳转到注册局官网查询。<br/>
访客IP: {{ userIP || '获取中...' }} 您的网络{{ isIPv6(userIP) ? 'IPv6' : 'IPv4' }}优先<br/>
</blockquote>
<div v-if="doc" class="markdown">
<div v-html="doc"></div>
</div>
</div>
</template>
<style scoped>
@import "../style.css";
@import "../../assets/css/tool-common.css";
.markdown :deep(a) {
color: #3EAF7C !important;
font-size: 1.3em;
text-decoration: none
}
@media (max-width: 768px) {
.markdown :deep(a) {
font-size: 1.1em;
}
.markdown :deep(img) {
height: auto;
max-width: 100%;
}
}
.el-menu--horizontal > .el-menu-item:nth-child(1) {
margin-right: auto;
}
.status-code {
font-weight: 600;
padding: 2px 8px;
border-radius: 4px;
display: inline-block;
margin-bottom: 2px;
}
.error-message {
margin-top: 20px;
padding: 15px;
background: #fef0f0;
color: #F56C6C;
border-radius: 6px;
text-align: center;
font-size: 1.1em;
}
</style>
<style>
:root {
--el-color-primary: #3EAF7C;
}
html.dark {
--el-color-primary: #3EAF7C;
}
.el-icon{
font-size: 1.3em;
}
</style>
-285
View File
@@ -1,285 +0,0 @@
body{
display: flex;
flex-direction: column;
margin: 0 auto;
}
/* 在样式开头添加 */
p {
margin-top: 0;
margin-bottom: 0;
}
.title{
display: flex;
flex-direction: column;
align-items: center;
}
h1 {
font-size: 3em;
text-align: center;
unicode-bidi: isolate;
}
html.dark h1{
color: #adbac7;
}
h2{
font-size: 1.5em;
unicode-bidi: isolate;
}
h3{
font-size: 1.3em;
}
.title p{
font-size: 25.6px;
color: #6A8BAD;
margin-right: 20px;
margin-left: 20px;
text-align: center;
}
.content{
display: flex;
flex-direction: column;
align-items: right;
width: 60%;
min-width: 400px;
margin: 0 auto;
margin-top: 40px;
}
@media (max-width: 768px) {
.content {
width: 95%;
min-width: unset;
}
}
.one-line{
display: flex;
flex-direction: row;
justify-content: flex-start;
align-items: center;
flex-wrap: wrap;
word-break: break-all;
hyphens: none; /* 防止自动添加连字符 */
margin-top: 1em;
}
.ip-info{
display: flex;
flex-direction: row;
justify-content: flex-start;
align-items: center;
flex-wrap: nowrap;
}
.one-line p{
font-size: 1.3em;
}
.ip-info p{
font-size: 1.3em;
}
b{
color: #2C3E50;
font-size: 1.3em;
}
html.dark b{
color: #adbac7;
}
blockquote p{
color: #999999;
}
blockquote {
background: #f9f9f9;
border-left: 10px solid #ccc;
margin: 1.5em 10px;
padding: 0.5em 10px;
quotes: "\201C""\201D""\2018""\2019";
/* 设置字间距:让文字看起来更透气 */
letter-spacing: 0.5px;
/* 设置行间距:提升多行文本的阅读体验 */
line-height: 2;
/* (可选) 设置单词间距,针对英文内容 */
word-spacing: 1px;
}
blockquote p {
display: inline;
margin: 0.5em;
}
blockquote a{
color: #3EAF7C;
font-size: 1em;
text-decoration: none
}
a {
color: #3EAF7C;
font-size: 1.3em;
text-decoration: none
}
a:hover {
color: none;
}
footer .one-line {
font-size: 1em;
text-align: center;
color: #757575;
}
footer .one-line a {
font-size: 1em;
text-align: center;
color: #3EAF7C;
text-decoration: none
}
footer {
display: flex;
flex-direction: column;
align-items: center;
margin-top: 60px;
}
@media (max-width: 768px) {
.content {
max-width: 95%;
min-width: unset;
margin-top: 30px;
}
h1 {
font-size: 1.9em;
}
h3{
font-size: 1.1em;
}
.title p {
font-size: 0.9em;
margin-right: 10px;
margin-left: 10px;
}
footer {
margin-top: 30px;
}
footer .one-line {
font-size: 0.75em;
flex-wrap: wrap;
justify-content: center;
text-align: center;
}
footer .one-line img {
height: 1.2em;
width: auto;
vertical-align: middle;
}
footer .one-line a {
font-size: 0.9em;
}
.one-line {
margin-top: 0.5em;
margin-bottom: 0.5em;
}
.one-line p{
font-size:1.1em
}
.one-line b{
font-size:1.1em
}
.one-line a{
font-size:1.1em
}
blockquote {
font-size:0.9em;
}
blockquote p {
font-size:0.5em;
display: inline;
margin: 0.5em;
}
blockquote a{
color: #3EAF7C;
font-size: 1em;
text-decoration: none
}
}
html.dark {
/* 在暗色模式下,覆盖根元素的文字颜色和背景色 */
color: rgba(255, 255, 255, 0.87);
background-color: #242424;
--el-color-primary: #3EAF7C;
}
html.dark blockquote {
/* 在暗色模式下,覆盖根元素的文字颜色和背景色 */
color: rgba(255, 255, 255, 0.87);
background-color: #1a1919;
border-left: 10px solid #111111;
}
:deep(.shiki span) {
font-family: 'JetBrains Mono', 'Fira Code', 'Cascadia Code', 'Consolas', 'Monaco', 'Courier New', monospace !important;
}
:deep(.shiki){
padding: 1rem;
border-radius: 10px;
overflow: auto;
max-width: 100%;
}
.spoiler {
position: relative; /* 使得伪元素可以定位 */
background-color: rgba(200, 200, 200, 0.3); /* 浅灰色半透明背景 */
color: rgba(0, 0, 0, 0.5); /* 半透明黑色文字 */
user-select: none; /* 禁止选中文字 */
overflow: hidden; /* 隐藏溢出的内容 */
cursor: pointer; /* 鼠标悬停时光标变为手型 */
filter: blur(3px); /* 添加模糊效果 */
padding: 5px; /* 为文本添加内边距以增强效果 */
transition: filter 0.3s ease; /* 添加平滑过渡效果 */
}
.spoiler:hover {
background-color: inherit; /* 悬停时使用父元素的背景色 */
color: inherit; /* 悬停时显示原色 */
filter: blur(0); /* 悬停消除模糊效果 */
}
.quate{
background-color: #F3F4F5;
color: #4E6E8E;
border-radius: 5px;
padding: 3px ;
}
html.dark .quate{
background-color: #2b313a;
color: #8e9eb0;
}
.header-anchor {
margin-right: .4rem;
text-decoration: none;
transition: opacity .2s;
}
h1>.header-anchor,
h2>.header-anchor,
h3>.header-anchor,
h4>.header-anchor,
h5>.header-anchor,
h6>.header-anchor {
opacity: 1;
color: #3EAF7C;
}
h1:hover>.header-anchor,
h2:hover>.header-anchor,
h3:hover>.header-anchor,
h4:hover>.header-anchor,
h5:hover>.header-anchor,
h6:hover>.header-anchor {
opacity: 1;
}
.header-anchor::before {
content: "#";
color: #3EAF7C;
}
-303
View File
@@ -1,303 +0,0 @@
/* tool-common.css - 网络工具页面公共样式 */
/* 输入框和按钮 */
.el-input {
width: 420px;
height: 50px;
font: 1.3em sans-serif;
margin-right: 10px;
}
.el-button {
width: 165px;
height: 50px;
font: 1.3em sans-serif;
}
.el-select {
margin-right: 10px;
}
:deep(.el-select__wrapper) {
height: 50px !important;
min-height: 50px !important;
padding-top: 0 !important;
}
:deep(.el-select__selected-item) {
line-height: 48px !important;
height: 48px !important;
font-size: 16px;
}
:deep(.el-select__caret) {
font-size: 18px;
line-height: 50px !important;
}
:deep(.el-input__wrapper) {
height: 50px !important;
box-shadow: 0 0 0 1px var(--el-border-color) inset !important;
}
:deep(.el-input__inner) {
height: 48px !important;
line-height: 48px !important;
font-size: 16px;
}
/* 结果区域 */
.result-section {
margin-top: 30px;
overflow-x: auto;
-webkit-overflow-scrolling: touch;
}
/* 表格样式 */
.result-table {
width: 100%;
border-collapse: collapse;
background: #fff;
border: 1px solid #dcdfe6;
}
html.dark .result-table {
background: #2e2d2d;
border: 1px solid #2e2e2e;
}
.result-table thead tr {
background-color: #c0c4cc;
}
html.dark .result-table thead tr {
background: #2e2d2d;
}
.result-table .table-header {
padding: 12px 15px;
font-weight: 600;
color: #303133;
text-align: left;
border: 1px solid #dcdfe6;
}
html.dark .result-table .table-header {
color: #cfcfcf;
border: 1px solid #1a1919;
}
.result-table tbody tr {
border-bottom: 1px solid #dcdfe6;
}
html.dark .result-table tbody tr {
border-bottom: 1px solid #1a1919;
}
.result-table tbody tr:last-child {
border-bottom: none;
}
.result-table tbody tr:hover {
background-color: #f5f7fa;
}
html.dark .result-table tbody tr:hover {
background-color: #393a3a;
}
.result-table .table-label {
padding: 12px 15px;
font-weight: 600;
color: #606266;
width: 150px;
text-align: left;
border: 1px solid #dcdfe6;
}
html.dark .result-table .table-label {
color: #c0c4cc;
border: 1px solid #1a1919;
}
.result-table .table-value {
padding: 12px 15px;
color: #303133;
border: 1px solid #dcdfe6;
}
html.dark .result-table .table-value {
color: #cfcfcf;
border: 1px solid #1a1919;
}
/* 状态样式 */
.status-code {
font-weight: 600;
padding: 4px 12px;
border-radius: 4px;
}
.status-success {
color: #67C23A;
background: #f0f9eb;
}
.status-warning {
color: #E6A23C;
background: #fdf6ec;
}
.status-error {
color: #F56C6C;
background: #fef0f0;
}
.valid {
color: #67C23A;
font-weight: 600;
}
.expired {
color: #F56C6C;
font-weight: 600;
}
.loss-ok {
color: #67C23A;
font-weight: 600;
}
.loss-warning {
color: #F56C6C;
font-weight: 600;
}
/* 错误消息 */
.error-message {
margin-top: 20px;
padding: 15px;
background: #fef0f0;
color: #F56C6C;
border-radius: 6px;
text-align: center;
font-size: 1.1em;
}
/* 代码块 */
pre {
background: #f8f9fa;
padding: 15px;
border-radius: 6px;
overflow-x: auto;
white-space: pre;
max-width: 100%;
}
pre code {
font-family: 'JetBrains Mono', 'Fira Code', 'Consolas', monospace;
font-size: 0.9em;
color: #303133;
}
html.dark pre {
background: #303133;
}
html.dark pre code {
color: #f8f9fa;
}
/* 徽章区域 */
.badge-section {
display: flex;
flex-direction: column;
gap: 20px;
margin-top: 20px;
}
.badge-item {
background: #fff;
padding: 20px;
border-radius: 8px;
border: 1px solid #e4e7ed;
}
.badge-item h4 {
margin: 0 0 15px 0;
color: #3EAF7C;
font-size: 1.2em;
}
.badge-item img {
display: block;
margin-bottom: 15px;
max-width: 200px;
}
/* 窄屏响应式:所有组件缩小 */
@media (max-width: 768px) {
.el-input,
.el-button {
width: 100% !important;
height: 40px !important;
font-size: 0.9em !important;
margin-right: 0 !important;
}
.el-input {
margin-bottom: 10px;
}
.el-select {
width: 100% !important;
margin-right: 0 !important;
margin-bottom: 10px;
}
.result-section {
margin-top: 20px;
}
.result-table {
font-size: 0.85em;
}
.result-table .table-label {
width: 80px;
padding: 6px 8px;
}
.result-table .table-value {
padding: 6px 8px;
}
.result-table .table-header {
padding: 8px 10px;
font-size: 0.9em;
}
pre {
padding: 10px;
font-size: 0.8em;
}
.code-block {
padding: 0.75rem;
font-size: 0.8em;
}
.error-message {
font-size: 0.9em;
padding: 10px;
}
.badge-item {
padding: 15px;
}
.badge-item h4 {
font-size: 1em;
}
}
-107
View File
@@ -1,107 +0,0 @@
export interface DocMeta {
title: string
description: string
}
export const docConfig: Record<string, DocMeta> = {
'/doc': {
title: 'IPv6 工具箱使用文档',
description: 'IPv6 工具箱使用文档'
},
'/doc/user/enable_ipv6': {
title: '个人宽带如何开启IPv6网络访问',
description: '介绍个人宽带如何开启IPv6网络访问'
},
'/doc/user/cmd_bash_disable_ipv6': {
title: '命令行禁用/启用IPv6本地网络',
description: 'macOS和Window10命令行禁用/启用IPv6本地网络'
},
'/doc/user/cmd_getip': {
title: '命令行(curl)获取 IPv4 和 IPv6 地址',
description: '使用命令行curl获取IPv4和IPv6地址'
},
'/doc/user/view_ipv6_adress_url': {
title: '浏览器访问 IPv6 地址',
description: '如何在浏览器中访问IPv6地址'
},
'/doc/user/ipv4_ipv6_prefix_precedence': {
title: 'Windows 10/11 设置 IPv4/IPv6 访问优先级',
description: 'Windows 10/11 设置 IPv4/IPv6 访问优先级'
},
'/doc/user/ipv6_daohang': {
title: '国内 IPv6 资源导航',
description: '国内IPv6资源导航列表'
},
'/doc/user/pure_ipv6_website': {
title: '国内纯 IPv6 网站导航',
description: '国内纯IPv6网站导航列表'
},
'/doc/user/dns': {
title: '全国各省 DNS 服务器列表',
description: '全国各省DNS服务器列表'
},
'/doc/user/ipv6_dns': {
title: 'IPv6 DNS 地址列表',
description: 'IPv6 DNS地址列表'
},
'/doc/user/AliyunAuthorizeSecurityGroup': {
title: '阿里云自动化添加安全组',
description: '阿里云自动化添加安全组'
},
'/doc/user/TencentCloudAddSecurityGroup': {
title: '腾讯云自动化添加安全组',
description: '腾讯云自动化添加安全组'
},
'/doc/server/website_enable_ipv6': {
title: '网站开启 IPv6 的三种方式',
description: '从传统二进制部署的Nginx,到云原生部署的K8S、Istio,分别介绍网站开启IPv6的三种方式'
},
'/doc/server/tencent_cloud_cvm_ipv6': {
title: '腾讯云 cvm 开启 IPv6',
description: '一文看懂在腾讯云CVM上开启IPv6,只需7步'
},
'/doc/server/nginx_ipv6': {
title: 'Nginx 开启 IPv6',
description: '一文看懂Nginx中开启IPv6,包含设置IPv6 SSL证书'
},
'/doc/server/ipv6webcheck': {
title: '如何确认一个网站是否开启 IPv6',
description: '如何确认一个网站是否开启IPv6'
},
'/doc/server/ipv6_sign': {
title: '网站增加支持IPv6访问标识',
description: 'HTML+CSS代码,可以提醒访客本网站支持IPv6访问'
},
'/doc/server/ipv6_domain_record': {
title: '如何为域名添加 IPv6 解析记录',
description: '如何为域名添加IPv6解析记录'
},
'/doc/server/http2': {
title: '原创网站如何开启 HTTP/2',
description: '介绍网站如何开启HTTP/2协议'
},
'/doc/rfc/rfc8200': {
title: 'IPv6 RFC8200 解读',
description: 'RFC8200(Internet Protocol, Version 6 (IPv6) Specification) 是IPv6的权威定义'
},
'/doc/rfc/ipv6_address_format': {
title: 'IPv6 地址标识方法',
description: '首选格式、压缩格式、内嵌IPv4地址的IPv6地址格式'
},
'/doc/user/tcpdump_ipv6': {
title: 'tcpdump 分析IPv6包',
description: '使用tcpdump分析IPv6包'
},
'/doc/user/wireshark_ipv6': {
title: 'WireShark 分析IPv6包头',
description: '使用WireShark分析IPv6包头'
},
'/doc/user/ipv6_ping': {
title: 'IPv6 Ping 检测原理',
description: 'IPv6 Ping检测原理'
}
}
export function getDocMeta(path: string): DocMeta {
return docConfig[path] || { title: '', description: '' }
}
-214
View File
@@ -1,214 +0,0 @@
/*
前端的一系列配置
*/
const config = {
siteUrl: "https://ipw.wsmdn.top/",
// 站点名称:用于页面标题 / 描述 / 页脚品牌展示
siteName: "柠檬味ipw.cn",
// 是否启用前端内置中间件(server/routes/middleware/[...slug].get.ts 本地转发)。
// true(默认):/middleware/* 候选为「外部节点… + 内置中间件(最后一位兜底)」;
// false:仅使用外部节点,候选列表不含内置中间件。
EnableInternalMiddleware: true,
// 外部独立中间件服务列表(base URL)。
// 与前端自带中间件(server/routes/middleware/[...slug].get.ts 本地转发)同级,
// 前端 /middleware/* 请求会依次尝试这些节点(出错重试下一个),
// 前端自带中间件始终放在候选列表最后一位兜底。
// 未配置或为空数组时,直接使用前端自带中间件。
Middleware: <string[]>[
"https://middleware-1.api-ipw.wsmdn.top/"
// "http://127.0.0.1:8091/",
// "https://middleware-2.wsmdn.top/",
],
// Umami 统计
umamiHost: "https://umami.wsmdn.top/",
umamiScriptUrl: "https://umami.wsmdn.top/zako.js",
umamiWebsiteId: "69a91329-b110-4cf7-a04a-be4360b1a8d3",
// 中华人民共和国备案系统
ICP: "苏ICP备2026012471号",
GongAn: "苏公网安备32132402000813号",
// 全站是否禁止搜索引擎索引
noindex: false,
// Worker IP查询接口
v4OnlyAPI: "https://4.wsmdn.top",
v6OnlyAPI: "https://6.wsmdn.top",
DualStackAPI: "https://test.wsmdn.top",
apiBaseUrls: [
{
label: "中国 江苏 移动",
id: "cn-jiangsu",
url: "https://cn-jiangsu.api-ipw.wsmdn.top/"
},
{
label: "中国 广东 深圳 龙岗 坪地街道 中国移动",
id: "cn-shenzhen",
url: "https://cn-shenzhen.api-ipw.wsmdn.top/"
}
],
IPLocationAPIs: [
{
label: "中国 江苏 移动",
id: "cn-jiangsu",
url: "https://cn-jiangsu.api-ipw.wsmdn.top/"
},
{
label: "中国 四川 沙渠 电信[ZFC]",
id: "cn2-sichuan",
url: "https://cn2-sichuan.api-ipw.wsmdn.top/"
}
],
TCPing:{
DualStack: [
{
label: "中国 江苏 移动",
id: "cn-jiangsu",
url :"https://cn-jiangsu.api-ipw.wsmdn.top/",
},
{
label: "中国 广东 深圳 龙岗 坪地街道 中国移动",
id: "cn-shenzhen",
url :"https://cn-shenzhen.api-ipw.wsmdn.top/",
},
],
IPv4: [
{
label: "中国 广东 广州 腾讯云",
id: "cn-guangzhou",
url :"https://cn-guangzhou.api-ipw.wsmdn.top/",
},
{
label: "新加坡 腾讯云",
id: "sg-1",
url :"https://sg-1.api-ipw.wsmdn.top/",
},
{
label: "中国 香港 新界 西贡区 将军澳 MICC[ZFC]",
id: "hk-shatin",
url :"https://china-hk-kowloon-shatindistrict-newcloud.ipwapi.zfap.wsmdn.top/",
},
{
label: "中国 陕西 西安 北经济技术开发区 未央区凤城 中国电信[ZFC]",
id: "cn-xian",
url :"https://cn-xian-shaanxiprovince.api-ipw.wsmdn.top/",
},
{
label: "美国/加利福尼亚州/洛杉矶/蒙特雷帕克/USCD[ZFC]",
id: "us-la",
url :"https://america-california-losangeles.ipwapi.zfap.wsmdn.top/",
}
],
IPv6: [
{
label: "中国 四川 沙渠 电信[ZFC]",
id: "cn2-sichuan",
url:"https://cn2-sichuan.api-ipw.wsmdn.top/",
},
{
label: "中国 香港 九龙城区 旺角东 Cloudie[ZFC]",
id: "lntl-cn-hk-kowloon",
url:"https://lntl-cn-hk-kowloon.api-ipw.wsmdn.top/",
}
]
},
SpeedTest:{
DualStack: [
{
label: "中国 江苏 移动",
id: "cn-jiangsu",
url :"https://cn-jiangsu.api-ipw.wsmdn.top/",
},
{
label: "中国 广东 深圳 龙岗 坪地街道 中国移动",
id: "cn-shenzhen",
url :"https://cn-shenzhen.api-ipw.wsmdn.top/",
},
],
IPv4: [
{
label: "中国 广东 广州 腾讯云",
id: "cn-guangzhou",
url :"https://cn-guangzhou.api-ipw.wsmdn.top/",
},
{
label: "新加坡 腾讯云",
id: "sg-1",
url :"https://sg-1.api-ipw.wsmdn.top/",
},
{
label: "中国 香港 新界 西贡区 将军澳 MICC[ZFC]",
id: "hk-shatin",
url :"https://china-hk-kowloon-shatindistrict-newcloud.ipwapi.zfap.wsmdn.top/",
},
{
label: "中国 陕西 西安 北经济技术开发区 未央区凤城 中国电信[ZFC]",
id: "cn-xian",
url :"https://cn-xian-shaanxiprovince.api-ipw.wsmdn.top/",
},
{
label: "美国/加利福尼亚州/洛杉矶/蒙特雷帕克/USCD[ZFC]",
id: "us-la",
url :"https://america-california-losangeles.ipwapi.zfap.wsmdn.top/",
}
],
IPv6: [
{
label: "中国 四川 沙渠 电信[ZFC]",
id: "cn2-sichuan",
url:"https://cn2-sichuan.api-ipw.wsmdn.top/",
},
{
label: "中国 香港 九龙城区 旺角东 Cloudie[ZFC]",
id: "lntl-cn-hk-kowloon",
url:"https://lntl-cn-hk-kowloon.api-ipw.wsmdn.top/",
}
]
},
NSLookup:[
{
label: "中国 江苏 移动",
id: "cn-jiangsu",
url :"https://cn-jiangsu.api-ipw.wsmdn.top/",
},
{
label: "中国 广州 腾讯云",
id: "cn-guangzhou",
url :"https://cn-guangzhou.api-ipw.wsmdn.top/",
},
{
label: "新加坡 腾讯云",
id: "sg-1",
url :"https://sg-1.api-ipw.wsmdn.top/",
},
{
label: "中国 四川 沙渠 电信[ZFC]",
id: "cn2-sichuan",
url:"https://cn2-sichuan.api-ipw.wsmdn.top/",
},
{
label: "中国 陕西 西安 北经济技术开发区 未央区凤城 中国电信[ZFC]",
id: "cn-xian",
url :"https://cn-xian-shaanxiprovince.api-ipw.wsmdn.top/",
},
{
label: "中国 香港 新界 西贡区 将军澳 MICC[ZFC]",
id: "hk-shatin",
url :"https://china-hk-kowloon-shatindistrict-newcloud.ipwapi.zfap.wsmdn.top/",
},
{
label: "中国 香港 九龙城区 旺角东 Cloudie[ZFC]",
id: "lntl-cn-hk-kowloon",
url:"https://lntl-cn-hk-kowloon.api-ipw.wsmdn.top/",
},
{
label: "中国 广东 深圳 龙岗 坪地街道 中国移动",
id: "cn-shenzhen",
url :"https://cn-shenzhen.api-ipw.wsmdn.top/",
},
{
label: "美国/加利福尼亚州/洛杉矶/蒙特雷帕克/USCD[ZFC]",
id: "us-la",
url :"https://america-california-losangeles.ipwapi.zfap.wsmdn.top/",
}
]
}
export { config }
-131
View File
@@ -1,131 +0,0 @@
import {config} from "./config/index";
import { docConfig } from "./config/doc";
// https://nuxt.com/docs/api/configuration/nuxt-config
const extractDomains = (obj: any): string[] => {
// 将对象转为 JSON 字符串,用正则匹配所有 https:// 开头的域名部分
const urls = JSON.stringify(obj).match(/https?:\/\/[^"\/\\\s]+/g) || [];
// 提取域名 (Origin) 并去重
const domains = [...new Set(urls.map(url => new URL(url).origin))];
return domains;
};
const allowedDomains = extractDomains(config);
export default defineNuxtConfig({
compatibilityDate: '2025-07-15',
devtools: { enabled: true },
modules: [
"nitro-cloudflare-dev",
'@element-plus/nuxt',
'@nuxtjs/sitemap',
'@nuxtjs/robots',
'@vueuse/nuxt',
"nuxt-security",
],
vite: {
optimizeDeps: {
include: [
'is-ip',
'shiki',
]
},
},
site: {
url: config.siteUrl,
name: 'Lemon IPW'
},
css: [
// 1. 引入 Element Plus 基础样式 (如果你还没有引入的话)
'element-plus/dist/index.css',
// 2. 🌟 关键:引入 Element Plus 官方的暗黑模式 CSS 变量文件
'element-plus/theme-chalk/dark/css-vars.css',
],
app:{
head: {
script: [
{
// 必须 innerHTML,不能 src(否则异步加载)
innerHTML: `
(function() {
var stored = localStorage.getItem('vueuse-color-scheme');
var prefersDark = window.matchMedia('(prefers-color-scheme: dark)').matches;
var dark = stored === 'dark' || (!stored && prefersDark);
if (dark) document.documentElement.classList.add('dark');
})();
`,
// 关键:不加 async/defer,确保同步阻塞执行
}
],
link: [
{ rel: 'icon', type: 'image/x-icon', href: '/favicon.svg' }
]
}
},
runtimeConfig: {
indexnowKey: '',
apiKeys: '',
public: {
siteUrl: config.siteUrl,
docConfig: docConfig,
},
},
routeRules: {
// /doc 文档站改为 SSG:构建时预渲染为静态 HTML,由边缘静态资源直接响应
'/doc/**': { prerender: true },
},
nitro: {
publicAssets: [
{
dir: 'public',
maxAge: 0
}
],
esbuild: {
options: {
target: 'es2022' // 明确告诉 Nitro 使用 es2022 进行打包
}
},
prerender: {
// 显式列出所有 doc 路由(来自 config/doc.ts 的 docConfig 键),
// 让动态 [...slug] 页面在构建时全部预渲染为静态 HTML
routes: ['/doc', ...Object.keys(docConfig).filter((p) => p !== '/doc')],
},
},
security: {
ssg: {
// /doc 预渲染页的 CSP 会因 SRI modulepreload 哈希全量拼入而单行超 2000 字符,
// 触发 Cloudflare _headers 限制;关闭静态页安全头写入 _headers。
// SSR 页面仍由运行时中间件下发完整安全头,不受影响。
nitroHeaders: false,
},
headers: {
contentSecurityPolicy: {
'script-src': [
"'self'",
"'strict-dynamic'",
"'nonce-{{nonce}}'",
"'wasm-unsafe-eval'",
...allowedDomains // 允许 Umami 发送数据
],
'connect-src': [
"'self'",
...allowedDomains,// 允许 Umami 发送数据
],
'style-src': ["'self'", 'https:', "'unsafe-inline'"],
'img-src': ["'self'", 'https://s0.wp.com', 'data:', 'https:'],
'font-src': ["'self'", 'https:', 'data:'],
}
}
},
sitemap: {
sources: [
'/api/__sitemap__/urls',
]
}
})
-49
View File
@@ -1,49 +0,0 @@
{
"name": "frontend-ssr",
"type": "module",
"private": true,
"scripts": {
"build": "nuxt build",
"dev": "nuxt dev",
"generate": "nuxt generate",
"preview": "pnpm run build && wrangler dev",
"postinstall": "nuxt prepare",
"deploy": "pnpm run build && wrangler deploy",
"cf-typegen": "wrangler types"
},
"dependencies": {
"@element-plus/icons-vue": "^2.3.2",
"@element-plus/nuxt": "^1.1.5",
"@nuxtjs/robots": "^6.1.3",
"@nuxtjs/sitemap": "^8.3.2",
"@nuxtjs/tailwindcss": "6.14.0",
"@shikijs/core": "^4.4.1",
"@shikijs/engine-javascript": "^4.4.1",
"@sindresorhus/slugify": "^3.0.0",
"@tailwindcss/vite": "^4.3.3",
"@vueuse/core": "^14.4.0",
"@vueuse/nuxt": "^14.4.0",
"better-sqlite3": "^12.11.1",
"element-plus": "^2.14.3",
"github-markdown-css": "^5.9.0",
"github-slugger": "^2.0.0",
"is-ip": "^5.0.1",
"markdown-it": "^14.3.0",
"markdown-it-anchor": "^9.2.1",
"nuxt": "^4.5.1",
"nuxt-security": "2.6.0",
"vue": "^3.5.40",
"vue-router": "^5.2.0"
},
"devDependencies": {
"@shikijs/langs": "^4.4.1",
"@shikijs/markdown-it": "^4.4.1",
"@shikijs/themes": "^4.4.1",
"@shikijs/types": "^4.4.1",
"@types/markdown-it": "^14.1.2",
"nitro-cloudflare-dev": "^0.2.2",
"sass-embedded": "^1.100.0",
"shiki": "^4.4.1",
"wrangler": "^4.118.0"
}
}
-12474
View File
File diff suppressed because it is too large. Load diff
-7
View File
@@ -1,7 +0,0 @@
allowBuilds:
'@parcel/watcher': true
better-sqlite3: true
esbuild: true
sharp: true
vue-demi: true
workerd: true
Binary file not shown.

Before

Width:  |  Height:  |  Size: 463 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 284 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 282 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 296 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 250 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 258 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 336 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 244 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 588 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 245 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 258 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 230 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 487 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 244 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 431 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 336 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 249 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 259 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 307 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 389 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 97 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 194 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 117 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 53 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 90 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 134 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 240 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 243 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 560 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 51 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 51 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 119 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 186 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 259 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 264 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 214 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 396 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 292 KiB

Loaded 100 of 220 files, more files were not shown because too many files have changed in this diff. Show more