diff --git a/docs/apis.md b/docs/apis.md new file mode 100755 index 0000000..cb8425c --- /dev/null +++ b/docs/apis.md @@ -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 | diff --git a/docs/config.md b/docs/config.md new file mode 100755 index 0000000..cb23180 --- /dev/null +++ b/docs/config.md @@ -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的转换 \ No newline at end of file diff --git a/docs/quickstart.md b/docs/quickstart.md new file mode 100755 index 0000000..8cb5aef --- /dev/null +++ b/docs/quickstart.md @@ -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 \ No newline at end of file diff --git a/go.mod b/go.mod index 6ebbf13..4e59cde 100644 --- a/go.mod +++ b/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 diff --git a/go.sum b/go.sum index a8963b4..0caa147 100644 --- a/go.sum +++ b/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= diff --git a/main.go b/main.go index 2b80bc8..3cdd95e 100755 --- a/main.go +++ b/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() } diff --git a/service/apis.go b/service/apis.go new file mode 100755 index 0000000..1e464e0 --- /dev/null +++ b/service/apis.go @@ -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( + ` + + + `) + +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 +} diff --git a/service/rtsp/multicast_proxy.go b/service/rtsp/multicast_proxy.go index 3fb68e3..baf73fb 100755 --- a/service/rtsp/multicast_proxy.go +++ b/service/rtsp/multicast_proxy.go @@ -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 diff --git a/service/rtsp/session.go b/service/rtsp/session.go index ba42572..c494f3c 100755 --- a/service/rtsp/session.go +++ b/service/rtsp/session.go @@ -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 diff --git a/service/rtsp/session_roles.go b/service/rtsp/session_roles.go index fce95c7..ca6bbdc 100755 --- a/service/rtsp/session_roles.go +++ b/service/rtsp/session_roles.go @@ -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连接 diff --git a/service/service.go b/service/service.go new file mode 100755 index 0000000..321f4a2 --- /dev/null +++ b/service/service.go @@ -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) + } +} diff --git a/service/streamapis.go b/service/streamapis.go new file mode 100755 index 0000000..bd1a44c --- /dev/null +++ b/service/streamapis.go @@ -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 +} diff --git a/service/wsp/session.go b/service/wsp/session.go index 6b7f882..c15cef7 100755 --- a/service/wsp/session.go +++ b/service/wsp/session.go @@ -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 diff --git a/utils/addr.go b/utils/addr.go index 600e897..3f64b21 100755 --- a/utils/addr.go +++ b/utils/addr.go @@ -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 {