github.com/qiniu/go-sdk/v7@v7.27.0/pili/url.go (about) 1 package pili 2 3 import ( 4 "crypto/hmac" 5 "crypto/md5" 6 "crypto/sha1" 7 "encoding/base64" 8 "fmt" 9 "math" 10 "net/url" 11 "path/filepath" 12 "reflect" 13 "strconv" 14 "strings" 15 "time" 16 17 "github.com/qiniu/dyn/text" 18 ) 19 20 // RTMPPublishURL 生成 RTMP 推流地址 21 func RTMPPublishURL(hub, domain, streamTitle string) string { 22 return fmt.Sprintf("rtmp://%s/%s/%s", domain, hub, streamTitle) 23 } 24 25 // SRTPublishURL 生成 SRT 推流地址 26 func SRTPublishURL(hub, domain, streamTitle string) string { 27 return fmt.Sprintf("srt://%s:%s?streamid=#!::h=%s/%s,m=publish,domain=%s", domain, SRTPort, hub, streamTitle, domain) 28 } 29 30 // RTMPPlayURL 生成 RTMP 播放地址 31 func RTMPPlayURL(hub, domain, streamTitle string) string { 32 return fmt.Sprintf("rtmp://%s/%s/%s", domain, hub, streamTitle) 33 } 34 35 // HLSPlayURL 生成 HLS 播放地址 36 func HLSPlayURL(hub, domain, streamTitle string) string { 37 return fmt.Sprintf("https://%s/%s/%s.m3u8", domain, hub, streamTitle) 38 } 39 40 // HDLPlayURL 生成 HDL 播放地址 41 func HDLPlayURL(hub, domain, streamTitle string) string { 42 return fmt.Sprintf("https://%s/%s/%s.flv", domain, hub, streamTitle) 43 } 44 45 // SignPublishURL 推流 URL 签算(直播鉴权) 46 // 支持鉴权规格为 47 // - static: 静态鉴权 48 // - expiry: 限时鉴权 49 // - expiry_sk: 限时鉴权SK 50 // - dynamic: 动态鉴权 51 func SignPublishURL(publishURL string, args SignPublishURLArgs) (string, error) { 52 u, err := url.Parse(publishURL) 53 if err != nil { 54 return "", ErrInvalidArgs 55 } 56 57 if args.ExpireAt <= 0 { 58 args.ExpireAt = time.Now().Add(10 * time.Minute).Unix() 59 } 60 61 isSRT := u.Scheme == SRTScheme 62 if isSRT { 63 u, err = parseSRTURL(u) 64 if err != nil { 65 return "", err 66 } 67 } 68 query := u.Query() 69 token := "" 70 switch args.SecurityType { 71 case SecurityTypeNull, SecurityTypeNone: 72 return publishURL, nil 73 case SecurityTypeStatic: 74 if len(args.PublishKey) == 0 { 75 return "", ErrInvalidArgs 76 } 77 query.Set(SecurityTypeStaticKeyPart, args.PublishKey) 78 u.RawQuery = query.Encode() 79 case SecurityTypeExpiry: 80 if len(args.PublishKey) == 0 { 81 return "", ErrInvalidArgs 82 } 83 query.Set(SecurityTypeExpiryTsPart, strconv.FormatInt(args.ExpireAt, 10)) 84 query.Del(SecurityTypeTokenPart) 85 u.RawQuery = query.Encode() 86 h := hmac.New(sha1.New, []byte(args.PublishKey)) 87 h.Write([]byte(u.Path + "?" + u.RawQuery)) 88 token = base64.URLEncoding.EncodeToString(h.Sum(nil)) 89 case SecurityTypeExpirySK: 90 if len(args.AccessKey) == 0 || len(args.SecretKey) == 0 { 91 return "", ErrInvalidArgs 92 } 93 query.Set(SecurityTypeExpirySkTsPart, strconv.FormatInt(args.ExpireAt, 10)) 94 query.Del(SecurityTypeTokenPart) 95 u.RawQuery = query.Encode() 96 h := hmac.New(sha1.New, []byte(args.SecretKey)) 97 h.Write([]byte(u.Path + "?" + u.RawQuery)) 98 token = fmt.Sprintf("%s:%s", args.AccessKey, base64.URLEncoding.EncodeToString(h.Sum(nil))) 99 case SecurityTypeDynamic: 100 query.Set(SecurityTypeDynamicNoncePart, strconv.Itoa(args.Nonce)) 101 query.Del(SecurityTypeTokenPart) 102 u.RawQuery = query.Encode() 103 h := hmac.New(sha1.New, []byte(args.PublishKey)) 104 h.Write([]byte(u.Path + "?" + u.RawQuery)) 105 token = base64.URLEncoding.EncodeToString(h.Sum(nil)) 106 default: 107 return "", ErrUnsupportedSecurityType 108 } 109 110 if isSRT { 111 return convertSRTURL(u, token), nil 112 } 113 114 ret := u.String() 115 if len(token) > 0 { 116 ret += "&token=" + token 117 } 118 119 return ret, nil 120 } 121 122 // SignPlayURL 播放 URL 签算(时间戳防盗链) 123 // 支持鉴权规格为 124 // - tsStartMD5: 时间戳防盗链开始时间限制 125 // - tsExpireMD5: 时间戳防盗链结束时间限制 126 func SignPlayURL(playURL string, args SignPlayURLArgs) (string, error) { 127 u, err := url.Parse(playURL) 128 if err != nil { 129 return "", ErrInvalidArgs 130 } 131 132 if args.TsBase <= 0 { 133 args.TsBase = 16 134 } 135 136 query := u.Query() 137 138 switch args.SecurityType { 139 case SecurityTypeNull, SecurityTypeNone: 140 return playURL, nil 141 case SecurityTypeTsStartMD5, SecurityTypeTsExpireMD5: 142 query.Set(args.TsPart, strconv.FormatInt(args.Timestamp, args.TsBase)) 143 query.Del(args.SignPart) 144 default: 145 return "", ErrUnsupportedSecurityType 146 } 147 148 ctx := magicCtx(u, query, args.Key) 149 150 sign, err := text.Subst(args.Rule, ctx, text.Fmttype_Text, true) 151 if err != nil { 152 return "", ErrInvalidRule 153 } 154 155 query.Set(args.SignPart, fmt.Sprintf("%x", md5.Sum([]byte(sign)))) 156 u.RawQuery = query.Encode() 157 return u.String(), nil 158 } 159 160 // magicCtx 生成魔法变量 161 // - $(key): 密钥 162 // - $(path): URL 中的 path 部分 163 // - $(streamKey): URL 中的 hub/stream 部分 164 // - $(streamTitle): URL 中的 stream 部分 165 // - $(path_<number>): URL 中的 path 部分,<number> 表示 path 层级 166 // - $(_<query>): URL 中的 query 字段,举例: key1=val,魔法变量中使用 $(_key1) 表示 val 167 func magicCtx(u *url.URL, query url.Values, key string) map[string]interface{} { 168 path := u.Path 169 ctx := map[string]interface{}{ 170 "key": key, 171 "path": path, 172 "streamKey": parseStreamKey(path), 173 "streamTitle": parseStreamTitile(path), 174 } 175 176 for k, v := range strings.Split(path, "/") { 177 if k == 0 { 178 continue 179 } 180 ctx["path_"+strconv.Itoa(k)] = v 181 } 182 183 for k, v := range query { 184 if len(v) != 0 { 185 q := "_" + k 186 ctx[q] = v[0] 187 } 188 } 189 return ctx 190 } 191 192 // convertSRTURL 转换 SRT URL 193 func convertSRTURL(u *url.URL, token string) string { 194 query := u.Query() 195 query.Set("m", "publish") 196 rawQuery := strings.Replace(query.Encode(), "&", ",", -1) 197 if len(token) > 0 { 198 rawQuery += ",token=" + token 199 } 200 if !strings.Contains(u.Host, ":") { 201 u.Host += ":" + SRTPort 202 } 203 204 return fmt.Sprintf("srt://%s?streamid=#!::h=%s,%s", u.Host, u.Path[1:], rawQuery) 205 } 206 207 // parseSRTURL 解析 SRT URL 208 // 相关参数放入 query 中 209 func parseSRTURL(u *url.URL) (*url.URL, error) { 210 if !strings.HasPrefix(u.Fragment, "!::") { 211 return nil, ErrInvalidArgs 212 } 213 214 query := url.Values{} 215 for _, split := range strings.Split(u.Fragment, ",") { 216 split2 := strings.SplitN(split, "=", 2) 217 if len(split2) != 2 { 218 return nil, ErrInvalidArgs 219 } 220 switch split2[0] { 221 case "!::h": 222 u.Path = "/" + split2[1] 223 case "m": 224 continue 225 default: 226 query.Set(split2[0], split2[1]) 227 } 228 } 229 u.RawQuery = query.Encode() 230 u.Fragment = "" 231 return u, nil 232 } 233 234 // url 拼接生成 API 访问 URL 235 func (m *Manager) url(format string, args ...interface{}) string { 236 query := "" 237 if len(args) > 0 { 238 if q, ok := isQuery(args[len(args)-1]); ok { 239 query = q.Encode() 240 args = args[:len(args)-1] 241 } 242 } 243 reqURL := m.apiHTTPScheme + m.apiHost + fmt.Sprintf(format, args...) 244 if !strings.Contains(reqURL, "?") && query != "" { 245 reqURL += "?" + query 246 } 247 return reqURL 248 } 249 250 func isQuery(q interface{}) (url.Values, bool) { 251 switch q := q.(type) { 252 case url.Values: 253 return q, true 254 default: 255 return nil, false 256 } 257 } 258 259 func setQuery(q url.Values, key string, v interface{}) { 260 if !isZero(v) { 261 q.Set(key, fmt.Sprint(v)) 262 } 263 } 264 265 // isZero reports whether v is the zero value for its type. 266 // FIXME IsZero was added to the reflect standard package in go 1.13 267 // compatible with older versions of go 1.13 268 func isZero(_v interface{}) bool { 269 v := reflect.ValueOf(_v) 270 switch v.Kind() { 271 case reflect.Bool: 272 return !v.Bool() 273 case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: 274 return v.Int() == 0 275 case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uintptr: 276 return v.Uint() == 0 277 case reflect.Float32, reflect.Float64: 278 return math.Float64bits(v.Float()) == 0 279 case reflect.Complex64, reflect.Complex128: 280 c := v.Complex() 281 return math.Float64bits(real(c)) == 0 && math.Float64bits(imag(c)) == 0 282 case reflect.Array: 283 for i := 0; i < v.Len(); i++ { 284 if !isZero(v.Index(i)) { 285 return false 286 } 287 } 288 return true 289 case reflect.Chan, reflect.Func, reflect.Interface, reflect.Map, reflect.Ptr, reflect.Slice, reflect.UnsafePointer: 290 return v.IsNil() 291 case reflect.String: 292 return v.Len() == 0 293 case reflect.Struct: 294 for i := 0; i < v.NumField(); i++ { 295 if !isZero(v.Field(i)) { 296 return false 297 } 298 } 299 return true 300 default: 301 return true 302 } 303 } 304 305 func parseStreamKey(path string) string { 306 noExt := strings.TrimSuffix(path, filepath.Ext(path)) 307 return strings.TrimPrefix(noExt, "/") 308 } 309 310 func parseStreamTitile(path string) string { 311 noExt := strings.TrimSuffix(path, filepath.Ext(path)) 312 noFirstSlash := strings.TrimPrefix(noExt, "/") 313 slash := strings.IndexByte(noFirstSlash, '/') 314 return noFirstSlash[slash+1:] 315 }