// Copyright 2020, Chef. All rights reserved. // https://github.com/q191201771/lal // // Use of this source code is governed by a MIT-style license // that can be found in the License file. // // Author: Chef (191201771@qq.com) package base import ( "fmt" "net" "net/http" "net/url" "strconv" "strings" ) // 见单元测试 // TODO chef: 考虑部分内容移入naza中 const ( DefaultRtmpPort = 1935 DefaultHttpPort = 80 DefaultHttpsPort = 443 DefaultRtspPort = 554 DefaultRtmpsPort = 443 DefaultRtspsPort = 322 ) type UrlPathContext struct { PathWithRawQuery string Path string PathWithoutLastItem string // 注意,没有前面的'/',也没有后面的'/' LastItemOfPath string // 注意,没有前面的'/' RawQuery string } type UrlContext struct { Url string Scheme string Username string Password string StdHost string // host or host:port HostWithPort string // 当原始url中不包含port时,填充scheme对应的默认port Host string // 不包含port Port int // 当原始url中不包含port时,填充scheme对应的默认port //UrlPathContext PathWithRawQuery string // 注意,有前面的'/' Path string // 注意,有前面的'/' PathWithoutLastItem string // 注意,没有前面的'/',也没有后面的'/' LastItemOfPath string // 注意,没有前面的'/' RawQuery string // 参数,注意,没有前面的'?' RawUrlWithoutUserInfo string filenameWithoutType string fileType string } func (u *UrlContext) GetFilenameWithoutType() string { u.calcFilenameAndTypeIfNeeded() return u.filenameWithoutType } func (u *UrlContext) GetFileType() string { u.calcFilenameAndTypeIfNeeded() return u.fileType } func (u *UrlContext) calcFilenameAndTypeIfNeeded() { if len(u.filenameWithoutType) == 0 || len(u.fileType) == 0 { ss := strings.Split(u.LastItemOfPath, ".") u.filenameWithoutType = ss[0] if len(ss) > 1 { u.fileType = ss[1] } } } // --------------------------------------------------------------------------------------------------------------------- // ParseUrl // // @param defaultPort: // 注意,如果rawUrl中显示指定了端口,则该参数不生效。 // 注意,如果设置为-1,内部依然会对常见协议(http, https, rtmp, rtsp)设置官方默认端口。 func ParseUrl(rawUrl string, defaultPort int) (ctx UrlContext, err error) { ctx.Url = rawUrl stdUrl, err := url.Parse(rawUrl) if err != nil { return ctx, err } if stdUrl.Scheme == "" { return ctx, fmt.Errorf("%w. url=%s", ErrInvalidUrl, rawUrl) } // 如果不存在,则设置默认的 if defaultPort == -1 { // TODO(chef): 测试大小写的情况 switch stdUrl.Scheme { case "http": defaultPort = DefaultHttpPort case "https": defaultPort = DefaultHttpsPort case "rtmp": defaultPort = DefaultRtmpPort case "rtsp": defaultPort = DefaultRtspPort case "rtmps": defaultPort = DefaultRtmpsPort case "rtsps": defaultPort = DefaultRtspsPort } } ctx.Scheme = stdUrl.Scheme ctx.StdHost = stdUrl.Host ctx.Username = stdUrl.User.Username() ctx.Password, _ = stdUrl.User.Password() h, p, err := net.SplitHostPort(stdUrl.Host) if err != nil { // url中端口不存在 ctx.Host = stdUrl.Host if defaultPort == -1 { ctx.HostWithPort = stdUrl.Host } else { ctx.HostWithPort = net.JoinHostPort(stdUrl.Host, fmt.Sprintf("%d", defaultPort)) ctx.Port = defaultPort } } else { // 端口存在 ctx.Port, err = strconv.Atoi(p) if err != nil { return ctx, err } ctx.Host = h ctx.HostWithPort = stdUrl.Host } pathCtx, err := parseUrlPath(stdUrl) if err != nil { return ctx, err } ctx.PathWithRawQuery = pathCtx.PathWithRawQuery ctx.Path = pathCtx.Path ctx.PathWithoutLastItem = pathCtx.PathWithoutLastItem ctx.LastItemOfPath = pathCtx.LastItemOfPath ctx.RawQuery = pathCtx.RawQuery ctx.RawUrlWithoutUserInfo = fmt.Sprintf("%s://%s%s", ctx.Scheme, ctx.StdHost, ctx.PathWithRawQuery) return ctx, nil } // --------------------------------------------------------------------------------------------------------------------- func ParseRtmpUrl(rawUrl string) (ctx UrlContext, err error) { ctx, err = ParseUrl(rawUrl, -1) if err != nil { return } if ctx.Scheme != "rtmp" && ctx.Scheme != "rtmps" || ctx.Host == "" || ctx.Path == "" { return ctx, fmt.Errorf("%w. url=%s", ErrInvalidUrl, rawUrl) } // 处理特殊case,具体见 testParseRtmpUrlCase1 // 注意,使用ffmpeg推流时,会把`rtmp://127.0.0.1/test110`中的test110作为appName(streamName则为空) // 这种其实已不算十分合法的rtmp url了 // 我们这里也处理一下,和ffmpeg保持一致 if ctx.PathWithoutLastItem == "" && ctx.LastItemOfPath != "" { tmp := ctx.PathWithoutLastItem ctx.PathWithoutLastItem = ctx.LastItemOfPath ctx.LastItemOfPath = tmp } // 处理特殊case, 具体见 testParseRtmpUrlCase2 // // PathWithRawQuery:/vyun?vhost=thirdVhost?token=88F4/lss_7 // // Path:/vyun-----------------------------------------------> /vyun?vhost=thirdVhost?token=88F4/lss_7 // PathWithoutLastItem:vyun---------------------------------> vyun?vhost=thirdVhost?token=88F4 // LastItemOfPath:------------------------------------------> lss_7 // RawQuery:vhost=thirdVhost?token=88F4/lss_7---------------> 空 // if strings.Count(ctx.PathWithRawQuery, "?") > 1 { index := strings.LastIndexByte(ctx.PathWithRawQuery, '/') ctx.Path = ctx.PathWithRawQuery ctx.PathWithoutLastItem = ctx.PathWithRawQuery[1:index] ctx.LastItemOfPath = ctx.PathWithRawQuery[index+1:] ctx.RawQuery = "" } return } func ParseRtspUrl(rawUrl string) (ctx UrlContext, err error) { ctx, err = ParseUrl(rawUrl, -1) if err != nil { return } // 注意,存在一种情况,使用rtsp pull session,直接拉取没有url path的流,所以不检查ctx.Path if (ctx.Scheme != "rtsp" && ctx.Scheme != "rtsps") || ctx.Host == "" { return ctx, fmt.Errorf("%w. url=%s", ErrInvalidUrl, rawUrl) } return } func ParseHttpflvUrl(rawUrl string) (ctx UrlContext, err error) { return parseHttpUrl(rawUrl, ".flv") } // --------------------------------------------------------------------------------------------------------------------- // ParseHttpRequest // // @return 完整url func ParseHttpRequest(req *http.Request) string { // TODO(chef): [refactor] scheme是否能从从req.URL.Scheme获取 var scheme string if req.TLS == nil { scheme = "http" } else { scheme = "https" } return fmt.Sprintf("%s://%s%s", scheme, req.Host, req.RequestURI) } // ----- private ------------------------------------------------------------------------------------------------------- func parseUrlPath(stdUrl *url.URL) (ctx UrlPathContext, err error) { ctx.Path = stdUrl.Path index := strings.LastIndexByte(ctx.Path, '/') if index == -1 { ctx.PathWithoutLastItem = "" ctx.LastItemOfPath = "" } else if index == 0 { if ctx.Path == "/" { ctx.PathWithoutLastItem = "" ctx.LastItemOfPath = "" } else { ctx.PathWithoutLastItem = "" ctx.LastItemOfPath = ctx.Path[1:] } } else { ctx.PathWithoutLastItem = ctx.Path[1:index] ctx.LastItemOfPath = ctx.Path[index+1:] } ctx.RawQuery = stdUrl.RawQuery if ctx.RawQuery == "" { ctx.PathWithRawQuery = ctx.Path } else { ctx.PathWithRawQuery = fmt.Sprintf("%s?%s", ctx.Path, ctx.RawQuery) } return ctx, nil } func parseHttpUrl(rawUrl string, filetype string) (ctx UrlContext, err error) { ctx, err = ParseUrl(rawUrl, -1) if err != nil { return } if (ctx.Scheme != "http" && ctx.Scheme != "https") || ctx.Host == "" || ctx.Path == "" || !strings.HasSuffix(ctx.LastItemOfPath, filetype) { return ctx, fmt.Errorf("%w. url=%s", ErrInvalidUrl, rawUrl) } return }