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  }