Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
00d309ffa0 | ||
|
|
9f46341769 | ||
|
|
4b8b370efb |
No files matched your search
@@ -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.
|
||||
@@ -1,20 +0,0 @@
|
||||
---
|
||||
name: Feature request
|
||||
about: 对于本项目的一些想法
|
||||
title: Feature
|
||||
labels: ''
|
||||
assignees: ''
|
||||
|
||||
---
|
||||
|
||||
**你的功能请求是否是为了解决某个痛点?请详细描述。**
|
||||
请清晰、简明地描述该痛点。例如:我认为[...]的设计并不好,我觉得这样做会更优秀。
|
||||
|
||||
**描述你期望的解决方案**
|
||||
请清晰、简明地描述你期望实现的效果或功能。
|
||||
|
||||
**描述你考虑过的替代方案**
|
||||
请清晰、简明地描述你考虑过的其他替代方案或功能。
|
||||
|
||||
**补充信息**
|
||||
请在此处补充关于该功能请求的其他背景信息或相关截图。
|
||||
@@ -1,10 +0,0 @@
|
||||
---
|
||||
name: 其他问题
|
||||
about: 请尽可能详细的描述您的问题,或者直接加入项目官方交流群询问
|
||||
title: ''
|
||||
labels: ''
|
||||
assignees: ''
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -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
|
||||
@@ -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}}"
|
||||
@@ -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 }}
|
||||
@@ -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 }}
|
||||
@@ -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
|
||||
@@ -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 +0,0 @@
|
||||
24
|
||||
@@ -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.
|
||||
@@ -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"]
|
||||
@@ -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 一键部署前端:
|
||||
|
||||
[](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宕机等情况
|
||||
@@ -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',
|
||||
},
|
||||
},
|
||||
})
|
||||
@@ -1,6 +0,0 @@
|
||||
import { defineTeekConfig } from "vitepress-theme-teek/config";
|
||||
|
||||
export const teekConfig = defineTeekConfig({
|
||||
homeCardListPosition: false,
|
||||
teekHome: false,
|
||||
});
|
||||
@@ -1,7 +0,0 @@
|
||||
// .vitepress/theme/index.ts
|
||||
import Teek from "vitepress-theme-teek";
|
||||
import "vitepress-theme-teek/index.css";
|
||||
|
||||
export default {
|
||||
extends: Teek,
|
||||
};
|
||||
@@ -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** 一键部署本前端(无需手动配置构建):
|
||||
|
||||
[](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` 切换)并配置对应产物入口即可。
|
||||
@@ -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` | 后端多平台构建与发布 |
|
||||
@@ -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和阿里云函数计算
|
||||
---
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
@@ -1,2 +0,0 @@
|
||||
allowBuilds:
|
||||
esbuild: true
|
||||
@@ -1,4 +0,0 @@
|
||||
# Tencent Cloud EdgeOne
|
||||
.env
|
||||
.edgeone/*
|
||||
.tef_dist/*
|
||||
@@ -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整套,免费额度够玩,小项目直接白嫖,性能还吊打一堆!!!绝绝子好用!!!
|
||||
|
||||
姐妹们快冲!!!小仙女的开发日常从今天开始闪闪发光啦💗💗💕安排了安排了~~有在用的宝子们快来评论区一起陷入爱河呀!!!😭💞
|
||||
*/
|
||||
@@ -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>
|
||||
|
||||
@@ -1,4 +0,0 @@
|
||||
# Tencent Cloud EdgeOne
|
||||
.env
|
||||
.edgeone/*
|
||||
.tef_dist/*
|
||||
@@ -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
|
||||
)
|
||||
@@ -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=
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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()
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
@@ -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.
|
||||
@@ -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"> | </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"> | </span>
|
||||
<a href="https://www.china-ipv6.cn/" target="_blank" rel="noreferrer" >国家IPv6发展监测平台</a>
|
||||
| 请遵守中国法律法规 |
|
||||
<a href="https://github.com/nomdn/ipw-cn" target="_blank" rel="noreferrer" >Github</a> |
|
||||
<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>
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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> <p>{{ yourIPv4 }} </p> <RouterLink :to="`/location?ip=${yourIPv4}`" target="_blank">查询归属地</RouterLink>
|
||||
</div>
|
||||
<div class="one-line">
|
||||
<b>IPv6</b> <p v-if="yourIPv6">{{ yourIPv6 }}</p><RouterLink :to="`/location?ip=${yourIPv6}`" target="_blank" v-if="yourIPv6"> 查询归属地</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>
|
||||
@@ -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> <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 }} {{ IPLocation.bilibili?.administrative_area }} {{ 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] }} {{ IPLocation.ip2region?.split("|")[1] }} {{ 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 }} {{ IPLocation.geocn?.city }} {{ 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 }} {{ IPLocation.maxmind_city?.administrative_area }} {{ 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 }} {{ IPLocation.qqwry?.administrative_area }} {{ 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 }} {{ IPLocation.dbip_city?.administrative_area }} {{ 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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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><a href="{{ siteUrlWithSlash }}ipv6webcheck/?site={{ extractHost(testDomain) }}" title="本站支持 IPv6访问" target='_blank'><img style='display:inline-block;vertical-align:middle' alt="本站支持 IPv6访问" src="{{ siteUrlWithSlash }}ipv6-s1.svg"></a></code></pre>
|
||||
<img src="/ipv6-s2.svg"/>
|
||||
<pre><code><a href="{{ siteUrlWithSlash }}ipv6webcheck/?site={{ extractHost(testDomain) }}" title="本站支持 IPv6访问" target='_blank'><img style='display:inline-block;vertical-align:middle' alt="本站支持 IPv6访问" src="{{ siteUrlWithSlash }}ipv6-s2.svg"></a></code></pre>
|
||||
<img src="/ipv6-s3.svg"/>
|
||||
<pre><code><a href="{{ siteUrlWithSlash }}ipv6webcheck/?site={{ extractHost(testDomain) }}" title="本站支持 IPv6访问" target='_blank'><img style='display:inline-block;vertical-align:middle' alt="本站支持 IPv6访问" src="{{ siteUrlWithSlash }}ipv6-s3.svg"></a></code></pre>
|
||||
<img src="/ipv6-s4.svg"/>
|
||||
<pre><code><a href="{{ siteUrlWithSlash }}ipv6webcheck/?site={{ extractHost(testDomain) }}" title="本站支持 IPv6访问" target='_blank'><img style='display:inline-block;vertical-align:middle' alt="本站支持 IPv6访问" src="{{ siteUrlWithSlash }}ipv6-s4.svg"></a></code></pre>
|
||||
<img src="/ipv6-s5.svg"/>
|
||||
<pre><code><a href="{{ siteUrlWithSlash }}ipv6webcheck/?site={{ extractHost(testDomain) }}" title="本站支持 IPv6访问" target='_blank'><img style='display:inline-block;vertical-align:middle' alt="本站支持 IPv6访问" src="{{ siteUrlWithSlash }}ipv6-s5.svg"></a></code></pre>
|
||||
<img src="/ipv6-s6.svg"/>
|
||||
<pre><code><a href="{{ siteUrlWithSlash }}ipv6webcheck/?site={{ extractHost(testDomain) }}" title="本站支持 IPv6访问" target='_blank'><img style='display:inline-block;vertical-align:middle' alt="本站支持 IPv6访问" src="{{ siteUrlWithSlash }}ipv6-s6.svg"></a></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><a href="{{ siteUrlWithSlash }}ipv6webcheck/?site={{ extractHost(testDomain) }}" title="本站支持 IPv6访问" target='_blank'><img style='display:inline-block;vertical-align:middle' alt="本站支持 IPv6访问" src="{{ siteUrlWithSlash }}ipv6-certified-lite-s1.svg"></a></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><a href="{{ siteUrlWithSlash }}ipv6webcheck/?site={{ extractHost(testDomain) }}" title="本站支持 IPv6访问" target='_blank'><img style='display:inline-block;vertical-align:middle' alt="本站支持 IPv6访问" src="{{ siteUrlWithSlash }}ipv6-certified-s1.svg"></a></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>
|
||||
@@ -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> <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 }} {{ IPLocation.bilibili?.administrative_area }} {{ 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] }} {{ IPLocation.ip2region?.split("|")[1] }} {{ 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 }} {{ IPLocation.geocn?.city }} {{ IPLocation.geocn?.district }}</td>
|
||||
<td class="table-value">{{ IPLocation.geocn?.isp }} {{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 }} {{ IPLocation.maxmind_city?.administrative_area }} {{ 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 }} {{ IPLocation.qqwry?.administrative_area }} {{ 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 }} {{ IPLocation.dbip_city?.administrative_area }} {{ 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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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><a href="{{ siteUrlWithSlash }}ssl/?site={{ extractHost(testDomain) }}" title="本站支持 SSL 安全访问" target='_blank'><img style='display:inline-block;vertical-align:middle' alt="本站支持 SSL 安全访问" src="{{ siteUrlWithSlash }}ssl-s1.svg"></a></code></pre>
|
||||
<img src="/ssl-s2.svg"/>
|
||||
<pre><code><a href="{{ siteUrlWithSlash }}ssl/?site={{ extractHost(testDomain) }}" title="本站支持 SSL 安全访问" target='_blank'><img style='display:inline-block;vertical-align:middle' alt="本站支持 SSL 安全访问" src="{{ siteUrlWithSlash }}ssl-s2.svg"></a></code></pre>
|
||||
<img src="/ssl-s3.svg"/>
|
||||
<pre><code><a href="{{ siteUrlWithSlash }}ssl/?site={{ extractHost(testDomain) }}" title="本站支持 SSL 安全访问" target='_blank'><img style='display:inline-block;vertical-align:middle' alt="本站支持 SSL 安全访问" src="{{ siteUrlWithSlash }}ssl-s3.svg"></a></code></pre>
|
||||
<img src="/ssl-s4.svg"/>
|
||||
<pre><code><a href="{{ siteUrlWithSlash }}ssl/?site={{ extractHost(testDomain) }}" title="本站支持 SSL 安全访问" target='_blank'><img style='display:inline-block;vertical-align:middle' alt="本站支持 SSL 安全访问" src="{{ siteUrlWithSlash }}ssl-s4.svg"></a></code></pre>
|
||||
<img src="/ssl-s5.svg"/>
|
||||
<pre><code><a href="{{ siteUrlWithSlash }}ssl/?site={{ extractHost(testDomain) }}" title="本站支持 SSL 安全访问" target='_blank'><img style='display:inline-block;vertical-align:middle' alt="本站支持 SSL 安全访问" src="{{ siteUrlWithSlash }}ssl-s5.svg"></a></code></pre>
|
||||
<img src="/ssl-s6.svg"/>
|
||||
<pre><code><a href="{{ siteUrlWithSlash }}ssl/?site={{ extractHost(testDomain) }}" title="本站支持 SSL 安全访问" target='_blank'><img style='display:inline-block;vertical-align:middle' alt="本站支持 SSL 安全访问" src="{{ siteUrlWithSlash }}ssl-s6.svg"></a></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><a href="{{ siteUrlWithSlash }}ssl/?site={{ extractHost(testDomain) }}" title="本站支持 SSL 安全访问" target='_blank'><img style='display:inline-block;vertical-align:middle' alt="本站支持 SSL 安全访问" src="{{ siteUrlWithSlash }}ssl-s1.svg"></a></code></pre>
|
||||
<img src="/ssl-s2.svg"/>
|
||||
<pre><code><a href="{{ siteUrlWithSlash }}ssl/?site={{ extractHost(testDomain) }}" title="本站支持 SSL 安全访问" target='_blank'><img style='display:inline-block;vertical-align:middle' alt="本站支持 SSL 安全访问" src="{{ siteUrlWithSlash }}ssl-s2.svg"></a></code></pre>
|
||||
<img src="/ssl-s3.svg"/>
|
||||
<pre><code><a href="{{ siteUrlWithSlash }}ssl/?site={{ extractHost(testDomain) }}" title="本站支持 SSL 安全访问" target='_blank'><img style='display:inline-block;vertical-align:middle' alt="本站支持 SSL 安全访问" src="{{ siteUrlWithSlash }}ssl-s3.svg"></a></code></pre>
|
||||
<img src="/ssl-s4.svg"/>
|
||||
<pre><code><a href="{{ siteUrlWithSlash }}ssl/?site={{ extractHost(testDomain) }}" title="本站支持 SSL 安全访问" target='_blank'><img style='display:inline-block;vertical-align:middle' alt="本站支持 SSL 安全访问" src="{{ siteUrlWithSlash }}ssl-s4.svg"></a></code></pre>
|
||||
<img src="/ssl-s5.svg"/>
|
||||
<pre><code><a href="{{ siteUrlWithSlash }}ssl/?site={{ extractHost(testDomain) }}" title="本站支持 SSL 安全访问" target='_blank'><img style='display:inline-block;vertical-align:middle' alt="本站支持 SSL 安全访问" src="{{ siteUrlWithSlash }}ssl-s5.svg"></a></code></pre>
|
||||
<img src="/ssl-s6.svg"/>
|
||||
<pre><code><a href="{{ siteUrlWithSlash }}ssl/?site={{ extractHost(testDomain) }}" title="本站支持 SSL 安全访问" target='_blank'><img style='display:inline-block;vertical-align:middle' alt="本站支持 SSL 安全访问" src="{{ siteUrlWithSlash }}ssl-s6.svg"></a></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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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: '' }
|
||||
}
|
||||
@@ -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 }
|
||||
@@ -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',
|
||||
]
|
||||
}
|
||||
|
||||
|
||||
})
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
@@ -1,7 +0,0 @@
|
||||
allowBuilds:
|
||||
'@parcel/watcher': true
|
||||
better-sqlite3: true
|
||||
esbuild: true
|
||||
sharp: true
|
||||
vue-demi: true
|
||||
workerd: true
|
||||
|
Before Width: | Height: | Size: 463 KiB |
|
Before Width: | Height: | Size: 284 KiB |
|
Before Width: | Height: | Size: 282 KiB |
|
Before Width: | Height: | Size: 296 KiB |
|
Before Width: | Height: | Size: 250 KiB |
|
Before Width: | Height: | Size: 258 KiB |
|
Before Width: | Height: | Size: 336 KiB |
|
Before Width: | Height: | Size: 244 KiB |
|
Before Width: | Height: | Size: 588 KiB |
|
Before Width: | Height: | Size: 245 KiB |
|
Before Width: | Height: | Size: 258 KiB |
|
Before Width: | Height: | Size: 230 KiB |
|
Before Width: | Height: | Size: 487 KiB |
|
Before Width: | Height: | Size: 244 KiB |
|
Before Width: | Height: | Size: 431 KiB |
|
Before Width: | Height: | Size: 336 KiB |
|
Before Width: | Height: | Size: 249 KiB |
|
Before Width: | Height: | Size: 259 KiB |
|
Before Width: | Height: | Size: 307 KiB |
|
Before Width: | Height: | Size: 389 KiB |
|
Before Width: | Height: | Size: 97 KiB |
|
Before Width: | Height: | Size: 194 KiB |
|
Before Width: | Height: | Size: 117 KiB |
|
Before Width: | Height: | Size: 53 KiB |
|
Before Width: | Height: | Size: 90 KiB |
|
Before Width: | Height: | Size: 134 KiB |
|
Before Width: | Height: | Size: 240 KiB |
|
Before Width: | Height: | Size: 243 KiB |
|
Before Width: | Height: | Size: 560 KiB |
|
Before Width: | Height: | Size: 51 KiB |
|
Before Width: | Height: | Size: 51 KiB |
|
Before Width: | Height: | Size: 119 KiB |
|
Before Width: | Height: | Size: 186 KiB |
|
Before Width: | Height: | Size: 259 KiB |
|
Before Width: | Height: | Size: 264 KiB |
|
Before Width: | Height: | Size: 214 KiB |
|
Before Width: | Height: | Size: 396 KiB |
|
Before Width: | Height: | Size: 292 KiB |