mirror of
https://github.com/cnotch/ipchub.git
synced 2025-09-27 03:45:54 +08:00
284 lines
8.1 KiB
Go
Executable File
284 lines
8.1 KiB
Go
Executable File
// 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 rtsp
|
||
|
||
import (
|
||
"bufio"
|
||
"bytes"
|
||
"crypto/md5"
|
||
"encoding/base64"
|
||
"encoding/hex"
|
||
"io"
|
||
"net/url"
|
||
"strings"
|
||
|
||
"github.com/cnotch/ipchub/utils/scan"
|
||
)
|
||
|
||
const (
|
||
rtspProto = "RTSP/1.0" // RTSP协议版本
|
||
basicAuthPrefix = "Basic " // 用户基础验证前缀
|
||
digestAuthPrefix = "Digest " // 摘要认证前缀
|
||
rtspURLPrefix = "rtsp://" // RTSP地址前缀
|
||
)
|
||
|
||
// 通用的 RTSP 方法。
|
||
//
|
||
// 除非特别说明,这些定义在 RFC2326 规范的 10 章中。
|
||
// 未实现的方法需要返回 "501 Not Implemented"
|
||
const (
|
||
MethodOptions = "OPTIONS" // 查询命令支持情况(C->S, S->C)
|
||
MethodDescribe = "DESCRIBE" // 获取媒体信息(C->S)
|
||
MethodAnnounce = "ANNOUNCE" // 声明要push的媒体信息(方向:C->S, S->C)
|
||
MethodSetup = "SETUP" // 构建传输会话,也可以调整传输参数(C->S);如果不允许调整,可以返回 455 错误
|
||
MethodPlay = "PLAY" // 开始发送媒体数据(C->S)
|
||
MethodPause = "PAUSE" // 暂停发送媒体数据(C->S)
|
||
MethodTeardown = "TEARDOWN" // 关闭发送通道;关闭后需要重新执行 Setup 方法(C->S)
|
||
MethodGetParameter = "GET_PARAMETER" // 获取参数;空body可作为心跳ping(C->S, S->C)
|
||
MethodSetParameter = "SET_PARAMETER" // 设置参数,应该每次只设置一个参数(C->S, S->C)
|
||
MethodRecord = "RECORD" // 启动录像(C->S)
|
||
MethodRedirect = "REDIRECT" // 跳转(S->C)
|
||
)
|
||
|
||
// Request 表示一个 RTSP 请求;
|
||
// 它可以是 sever 接收或发送的,也可以是 client 接收或发送的。
|
||
type Request struct {
|
||
Method string // RTSP 的方法(OPTIONS、DESCRIBE...)
|
||
URL *url.URL // 请求的 URI。
|
||
Proto string // 协议版本,默认 "RTSP/1.0"
|
||
Header Header // 包含请求的头字段。
|
||
Body string // 请求的消息体。
|
||
}
|
||
|
||
// ReadRequest 根据规范从 r 中读取 Request
|
||
func ReadRequest(r *bufio.Reader) (*Request, error) {
|
||
var err error
|
||
|
||
// 读取并解析首行
|
||
var line string
|
||
line, err = readLine(r)
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
|
||
var req = new(Request)
|
||
s1 := strings.Index(line, " ")
|
||
s2 := strings.Index(line[s1+1:], " ")
|
||
if s1 < 0 || s2 < 0 {
|
||
return nil, &badStringError{"malformed RTSP request", line}
|
||
}
|
||
s2 += s1 + 1
|
||
|
||
// 解析结束
|
||
method := strings.TrimSpace(line[:s1])
|
||
rurl := strings.TrimSpace(line[s1+1 : s2])
|
||
proto := strings.TrimSpace(line[s2+1:])
|
||
|
||
// 检查请求的首行内容
|
||
// 检查方法命令名
|
||
if len(method) == 0 || method[0] == '$' { // RTP 包的第一个字节是 `$`
|
||
return nil, &badStringError{"invalid method", method}
|
||
}
|
||
// 只有 "OPTIONS" 命令的请求的URL为 *
|
||
if method != MethodOptions && rurl == "*" {
|
||
return nil, &badStringError{"invalid Request-URI", rurl}
|
||
}
|
||
if req.URL, err = url.ParseRequestURI(rurl); err != nil {
|
||
return nil, err
|
||
}
|
||
req.Method = method
|
||
req.Proto = proto
|
||
// 去除结尾的 :
|
||
if strings.LastIndex(req.URL.Host, ":") > strings.LastIndex(req.URL.Host, "]") {
|
||
req.URL.Host = strings.TrimSuffix(req.URL.Host, ":")
|
||
}
|
||
|
||
// 读取 Header
|
||
if req.Header, err = ReadHeader(r); err != nil {
|
||
return nil, err
|
||
}
|
||
|
||
// 读取Body
|
||
cl := req.Header.Int(FieldContentLength)
|
||
if cl > 0 {
|
||
// 读取 n 字节的字串Body
|
||
body := make([]byte, cl)
|
||
_, err = io.ReadFull(r, body)
|
||
req.Body = string(body)
|
||
}
|
||
return req, nil
|
||
}
|
||
|
||
// Write 根据规范将 Request 输出到 w
|
||
func (req *Request) Write(w io.Writer) error {
|
||
ws, ok := w.(writeStringer)
|
||
if !ok {
|
||
ws = stringWriter{w}
|
||
}
|
||
|
||
// TODO:如果存在用户名密码,去掉;
|
||
ruri := req.URL.String()
|
||
|
||
// 写请求方法行
|
||
ws.WriteString(req.Method)
|
||
ws.WriteString(" ")
|
||
ws.WriteString(ruri)
|
||
ws.WriteString(" RTSP/1.0\r\n")
|
||
|
||
// 写 Header
|
||
if len(req.Body) > 0 {
|
||
req.Header.SetInt(FieldContentLength, len(req.Body))
|
||
} else {
|
||
delete(req.Header, FieldContentLength)
|
||
}
|
||
if err := req.Header.Write(w); err != nil {
|
||
return err
|
||
}
|
||
|
||
// 写 Content
|
||
if len(req.Body) > 0 {
|
||
if _, err := ws.WriteString(req.Body); err != nil {
|
||
return err
|
||
}
|
||
}
|
||
|
||
return nil
|
||
}
|
||
|
||
func (req *Request) String() string {
|
||
buf := bytes.Buffer{}
|
||
req.Write(&buf)
|
||
return buf.String()
|
||
}
|
||
|
||
// BasicAuth returns the username and password provided in the request's
|
||
// Authorization header, if the request uses HTTP Basic Authentication.
|
||
// See RFC 2617, Section 2.
|
||
func (req *Request) BasicAuth() (username, password string, ok bool) {
|
||
auth := req.Header.get(FieldAuthorization)
|
||
if auth == "" {
|
||
return
|
||
}
|
||
return parseBasicAuth(auth)
|
||
}
|
||
|
||
// SetBasicAuth sets the request's Authorization header to use HTTP
|
||
// Basic Authentication with the provided username and password.
|
||
//
|
||
// With HTTP Basic Authentication the provided username and password
|
||
// are not encrypted.
|
||
func (req *Request) SetBasicAuth(username, password string) {
|
||
req.Header.set(FieldAuthorization, formatBasicAuth(username, password))
|
||
}
|
||
|
||
func formatBasicAuth(username, password string) string {
|
||
auth := username + ":" + password
|
||
return basicAuthPrefix + base64.StdEncoding.EncodeToString([]byte(auth))
|
||
}
|
||
|
||
// parseBasicAuth parses an HTTP Basic Authentication string.
|
||
// "Basic QWxhZGRpbjpvcGVuIHNlc2FtZQ==" returns ("Aladdin", "open sesame", true).
|
||
func parseBasicAuth(auth string) (username, password string, ok bool) {
|
||
// Case insensitive prefix match. See Issue 22736.
|
||
if len(auth) < len(basicAuthPrefix) || !strings.EqualFold(auth[:len(basicAuthPrefix)], basicAuthPrefix) {
|
||
return
|
||
}
|
||
c, err := base64.StdEncoding.DecodeString(auth[len(basicAuthPrefix):])
|
||
if err != nil {
|
||
return
|
||
}
|
||
cs := string(c)
|
||
s := strings.IndexByte(cs, ':')
|
||
if s < 0 {
|
||
return
|
||
}
|
||
return cs[:s], cs[s+1:], true
|
||
}
|
||
|
||
// DigestAuth 获取摘要认证信息
|
||
func (req *Request) DigestAuth() (username, response string, ok bool) {
|
||
auth := req.Header.get(FieldAuthorization)
|
||
|
||
// Case insensitive prefix match. See Issue 22736.
|
||
if len(auth) < len(digestAuthPrefix) || !strings.EqualFold(auth[:len(digestAuthPrefix)], digestAuthPrefix) {
|
||
return
|
||
}
|
||
|
||
auth = auth[len(digestAuthPrefix):]
|
||
|
||
tailing := auth
|
||
substr := ""
|
||
continueScan := true
|
||
for continueScan {
|
||
tailing, substr, continueScan = scan.Comma.Scan(tailing)
|
||
k, v, _ := scan.EqualPair.Scan(substr)
|
||
switch k {
|
||
case "username":
|
||
username = v
|
||
case "response":
|
||
response = v
|
||
}
|
||
}
|
||
|
||
return username, response, len(username) > 0 && len(response) > 0
|
||
}
|
||
|
||
// SetDigestAuth 为请求设置数字认证
|
||
func (req *Request) SetDigestAuth(url *url.URL, realm, nonce, username, password string) {
|
||
req.Header.set(FieldAuthorization, formatDigestAuth(realm, nonce, req.Method, url.String(), username, password))
|
||
}
|
||
|
||
// formatDigestAuth .
|
||
func formatDigestAuth(realm, nonce, method, url string, username, password string) string {
|
||
response := FormatDigestAuthResponse(realm, nonce, method, url, username, password)
|
||
|
||
buf := bytes.Buffer{}
|
||
|
||
buf.WriteString(`Digest username="`)
|
||
buf.WriteString(username)
|
||
buf.WriteString(`", realm="`)
|
||
buf.WriteString(realm)
|
||
buf.WriteString(`", nonce="`)
|
||
buf.WriteString(nonce)
|
||
buf.WriteString(`", uri="`)
|
||
buf.WriteString(url)
|
||
buf.WriteString(`", response="`)
|
||
buf.WriteString(response)
|
||
buf.WriteByte('"')
|
||
return buf.String()
|
||
}
|
||
|
||
// FormatDigestAuthResponse response= md5(md5(username:realm:password):nonce:md5(public_method:url));
|
||
func FormatDigestAuthResponse(realm, nonce, method, url string, username, password string) string {
|
||
buf := bytes.Buffer{}
|
||
|
||
// response= md5(md5(username:realm:password):nonce:md5(public_method:url));
|
||
buf.WriteString(username)
|
||
buf.WriteByte(':')
|
||
buf.WriteString(realm)
|
||
buf.WriteByte(':')
|
||
buf.WriteString(password)
|
||
|
||
md5Digest := md5.Sum(buf.Bytes())
|
||
md5UserRealmPwd := hex.EncodeToString(md5Digest[:])
|
||
|
||
buf.Reset()
|
||
buf.WriteString(method)
|
||
buf.WriteByte(':')
|
||
buf.WriteString(url)
|
||
md5Digest = md5.Sum(buf.Bytes())
|
||
md5MethodURL := hex.EncodeToString(md5Digest[:])
|
||
|
||
buf.Reset()
|
||
buf.WriteString(md5UserRealmPwd)
|
||
buf.WriteByte(':')
|
||
buf.WriteString(nonce)
|
||
buf.WriteByte(':')
|
||
buf.WriteString(md5MethodURL)
|
||
|
||
md5Digest = md5.Sum(buf.Bytes())
|
||
return hex.EncodeToString(md5Digest[:])
|
||
}
|