diff --git a/Dockerfile b/Dockerfile index f3dbcd1..9cf4825 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,4 +1,4 @@ -FROM golang:1.11.5-alpine +FROM golang:1.13.1-alpine WORKDIR /go/src/github.com/pierre-emmanuelJ/iptv-proxy COPY . . diff --git a/pkg/routes/routes.go b/pkg/routes/routes.go index ea954da..c217b82 100644 --- a/pkg/routes/routes.go +++ b/pkg/routes/routes.go @@ -56,7 +56,7 @@ func Routes(proxyConfig *config.ProxyConfig, r *gin.RouterGroup) { r.GET(fmt.Sprintf("/live/%s/%s/:id", proxyConfig.User, proxyConfig.Password), p.xtreamStreamLive) r.GET(fmt.Sprintf("/movie/%s/%s/:id", proxyConfig.User, proxyConfig.Password), p.xtreamStreamMovie) r.GET(fmt.Sprintf("/series/%s/%s/:id", proxyConfig.User, proxyConfig.Password), p.xtreamStreamSeries) - r.GET("/hlsr/:token/:username/:password/:channel/:tmp/:extension", p.hlsrStream) + r.GET(fmt.Sprintf("/hlsr/:token/%s/%s/:channel/:hash/:chunk", proxyConfig.User, proxyConfig.Password), p.hlsrStream) if strings.Contains(p.XtreamBaseURL, p.RemoteURL.Host) && p.XtreamUser == p.RemoteURL.Query().Get("username") && @@ -106,20 +106,58 @@ func (p *proxy) getM3U(c *gin.Context) { func (p *proxy) reverseProxy(c *gin.Context) { rpURL, err := url.Parse(p.Track.URI) if err != nil { - log.Fatal(err) + c.AbortWithError(http.StatusInternalServerError, err) + return } - stream(c, rpURL) + p.stream(c, rpURL) } -func stream(c *gin.Context, oriURL *url.URL) { - resp, err := http.Get(oriURL.String()) +func (p *proxy) stream(c *gin.Context, oriURL *url.URL) { + client := &http.Client{ + CheckRedirect: func(req *http.Request, via []*http.Request) error { + return http.ErrUseLastResponse + }, + } + + resp, err := client.Get(oriURL.String()) if err != nil { c.AbortWithError(http.StatusInternalServerError, err) return } defer resp.Body.Close() + if resp.StatusCode == http.StatusFound { + location, err := resp.Location() + if err != nil { + c.AbortWithError(http.StatusInternalServerError, err) + return + } + id := c.Param("id") + if strings.Contains(location.String(), id) { + hlsChannelsRedirectURLLock.Lock() + hlsChannelsRedirectURL[id] = *location + hlsChannelsRedirectURLLock.Unlock() + + hlsResp, err := http.Get(location.String()) + if err != nil { + c.AbortWithError(http.StatusInternalServerError, err) + return + } + defer hlsResp.Body.Close() + + b, err := ioutil.ReadAll(hlsResp.Body) + if err != nil { + c.AbortWithError(http.StatusInternalServerError, err) + return + } + body := string(b) + body = strings.ReplaceAll(body, "/"+p.XtreamUser+"/"+p.XtreamPassword+"/", "/"+p.User+"/"+p.Password+"/") + c.Data(http.StatusOK, hlsResp.Header.Get("Content-Type"), []byte(body)) + return + } + } + copyHTTPHeader(c, resp.Header) c.Status(resp.StatusCode) c.Stream(func(w io.Writer) bool { diff --git a/pkg/routes/xtream.go b/pkg/routes/xtream.go index dea5377..e35ab50 100644 --- a/pkg/routes/xtream.go +++ b/pkg/routes/xtream.go @@ -2,6 +2,7 @@ package routes import ( "encoding/base64" + "errors" "fmt" "io/ioutil" "log" @@ -24,10 +25,13 @@ type cacheMeta struct { time.Time } +var hlsChannelsRedirectURL map[string]url.URL = map[string]url.URL{} +var hlsChannelsRedirectURLLock = sync.RWMutex{} + // XXX Use key/value storage e.g: etcd, redis... // and remove that dirty globals var xtreamM3uCache map[string]cacheMeta = map[string]cacheMeta{} -var lock = sync.RWMutex{} +var xtreamM3uCacheLock = sync.RWMutex{} func (p *proxy) cacheXtreamM3u(m3uURL *url.URL) error { playlist, err := m3u.Parse(m3uURL.String()) @@ -45,14 +49,14 @@ func (p *proxy) cacheXtreamM3u(m3uURL *url.URL) error { return err } - lock.Lock() + xtreamM3uCacheLock.Lock() path, err := writeCacheTmp([]byte(result), m3uURL.String()) if err != nil { return err } xtreamM3uCache[m3uURL.String()] = cacheMeta{path, time.Now()} - lock.Unlock() + xtreamM3uCacheLock.Unlock() return nil } @@ -103,24 +107,24 @@ func (p *proxy) xtreamGet(c *gin.Context) { return } - lock.RLock() + xtreamM3uCacheLock.RLock() meta, ok := xtreamM3uCache[m3uURL.String()] d := time.Now().Sub(meta.Time) if !ok || d.Hours() >= float64(p.M3UCacheExpiration) { log.Printf("[iptv-proxy] %v | %s | xtream cache m3u file\n", time.Now().Format("2006/01/02 - 15:04:05"), c.ClientIP()) - lock.RUnlock() + xtreamM3uCacheLock.RUnlock() if err := p.cacheXtreamM3u(m3uURL); err != nil { c.AbortWithError(http.StatusInternalServerError, err) return } } else { - lock.RUnlock() + xtreamM3uCacheLock.RUnlock() } c.Header("Content-Disposition", "attachment; filename=\"iptv.m3u\"") - lock.RLock() + xtreamM3uCacheLock.RLock() path := xtreamM3uCache[m3uURL.String()].string - lock.RUnlock() + xtreamM3uCacheLock.RUnlock() data, err := ioutil.ReadFile(path) if err != nil { c.AbortWithError(http.StatusInternalServerError, err) @@ -232,7 +236,7 @@ func (p *proxy) xtreamStream(c *gin.Context) { return } - stream(c, rpURL) + p.stream(c, rpURL) } func (p *proxy) xtreamStreamLive(c *gin.Context) { @@ -243,7 +247,7 @@ func (p *proxy) xtreamStreamLive(c *gin.Context) { return } - stream(c, rpURL) + p.stream(c, rpURL) } func (p *proxy) xtreamStreamMovie(c *gin.Context) { @@ -254,7 +258,7 @@ func (p *proxy) xtreamStreamMovie(c *gin.Context) { return } - stream(c, rpURL) + p.stream(c, rpURL) } func (p *proxy) xtreamStreamSeries(c *gin.Context) { @@ -265,17 +269,38 @@ func (p *proxy) xtreamStreamSeries(c *gin.Context) { return } - stream(c, rpURL) + p.stream(c, rpURL) } func (p *proxy) hlsrStream(c *gin.Context) { - req, err := url.Parse(fmt.Sprintf("%s%s", p.XtreamBaseURL, c.Request.URL.String())) + hlsChannelsRedirectURLLock.RLock() + url, ok := hlsChannelsRedirectURL[c.Param("channel")+".m3u8"] + if !ok { + c.AbortWithError(http.StatusNotFound, errors.New("HSL redirect url not found")) + return + } + hlsChannelsRedirectURLLock.RUnlock() + + req, err := url.Parse( + fmt.Sprintf( + "%s://%s/hlsr/%s/%s/%s/%s/%s/%s", + url.Scheme, + url.Host, + c.Param("token"), + p.XtreamUser, + p.XtreamPassword, + c.Param("channel"), + c.Param("hash"), + c.Param("chunk"), + ), + ) + if err != nil { c.AbortWithError(http.StatusInternalServerError, err) return } - stream(c, req) + p.stream(c, req) } func xtreamReplaceURL(playlist *m3u.Playlist, user, password string, hostConfig *config.HostConfiguration, https bool) (*m3u.Playlist, error) {