Initial commit with project setup and basic structure.
This commit is contained in:
parent
2f6c0eaa38
commit
93f05f13c1
4
.gitignore
vendored
Normal file
4
.gitignore
vendored
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
app/__pycache__/__init__.*.pyc
|
||||||
|
app/__pycache__/eip_client.*.pyc
|
||||||
|
app/__pycache__/*.pyc
|
||||||
|
app/routers/__pycache__/*.pyc
|
||||||
185
API.md
Normal file
185
API.md
Normal file
@ -0,0 +1,185 @@
|
|||||||
|
## EIP API 文档
|
||||||
|
|
||||||
|
**Base URL**: `https://smart.jdbox.xyz:58001`
|
||||||
|
|
||||||
|
### 鉴权
|
||||||
|
- **名称**: API 鉴权
|
||||||
|
- **方法**: POST
|
||||||
|
- **路径**: `/client/auth`
|
||||||
|
- **请求头**:
|
||||||
|
- `Content-Type: application/json`
|
||||||
|
- **请求体**:
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"username": "<你的用户名>",
|
||||||
|
"password": "<你的密码>"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
- **说明**: 成功后返回 Token,请在后续请求中以 `X-Token` 头部携带。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 代理链路节点 - 获取城市列表
|
||||||
|
- **名称**: 获取代理链路节点
|
||||||
|
- **方法**: GET
|
||||||
|
- **路径**: `/edge/city`
|
||||||
|
- **请求头**:
|
||||||
|
- `X-Token: <你的Token>`
|
||||||
|
- `Accept: application/json`
|
||||||
|
- **说明**: 返回可用城市(或区域)节点信息列表。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 代理链路节点 - 按 cityhash 查询设备
|
||||||
|
- **名称**: cityhash获取代理链路节点信息
|
||||||
|
- **方法**: POST
|
||||||
|
- **路径**: `/edge/device`
|
||||||
|
- **请求头**:
|
||||||
|
- `X-Token: <你的Token>`
|
||||||
|
- `Content-Type: application/json`
|
||||||
|
- **请求体**:
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"geo": "<cityhash>",
|
||||||
|
"offset": 0,
|
||||||
|
"num": 200
|
||||||
|
}
|
||||||
|
```
|
||||||
|
- **参数说明**:
|
||||||
|
- `geo`: 城市哈希(cityhash)
|
||||||
|
- `offset`: 起始偏移
|
||||||
|
- `num`: 返回数量上限
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 代理链路节点 - 指定城市、数量查询
|
||||||
|
- **名称**: 查询指定城市、指定数量代理信息
|
||||||
|
- **方法**: POST
|
||||||
|
- **路径**: `/edge/device`
|
||||||
|
- **请求头**:
|
||||||
|
- `X-Token: <你的Token>`
|
||||||
|
- `Content-Type: application/json`
|
||||||
|
- **请求体**:
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"geo": "<cityhash>",
|
||||||
|
"offset": 0,
|
||||||
|
"num": 5
|
||||||
|
}
|
||||||
|
```
|
||||||
|
- **说明**: 与上一个接口一致,按 `num` 控制返回的设备数量。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 网关 - 获取账户下已授权网关
|
||||||
|
- **名称**: 获取账户下已授权EIP网关信息
|
||||||
|
- **方法**: GET
|
||||||
|
- **路径**: `/gateway/list`
|
||||||
|
- **请求头**:
|
||||||
|
- `X-Token: <你的Token>`
|
||||||
|
- `Accept: application/json`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 网关 - 获取网关配置
|
||||||
|
- **名称**: 获取网关配置
|
||||||
|
- **方法**: POST
|
||||||
|
- **路径**: `/gateway/config/get`
|
||||||
|
- **请求头**:
|
||||||
|
- `X-Token: <你的Token>`
|
||||||
|
- `Content-Type: application/json`
|
||||||
|
- `Accept: application/json`
|
||||||
|
- **请求体**:
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"macaddr": "<网关MAC地址>"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 网关 - 设置网关代理链路
|
||||||
|
- **名称**: 配置网关代理链路
|
||||||
|
- **方法**: POST
|
||||||
|
- **路径**: `/gateway/config/set`
|
||||||
|
- **请求头**:
|
||||||
|
- `X-Token: <你的Token>`
|
||||||
|
- `Content-Type: application/json`
|
||||||
|
- `Accept: application/json`
|
||||||
|
- **请求体示例**:
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"macaddr": "<网关MAC地址>",
|
||||||
|
"config": {
|
||||||
|
"id": 1,
|
||||||
|
"rules": [
|
||||||
|
{
|
||||||
|
"table": 1,
|
||||||
|
"enable": true,
|
||||||
|
"edge": ["<设备ID1>"] ,
|
||||||
|
"network": ["<内网IP>"] ,
|
||||||
|
"cityhash": "<cityhash>"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
- **字段说明**:
|
||||||
|
- `id`: 配置版本或规则集标识
|
||||||
|
- `rules[*].table`: 规则表编号
|
||||||
|
- `rules[*].enable`: 是否启用
|
||||||
|
- `rules[*].edge`: 目标边缘设备ID列表
|
||||||
|
- `rules[*].network`: 需要走代理的内网网段或IP列表
|
||||||
|
- `rules[*].cityhash`: 目标城市哈希
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 网关 - 清空网关配置链路
|
||||||
|
- **名称**: 清空网关配置的链路
|
||||||
|
- **方法**: POST
|
||||||
|
- **路径**: `/gateway/config/set`
|
||||||
|
- **请求头**:
|
||||||
|
- `X-Token: <你的Token>`
|
||||||
|
- `Content-Type: application/json`
|
||||||
|
- **请求体示例**:
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"macaddr": "<网关MAC地址>",
|
||||||
|
"config": {
|
||||||
|
"id": 1002,
|
||||||
|
"rules": [
|
||||||
|
{
|
||||||
|
"table": 1,
|
||||||
|
"enable": false,
|
||||||
|
"edge": ["", "", "", ""],
|
||||||
|
"network": [],
|
||||||
|
"cityhash": ""
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
- **说明**: 通过禁用规则并清空 `edge/network/cityhash` 实现清空配置。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 网关 - 获取运行状态
|
||||||
|
- **名称**: 获取网关运行状态
|
||||||
|
- **方法**: POST
|
||||||
|
- **路径**: `/gateway/status`
|
||||||
|
- **请求头**:
|
||||||
|
- `X-Token: <你的Token>`
|
||||||
|
- `Content-Type: application/json`
|
||||||
|
- **请求体**:
|
||||||
|
```json
|
||||||
|
{ "macaddr": "<网关MAC地址>" }
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 通用说明
|
||||||
|
- **认证**: 除 `/client/auth` 外,其余接口均需在请求头添加 `X-Token`。
|
||||||
|
- **内容类型**: JSON 请求统一使用 `Content-Type: application/json`。
|
||||||
|
- **响应**: 均返回 JSON;具体字段以实际接口返回为准。
|
||||||
|
|
||||||
|
|
||||||
391
EIP.postman_collection.json
Normal file
391
EIP.postman_collection.json
Normal file
@ -0,0 +1,391 @@
|
|||||||
|
{
|
||||||
|
"info": {
|
||||||
|
"_postman_id": "2544013c-a940-46e2-a945-8f8318944008",
|
||||||
|
"name": "EIP",
|
||||||
|
"schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json",
|
||||||
|
"_exporter_id": "49242368",
|
||||||
|
"_collection_link": "https://lixin0229-2646365.postman.co/workspace/shihao's-Workspace~249f47cc-12a0-4152-8c64-d21cf5552a6c/collection/49242368-2544013c-a940-46e2-a945-8f8318944008?action=share&source=collection_link&creator=49242368"
|
||||||
|
},
|
||||||
|
"item": [
|
||||||
|
{
|
||||||
|
"name": "API 鉴权",
|
||||||
|
"request": {
|
||||||
|
"method": "POST",
|
||||||
|
"header": [
|
||||||
|
{
|
||||||
|
"key": "Content-Type",
|
||||||
|
"value": "application/json",
|
||||||
|
"type": "text"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"body": {
|
||||||
|
"mode": "raw",
|
||||||
|
"raw": "{\r\n \"username\": \"UID000000151\",\r\n \"password\": \"Hnz3HuWCqEX9vdzpBodY304T6CEJJJ\"\r\n}",
|
||||||
|
"options": {
|
||||||
|
"raw": {
|
||||||
|
"language": "json"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"url": {
|
||||||
|
"raw": "https://smart.jdbox.xyz:58001/client/auth",
|
||||||
|
"protocol": "https",
|
||||||
|
"host": [
|
||||||
|
"smart",
|
||||||
|
"jdbox",
|
||||||
|
"xyz"
|
||||||
|
],
|
||||||
|
"port": "58001",
|
||||||
|
"path": [
|
||||||
|
"client",
|
||||||
|
"auth"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"response": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "获取代理链路节点",
|
||||||
|
"protocolProfileBehavior": {
|
||||||
|
"disableBodyPruning": true
|
||||||
|
},
|
||||||
|
"request": {
|
||||||
|
"method": "GET",
|
||||||
|
"header": [
|
||||||
|
{
|
||||||
|
"key": "X-Token",
|
||||||
|
"value": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJhdWQiOiJqZGJveCIsImV4cCI6MTc2MTAxMTc0MywianRpIjoiY2VhZmVlOTgtMzY5ZC00OTUzLThiZTYtMDVhYjA3NGNmMTZmIiwiaWF0IjoxNzYwNDA2OTQzLCJpc3MiOiJVSUQwMDAwMDAxNTEiLCJuYmYiOjE3NjA0MDY5NDMsInN1YiI6ImpkYm94LXNtYXJ0LWxpbmsifQ._LpBTBRAzOPm8Y62-jR7gCtsgSc-tMoxt06wj0rv7b8",
|
||||||
|
"type": "text"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "Accept",
|
||||||
|
"value": "application/json",
|
||||||
|
"type": "text"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"body": {
|
||||||
|
"mode": "raw",
|
||||||
|
"raw": "",
|
||||||
|
"options": {
|
||||||
|
"raw": {
|
||||||
|
"language": "json"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"url": {
|
||||||
|
"raw": "https://smart.jdbox.xyz:58001/edge/city",
|
||||||
|
"protocol": "https",
|
||||||
|
"host": [
|
||||||
|
"smart",
|
||||||
|
"jdbox",
|
||||||
|
"xyz"
|
||||||
|
],
|
||||||
|
"port": "58001",
|
||||||
|
"path": [
|
||||||
|
"edge",
|
||||||
|
"city"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"response": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "cityhash获取代理链路节点信息",
|
||||||
|
"request": {
|
||||||
|
"method": "POST",
|
||||||
|
"header": [
|
||||||
|
{
|
||||||
|
"key": "Content-Type",
|
||||||
|
"value": "application/json",
|
||||||
|
"type": "text"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "X-Token",
|
||||||
|
"value": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJhdWQiOiJqZGJveCIsImV4cCI6MTc2MTAxMTc0MywianRpIjoiY2VhZmVlOTgtMzY5ZC00OTUzLThiZTYtMDVhYjA3NGNmMTZmIiwiaWF0IjoxNzYwNDA2OTQzLCJpc3MiOiJVSUQwMDAwMDAxNTEiLCJuYmYiOjE3NjA0MDY5NDMsInN1YiI6ImpkYm94LXNtYXJ0LWxpbmsifQ._LpBTBRAzOPm8Y62-jR7gCtsgSc-tMoxt06wj0rv7b8",
|
||||||
|
"type": "text"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"body": {
|
||||||
|
"mode": "raw",
|
||||||
|
"raw": "{\r\n \"geo\": \"093326b72938f2b2d636ad1a392aaab4978e61694aebaf7a88cb9fb85727165c\",\r\n \"offset\": 0,\r\n \"num\": 200\r\n}",
|
||||||
|
"options": {
|
||||||
|
"raw": {
|
||||||
|
"language": "json"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"url": {
|
||||||
|
"raw": "https://smart.jdbox.xyz:58001/edge/device",
|
||||||
|
"protocol": "https",
|
||||||
|
"host": [
|
||||||
|
"smart",
|
||||||
|
"jdbox",
|
||||||
|
"xyz"
|
||||||
|
],
|
||||||
|
"port": "58001",
|
||||||
|
"path": [
|
||||||
|
"edge",
|
||||||
|
"device"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"response": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "查询指定城市、指定数量代理信息",
|
||||||
|
"request": {
|
||||||
|
"method": "POST",
|
||||||
|
"header": [
|
||||||
|
{
|
||||||
|
"key": "Content-Type",
|
||||||
|
"value": "application/json",
|
||||||
|
"type": "text"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "X-Token",
|
||||||
|
"value": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJhdWQiOiJqZGJveCIsImV4cCI6MTc2MTAxMTc0MywianRpIjoiY2VhZmVlOTgtMzY5ZC00OTUzLThiZTYtMDVhYjA3NGNmMTZmIiwiaWF0IjoxNzYwNDA2OTQzLCJpc3MiOiJVSUQwMDAwMDAxNTEiLCJuYmYiOjE3NjA0MDY5NDMsInN1YiI6ImpkYm94LXNtYXJ0LWxpbmsifQ._LpBTBRAzOPm8Y62-jR7gCtsgSc-tMoxt06wj0rv7b8",
|
||||||
|
"type": "text"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"body": {
|
||||||
|
"mode": "raw",
|
||||||
|
"raw": "{\r\n \"geo\": \"807deb1e7e56a8926342fc7a2b8dd0f4cfad0218fc14dbd2834613340bf87db1\",\r\n \"offset\": 0,\r\n \"num\": 5\r\n}",
|
||||||
|
"options": {
|
||||||
|
"raw": {
|
||||||
|
"language": "json"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"url": {
|
||||||
|
"raw": "https://smart.jdbox.xyz:58001/edge/device",
|
||||||
|
"protocol": "https",
|
||||||
|
"host": [
|
||||||
|
"smart",
|
||||||
|
"jdbox",
|
||||||
|
"xyz"
|
||||||
|
],
|
||||||
|
"port": "58001",
|
||||||
|
"path": [
|
||||||
|
"edge",
|
||||||
|
"device"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"response": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "获取账户下已授权EIP网关信息",
|
||||||
|
"request": {
|
||||||
|
"method": "GET",
|
||||||
|
"header": [
|
||||||
|
{
|
||||||
|
"key": "X-Token",
|
||||||
|
"value": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJhdWQiOiJqZGJveCIsImV4cCI6MTc2MTAxMTc0MywianRpIjoiY2VhZmVlOTgtMzY5ZC00OTUzLThiZTYtMDVhYjA3NGNmMTZmIiwiaWF0IjoxNzYwNDA2OTQzLCJpc3MiOiJVSUQwMDAwMDAxNTEiLCJuYmYiOjE3NjA0MDY5NDMsInN1YiI6ImpkYm94LXNtYXJ0LWxpbmsifQ._LpBTBRAzOPm8Y62-jR7gCtsgSc-tMoxt06wj0rv7b8",
|
||||||
|
"type": "text"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "Accept",
|
||||||
|
"value": "application/json",
|
||||||
|
"type": "text"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"url": {
|
||||||
|
"raw": "https://smart.jdbox.xyz:58001/gateway/list",
|
||||||
|
"protocol": "https",
|
||||||
|
"host": [
|
||||||
|
"smart",
|
||||||
|
"jdbox",
|
||||||
|
"xyz"
|
||||||
|
],
|
||||||
|
"port": "58001",
|
||||||
|
"path": [
|
||||||
|
"gateway",
|
||||||
|
"list"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"response": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "获取网关配置",
|
||||||
|
"request": {
|
||||||
|
"method": "POST",
|
||||||
|
"header": [
|
||||||
|
{
|
||||||
|
"key": "X-Token",
|
||||||
|
"value": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJhdWQiOiJqZGJveCIsImV4cCI6MTc2MTAxMTc0MywianRpIjoiY2VhZmVlOTgtMzY5ZC00OTUzLThiZTYtMDVhYjA3NGNmMTZmIiwiaWF0IjoxNzYwNDA2OTQzLCJpc3MiOiJVSUQwMDAwMDAxNTEiLCJuYmYiOjE3NjA0MDY5NDMsInN1YiI6ImpkYm94LXNtYXJ0LWxpbmsifQ._LpBTBRAzOPm8Y62-jR7gCtsgSc-tMoxt06wj0rv7b8",
|
||||||
|
"type": "text"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "Content-Type",
|
||||||
|
"value": "application/json",
|
||||||
|
"type": "text"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "Accept",
|
||||||
|
"value": "application/json",
|
||||||
|
"type": "text"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"body": {
|
||||||
|
"mode": "raw",
|
||||||
|
"raw": "{\r\n \"macaddr\": \"00163E1A2F0E\"\r\n}",
|
||||||
|
"options": {
|
||||||
|
"raw": {
|
||||||
|
"language": "json"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"url": {
|
||||||
|
"raw": "https://smart.jdbox.xyz:58001/gateway/config/get",
|
||||||
|
"protocol": "https",
|
||||||
|
"host": [
|
||||||
|
"smart",
|
||||||
|
"jdbox",
|
||||||
|
"xyz"
|
||||||
|
],
|
||||||
|
"port": "58001",
|
||||||
|
"path": [
|
||||||
|
"gateway",
|
||||||
|
"config",
|
||||||
|
"get"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"response": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "配置网关代理链路",
|
||||||
|
"request": {
|
||||||
|
"method": "POST",
|
||||||
|
"header": [
|
||||||
|
{
|
||||||
|
"key": "X-Token",
|
||||||
|
"value": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJhdWQiOiJqZGJveCIsImV4cCI6MTc2MTAxMTc0MywianRpIjoiY2VhZmVlOTgtMzY5ZC00OTUzLThiZTYtMDVhYjA3NGNmMTZmIiwiaWF0IjoxNzYwNDA2OTQzLCJpc3MiOiJVSUQwMDAwMDAxNTEiLCJuYmYiOjE3NjA0MDY5NDMsInN1YiI6ImpkYm94LXNtYXJ0LWxpbmsifQ._LpBTBRAzOPm8Y62-jR7gCtsgSc-tMoxt06wj0rv7b8",
|
||||||
|
"type": "text"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "Content-Type",
|
||||||
|
"value": "application/json",
|
||||||
|
"type": "text"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "Accept",
|
||||||
|
"value": "application/json",
|
||||||
|
"type": "text"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"body": {
|
||||||
|
"mode": "raw",
|
||||||
|
"raw": "{\r\n \"macaddr\": \"00163E1A2F0E\",\r\n \"config\": {\r\n \"id\": 1,\r\n \"rules\": [\r\n { \"table\": 1, \"enable\": true, \"edge\": [\"DCD87C557EC3\"], \"network\": [\"172.30.168.2\"], \"cityhash\": \"20c5485e25a2f55681fa38f3598b34a6876e60c3188e4b5174042e4e390a1808\" }\r\n ]\r\n }\r\n}",
|
||||||
|
"options": {
|
||||||
|
"raw": {
|
||||||
|
"language": "json"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"url": {
|
||||||
|
"raw": "https://smart.jdbox.xyz:58001/gateway/config/set",
|
||||||
|
"protocol": "https",
|
||||||
|
"host": [
|
||||||
|
"smart",
|
||||||
|
"jdbox",
|
||||||
|
"xyz"
|
||||||
|
],
|
||||||
|
"port": "58001",
|
||||||
|
"path": [
|
||||||
|
"gateway",
|
||||||
|
"config",
|
||||||
|
"set"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"response": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "清空网关配置的链路",
|
||||||
|
"request": {
|
||||||
|
"method": "POST",
|
||||||
|
"header": [
|
||||||
|
{
|
||||||
|
"key": "X-Token",
|
||||||
|
"value": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJhdWQiOiJqZGJveCIsImV4cCI6MTc2MTAxMTc0MywianRpIjoiY2VhZmVlOTgtMzY5ZC00OTUzLThiZTYtMDVhYjA3NGNmMTZmIiwiaWF0IjoxNzYwNDA2OTQzLCJpc3MiOiJVSUQwMDAwMDAxNTEiLCJuYmYiOjE3NjA0MDY5NDMsInN1YiI6ImpkYm94LXNtYXJ0LWxpbmsifQ._LpBTBRAzOPm8Y62-jR7gCtsgSc-tMoxt06wj0rv7b8",
|
||||||
|
"type": "text"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "Content-Type",
|
||||||
|
"value": "application/json",
|
||||||
|
"type": "text"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"body": {
|
||||||
|
"mode": "raw",
|
||||||
|
"raw": "{\r\n \"macaddr\": \"00163E1A2F0E\",\r\n \"config\": {\r\n \"id\": 1002,\r\n \"rules\": [\r\n {\r\n \"table\": 1,\r\n \"enable\": false,\r\n \"edge\": [\"\", \"\", \"\", \"\"],\r\n \"network\": [],\r\n \"cityhash\": \"\"\r\n }\r\n ]\r\n }\r\n}\r\n",
|
||||||
|
"options": {
|
||||||
|
"raw": {
|
||||||
|
"language": "json"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"url": {
|
||||||
|
"raw": "https://smart.jdbox.xyz:58001/gateway/config/set",
|
||||||
|
"protocol": "https",
|
||||||
|
"host": [
|
||||||
|
"smart",
|
||||||
|
"jdbox",
|
||||||
|
"xyz"
|
||||||
|
],
|
||||||
|
"port": "58001",
|
||||||
|
"path": [
|
||||||
|
"gateway",
|
||||||
|
"config",
|
||||||
|
"set"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"response": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "获取网关运行状态",
|
||||||
|
"request": {
|
||||||
|
"method": "POST",
|
||||||
|
"header": [
|
||||||
|
{
|
||||||
|
"key": "X-Token",
|
||||||
|
"value": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJhdWQiOiJqZGJveCIsImV4cCI6MTc2MTAxMTc0MywianRpIjoiY2VhZmVlOTgtMzY5ZC00OTUzLThiZTYtMDVhYjA3NGNmMTZmIiwiaWF0IjoxNzYwNDA2OTQzLCJpc3MiOiJVSUQwMDAwMDAxNTEiLCJuYmYiOjE3NjA0MDY5NDMsInN1YiI6ImpkYm94LXNtYXJ0LWxpbmsifQ._LpBTBRAzOPm8Y62-jR7gCtsgSc-tMoxt06wj0rv7b8",
|
||||||
|
"type": "text"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "Content-Type",
|
||||||
|
"value": "application/json",
|
||||||
|
"type": "text"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"body": {
|
||||||
|
"mode": "raw",
|
||||||
|
"raw": "{ \"macaddr\": \"00163E1A2F0E\" }",
|
||||||
|
"options": {
|
||||||
|
"raw": {
|
||||||
|
"language": "json"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"url": {
|
||||||
|
"raw": "https://smart.jdbox.xyz:58001/gateway/status",
|
||||||
|
"protocol": "https",
|
||||||
|
"host": [
|
||||||
|
"smart",
|
||||||
|
"jdbox",
|
||||||
|
"xyz"
|
||||||
|
],
|
||||||
|
"port": "58001",
|
||||||
|
"path": [
|
||||||
|
"gateway",
|
||||||
|
"status"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"response": []
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
17
app/.env
Normal file
17
app/.env
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
# EIP 鉴权(来自你的 Postman 集合)
|
||||||
|
EIP_USERNAME=UID000000151
|
||||||
|
EIP_PASSWORD=Hnz3HuWCqEX9vdzpBodY304T6CEJJJ
|
||||||
|
|
||||||
|
# EIP 基础配置
|
||||||
|
EIP_BASE_URL=https://smart.jdbox.xyz:58001
|
||||||
|
EIP_GATEWAY_MAC=00163E1A2F0E
|
||||||
|
|
||||||
|
# 轮换默认参数(如不指定,请在请求体传递 cityhash)
|
||||||
|
EIP_DEFAULT_CITYHASH=
|
||||||
|
EIP_DEFAULT_NUM=10
|
||||||
|
|
||||||
|
# Redis 连接
|
||||||
|
REDIS_URL=redis://localhost:6379/0
|
||||||
|
|
||||||
|
# 日志级别
|
||||||
|
LOG_LEVEL=INFO
|
||||||
2
app/__init__.py
Normal file
2
app/__init__.py
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
# Make app a package
|
||||||
|
|
||||||
7
app/__main__.py
Normal file
7
app/__main__.py
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
import uvicorn
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
uvicorn.run("app.main:app", host="0.0.0.0", port=8000, reload=True)
|
||||||
|
|
||||||
|
|
||||||
22
app/config.py
Normal file
22
app/config.py
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
import os
|
||||||
|
from pydantic import BaseModel
|
||||||
|
from dotenv import load_dotenv
|
||||||
|
|
||||||
|
|
||||||
|
load_dotenv()
|
||||||
|
|
||||||
|
|
||||||
|
class Settings(BaseModel):
|
||||||
|
eip_base_url: str = os.getenv("EIP_BASE_URL", "https://smart.jdbox.xyz:58001")
|
||||||
|
eip_username: str = os.getenv("EIP_USERNAME", "")
|
||||||
|
eip_password: str = os.getenv("EIP_PASSWORD", "")
|
||||||
|
eip_gateway_mac: str = os.getenv("EIP_GATEWAY_MAC", "")
|
||||||
|
eip_default_cityhash: str = os.getenv("EIP_DEFAULT_CITYHASH", "")
|
||||||
|
eip_default_num: int = int(os.getenv("EIP_DEFAULT_NUM", "10"))
|
||||||
|
redis_url: str = os.getenv("REDIS_URL", "redis://localhost:6379/0")
|
||||||
|
log_level: str = os.getenv("LOG_LEVEL", "INFO")
|
||||||
|
|
||||||
|
|
||||||
|
settings = Settings()
|
||||||
|
|
||||||
|
|
||||||
98
app/eip_client.py
Normal file
98
app/eip_client.py
Normal file
@ -0,0 +1,98 @@
|
|||||||
|
import time
|
||||||
|
from typing import Any, Dict, List, Optional
|
||||||
|
|
||||||
|
import httpx
|
||||||
|
from pydantic import BaseModel
|
||||||
|
from tenacity import retry, stop_after_attempt, wait_exponential
|
||||||
|
|
||||||
|
from .config import settings
|
||||||
|
|
||||||
|
|
||||||
|
class AuthResponse(BaseModel):
|
||||||
|
token: str
|
||||||
|
exp: Optional[int] = None # epoch seconds
|
||||||
|
|
||||||
|
|
||||||
|
class EipClient:
|
||||||
|
def __init__(self, base_url: str, username: str, password: str) -> None:
|
||||||
|
self.base_url = base_url.rstrip("/")
|
||||||
|
self.username = username
|
||||||
|
self.password = password
|
||||||
|
self._token: Optional[str] = None
|
||||||
|
self._token_exp: Optional[int] = None
|
||||||
|
self._client = httpx.Client(timeout=15.0)
|
||||||
|
|
||||||
|
def _is_token_valid(self) -> bool:
|
||||||
|
if not self._token or not self._token_exp:
|
||||||
|
return False
|
||||||
|
# refresh 60s earlier
|
||||||
|
return time.time() < (self._token_exp - 60)
|
||||||
|
|
||||||
|
@retry(stop=stop_after_attempt(3), wait=wait_exponential(multiplier=0.5, min=0.5, max=2))
|
||||||
|
def authenticate(self) -> AuthResponse:
|
||||||
|
url = f"{self.base_url}/client/auth"
|
||||||
|
resp = self._client.post(url, json={"username": self.username, "password": self.password})
|
||||||
|
resp.raise_for_status()
|
||||||
|
data = resp.json()
|
||||||
|
# 尝试从响应中提取 exp;如果没有,设置 1 小时后过期
|
||||||
|
exp = data.get("exp") or int(time.time()) + 3600
|
||||||
|
token = data.get("token") or data.get("X-Token") or data.get("access_token")
|
||||||
|
if not token:
|
||||||
|
# 兼容后端直接返回字符串 token 的情况
|
||||||
|
if isinstance(data, str) and data:
|
||||||
|
token = data
|
||||||
|
else:
|
||||||
|
raise ValueError("Auth response missing token")
|
||||||
|
self._token = token
|
||||||
|
self._token_exp = exp
|
||||||
|
return AuthResponse(token=token, exp=exp)
|
||||||
|
|
||||||
|
def _get_headers(self) -> Dict[str, str]:
|
||||||
|
if not self._is_token_valid():
|
||||||
|
self.authenticate()
|
||||||
|
assert self._token
|
||||||
|
return {"X-Token": self._token, "Accept": "application/json"}
|
||||||
|
|
||||||
|
@retry(stop=stop_after_attempt(3), wait=wait_exponential(multiplier=0.5, min=0.5, max=2))
|
||||||
|
def list_cities(self) -> List[Dict[str, Any]]:
|
||||||
|
url = f"{self.base_url}/edge/city"
|
||||||
|
resp = self._client.get(url, headers=self._get_headers())
|
||||||
|
resp.raise_for_status()
|
||||||
|
data = resp.json()
|
||||||
|
return data if isinstance(data, list) else data.get("data", [])
|
||||||
|
|
||||||
|
@retry(stop=stop_after_attempt(3), wait=wait_exponential(multiplier=0.5, min=0.5, max=2))
|
||||||
|
def list_devices(self, geo: str, offset: int, num: int) -> List[Dict[str, Any]]:
|
||||||
|
url = f"{self.base_url}/edge/device"
|
||||||
|
payload = {"geo": geo, "offset": offset, "num": num}
|
||||||
|
resp = self._client.post(url, headers=self._get_headers(), json=payload)
|
||||||
|
resp.raise_for_status()
|
||||||
|
data = resp.json()
|
||||||
|
return data if isinstance(data, list) else data.get("data", [])
|
||||||
|
|
||||||
|
@retry(stop=stop_after_attempt(3), wait=wait_exponential(multiplier=0.5, min=0.5, max=2))
|
||||||
|
def gateway_config_get(self, macaddr: str) -> Dict[str, Any]:
|
||||||
|
url = f"{self.base_url}/gateway/config/get"
|
||||||
|
resp = self._client.post(url, headers=self._get_headers(), json={"macaddr": macaddr})
|
||||||
|
resp.raise_for_status()
|
||||||
|
return resp.json()
|
||||||
|
|
||||||
|
@retry(stop=stop_after_attempt(3), wait=wait_exponential(multiplier=0.5, min=0.5, max=2))
|
||||||
|
def gateway_config_set(self, macaddr: str, config: Dict[str, Any]) -> Dict[str, Any]:
|
||||||
|
url = f"{self.base_url}/gateway/config/set"
|
||||||
|
payload = {"macaddr": macaddr, "config": config}
|
||||||
|
resp = self._client.post(url, headers=self._get_headers(), json=payload)
|
||||||
|
resp.raise_for_status()
|
||||||
|
return resp.json()
|
||||||
|
|
||||||
|
@retry(stop=stop_after_attempt(3), wait=wait_exponential(multiplier=0.5, min=0.5, max=2))
|
||||||
|
def gateway_status(self, macaddr: str) -> Dict[str, Any]:
|
||||||
|
url = f"{self.base_url}/gateway/status"
|
||||||
|
resp = self._client.post(url, headers=self._get_headers(), json={"macaddr": macaddr})
|
||||||
|
resp.raise_for_status()
|
||||||
|
return resp.json()
|
||||||
|
|
||||||
|
|
||||||
|
client_singleton = EipClient(settings.eip_base_url, settings.eip_username, settings.eip_password)
|
||||||
|
|
||||||
|
|
||||||
15
app/main.py
Normal file
15
app/main.py
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
from fastapi import FastAPI
|
||||||
|
from .config import settings
|
||||||
|
from .routers.proxy import router as proxy_router
|
||||||
|
|
||||||
|
app = FastAPI(title="EIP Rotation Service")
|
||||||
|
|
||||||
|
|
||||||
|
@app.get("/health")
|
||||||
|
def health_check():
|
||||||
|
return {"status": "ok"}
|
||||||
|
|
||||||
|
|
||||||
|
app.include_router(proxy_router, prefix="/proxy", tags=["proxy"])
|
||||||
|
|
||||||
|
|
||||||
49
app/redis_store.py
Normal file
49
app/redis_store.py
Normal file
@ -0,0 +1,49 @@
|
|||||||
|
import datetime
|
||||||
|
from typing import Iterable, Optional
|
||||||
|
|
||||||
|
import redis
|
||||||
|
|
||||||
|
from .config import settings
|
||||||
|
|
||||||
|
|
||||||
|
class RedisStore:
|
||||||
|
def __init__(self, url: str) -> None:
|
||||||
|
self._r = redis.Redis.from_url(url, decode_responses=True)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _today_key(prefix: str) -> str:
|
||||||
|
today = datetime.datetime.utcnow().strftime("%Y%m%d")
|
||||||
|
return f"eip:{prefix}:{today}"
|
||||||
|
|
||||||
|
def add_used_ip_today(self, ip: str) -> None:
|
||||||
|
key = self._today_key("used")
|
||||||
|
self._r.sadd(key, ip)
|
||||||
|
# 设置 2 天过期,跨时区冗余
|
||||||
|
self._r.expire(key, 172800)
|
||||||
|
|
||||||
|
def is_ip_used_today(self, ip: str) -> bool:
|
||||||
|
key = self._today_key("used")
|
||||||
|
return bool(self._r.sismember(key, ip))
|
||||||
|
|
||||||
|
def get_used_count_today(self) -> int:
|
||||||
|
key = self._today_key("used")
|
||||||
|
return int(self._r.scard(key))
|
||||||
|
|
||||||
|
def set_current(self, ip: str, edge_id: Optional[str]) -> None:
|
||||||
|
self._r.hset("eip:current", mapping={"ip": ip, "edge": edge_id or ""})
|
||||||
|
|
||||||
|
def get_current(self) -> dict:
|
||||||
|
data = self._r.hgetall("eip:current")
|
||||||
|
return data or {}
|
||||||
|
|
||||||
|
def cache_candidates(self, items: Iterable[str]) -> None:
|
||||||
|
key = self._today_key("candidates")
|
||||||
|
if items:
|
||||||
|
self._r.delete(key)
|
||||||
|
self._r.rpush(key, *list(items))
|
||||||
|
self._r.expire(key, 172800)
|
||||||
|
|
||||||
|
|
||||||
|
store_singleton = RedisStore(settings.redis_url)
|
||||||
|
|
||||||
|
|
||||||
77
app/rotation_service.py
Normal file
77
app/rotation_service.py
Normal file
@ -0,0 +1,77 @@
|
|||||||
|
from typing import Any, Dict, List, Optional, Tuple
|
||||||
|
|
||||||
|
from .config import settings
|
||||||
|
from .eip_client import client_singleton as eip
|
||||||
|
from .redis_store import store_singleton as kv
|
||||||
|
|
||||||
|
|
||||||
|
def _extract_device_ip_and_id(device: Dict[str, Any]) -> Tuple[Optional[str], Optional[str]]:
|
||||||
|
# 由于未知后端返回结构,尽可能容错地提取
|
||||||
|
ip = (
|
||||||
|
device.get("ip")
|
||||||
|
or device.get("public_ip")
|
||||||
|
or device.get("eip")
|
||||||
|
or device.get("addr")
|
||||||
|
or device.get("address")
|
||||||
|
)
|
||||||
|
edge_id = device.get("id") or device.get("edge") or device.get("mac") or device.get("device_id")
|
||||||
|
# 若 edge 是数组或对象,尝试取可打印的第一项
|
||||||
|
if isinstance(edge_id, list) and edge_id:
|
||||||
|
edge_id = edge_id[0]
|
||||||
|
if isinstance(edge_id, dict):
|
||||||
|
edge_id = edge_id.get("id") or edge_id.get("mac") or edge_id.get("edge")
|
||||||
|
return (str(ip) if ip else None, str(edge_id) if edge_id else None)
|
||||||
|
|
||||||
|
|
||||||
|
def select_unused_ip(devices: List[Dict[str, Any]]) -> Tuple[Optional[str], Optional[str]]:
|
||||||
|
for d in devices:
|
||||||
|
ip, edge_id = _extract_device_ip_and_id(d)
|
||||||
|
if not ip:
|
||||||
|
# 没有可识别的 IP,跳过
|
||||||
|
continue
|
||||||
|
if not kv.is_ip_used_today(ip):
|
||||||
|
return ip, edge_id
|
||||||
|
return None, None
|
||||||
|
|
||||||
|
|
||||||
|
def apply_gateway_route(edge_id: Optional[str], ip: str) -> Dict[str, Any]:
|
||||||
|
# 将选中的 edge 配置到网关规则中
|
||||||
|
rule = {
|
||||||
|
"table": 1,
|
||||||
|
"enable": True,
|
||||||
|
"edge": [edge_id] if edge_id else [],
|
||||||
|
"network": [],
|
||||||
|
"cityhash": settings.eip_default_cityhash or "",
|
||||||
|
}
|
||||||
|
config = {"id": 1, "rules": [rule]}
|
||||||
|
return eip.gateway_config_set(settings.eip_gateway_mac, config)
|
||||||
|
|
||||||
|
|
||||||
|
def rotate(cityhash: Optional[str] = None, num: Optional[int] = None) -> Dict[str, Any]:
|
||||||
|
geo = cityhash or settings.eip_default_cityhash
|
||||||
|
if not geo:
|
||||||
|
raise ValueError("cityhash 不能为空,请在请求中或环境变量中提供")
|
||||||
|
n = int(num or settings.eip_default_num or 10)
|
||||||
|
devices = eip.list_devices(geo=geo, offset=0, num=n)
|
||||||
|
ip, edge_id = select_unused_ip(devices)
|
||||||
|
if not ip:
|
||||||
|
return {"changed": False, "reason": "没有可用且今天未使用的 IP"}
|
||||||
|
|
||||||
|
_ = apply_gateway_route(edge_id=edge_id, ip=ip)
|
||||||
|
kv.add_used_ip_today(ip)
|
||||||
|
kv.set_current(ip=ip, edge_id=edge_id)
|
||||||
|
status = eip.gateway_status(settings.eip_gateway_mac)
|
||||||
|
return {"changed": True, "ip": ip, "edge": edge_id, "status": status}
|
||||||
|
|
||||||
|
|
||||||
|
def status() -> Dict[str, Any]:
|
||||||
|
cur = kv.get_current()
|
||||||
|
used_count = kv.get_used_count_today()
|
||||||
|
gw = {}
|
||||||
|
try:
|
||||||
|
gw = eip.gateway_status(settings.eip_gateway_mac)
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
return {"current": cur, "used_today": used_count, "gateway": gw}
|
||||||
|
|
||||||
|
|
||||||
27
app/routers/proxy.py
Normal file
27
app/routers/proxy.py
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
from typing import Optional
|
||||||
|
|
||||||
|
from fastapi import APIRouter
|
||||||
|
from pydantic import BaseModel
|
||||||
|
|
||||||
|
from ..rotation_service import rotate as rotate_impl, status as status_impl
|
||||||
|
|
||||||
|
|
||||||
|
router = APIRouter()
|
||||||
|
|
||||||
|
|
||||||
|
class RotateRequest(BaseModel):
|
||||||
|
cityhash: Optional[str] = None
|
||||||
|
num: Optional[int] = None
|
||||||
|
|
||||||
|
|
||||||
|
@router.post("/rotate")
|
||||||
|
def rotate(req: RotateRequest):
|
||||||
|
result = rotate_impl(cityhash=req.cityhash, num=req.num)
|
||||||
|
return result
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("/status")
|
||||||
|
def get_status():
|
||||||
|
return status_impl()
|
||||||
|
|
||||||
|
|
||||||
8
requirements.txt
Normal file
8
requirements.txt
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
fastapi==0.115.0
|
||||||
|
uvicorn[standard]==0.30.6
|
||||||
|
httpx==0.27.2
|
||||||
|
pydantic==2.9.2
|
||||||
|
python-dotenv==1.0.1
|
||||||
|
redis==5.0.8
|
||||||
|
tenacity==9.0.0
|
||||||
|
|
||||||
Loading…
x
Reference in New Issue
Block a user