Skip to content

Commit

Permalink
Merge pull request #503 from OdyseeTeam/migrate-paid-77
Browse files Browse the repository at this point in the history
Migrate to CDN77
  • Loading branch information
anbsky authored Oct 11, 2023
2 parents 09e9568 + b50c028 commit 6b9a6a5
Show file tree
Hide file tree
Showing 10 changed files with 150 additions and 115 deletions.
30 changes: 7 additions & 23 deletions app/query/caller_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,10 @@ type riggedTimeSource struct {
FrozenTime time.Time
}

func (r riggedTimeSource) Now() time.Time {
return r.FrozenTime
}

func (r riggedTimeSource) NowUnix() int64 {
return r.FrozenTime.Unix()
}
Expand Down Expand Up @@ -147,19 +151,8 @@ func TestCaller_CallAmbivalentMethodsWithWallet(t *testing.T) {
},
JSONRPC: "2.0",
})
expectedRequestLbrynetX := test.ReqToStr(t, &jsonrpc.RPCRequest{
Method: m,
Params: map[string]any{
"wallet_id": sdkrouter.WalletID(dummyUserID),
"new_sdk_server": config.GetLbrynetXServer(),
},
JSONRPC: "2.0",
})

if expectedRequest != receivedRequest.Body {
// TODO: Remove when new_sdk_server is not used anymore
assert.EqualValues(t, expectedRequestLbrynetX, receivedRequest.Body)
}
assert.EqualValues(t, expectedRequest, receivedRequest.Body)
})
}

Expand Down Expand Up @@ -700,9 +693,6 @@ func TestCaller_Status(t *testing.T) {
}

func TestCaller_GetFreeUnauthenticated(t *testing.T) {
config.Override("FreeContentURL", "https://player.odycdn.com/api/v4/streams/free/")
defer config.RestoreOverridden()

srvAddress := test.RandServerAddress(t)
uri := "what#19b9c243bea0c45175e6a6027911abbad53e983e"

Expand All @@ -714,13 +704,10 @@ func TestCaller_GetFreeUnauthenticated(t *testing.T) {
getResponse := &ljsonrpc.GetResponse{}
err = resp.GetObject(&getResponse)
require.NoError(t, err)
assert.Equal(t, "https://player.odycdn.com/api/v4/streams/free/what/19b9c243bea0c45175e6a6027911abbad53e983e/d51692", getResponse.StreamingURL)
assert.Equal(t, "https://player.odycdn.com/v6/streams/19b9c243bea0c45175e6a6027911abbad53e983e/d51692.mp4", getResponse.StreamingURL)
}

func TestCaller_GetFreeAuthenticated(t *testing.T) {
config.Override("FreeContentURL", "https://player.odycdn.com/api/v4/streams/free/")
defer config.RestoreOverridden()

uri := "what"

dummyUserID := 123321
Expand All @@ -737,7 +724,7 @@ func TestCaller_GetFreeAuthenticated(t *testing.T) {
getResponse := &ljsonrpc.GetResponse{}
err = resp.GetObject(&getResponse)
require.NoError(t, err)
assert.Equal(t, "https://player.odycdn.com/api/v4/streams/free/what/19b9c243bea0c45175e6a6027911abbad53e983e/d51692", getResponse.StreamingURL)
assert.Equal(t, "https://player.odycdn.com/v6/streams/19b9c243bea0c45175e6a6027911abbad53e983e/d51692.mp4", getResponse.StreamingURL)
}

func TestCaller_GetCouldntFindClaim(t *testing.T) {
Expand All @@ -756,9 +743,6 @@ func TestCaller_GetCouldntFindClaim(t *testing.T) {
}

func TestCaller_GetInvalidURLAuthenticated(t *testing.T) {
config.Override("FreeContentURL", "https://player.odycdn.com/api")
defer config.RestoreOverridden()

uri := "what#@1||||"

dummyUserID := 123321
Expand Down
2 changes: 0 additions & 2 deletions app/query/const.go
Original file line number Diff line number Diff line change
Expand Up @@ -129,5 +129,3 @@ var walletSpecificMethods = []string{
"wallet_unlock",
"wallet_status",
}

var controversialChannels = map[string]bool{}
45 changes: 22 additions & 23 deletions app/query/paid.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,34 +2,33 @@ package query

import (
"crypto/md5"
"encoding/hex"
"encoding/base64"
"fmt"
"path"
"strings"
)

type qkv [2]string
func signStreamURL77(host, filePath, secureToken string, expiryTimestamp int64) (string, error) {
strippedPath := path.Dir(filePath)

type urlQuery struct {
basePath string
}

func (q urlQuery) render(kvs ...qkv) string {
qs := ""
for _, kv := range kvs {
qs += fmt.Sprintf("%s=%s", kv[0], kv[1])
hash := strippedPath + secureToken
if expiryTimestamp > 0 {
hash = fmt.Sprintf("%d%s", expiryTimestamp, hash)
}
return qs
}

func (q urlQuery) hash(kvs ...qkv) string {
h := md5.New()
h.Write([]byte(fmt.Sprintf("%s?%s", q.basePath, q.render(kvs...))))
return hex.EncodeToString(h.Sum(nil))
}
finalHash := md5.Sum([]byte(hash))
encodedFinalHash := base64.StdEncoding.EncodeToString(finalHash[:])
encodedFinalHash = strings.NewReplacer("+", "-", "/", "_").Replace(encodedFinalHash)

// signedURL := "https://" + fmt.Sprintf("%s/%s", host, encodedFinalHash)
// if expiryTimestamp > 0 {
// signedURL += fmt.Sprintf(",%d", expiryTimestamp)
// }
// signedURL += filePath

if expiryTimestamp > 0 {
return fmt.Sprintf("%s,%d", encodedFinalHash, expiryTimestamp), nil
}

func signStreamURL(path, query string) string {
h := md5.New()
h.Write([]byte(fmt.Sprintf("%s?%s", path, query)))
s := hex.EncodeToString(h.Sum(nil))
logger.Log().Debugf("signing url: %s?%s, signed: %s", path, query, s)
return s
return encodedFinalHash, nil
}
44 changes: 37 additions & 7 deletions app/query/paid_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,13 @@ package query
import (
"context"
"database/sql"
"fmt"
"log"
"net/url"
"path/filepath"
"strings"
"testing"
"time"

"github.com/OdyseeTeam/odysee-api/app/auth"
"github.com/OdyseeTeam/odysee-api/app/sdkrouter"
Expand All @@ -22,6 +24,7 @@ import (
"github.com/OdyseeTeam/player-server/pkg/paid"

ljsonrpc "github.com/lbryio/lbry.go/v2/extras/jsonrpc"
"github.com/stretchr/testify/require"
"github.com/stretchr/testify/suite"
"github.com/ybbus/jsonrpc"
)
Expand Down Expand Up @@ -150,33 +153,47 @@ func (s *paidContentSuite) TestNoAccess() {
}

func (s *paidContentSuite) TestAccess() {
sp := "https://secure.odycdn.com/v5/streams/start"
pcfg := config.GetStreamsV6()
host := pcfg["paidhost"]
token := pcfg["token"]

timeSource = riggedTimeSource{time.Now()}
defer func() { timeSource = realTimeSource{} }()

signShortcut := func(host, path string) string {
hash, err := signStreamURL77(host, path, token, timeSource.Now().Add(24*time.Hour).Unix())
s.Require().NoError(err)
if strings.Contains(host, "live") {
return fmt.Sprintf("https://%s/%s%s", host, hash, path)
}
return fmt.Sprintf("https://%s/%s%s?%s=%s", host, hash, path, paramHash77, hash)
}

cases := []struct {
url, needUrl string
baseURL string
}{
{
url: urlRentalActive,
needUrl: sp + "/22acd6a6ab1c83d8c265d652c3842420810006be/96a3e2?hash-hls=33c2dc5a5aaf863e469488009b9164a6&ip=8.8.8.8&hash=90c0a6f1859842493354b462cc857c0c",
needUrl: signShortcut(host, "/v6/streams/22acd6a6ab1c83d8c265d652c3842420810006be/96a3e2e53a448dfd8e63eb4d7e035c698f35db593393097bdb38d9b2dc706cc3a0cfd97ea386087893c8d6843342aa87.mp4"),
},
{

url: urlPurchase,
needUrl: sp + "/2742f9e8eea0c4654ea8b51507dbb7f23f1f5235/2ef2a4?hash-hls=4e42be75b03ce2237e8ff8284c794392&ip=8.8.8.8&hash=910a69e8e189288c29a5695314b48e89",
needUrl: signShortcut(host, "/v6/streams/2742f9e8eea0c4654ea8b51507dbb7f23f1f5235/2ef2a4747d48a5706e3285e0a4043bb5ce9849f9a6d184062d56662370f8a84e18e84b66bc3eb3177cf38a42aaa25b06.mp4"),
},
{
url: urlMembersOnly,
needUrl: sp + "/7de672e799d17fc562ae7b381db1722a81856410/ad42aa?hash-hls=5e25826a1957b73084e85e5878fef08b&ip=8.8.8.8&hash=bcc9a904ae8621e910427f2eb3637be7",
needUrl: signShortcut(host, "/v6/streams/7de672e799d17fc562ae7b381db1722a81856410/ad42aa37738a6a2412bb58bb81c48afc06199f3f2d756fed42a5bc4ac0c58c3d5a52180eb59055521fb7aad7a4eac966.mp4"),
},
{
url: urlV2PurchaseRental,
needUrl: sp + "/970deae1469f2b4c7cc7286793b82676053ab3cd/2c2b26?hash-hls=eeb152996b8bc41279a7e76d8655a316&ip=8.8.8.8&hash=1acdcd58c789fac2d9813a5eca97e919",
needUrl: signShortcut(host, "/v6/streams/970deae1469f2b4c7cc7286793b82676053ab3cd/2c2b26b612c2c50f355ace21a12c4e1cb1fbf3f5c5dded2fb74eb788a42ea1903cb05b2b8ee8465d9d9c00e65b044aa1.mp4"),
},
{
url: urlLivestream,
baseURL: "https://cloud.odysee.live/secure/content/f9660d617e226959102e84436533638858d0b572/master.m3u8",
needUrl: "https://cloud.odysee.live/secure/content/f9660d617e226959102e84436533638858d0b572/master.m3u8?ip=8.8.8.8&hash=414505d9387c3809b11229bc3e238c62",
baseURL: "https://cloud.odysee.live/content/f9660d617e226959102e84436533638858d0b572/master.m3u8",
needUrl: signShortcut("cloud.odysee.live", "/content/f9660d617e226959102e84436533638858d0b572/master.m3u8"),
},
}
for _, tc := range cases {
Expand Down Expand Up @@ -235,3 +252,16 @@ func (s *paidContentSuite) getClaim(url string) *ljsonrpc.Claim {
s.Require().NoError(err)
return claim
}

func TestSignStreamURL77(t *testing.T) {
cdnResourceURL := "player.odycdn.com"
filePath := "/api/v4/streams/tc/all-the-times-we-nearly-blew-up-the" +
"/ac809d68d201e2f58dcd241b5aaeefe817634dda" +
"/2f562bd1dd318db726014d255c3c7f4e5cae3e746f77647e00ad7e9b272d193bcad634b515bf0a2bc471719cfdde0c00" +
"/master.m3u8"
secureToken := "aiphaechiSietee3heiKaezosaitip0i"
expiryTimestamp := int64(1695977338)
hash, err := signStreamURL77(cdnResourceURL, filePath, secureToken, expiryTimestamp)
require.NoError(t, err)
require.Equal(t, "Syc1EWOyivHWw9L4aquM1g==,1695977338", hash)
}
42 changes: 14 additions & 28 deletions app/query/processors.go
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,8 @@ const (
iapiTypeMembershipLiveStream = "Exclusive livestreams"

releaseTimeRoundDownSec = 300

paramHash77 = "hash77" // Nested hash parameter for signed hls url to use with CDN77
)

var errNeedSignedUrl = errors.Err("need signed url")
Expand All @@ -60,6 +62,7 @@ var rePurchaseFree = regexp.MustCompile(`(?i)does not have a purchase price`)
var timeSource TimeSource = realTimeSource{}

type TimeSource interface {
Now() time.Time
NowUnix() int64
NowAfter(time.Time) bool
}
Expand Down Expand Up @@ -92,6 +95,7 @@ func (c *ClaimSearchParams) NotTagsContains(tags ...string) bool {
return sliceContains(c.NotTags, tags...)
}

func (ts realTimeSource) Now() time.Time { return time.Now() }
func (ts realTimeSource) NowUnix() int64 { return time.Now().Unix() }
func (ts realTimeSource) NowAfter(t time.Time) bool { return time.Now().After(t) }

Expand Down Expand Up @@ -129,7 +133,7 @@ func preflightHookGet(caller *Caller, ctx context.Context) (*jsonrpc.RPCResponse
return nil, err
}
stream := claim.Value.GetStream()
pcfg := config.GetStreamsV5()
stConfig := config.GetStreamsV6()

hasAccess, err := checkStreamAccess(logging.AddToContext(ctx, logger), claim)
if !hasAccess {
Expand All @@ -142,19 +146,15 @@ func preflightHookGet(caller *Caller, ctx context.Context) (*jsonrpc.RPCResponse
return nil, errors.Err(m)
}
sdHash := hex.EncodeToString(src.SdHash)
startUrl := fmt.Sprintf("%s/%s/%s", pcfg["startpath"], claim.ClaimID, sdHash[:6])
hlsUrl := fmt.Sprintf("%s/%s/%s/master.m3u8", pcfg["hlspath"], claim.ClaimID, sdHash)
cu, err := auth.GetCurrentUserData(ctx)
hash, err := signStreamURL77(
stConfig["paidhost"], fmt.Sprintf(stConfig["startpath"], claim.ClaimID, sdHash),
stConfig["token"], timeSource.Now().Add(24*time.Hour).Unix())
if err != nil {
return nil, err
}
ip := cu.IP()
hlsHash := signStreamURL(hlsUrl, fmt.Sprintf("ip=%s&pass=%s", ip, pcfg["paidpass"]))
signedUrl := fmt.Sprintf("https://%s/%s%s", stConfig["paidhost"], hash, fmt.Sprintf(stConfig["startpath"], claim.ClaimID, sdHash))

startQuery := fmt.Sprintf("hash-hls=%s&ip=%s&pass=%s", hlsHash, ip, pcfg["paidpass"])
responseResult[ParamStreamingUrl] = fmt.Sprintf(
"%s%s?hash-hls=%s&ip=%s&hash=%s",
pcfg["paidhost"], startUrl, hlsHash, ip, signStreamURL(startUrl, startQuery))
responseResult[ParamStreamingUrl] = signedUrl + fmt.Sprintf("?%s=%s", paramHash77, hash)
response.Result = responseResult
return response, nil
} else if errors.Is(err, errNeedSignedLivestreamUrl) {
Expand All @@ -166,15 +166,12 @@ func preflightHookGet(caller *Caller, ctx context.Context) (*jsonrpc.RPCResponse
if err != nil {
return nil, errors.Err("invalid base_streaming_url supplied")
}
cu, err := auth.GetCurrentUserData(ctx)
hash, err := signStreamURL77(u.Host, u.Path, stConfig["token"], timeSource.Now().Add(24*time.Hour).Unix())
if err != nil {
return nil, err
}
ip := cu.IP()
query := fmt.Sprintf("ip=%s&pass=%s", ip, pcfg["paidpass"])
responseResult[ParamStreamingUrl] = fmt.Sprintf(
"%s?ip=%s&hash=%s",
baseUrl, ip, signStreamURL(u.Path, query))

responseResult[ParamStreamingUrl] = fmt.Sprintf("https://%s/%s%s", u.Host, hash, u.Path)
response.Result = responseResult
return response, nil
}
Expand Down Expand Up @@ -259,23 +256,12 @@ func preflightHookGet(caller *Caller, ctx context.Context) (*jsonrpc.RPCResponse
}
logger.Debug("stream token created", "stream", claim.Name+"/"+claim.ClaimID, "txid", purchaseTxId, "size", size)
cdnUrl := config.Config.Viper.GetString("PaidContentURL")
hasValidChannel := claim.SigningChannel != nil && claim.SigningChannel.ClaimID != ""
if hasValidChannel && controversialChannels[claim.SigningChannel.ClaimID] {
cdnUrl = strings.Replace(cdnUrl, "player.", "source.", -1)
}
contentURL = fmt.Sprintf(
"%v%s/%s/%s/%s",
cdnUrl, claim.Name, claim.ClaimID, sdHash, token)
responseResult[ParamPurchaseReceipt] = claim.PurchaseReceipt
} else {
cdnUrl := config.Config.Viper.GetString("FreeContentURL")
hasValidChannel := claim.SigningChannel != nil && claim.SigningChannel.ClaimID != ""
if hasValidChannel && controversialChannels[claim.SigningChannel.ClaimID] {
cdnUrl = strings.Replace(cdnUrl, "player.", "source.", -1)
}
contentURL = fmt.Sprintf(
"%v%s/%s/%s",
cdnUrl, claim.Name, claim.ClaimID, sdHash)
contentURL = "https://" + stConfig["host"] + fmt.Sprintf(stConfig["startpath"], claim.ClaimID, sdHash)
}

responseResult[ParamStreamingUrl] = contentURL
Expand Down
13 changes: 5 additions & 8 deletions apps/lbrytv/config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -134,6 +134,11 @@ func GetStreamsV5() map[string]string {
return Config.Viper.GetStringMapString("StreamsV5")
}

// GetStreamsV6 returns config map for v6 streams endpoint.
func GetStreamsV6() map[string]string {
return Config.Viper.GetStringMapString("StreamsV6")
}

// GetReflectorUpstream returns config map for publish reflector server.
func GetReflectorUpstream() map[string]string {
return Config.Viper.GetStringMapString("ReflectorUpstream")
Expand Down Expand Up @@ -167,14 +172,6 @@ func GetLbrynetServers() map[string]string {
}
}

func GetLbrynetXServer() string {
return Config.Viper.GetString("LbrynetXServer")
}

func GetLbrynetXPercentage() int {
return Config.Viper.GetInt("LbrynetXPercentage")
}

func GetTokenCacheTimeout() time.Duration {
return Config.Viper.GetDuration("TokenCacheTimeout") * time.Second
}
Expand Down
8 changes: 4 additions & 4 deletions docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,13 @@ version: "3.9"
services:
lbrynet:
image: odyseeteam/lbrynet-tv:0.110.0
platform: linux/amd64
container_name: lbrynet
ports:
- "15279:5279"
volumes:
- storage:/storage
environment:
SDK_CONFIG: /daemon/daemon_settings.yml
- lbrynet:/storage
- ./docker/daemon_settings.yml:/daemon/daemon_settings.yml
labels:
com.centurylinklabs.watchtower.enable: true
redis:
Expand Down Expand Up @@ -50,5 +50,5 @@ services:

volumes:
pgdata: {}
storage: {}
lbrynet: {}
minio-data: {}
Loading

0 comments on commit 6b9a6a5

Please sign in to comment.