mirror of
https://github.com/cnotch/ipchub.git
synced 2025-09-26 19:41:18 +08:00
add service
This commit is contained in:
389
docs/apis.md
Executable file
389
docs/apis.md
Executable file
@@ -0,0 +1,389 @@
|
||||
## 1 系统API
|
||||
系统API无需登录,可以匿名访问
|
||||
### 1.1 服务器信息查询
|
||||
GET /api/v1/server
|
||||
#### 1.1.1 参数和响应
|
||||
|
||||
+ Body参数
|
||||
无
|
||||
+ 查询参数
|
||||
无
|
||||
+ 响应(200)
|
||||
|
||||
项目 | 类型 | 说明
|
||||
-|-|-
|
||||
vendor|string| 软件提供商 |
|
||||
name | string | 服务名称 |
|
||||
version | string | 服务版本 |
|
||||
os | string | 服务运行的平台 |
|
||||
arch | string | 服务运行的平台架构 |
|
||||
start_on | string(timestamp) | 服务启动时间(RFC3339Nano 格式) |
|
||||
duration | string | 持续时间 |
|
||||
|
||||
#### 1.1.2 示例
|
||||
curl 示例:
|
||||
``` shell
|
||||
curl http://locahost:1554/api/v1/server
|
||||
```
|
||||
|
||||
响应:
|
||||
``` json
|
||||
{
|
||||
"vendor": "CAOHONGJU",
|
||||
"name": "ipchub",
|
||||
"version": "V0.8.0",
|
||||
"os": "Darwin",
|
||||
"arch": "AMD64",
|
||||
"start_on": "2019-07-15T14:02:16.638804+08:00",
|
||||
"duration": "23.319603373s"
|
||||
}
|
||||
```
|
||||
|
||||
### 1.2 运行信息查询
|
||||
GET /api/v1/runtime?extra={0|1}
|
||||
#### 1.2.1 参数和响应
|
||||
+ Body参数
|
||||
无
|
||||
+ 查询参数
|
||||
|
||||
项目 | 类型 | 说明及示例
|
||||
-|-|-
|
||||
extra | number | 0 或 1 ;如果=1响应会包含额外信息|
|
||||
+ 响应(200)
|
||||
|
||||
获取运行是信息,extra=1返回额外信息
|
||||
项目 | 类型 | 说明及示例
|
||||
-|-|-
|
||||
on | timestamp | 采集时间(RFC3339Nano 格式)|
|
||||
proc | object | 进程相关的统计信息 |
|
||||
proc.cpu | number |cpu使用情况|
|
||||
proc.priv | number |物理内存使用情况(kb)|
|
||||
proc.cpu | number |虚拟内存使用情况(kb)|
|
||||
proc.uptime|number | 进程运行时间(s)|
|
||||
streams | object | 流信息 |
|
||||
streams.sc| number | 流媒体源数量 |
|
||||
streams.cc|number|流媒体消费者数量 |
|
||||
rtsp| object| RTSP连接信息 |
|
||||
rtsp.total|number|总链接数 |
|
||||
rtsp.active | number | 活跃连接数 |
|
||||
wsp| object| WSP连接信息 |
|
||||
wsp.total|number|总链接数 |
|
||||
wsp.active | number | 活跃连接数 |
|
||||
flv| object| flv连接信息 |
|
||||
flv.total|number|总链接数 |
|
||||
flv.active | number | 活跃连接数 |
|
||||
extra | object | 额外信息 |
|
||||
|
||||
#### 1.2.1 示例
|
||||
curl 示例一:
|
||||
```
|
||||
curl http://localhost:1554/api/v1/runtime
|
||||
```
|
||||
响应:
|
||||
``` json
|
||||
{
|
||||
"on": "2019-07-15T14:05:20.524916+08:00",
|
||||
"proc": {
|
||||
"cpu": 0,
|
||||
"priv": 6876,
|
||||
"virt": 2545968,
|
||||
"uptime": 183
|
||||
},
|
||||
"streams": {
|
||||
"sources": 0,
|
||||
"consumers": 0
|
||||
},
|
||||
"rtsp": {
|
||||
"total": 0,
|
||||
"active": 0
|
||||
},
|
||||
"wsp": {
|
||||
"total": 0,
|
||||
"active": 0
|
||||
},
|
||||
"rtmp": {
|
||||
"total": 0,
|
||||
"active": 0
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
示例二:
|
||||
```
|
||||
curl http://localhost:1554/api/v1/runtime?extra=1
|
||||
```
|
||||
响应:
|
||||
``` json
|
||||
{
|
||||
"on": "2019-07-15T14:06:41.012543+08:00",
|
||||
"proc": {
|
||||
"cpu": 0,
|
||||
"priv": 6912,
|
||||
"virt": 2545968,
|
||||
"uptime": 264
|
||||
},
|
||||
"streams": {
|
||||
"sources": 0,
|
||||
"consumers": 0
|
||||
},
|
||||
"rtsp": {
|
||||
"total": 0,
|
||||
"active": 0
|
||||
},
|
||||
"wsp": {
|
||||
"total": 0,
|
||||
"active": 0
|
||||
},
|
||||
"rtmp": {
|
||||
"total": 0,
|
||||
"active": 0
|
||||
},
|
||||
"extra": {
|
||||
"heap": {
|
||||
"inuse": 1768,
|
||||
"sys": 64992,
|
||||
"alloc": 641,
|
||||
"idle": 63224,
|
||||
"released": 0,
|
||||
"objects": 3988
|
||||
},
|
||||
"mcache": {
|
||||
"inuse": 13,
|
||||
"sys": 16
|
||||
},
|
||||
"mspan": {
|
||||
"inuse": 28,
|
||||
"sys": 32
|
||||
},
|
||||
"stack": {
|
||||
"inuse": 544,
|
||||
"sys": 544
|
||||
},
|
||||
"gc": {
|
||||
"cpu": 0,
|
||||
"sys": 2182
|
||||
},
|
||||
"go": {
|
||||
"count": 11,
|
||||
"procs": 8,
|
||||
"sys": 70462,
|
||||
"alloc": 641
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 1.3 登录
|
||||
POST api/v1/login
|
||||
|
||||
#### 1.3.1 参数和响应
|
||||
+ Body 参数
|
||||
|
||||
项目 | 类型 | 说明及示例
|
||||
-|-|-
|
||||
username | string | 用户名称|
|
||||
password | string | 密码 |
|
||||
+ 查询参数
|
||||
无
|
||||
+ 响应(200)
|
||||
|
||||
项目 | 类型 | 说明及示例
|
||||
-|-|-
|
||||
access_token | string | 访问令牌|
|
||||
refresh_token | string | 刷新令牌 |
|
||||
|
||||
#### 1.3.2 示例
|
||||
curl 示例:
|
||||
```
|
||||
curl -H "Content-Type: application/json" -X POST --data '{"username":"admin","password":"admin"}' http://localhost:1554/api/v1/login
|
||||
```
|
||||
|
||||
响应:
|
||||
``` json
|
||||
{
|
||||
"access_token": "e8962d3214957043680e111d14e73721",
|
||||
"refresh_token": "b447808fe9ada297bae6e2e898711bb4"
|
||||
}
|
||||
```
|
||||
|
||||
#### 1.3.3 使用access_token
|
||||
所有需要授权的访问都需要 access_token。使用查询参数 token={access_token}
|
||||
包括:
|
||||
+ 访问Api
|
||||
http://.../api/v1/streams/rtsp/room/door?token=your_access_token
|
||||
+ http-flv
|
||||
http://.../streams/room/door.flv?token=your_access_token
|
||||
+ websocket-flv
|
||||
ws://.../ws/room/door.flv?token=your_access_token
|
||||
+ wps
|
||||
ws://.../ws/room/door?token=your_access_token
|
||||
+ rtmp
|
||||
rtmp://.../room/door?token=your_access_token
|
||||
|
||||
### 1.4 刷新access token
|
||||
GET api/v1/refreshtoken?token={refresh_tokebn}
|
||||
#### 1.3.1 参数和响应
|
||||
+ Body 参数
|
||||
无
|
||||
+ 查询参数
|
||||
|
||||
项目 | 类型 | 说明及示例
|
||||
-|-|-
|
||||
token | string | 登录或上次Refreshtoken返回的refresh_token|
|
||||
+ 响应(200)
|
||||
|
||||
项目 | 类型 | 说明及示例
|
||||
-|-|-
|
||||
access_token | string | 访问令牌|
|
||||
refresh_token | string | 刷新令牌 |
|
||||
|
||||
## 2 用户管理
|
||||
需要管理员权限
|
||||
### 2.1 获取用户信息
|
||||
GET api/v1/users/{username}
|
||||
|
||||
#### 1.3.1 参数和响应
|
||||
+ Body 参数
|
||||
无
|
||||
+ 查询参数
|
||||
无
|
||||
+ 路径参数
|
||||
|
||||
项目 | 类型 | 说明及示例
|
||||
-|-|-
|
||||
username | string | 用户名称|
|
||||
+ 响应(200)
|
||||
|
||||
项目 | 类型 | 说明及示例
|
||||
-|-|-
|
||||
name | string | 用户名 |
|
||||
admin | string | 是否是管理员 |
|
||||
push | string |推送权限 |
|
||||
pull | string | 拉取权限 |
|
||||
|
||||
### 2.2 删除用户
|
||||
DELETE api/v1/users/{username}
|
||||
|
||||
删除用户信息,但不会断开已有连接
|
||||
|
||||
### 2.3 创建或更新用户信息
|
||||
POST api/v1/users?update_password={0|1}
|
||||
|
||||
update_password 如果用户已存在,1 更新密码,其他值不会更新密码
|
||||
|
||||
### 2.4 获取用户列表
|
||||
GET api/v1/users
|
||||
|
||||
#### 1.3.1 参数和响应
|
||||
+ Body 参数
|
||||
无
|
||||
+ 查询参数
|
||||
|
||||
项目 | 类型 | 说明及示例
|
||||
-|-|-
|
||||
page_size | number | 分页大小 |
|
||||
page_token | string | 上次查询时返回的页token |
|
||||
+ 路径参数
|
||||
无
|
||||
+ 响应(200)
|
||||
|
||||
项目 | 类型 | 说明及示例 |
|
||||
-|-|-|
|
||||
total | number | 用户总数 |
|
||||
next_page_token | string | 下次查询的token |
|
||||
users | array | 用户列表 |
|
||||
name | string | 用户名 |
|
||||
admin | string | 是否是管理员 |
|
||||
push | string |推送权限 |
|
||||
pull | string | 拉取权限 |
|
||||
|
||||
|
||||
## 3 路由管理
|
||||
### 3.1 基本对象
|
||||
#### 3.1.1 路由
|
||||
属性 | 类型 | 说明及示例
|
||||
-|-|-
|
||||
pattern | string | 本地路径模式字串|
|
||||
url | string | 路由的目标地址,用户名和密码可以直接写在url中 |
|
||||
keepalive | bool|是否保持连接;如果没有消费者是否继续保持连接,如果为false在5分钟后自动断开 |
|
||||
|
||||
#### 3.1.2 路由表
|
||||
属性 | 类型 | 说明及示例
|
||||
-|-|-
|
||||
total |number |路由表中总个数
|
||||
next_page_token | string |下一页查询需带上的 page_token
|
||||
routes | array|路由信息数组
|
||||
|
||||
### 3.2 获取路由信息
|
||||
GET api/v1/routes/{pattern=**}
|
||||
|
||||
### 3.2 删除路由
|
||||
DELETE api/v1/routes/{pattern=**}
|
||||
但不会断开已有连接
|
||||
|
||||
### 3.3 创建路由
|
||||
POST api/v1/routes
|
||||
创建或更新路由信息
|
||||
|
||||
### 3.4 获取路由表
|
||||
GET api/v1/routes
|
||||
|
||||
+ 查询参数
|
||||
|
||||
项目 | 类型 | 说明及示例
|
||||
-|-|-
|
||||
page_size | number | 分页大小 |
|
||||
page_token | string | 上次查询时返回的页token |
|
||||
|
||||
### 4 流管理
|
||||
### 4.1 基本对象
|
||||
#### 4.1.1 流
|
||||
属性 | 类型 | 说明及示例
|
||||
-|-|-
|
||||
start_on | string(timestamp) | 流启动时间(RFC3339Nano 格式) |
|
||||
path | string | 流路径|
|
||||
addr | string | 流提供者的地址,push或pull|
|
||||
size | number | 流的大小|
|
||||
video| object | 视频元数据|
|
||||
audio| object | 音频元数据|
|
||||
cc | number | 正在消费流的消费者数量|
|
||||
cs | array | 正在消费流的消费者数组|
|
||||
[].id | number | 消费者ID|
|
||||
[].start_on | string(timestamp) | 消费启动时间(RFC3339Nano 格式) |
|
||||
[].packet_type | string | 消费的包类型|
|
||||
[].extra | string | 消费者额外描述|
|
||||
[].flow | object | 消费者接收和发送的流量统计|
|
||||
[].flow.inbytes | number | 消费者接收和发送的流量统计(kb)|
|
||||
[].flow.outbytes | number | 消费者接收和发送的流量统计(kb)|
|
||||
|
||||
#### 4.1.2 流列表
|
||||
属性 | 类型 | 说明及示例
|
||||
-|-|-
|
||||
total | number|流总个数
|
||||
next_page_token | string |下一页查询需带上的 page_token
|
||||
streams | array|流数组
|
||||
|
||||
### 4.2 获取流列表
|
||||
GET api/v1/streams?c={0|1}
|
||||
获取流列表
|
||||
+ 查询参数
|
||||
|
||||
项目 | 类型 | 说明及示例
|
||||
-|-|-
|
||||
page_size | number | 分页大小 |
|
||||
page_token | string | 上次查询时返回的页token |
|
||||
c |number|是否返回消费者信息 1 返回,其他值不返回|
|
||||
|
||||
### 4.3 获取流信息
|
||||
GET api/v1/streams/{path=**}?c={0|1}
|
||||
|
||||
### 4.4 删除流
|
||||
DELETE api/v1/streams/{path=**}
|
||||
|
||||
### 4.5 停止指定消费者
|
||||
DELETE api/v1/streams/{path=**}:consumer?cid={cid}
|
||||
+ 查询参数
|
||||
|
||||
项目 | 类型 | 说明及示例
|
||||
-|-|-
|
||||
cid | number | 消费者id |
|
108
docs/config.md
Executable file
108
docs/config.md
Executable file
@@ -0,0 +1,108 @@
|
||||
## 1. 跟配置
|
||||
属性 | 说明 | 示例
|
||||
-|-|-
|
||||
listen | 侦听地址,":1554" | 默认:":1554" |
|
||||
auth | 访问流媒体时,是否启用身份和权限验证 |默认:false |
|
||||
cache_gop | 是否缓存GOP,缓存GOP会提高打开速度|默认:false |
|
||||
hlspath | hls临时文件存储目录,不设置则在内存存储|默认:空字串 |
|
||||
profile | 是否启动在线诊断|默认:false |
|
||||
tls | 安全连接配置 |如果需要http范围,设置该配置向 |
|
||||
routetable | 路由表提供者 | 默认:json|
|
||||
users | 用户提供者 |默认:json |
|
||||
log | 日志配置 | |
|
||||
|
||||
## 2. tls 配置
|
||||
属性 | 说明 | 示例
|
||||
-|-|-
|
||||
listen | 安全连接侦听地址 |默认":443" |
|
||||
cert | 证书内容或文件 | |
|
||||
key | 私钥内容或文件 | |
|
||||
|
||||
## 3. 路由表配置文件
|
||||
属性 | 说明 | 示例
|
||||
-|-|-
|
||||
pattern | 本地路径模式字串 | 当以'/'结尾,表示一个以pattern开头的请求都路由到下面的url |
|
||||
url | 路由的目标地址,用户名和密码可以直接写在url中 | rtsp://admin@adminlocalhost/live2 |
|
||||
keepalive | 是否保持连接;如果没有消费者是否继续保持连接,如果为false在5分钟后自动断开 | false/true |
|
||||
|
||||
### 3.1 pattern
|
||||
模式字串有两种形式:
|
||||
+ 精确形式
|
||||
+ 目录形式
|
||||
|
||||
目录形式以'/'字符结束,表示以此pattern开始的流路径都将路由到它对应的url。它适合于多层组织结构的路由导航。
|
||||
### 3.2 完整实例:
|
||||
``` json
|
||||
[
|
||||
{
|
||||
"pattern": "/entrance/A1",
|
||||
"url": "rtsp://admin:admin@localhost:5540/live2",
|
||||
"keepalive": true
|
||||
},
|
||||
{
|
||||
"pattern": "/hr/",
|
||||
"url": "rtsp://admin:admin@localhost:8540/video",
|
||||
"keepalive": false
|
||||
}
|
||||
]
|
||||
```
|
||||
|
||||
|
||||
访问流媒体描述
|
||||
+ rtsp://localhost:1554/entrance/A1
|
||||
|
||||
将路由到 rtsp://admin:admin@localhost:5540/live2
|
||||
+ rtsp://localhost:1554/hr/door
|
||||
|
||||
将路由到 rtsp://admin:admin@localhost:8540/video/door
|
||||
|
||||
## 4. 用户配置文件
|
||||
属性 | 说明 | 示例
|
||||
-|-|-
|
||||
name | 用户名 | admin |
|
||||
password | 密码 | |
|
||||
admin | 是否是管理员 | false/true |
|
||||
push | 推送权限 | /rooms/+/entrace |
|
||||
pull | 拉取权限 | * |
|
||||
|
||||
### 4.1 完整示例:
|
||||
``` json
|
||||
[
|
||||
{
|
||||
"name":"admin",
|
||||
"password":"admin",
|
||||
"admin":true,
|
||||
"push":"*",
|
||||
"pull":"*"
|
||||
},
|
||||
{
|
||||
"name":"user1",
|
||||
"password":"user1",
|
||||
"push":"/rooms/+/entrance",
|
||||
"pull":"/test/*;/rooms/*"
|
||||
}
|
||||
]
|
||||
```
|
||||
|
||||
### 4.2 权限配置格式说明
|
||||
+ `*` 0-n 段通配
|
||||
+ `+` 表示可以一个路径端通配
|
||||
|
||||
可以通过分号设置多个
|
||||
#### 4.2.1 例子1
|
||||
当权限设置为 /a
|
||||
+ 路径 /a 通过授权
|
||||
+ 路径 /a/b 不通过授权
|
||||
|
||||
#### 4.2.2 例子2
|
||||
当权限设置为 /a/*
|
||||
+ 路径 /a 通过授权
|
||||
+ 路径 /a/b, /a/c, /a/b/c 都通过授权
|
||||
|
||||
#### 4.2.3 例子3
|
||||
当权限设置为 /a/+/c/*
|
||||
+ 路径 a/b/c, a/d/c, a/b/c/d, a/b/c/d/e 都通过授权
|
||||
+ 路径 a/c 不通过授权
|
||||
|
||||
## 5 ffmpeg
|
||||
将ffmpeg设置到环境变量路径中,供rtsp到rtmp的转换
|
96
docs/quickstart.md
Executable file
96
docs/quickstart.md
Executable file
@@ -0,0 +1,96 @@
|
||||
## 1. 安装
|
||||
即拷即用,根据自己的操作系统版本拷贝相应的可执行文件。
|
||||
|
||||
## 2. 配置
|
||||
服务器需要配置自己的摄像头拉流。
|
||||
默认配置拉流的路由信息在:routetable.json中;详细参考配置文档说明。
|
||||
|
||||
以下是一个典型的例子:
|
||||
``` json
|
||||
[
|
||||
{
|
||||
"pattern": "/group/door",
|
||||
"url": "rtsp://admin:888888@192.168.110.250:8554/H264MainStream",
|
||||
"keepalive":true
|
||||
},
|
||||
{
|
||||
"pattern": "/hr/",
|
||||
"url": "rtsp://admin:admin@192.168.110.145:1554",
|
||||
"keepalive": false
|
||||
}
|
||||
]
|
||||
```
|
||||
我们配置了两个路由:
|
||||
+ /group/door
|
||||
|
||||
集团大门直接连接到摄像头
|
||||
+ /hr/
|
||||
|
||||
人力资源部门的摄像头路由到下级的服务器中;hr的服务器包含:/door/video1和/door/video2
|
||||
|
||||
## 3. 使用
|
||||
服务器提供了多种访问终端摄像头的方式,包括:
|
||||
+ rtsp
|
||||
+ websocket-rtsp
|
||||
+ wsp(websocket 代理模式)
|
||||
+ http-flv
|
||||
+ websocket-flv
|
||||
+ http-hls
|
||||
|
||||
下面我们分别使用不同的方式访问上面两个路由的摄像头
|
||||
|
||||
### 3.1 使用rtsp访问
|
||||
```
|
||||
ffplay -rtsp_transport tcp rtsp://localhost:1554/group/door -fflags nobuffer
|
||||
ffplay -rtsp_transport udp rtsp://localhost:1554/group/door -fflags nobuffer
|
||||
ffplay -rtsp_transport udp_multicast rtsp://localhost:1554/group/door -fflags nobuffer
|
||||
```
|
||||
上面分别使用了 TCP、UDP、multicast 等三种方式访问
|
||||
要访问hr的/door/video1,只要将/group/door换成/hr/door/video1即可
|
||||
```
|
||||
ffplay -rtsp_transport tcp rtsp://localhost:1554/hr/door/video1 -fflags nobuffer
|
||||
```
|
||||
|
||||
rtsp://localhost:1554/hr/door/video1 请求在服务器内自动变成去拉取rtsp://admin:admin@192.168.110.145:1554/door/video1
|
||||
|
||||
### 3.2 使用websocket-rtsp
|
||||
打开demo地址:http://localhost:1554/demos/rtsp
|
||||
输入:ws://localhost:1554/ws/group/door 即可访问
|
||||
|
||||
### 3.3 使用wsp访问
|
||||
和上面一样,打开demo地址:http://localhost:1554/demos/wsp
|
||||
输入:rtsp://localhost:1554/group/door 即可访问
|
||||
|
||||
### 3.4 使用http-flv访问
|
||||
打开demo地址:http://localhost:1554/demos/flv
|
||||
输入:http://locaolhost:1554/streams/group/door.flv 即可访问
|
||||
由于 Chrome 对长连接的流限制为6个,因此如果使用 Chrome 打开更多建议使用websocket-flv
|
||||
|
||||
### 3.5 使用 websocket-flv访问
|
||||
打开demo地址:http://localhost:1554/demos/flv
|
||||
输入:ws://locaolhost:1554/ws/group/door.flv 即可访问
|
||||
|
||||
### 3.6 使用 http-hls访问
|
||||
由于 iOS的Safari不支持上述任何http访问模式,请使用 http-hls
|
||||
在浏览器输入: http://localhost:1554/streams/group/door.m3m8 即可访问
|
||||
**注意:** 由于http-hls的段文件默认被放在内存中,占用大量的内存;如系统内存不足,请配置存储路径。
|
||||
|
||||
## 4. 需要授权的情况
|
||||
除rtsp外,其他使用token进行访问
|
||||
如果 http-flv,
|
||||
输入:http://locaolhost:1554/streams/group/door.flv?token=7f97509e321a18ccf281607f4c0bd4fb
|
||||
其中 token 通过登录api获得
|
||||
|
||||
对于配置用户,参考配置和Api文档
|
||||
|
||||
## 5. 浏览器支持情况
|
||||
wsp、http-flv、websocket-flv等浏览器访问,支持:
|
||||
+ Firefox v.42+
|
||||
+ Chrome v.23+
|
||||
+ OSX Safari v.8+
|
||||
+ MS Edge v.13+
|
||||
+ Opera v.15+
|
||||
+ Android browser v.5.0+
|
||||
+ IE Mobile v.11+
|
||||
|
||||
不支持 iOS Safari 和 IE
|
1
go.mod
1
go.mod
@@ -4,6 +4,7 @@ go 1.14
|
||||
|
||||
require (
|
||||
github.com/BurntSushi/toml v0.3.1 // indirect
|
||||
github.com/cnotch/apirouter v0.0.0-20200731232942-89e243a791f3
|
||||
github.com/cnotch/bitutil v0.0.0-20200512012328-08db448fb960
|
||||
github.com/cnotch/loader v0.0.0-20200405015128-d9d964d09439
|
||||
github.com/cnotch/scheduler v0.0.0-20200522024700-1d2da93eefc5
|
||||
|
5
go.sum
5
go.sum
@@ -1,9 +1,13 @@
|
||||
github.com/BurntSushi/toml v0.3.1 h1:WXkYYl6Yr3qBf1K79EBnL4mak0OimBfB0XUf9Vl28OQ=
|
||||
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
|
||||
github.com/cnotch/apirouter v0.0.0-20200731232942-89e243a791f3 h1:Y8fe6nOk/UMsVZOPPLEVd9axxbIuBBjhZ+g6RpMz6vI=
|
||||
github.com/cnotch/apirouter v0.0.0-20200731232942-89e243a791f3/go.mod h1:5deJPLON/x/s2dLOQfuKS0lenhOIT4xX0pvtN/OEIuY=
|
||||
github.com/cnotch/bitutil v0.0.0-20200512012328-08db448fb960 h1:mZGYgYjlZOh+oMLvv3uxT61J0y8SWStIoOaqWU9S3Lk=
|
||||
github.com/cnotch/bitutil v0.0.0-20200512012328-08db448fb960/go.mod h1:ivZKsbAhsndNi8A5NFiDkBUY3+lVINm+IXNUnQNn6JM=
|
||||
github.com/cnotch/loader v0.0.0-20200405015128-d9d964d09439 h1:iNWyllf6zuby+nDNC6zKEkM7aUFbp4RccfWVdQ3HFfQ=
|
||||
github.com/cnotch/loader v0.0.0-20200405015128-d9d964d09439/go.mod h1:oWpDagHB6p+Kqqq7RoRZKyC4XAXft50hR8pbTxdbYYs=
|
||||
github.com/cnotch/queue v0.0.0-20200326024423-6e88bdbf2ad4 h1:bU2h1mvmsh6V1gQDGKgA9lHfBMLCmaVs1t2xyloDYHY=
|
||||
github.com/cnotch/queue v0.0.0-20200326024423-6e88bdbf2ad4/go.mod h1:zOssjAlNusOxvtaqT+EMA+Iyi8rrtKr4/XfzN1Fgoeg=
|
||||
github.com/cnotch/scheduler v0.0.0-20200522024700-1d2da93eefc5 h1:m9Wx/d4iPXFmE0f2zJ6iQ8tXZ52kOZO9qs/kMevEHxk=
|
||||
github.com/cnotch/scheduler v0.0.0-20200522024700-1d2da93eefc5/go.mod h1:F4GE3SZkJZ8an1Y0ZCqvSM3jeozNuKzoC67erG1PhIo=
|
||||
github.com/cnotch/xlog v0.0.0-20201208005456-cfda439cd3a0 h1:YXATGJEn/ymZjZOGCFfE5248ABcLbfwpd/dQGfByxGQ=
|
||||
@@ -39,6 +43,7 @@ github.com/pixelbender/go-sdp v1.1.0 h1:rkm9aFBNKrnB+YGfhLmAkal3pC8XYXb9h+172Plr
|
||||
github.com/pixelbender/go-sdp v1.1.0/go.mod h1:6IBlz9+BrUHoFTea7gcp4S54khtOhjCW/nVDLhmZBAs=
|
||||
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/sqs/goreturns v0.0.0-20181028201513-538ac6014518/go.mod h1:CKI4AZ4XmGV240rTHfO0hfE83S6/a3/Q1siZJ/vXf7A=
|
||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=
|
||||
github.com/stretchr/testify v1.6.1 h1:hDPOHmpOpP40lSULcqw7IrRb/u7w6RpDC9399XyoNd0=
|
||||
|
17
main.go
17
main.go
@@ -5,9 +5,12 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/cnotch/ipchub/config"
|
||||
"github.com/cnotch/ipchub/provider/auth"
|
||||
"github.com/cnotch/ipchub/provider/route"
|
||||
"github.com/cnotch/ipchub/service"
|
||||
"github.com/cnotch/scheduler"
|
||||
"github.com/cnotch/xlog"
|
||||
)
|
||||
@@ -29,12 +32,12 @@ func main() {
|
||||
userProvider := config.LoadUsersProvider(auth.JSON)
|
||||
auth.Reset(userProvider.(auth.UserProvider))
|
||||
|
||||
// // Start new service
|
||||
// svc, err := service.NewService(context.Background(), xlog.L())
|
||||
// if err != nil {
|
||||
// xlog.L().Panic(err.Error())
|
||||
// }
|
||||
// Start new service
|
||||
svc, err := service.NewService(context.Background(), xlog.L())
|
||||
if err != nil {
|
||||
xlog.L().Panic(err.Error())
|
||||
}
|
||||
|
||||
// // Listen and serve
|
||||
// svc.Listen()
|
||||
// Listen and serve
|
||||
svc.Listen()
|
||||
}
|
||||
|
536
service/apis.go
Executable file
536
service/apis.go
Executable file
@@ -0,0 +1,536 @@
|
||||
// Copyright (c) 2019,CAOHONGJU All rights reserved.
|
||||
// Use of this source code is governed by a MIT-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package service
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"path"
|
||||
"runtime"
|
||||
"sort"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/cnotch/apirouter"
|
||||
"github.com/cnotch/ipchub/config"
|
||||
"github.com/cnotch/ipchub/media"
|
||||
"github.com/cnotch/ipchub/provider/auth"
|
||||
"github.com/cnotch/ipchub/provider/route"
|
||||
"github.com/cnotch/ipchub/stats"
|
||||
)
|
||||
|
||||
const (
|
||||
usernameHeaderKey = "user_name_in_token"
|
||||
)
|
||||
|
||||
var (
|
||||
buffers = sync.Pool{
|
||||
New: func() interface{} {
|
||||
return bytes.NewBuffer(make([]byte, 0, 1024*2))
|
||||
},
|
||||
}
|
||||
noAuthRequired = map[string]bool{
|
||||
"/api/v1/login": true,
|
||||
"/api/v1/server": true,
|
||||
"/api/v1/runtime": true,
|
||||
"/api/v1/refreshtoken": true,
|
||||
}
|
||||
)
|
||||
|
||||
var crossdomainxml = []byte(
|
||||
`<?xml version="1.0" ?><cross-domain-policy>
|
||||
<allow-access-from domain="*" />
|
||||
<allow-http-request-headers-from domain="*" headers="*"/>
|
||||
</cross-domain-policy>`)
|
||||
|
||||
func (s *Service) initApis(mux *http.ServeMux) {
|
||||
api := apirouter.NewForGRPC(
|
||||
// 系统信息类API
|
||||
apirouter.POST("/api/v1/login", s.onLogin),
|
||||
apirouter.GET("/api/v1/server", s.onGetServerInfo),
|
||||
apirouter.GET("/api/v1/runtime", s.onGetRuntime),
|
||||
apirouter.GET("/api/v1/refreshtoken", s.onRefreshToken),
|
||||
|
||||
// 流管理API
|
||||
apirouter.GET("/api/v1/streams", s.onListStreams),
|
||||
apirouter.GET("/api/v1/streams/{path=**}", s.onGetStreamInfo),
|
||||
apirouter.DELETE("/api/v1/streams/{path=**}", s.onStopStream),
|
||||
apirouter.DELETE("/api/v1/streams/{path=**}:consumer", s.onStopConsumer),
|
||||
|
||||
// 路由管理API
|
||||
apirouter.GET("/api/v1/routes", s.onListRoutes),
|
||||
apirouter.GET("/api/v1/routes/{pattern=**}", s.onGetRoute),
|
||||
apirouter.DELETE("/api/v1/routes/{pattern=**}", s.onDelRoute),
|
||||
apirouter.POST("/api/v1/routes", s.onSaveRoute),
|
||||
|
||||
// 用户管理API
|
||||
apirouter.GET("/api/v1/users", s.onListUsers),
|
||||
apirouter.GET("/api/v1/users/{userName=*}", s.onGetUser),
|
||||
apirouter.DELETE("/api/v1/users/{userName=*}", s.onDelUser),
|
||||
apirouter.POST("/api/v1/users", s.onSaveUser),
|
||||
)
|
||||
|
||||
iterc := apirouter.ChainInterceptor(apirouter.PreInterceptor(s.authInterceptor),
|
||||
apirouter.PreInterceptor(roleInterceptor))
|
||||
|
||||
// api add to mux
|
||||
mux.HandleFunc("/api/", func(w http.ResponseWriter, r *http.Request) {
|
||||
if path.Base(r.URL.Path) == "crossdomain.xml" {
|
||||
w.Header().Set("Content-Type", "application/xml")
|
||||
w.Write(crossdomainxml)
|
||||
return
|
||||
}
|
||||
|
||||
path := strings.ToLower(r.URL.Path)
|
||||
if _, ok := noAuthRequired[path]; ok || iterc.PreHandle(w, r) {
|
||||
w.Header().Set("Access-Control-Allow-Origin", "*")
|
||||
api.ServeHTTP(w, r)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// 刷新Token
|
||||
func (s *Service) onRefreshToken(w http.ResponseWriter, r *http.Request, pathParams apirouter.Params) {
|
||||
token := r.URL.Query().Get("token")
|
||||
if token != "" {
|
||||
newtoken := s.tokens.Refresh(token)
|
||||
if newtoken != nil {
|
||||
if err := jsonTo(w, newtoken); err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
}
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
http.Error(w, "Token is not valid", http.StatusUnauthorized)
|
||||
return
|
||||
}
|
||||
|
||||
// 登录
|
||||
func (s *Service) onLogin(w http.ResponseWriter, r *http.Request, pathParams apirouter.Params) {
|
||||
type UserCredentials struct {
|
||||
Username string `json:"username"`
|
||||
Password string `json:"password"`
|
||||
}
|
||||
|
||||
// 提取凭证
|
||||
var uc UserCredentials
|
||||
err := json.NewDecoder(r.Body).Decode(&uc)
|
||||
if err != nil {
|
||||
// 尝试 Form解析
|
||||
uc.Username = r.FormValue("username")
|
||||
uc.Password = r.FormValue("password")
|
||||
if len(uc.Username) == 0 || len(uc.Password) == 0 {
|
||||
http.Error(w, "用户名或密码错误", http.StatusForbidden)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// 验证用户和密码
|
||||
u := auth.Get(uc.Username)
|
||||
if u == nil || u.ValidatePassword(uc.Password) != nil {
|
||||
http.Error(w, "用户名或密码错误", http.StatusForbidden)
|
||||
return
|
||||
}
|
||||
|
||||
// 新建Token,并返回
|
||||
token := s.tokens.NewToken(u.Name)
|
||||
|
||||
if err := jsonTo(w, token); err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
}
|
||||
}
|
||||
|
||||
// 获取运行时信息
|
||||
func (s *Service) onGetServerInfo(w http.ResponseWriter, r *http.Request, pathParams apirouter.Params) {
|
||||
type server struct {
|
||||
Vendor string `json:"vendor"`
|
||||
Name string `json:"name"`
|
||||
Version string `json:"version"`
|
||||
OS string `json:"os"`
|
||||
Arch string `json:"arch"`
|
||||
StartOn string `json:"start_on"`
|
||||
Duration string `json:"duration"`
|
||||
}
|
||||
srv := server{
|
||||
Vendor: config.Vendor,
|
||||
Name: config.Name,
|
||||
Version: config.Version,
|
||||
OS: strings.Title(runtime.GOOS),
|
||||
Arch: strings.ToUpper(runtime.GOARCH),
|
||||
StartOn: stats.StartingTime.Format(time.RFC3339Nano),
|
||||
Duration: time.Now().Sub(stats.StartingTime).String(),
|
||||
}
|
||||
|
||||
if err := jsonTo(w, &srv); err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
}
|
||||
}
|
||||
|
||||
// 获取运行时信息
|
||||
func (s *Service) onGetRuntime(w http.ResponseWriter, r *http.Request, pathParams apirouter.Params) {
|
||||
const extraKey = "extra"
|
||||
|
||||
type sccc struct {
|
||||
SC int `json:"sources"`
|
||||
CC int `json:"consumers"`
|
||||
}
|
||||
type runtime struct {
|
||||
On string `json:"on"`
|
||||
Proc stats.Proc `json:"proc"`
|
||||
Streams sccc `json:"streams"`
|
||||
Rtsp stats.ConnsSample `json:"rtsp"`
|
||||
Rtmp stats.ConnsSample `json:"rtmp"`
|
||||
Wsp stats.ConnsSample `json:"wsp"`
|
||||
Flv stats.ConnsSample `json:"flv"`
|
||||
Extra *stats.Runtime `json:"extra,omitempty"`
|
||||
}
|
||||
sc, cc := media.Count()
|
||||
|
||||
rt := runtime{
|
||||
On: time.Now().Format(time.RFC3339Nano),
|
||||
Proc: stats.MeasureRuntime(),
|
||||
Streams: sccc{sc, cc},
|
||||
Rtsp: stats.RtspConns.GetSample(),
|
||||
Wsp: stats.WspConns.GetSample(),
|
||||
Flv: stats.FlvConns.GetSample(),
|
||||
}
|
||||
|
||||
params := r.URL.Query()
|
||||
if strings.TrimSpace(params.Get(extraKey)) == "1" {
|
||||
rt.Extra = stats.MeasureFullRuntime()
|
||||
}
|
||||
|
||||
if err := jsonTo(w, &rt); err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Service) onListStreams(w http.ResponseWriter, r *http.Request, pathParams apirouter.Params) {
|
||||
params := r.URL.Query()
|
||||
pageSize, pageToken, err := listParamers(params)
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
includeCS := strings.TrimSpace(params.Get("c")) == "1"
|
||||
|
||||
count, sinfos := media.Infos(pageToken, pageSize, includeCS)
|
||||
type streamInfos struct {
|
||||
Total int `json:"total"`
|
||||
NextPageToken string `json:"next_page_token"`
|
||||
Streams []*media.StreamInfo `json:"streams,omitempty"`
|
||||
}
|
||||
|
||||
list := &streamInfos{
|
||||
Total: count,
|
||||
Streams: sinfos,
|
||||
}
|
||||
if len(sinfos) > 0 {
|
||||
list.NextPageToken = sinfos[len(sinfos)-1].Path
|
||||
}
|
||||
|
||||
if err := jsonTo(w, list); err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Service) onGetStreamInfo(w http.ResponseWriter, r *http.Request, pathParams apirouter.Params) {
|
||||
path := pathParams.ByName("path")
|
||||
|
||||
var rt *media.Stream
|
||||
|
||||
rt = media.Get(path)
|
||||
if rt == nil {
|
||||
http.NotFound(w, r)
|
||||
return
|
||||
}
|
||||
|
||||
params := r.URL.Query()
|
||||
includeCS := strings.TrimSpace(params.Get("c")) == "1"
|
||||
|
||||
si := rt.Info(includeCS)
|
||||
|
||||
if err := jsonTo(w, si); err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Service) onStopStream(w http.ResponseWriter, r *http.Request, pathParams apirouter.Params) {
|
||||
path := pathParams.ByName("path")
|
||||
|
||||
var rt *media.Stream
|
||||
|
||||
rt = media.Get(path)
|
||||
if rt != nil {
|
||||
rt.Close()
|
||||
}
|
||||
|
||||
w.WriteHeader(http.StatusOK)
|
||||
}
|
||||
|
||||
func (s *Service) onStopConsumer(w http.ResponseWriter, r *http.Request, pathParams apirouter.Params) {
|
||||
path := pathParams.ByName("path")
|
||||
param := r.URL.Query().Get("cid")
|
||||
no, err := strconv.ParseInt(param, 10, 64)
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
var rt *media.Stream
|
||||
rt = media.Get(path)
|
||||
if rt != nil {
|
||||
rt.StopConsume(media.CID(no))
|
||||
}
|
||||
|
||||
w.WriteHeader(http.StatusOK)
|
||||
}
|
||||
|
||||
func (s *Service) onListRoutes(w http.ResponseWriter, r *http.Request, pathParams apirouter.Params) {
|
||||
params := r.URL.Query()
|
||||
pageSize, pageToken, err := listParamers(params)
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
routes := route.All()
|
||||
sort.Slice(routes, func(i, j int) bool {
|
||||
return routes[i].Pattern < routes[j].Pattern
|
||||
})
|
||||
|
||||
begini := 0
|
||||
for _, r1 := range routes {
|
||||
if r1.Pattern <= pageToken {
|
||||
begini++
|
||||
continue
|
||||
}
|
||||
break
|
||||
}
|
||||
|
||||
type routeInfos struct {
|
||||
Total int `json:"total"`
|
||||
NextPageToken string `json:"next_page_token"`
|
||||
Routes []*route.Route `json:"routes,omitempty"`
|
||||
}
|
||||
|
||||
list := &routeInfos{
|
||||
Total: len(routes),
|
||||
NextPageToken: pageToken,
|
||||
Routes: make([]*route.Route, 0, pageSize),
|
||||
}
|
||||
|
||||
j := 0
|
||||
for i := begini; i < len(routes) && j < pageSize; i++ {
|
||||
j++
|
||||
list.Routes = append(list.Routes, routes[i])
|
||||
list.NextPageToken = routes[i].Pattern
|
||||
}
|
||||
|
||||
if err := jsonTo(w, list); err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Service) onGetRoute(w http.ResponseWriter, r *http.Request, pathParams apirouter.Params) {
|
||||
pattern := pathParams.ByName("pattern")
|
||||
r1 := route.Get(pattern)
|
||||
if r1 == nil {
|
||||
http.NotFound(w, r)
|
||||
return
|
||||
}
|
||||
|
||||
if err := jsonTo(w, r1); err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Service) onDelRoute(w http.ResponseWriter, r *http.Request, pathParams apirouter.Params) {
|
||||
pattern := pathParams.ByName("pattern")
|
||||
err := route.Del(pattern)
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
} else {
|
||||
w.WriteHeader(http.StatusOK)
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Service) onSaveRoute(w http.ResponseWriter, r *http.Request, pathParams apirouter.Params) {
|
||||
r1 := &route.Route{}
|
||||
err := json.NewDecoder(r.Body).Decode(r1)
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
err = route.Save(r1)
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
} else {
|
||||
w.WriteHeader(http.StatusOK)
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Service) onListUsers(w http.ResponseWriter, r *http.Request, pathParams apirouter.Params) {
|
||||
params := r.URL.Query()
|
||||
pageSize, pageToken, err := listParamers(params)
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
users := auth.All()
|
||||
sort.Slice(users, func(i, j int) bool {
|
||||
return users[i].Name < users[j].Name
|
||||
})
|
||||
|
||||
begini := 0
|
||||
for _, u := range users {
|
||||
if u.Name <= pageToken {
|
||||
begini++
|
||||
continue
|
||||
}
|
||||
break
|
||||
}
|
||||
|
||||
type userInfos struct {
|
||||
Total int `json:"total"`
|
||||
NextPageToken string `json:"next_page_token"`
|
||||
Users []auth.User `json:"users,omitempty"`
|
||||
}
|
||||
|
||||
list := &userInfos{
|
||||
Total: len(users),
|
||||
NextPageToken: pageToken,
|
||||
Users: make([]auth.User, 0, pageSize),
|
||||
}
|
||||
|
||||
j := 0
|
||||
for i := begini; i < len(users) && j < pageSize; i++ {
|
||||
j++
|
||||
u := *users[i]
|
||||
u.Password = ""
|
||||
list.Users = append(list.Users, u)
|
||||
list.NextPageToken = u.Name
|
||||
}
|
||||
|
||||
if err := jsonTo(w, list); err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Service) onGetUser(w http.ResponseWriter, r *http.Request, pathParams apirouter.Params) {
|
||||
userName := pathParams.ByName("userName")
|
||||
u := auth.Get(userName)
|
||||
if u == nil {
|
||||
http.NotFound(w, r)
|
||||
return
|
||||
}
|
||||
|
||||
u2 := *u
|
||||
u2.Password = ""
|
||||
if err := jsonTo(w, &u2); err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Service) onDelUser(w http.ResponseWriter, r *http.Request, pathParams apirouter.Params) {
|
||||
userName := pathParams.ByName("userName")
|
||||
err := auth.Del(userName)
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
} else {
|
||||
w.WriteHeader(http.StatusOK)
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Service) onSaveUser(w http.ResponseWriter, r *http.Request, pathParams apirouter.Params) {
|
||||
u := &auth.User{}
|
||||
err := json.NewDecoder(r.Body).Decode(u)
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
updatePassword := r.URL.Query().Get("update_password") == "1"
|
||||
err = auth.Save(u, updatePassword)
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
} else {
|
||||
w.WriteHeader(http.StatusOK)
|
||||
}
|
||||
}
|
||||
|
||||
func jsonTo(w io.Writer, o interface{}) error {
|
||||
formatted := buffers.Get().(*bytes.Buffer)
|
||||
formatted.Reset()
|
||||
defer buffers.Put(formatted)
|
||||
|
||||
body, err := json.Marshal(o)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := json.Indent(formatted, body, "", "\t"); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if _, err := w.Write(formatted.Bytes()); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func listParamers(params url.Values) (pageSize int, pageToken string, err error) {
|
||||
pageSizeStr := params.Get("page_size")
|
||||
pageSize = 20
|
||||
if pageSizeStr != "" {
|
||||
var err error
|
||||
pageSize, err = strconv.Atoi(pageSizeStr)
|
||||
if err != nil {
|
||||
return pageSize, pageToken, err
|
||||
}
|
||||
}
|
||||
pageToken = params.Get("page_token")
|
||||
return
|
||||
}
|
||||
|
||||
// ?token=
|
||||
func (s *Service) authInterceptor(w http.ResponseWriter, r *http.Request) bool {
|
||||
token := r.URL.Query().Get("token")
|
||||
if token != "" {
|
||||
username := s.tokens.AccessCheck(token)
|
||||
if username != "" {
|
||||
r.Header.Set(usernameHeaderKey, username)
|
||||
return true // 继续执行
|
||||
}
|
||||
}
|
||||
|
||||
http.Error(w, "Token is not valid", http.StatusUnauthorized)
|
||||
return false
|
||||
}
|
||||
|
||||
func roleInterceptor(w http.ResponseWriter, r *http.Request) bool {
|
||||
// 流查询方法,无需管理员身份
|
||||
if r.Method == http.MethodGet && strings.HasPrefix(r.URL.Path, "/api/v1/streams") {
|
||||
return true
|
||||
}
|
||||
|
||||
userName := r.Header.Get(usernameHeaderKey)
|
||||
u := auth.Get(userName)
|
||||
if u == nil || !u.Admin {
|
||||
http.Error(w /*http.StatusText(http.StatusForbidden)*/, "访问被拒绝,请用管理员登录", http.StatusForbidden)
|
||||
return false
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
@@ -11,8 +11,8 @@ import (
|
||||
"sync"
|
||||
|
||||
"github.com/cnotch/ipchub/media"
|
||||
"github.com/cnotch/ipchub/utils"
|
||||
"github.com/cnotch/xlog"
|
||||
"github.com/emitter-io/address"
|
||||
)
|
||||
|
||||
// 组播代理
|
||||
@@ -104,11 +104,11 @@ func (proxy *multicastProxy) TTL() int {
|
||||
|
||||
func (proxy *multicastProxy) SourceIP() string {
|
||||
if len(proxy.sourceIP) == 0 {
|
||||
addrs, err := address.GetPublic()
|
||||
if err != nil {
|
||||
addrs := utils.GetLocalIP()
|
||||
if len(addrs) == 0 {
|
||||
proxy.sourceIP = "Unknown"
|
||||
} else {
|
||||
proxy.sourceIP = addrs[0].IP.String()
|
||||
proxy.sourceIP = addrs[0]
|
||||
}
|
||||
}
|
||||
return proxy.sourceIP
|
||||
|
@@ -254,7 +254,7 @@ func (s *Session) onDescribe(resp *Response, req *Request) {
|
||||
}
|
||||
|
||||
// 从流中取 sdp
|
||||
sdpRaw := stream.Attr("sdp")
|
||||
sdpRaw := stream.Sdp()
|
||||
if len(sdpRaw) == 0 {
|
||||
resp.StatusCode = StatusNotFound
|
||||
return
|
||||
|
@@ -251,6 +251,7 @@ func (s *Session) asTCPConsumer(stream *media.Stream, resp *Response) (err error
|
||||
func (s *Session) asUDPConsumer(stream *media.Stream, resp *Response) (err error) {
|
||||
c := &udpConsumer{
|
||||
Session: s,
|
||||
source: stream,
|
||||
}
|
||||
|
||||
// 创建udp连接
|
||||
|
196
service/service.go
Executable file
196
service/service.go
Executable file
@@ -0,0 +1,196 @@
|
||||
// Copyright (c) 2019,CAOHONGJU All rights reserved.
|
||||
// Use of this source code is governed by a MIT-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package service
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/tls"
|
||||
"fmt"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/http/pprof"
|
||||
"os"
|
||||
"os/signal"
|
||||
"syscall"
|
||||
"time"
|
||||
|
||||
"github.com/cnotch/ipchub/config"
|
||||
"github.com/cnotch/ipchub/media"
|
||||
"github.com/cnotch/ipchub/network/socket/listener"
|
||||
"github.com/cnotch/ipchub/provider/auth"
|
||||
"github.com/cnotch/ipchub/provider/route"
|
||||
"github.com/cnotch/ipchub/service/rtsp"
|
||||
"github.com/cnotch/ipchub/service/wsp"
|
||||
"github.com/cnotch/scheduler"
|
||||
"github.com/cnotch/xlog"
|
||||
"github.com/emitter-io/address"
|
||||
"github.com/kelindar/tcp"
|
||||
)
|
||||
|
||||
// Service 网络服务对象(服务的入口)
|
||||
type Service struct {
|
||||
context context.Context
|
||||
cancel context.CancelFunc
|
||||
logger *xlog.Logger
|
||||
tlsusing bool
|
||||
http *http.Server
|
||||
rtsp *tcp.Server
|
||||
wsp *tcp.Server
|
||||
tokens *auth.TokenManager
|
||||
}
|
||||
|
||||
// NewService 创建服务
|
||||
func NewService(ctx context.Context, l *xlog.Logger) (s *Service, err error) {
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
s = &Service{
|
||||
context: ctx,
|
||||
cancel: cancel,
|
||||
logger: l,
|
||||
http: new(http.Server),
|
||||
rtsp: new(tcp.Server),
|
||||
wsp: new(tcp.Server),
|
||||
tokens: new(auth.TokenManager),
|
||||
}
|
||||
|
||||
// 设置 http 的Handler
|
||||
mux := http.NewServeMux()
|
||||
|
||||
// 管理员控制台
|
||||
if consoleAppDir, ok := config.ConsoleAppDir(); ok {
|
||||
mux.Handle("/", http.FileServer(http.Dir(consoleAppDir)))
|
||||
}
|
||||
|
||||
// Demo应用
|
||||
if demosAppDir, ok := config.DemosAppDir(); ok {
|
||||
mux.Handle("/demos/", http.StripPrefix("/demos/", http.FileServer(http.Dir(demosAppDir))))
|
||||
}
|
||||
|
||||
if config.Profile() {
|
||||
mux.HandleFunc("/debug/pprof/", pprof.Index)
|
||||
mux.HandleFunc("/debug/pprof/cmdline", pprof.Cmdline)
|
||||
mux.HandleFunc("/debug/pprof/profile", pprof.Profile)
|
||||
mux.HandleFunc("/debug/pprof/symbol", pprof.Symbol)
|
||||
mux.HandleFunc("/debug/pprof/trace", pprof.Trace)
|
||||
}
|
||||
|
||||
s.initApis(mux)
|
||||
s.initHTTPStreams(mux)
|
||||
s.http.Handler = mux
|
||||
|
||||
// 设置 rtsp AcceptHandler
|
||||
s.rtsp.OnAccept = rtsp.CreateAcceptHandler()
|
||||
|
||||
// 设置 wsp AcceptHandler
|
||||
s.wsp.OnAccept = wsp.CreateAcceptHandler()
|
||||
|
||||
// 启动定时存储拉流信息
|
||||
scheduler.PeriodFunc(time.Minute*5, time.Minute*5, func() {
|
||||
route.Flush()
|
||||
auth.Flush()
|
||||
s.tokens.ExpCheck()
|
||||
}, "The task of scheduled storage of routing tables and authorization information tables(5minutes")
|
||||
|
||||
s.logger.Info("service configured")
|
||||
return s, nil
|
||||
}
|
||||
|
||||
// Listen starts the service.
|
||||
func (s *Service) Listen() (err error) {
|
||||
defer s.Close()
|
||||
s.hookSignals()
|
||||
|
||||
// http rtsp rtmp ws
|
||||
addr, err := address.Parse(config.Addr(), 554)
|
||||
if err != nil {
|
||||
s.logger.Panic(err.Error())
|
||||
}
|
||||
|
||||
s.listen(addr, nil)
|
||||
|
||||
// https wss
|
||||
tlsconf := config.GetTLSConfig()
|
||||
if tlsconf != nil {
|
||||
tls, err := tlsconf.Load()
|
||||
if err == nil {
|
||||
if tlsAddr, err := address.Parse(tlsconf.ListenAddr, 443); err == nil {
|
||||
s.listen(tlsAddr, tls)
|
||||
s.tlsusing = true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
s.logger.Infof("service started(%s).", config.Version)
|
||||
s.logger = xlog.L()
|
||||
// Block
|
||||
select {}
|
||||
}
|
||||
|
||||
// listen configures an main listener on a specified address.
|
||||
func (s *Service) listen(addr *net.TCPAddr, conf *tls.Config) {
|
||||
// Create new listener
|
||||
s.logger.Infof("starting the listener, addr = %s.", addr.String())
|
||||
|
||||
l, err := listener.New(addr.String(), conf)
|
||||
if err != nil {
|
||||
s.logger.Panic(err.Error())
|
||||
}
|
||||
|
||||
// Set the read timeout on our mux listener
|
||||
timeout := time.Duration(int64(config.NetTimeout()) / 3)
|
||||
l.SetReadTimeout(timeout)
|
||||
|
||||
// Set Error handler
|
||||
l.HandleError(listener.ErrorHandler(func(err error) bool {
|
||||
xlog.Warn(err.Error())
|
||||
return true
|
||||
}))
|
||||
|
||||
// Configure the matchers
|
||||
l.ServeAsync(rtsp.MatchRTSP(), s.rtsp.Serve)
|
||||
l.ServeAsync(listener.MatchHTTP(), s.http.Serve)
|
||||
go l.Serve()
|
||||
}
|
||||
|
||||
// Close closes gracefully the service.,
|
||||
func (s *Service) Close() {
|
||||
if s.cancel != nil {
|
||||
s.cancel()
|
||||
}
|
||||
|
||||
// 停止计划任务
|
||||
jobs := scheduler.Jobs()
|
||||
for _, job := range jobs {
|
||||
job.Cancel()
|
||||
}
|
||||
|
||||
// 清空注册
|
||||
media.UnregistAll()
|
||||
// 退出前确保最新数据被存储
|
||||
route.Flush()
|
||||
auth.Flush()
|
||||
}
|
||||
|
||||
// OnSignal starts the signal processing and makes su
|
||||
func (s *Service) hookSignals() {
|
||||
c := make(chan os.Signal, 1)
|
||||
signal.Notify(c, syscall.SIGINT, syscall.SIGTERM)
|
||||
go func() {
|
||||
for sig := range c {
|
||||
s.onSignal(sig)
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
// OnSignal will be called when a OS-level signal is received.
|
||||
func (s *Service) onSignal(sig os.Signal) {
|
||||
switch sig {
|
||||
case syscall.SIGTERM:
|
||||
fallthrough
|
||||
case syscall.SIGINT:
|
||||
s.logger.Warn(fmt.Sprintf("received signal %s, exiting...", sig.String()))
|
||||
s.Close()
|
||||
os.Exit(0)
|
||||
}
|
||||
}
|
117
service/streamapis.go
Executable file
117
service/streamapis.go
Executable file
@@ -0,0 +1,117 @@
|
||||
// Copyright (c) 2019,CAOHONGJU All rights reserved.
|
||||
// Use of this source code is governed by a MIT-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package service
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"path"
|
||||
|
||||
"github.com/cnotch/ipchub/config"
|
||||
"github.com/cnotch/ipchub/network/websocket"
|
||||
"github.com/cnotch/ipchub/provider/auth"
|
||||
|
||||
"github.com/cnotch/apirouter"
|
||||
"github.com/cnotch/ipchub/utils/scan"
|
||||
)
|
||||
|
||||
// 初始化流式访问
|
||||
func (s *Service) initHTTPStreams(mux *http.ServeMux) {
|
||||
mux.Handle("/ws/", apirouter.WrapHandler(http.HandlerFunc(s.onWebSocketRequest), apirouter.PreInterceptor(s.streamInterceptor)))
|
||||
// mux.Handle("/streams/", apirouter.WrapHandler(http.HandlerFunc(s.onStreamsRequest), apirouter.PreInterceptor(s.streamInterceptor)))
|
||||
}
|
||||
|
||||
// websocket 请求处理
|
||||
func (s *Service) onWebSocketRequest(w http.ResponseWriter, r *http.Request) {
|
||||
username := r.Header.Get(usernameHeaderKey)
|
||||
streamPath, ext := extractStreamPathAndExt(r.URL.Path)
|
||||
_ = ext
|
||||
|
||||
if ws, ok := websocket.TryUpgrade(w, r, streamPath, username); ok {
|
||||
|
||||
if ws.Subprotocol() == "rtsp" { // rtsp 直连
|
||||
// rtsp接入
|
||||
s.rtsp.OnAccept(ws)
|
||||
return
|
||||
}
|
||||
|
||||
if ws.Subprotocol() == "control" || ws.Subprotocol() == "data" {
|
||||
// 代理访问
|
||||
s.wsp.OnAccept(ws)
|
||||
return
|
||||
}
|
||||
|
||||
// if ext == ".flv" {
|
||||
// go flv.ConsumeByWebsocket(s.logger, streamPath, r.RemoteAddr, ws)
|
||||
// return
|
||||
// }
|
||||
|
||||
s.logger.Warnf("websocket sub-protocol is not supported: %s.", ws.Subprotocol())
|
||||
ws.Close()
|
||||
}
|
||||
}
|
||||
|
||||
// // streams 请求处理(flv,mu38,ts)
|
||||
// func (s *Service) onStreamsRequest(w http.ResponseWriter, r *http.Request) {
|
||||
// // 获取文件后缀和流路径
|
||||
// streamPath, ext := extractStreamPathAndExt(r.URL.Path)
|
||||
// s.logger.Debug("http access stream media.",
|
||||
// xlog.F("path", streamPath),
|
||||
// xlog.F("ext", ext))
|
||||
|
||||
// w.Header().Set("Access-Control-Allow-Origin", "*")
|
||||
// switch ext {
|
||||
// case ".flv":
|
||||
// flv.ConsumeByHTTP(s.logger, streamPath, r.RemoteAddr, w)
|
||||
// case ".m3u8":
|
||||
// hls.GetM3u8(s.logger, streamPath, r.RemoteAddr, w)
|
||||
// case ".ts":
|
||||
// hls.GetTS(s.logger, streamPath, r.RemoteAddr, w)
|
||||
// default:
|
||||
// s.logger.Warnf("request file ext is not supported: %s.", ext)
|
||||
// http.NotFound(w, r)
|
||||
// }
|
||||
// }
|
||||
|
||||
func (s *Service) streamInterceptor(w http.ResponseWriter, r *http.Request) bool {
|
||||
if path.Base(r.URL.Path) == "crossdomain.xml" {
|
||||
w.Header().Set("Content-Type", "application/xml")
|
||||
w.Write(crossdomainxml)
|
||||
return false
|
||||
}
|
||||
|
||||
if !config.Auth() {
|
||||
// 不启用媒体流访问验证
|
||||
return true
|
||||
}
|
||||
|
||||
if s.authInterceptor(w, r) {
|
||||
return permissionInterceptor(w, r)
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
// 验证用户是否有权限播放指定的流
|
||||
func permissionInterceptor(w http.ResponseWriter, r *http.Request) bool {
|
||||
userName := r.Header.Get(usernameHeaderKey)
|
||||
u := auth.Get(userName)
|
||||
|
||||
streamPath, _ := extractStreamPathAndExt(r.URL.Path)
|
||||
|
||||
if u == nil || !u.ValidatePermission(streamPath, auth.PullRight) {
|
||||
http.Error(w, http.StatusText(http.StatusForbidden), http.StatusForbidden)
|
||||
return false
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
// 提取请求路径中的流path和格式后缀
|
||||
func extractStreamPathAndExt(requestPath string) (streamPath, ext string) {
|
||||
ext = path.Ext(requestPath)
|
||||
_, substr, _ := scan.NewScanner('/', nil).Scan(requestPath[1:])
|
||||
streamPath = requestPath[1+len(substr) : len(requestPath)-len(ext)]
|
||||
return
|
||||
}
|
@@ -277,7 +277,7 @@ func (s *Session) onDescribe(resp *rtsp.Response, req *rtsp.Request) {
|
||||
}
|
||||
|
||||
// 从流中取 sdp
|
||||
sdpRaw := stream.Attr("sdp")
|
||||
sdpRaw := stream.Sdp()
|
||||
if len(sdpRaw) == 0 {
|
||||
resp.StatusCode = rtsp.StatusNotFound
|
||||
return
|
||||
|
@@ -8,6 +8,7 @@ import (
|
||||
"fmt"
|
||||
"net"
|
||||
"strings"
|
||||
|
||||
"github.com/emitter-io/address"
|
||||
)
|
||||
|
||||
@@ -18,6 +19,21 @@ func GetIP(addr net.Addr) string {
|
||||
return s[:i]
|
||||
}
|
||||
|
||||
// GetLocalIP 获取本地IP
|
||||
func GetLocalIP() []string {
|
||||
addrs, _ := net.InterfaceAddrs()
|
||||
ips := []string{}
|
||||
for _, address := range addrs {
|
||||
// 检查ip地址判断是否回环地址
|
||||
if ipnet, ok := address.(*net.IPNet); ok && !ipnet.IP.IsLoopback() {
|
||||
if ipnet.IP.To4() != nil {
|
||||
ips = append(ips, ipnet.IP.String())
|
||||
}
|
||||
}
|
||||
}
|
||||
return ips
|
||||
}
|
||||
|
||||
// IsLocalhostIP 判断是否为本机IP
|
||||
func IsLocalhostIP(ip net.IP) bool {
|
||||
for _, localhost := range loopbackBlocks {
|
||||
|
Reference in New Issue
Block a user