diff --git a/client.go b/client.go new file mode 100644 index 0000000..0dc1e1b --- /dev/null +++ b/client.go @@ -0,0 +1,201 @@ +package rumblelivestreamlib + +import ( + "encoding/json" + "fmt" + "io" + "log" + "net/http" + "net/http/cookiejar" + "net/url" + "strings" + + "github.com/robertkrimen/otto" +) + +const ( + urlWeb = "https://rumble.com" + urlGetSalts = urlWeb + "/service.php?name=user.get_salts" + urlUserLogin = urlWeb + "/service.php?name=user.login" + urlUserLogout = urlWeb + "/service.php?name=user.logout" +) + +type Client struct { + httpClient *http.Client + StreamKey string + StreamUrl string +} + +func (c *Client) printCookies() { + u, err := url.Parse(urlWeb) + if err != nil { + log.Fatal("url.Parse err=", err) + } + fmt.Println("Cookies:") + for _, cookie := range c.httpClient.Jar.Cookies(u) { + fmt.Println(cookie) + } +} + +func NewClient(streamKey string, streamUrl string) (*Client, error) { + cl, err := newHttpClient() + if err != nil { + return nil, pkgErr("error creating http client", err) + } + + return &Client{cl, streamKey, streamUrl}, nil +} + +func newHttpClient() (*http.Client, error) { + jar, err := cookiejar.New(nil) + if err != nil { + return nil, fmt.Errorf("error creating cookiejar: %v", err) + } + + return &http.Client{Jar: jar}, nil +} + +type GetSaltsData struct { + Salts []string `json:"salts"` +} + +type GetSaltsResponse struct { + Data GetSaltsData `json:"data"` +} + +func (c *Client) Login(username string, password string) error { + if c.httpClient == nil { + return pkgErr("", fmt.Errorf("http client is nil")) + } + + salts, err := c.getSalts(username) + if err != nil { + return pkgErr("error getting salts", err) + } + + err = c.userLogin(username, password, salts) + if err != nil { + return pkgErr("error logging in", err) + } + + return nil +} + +func (c *Client) getWeb() error { + resp, err := c.httpClient.Get(urlWeb) + if err != nil { + return fmt.Errorf("http Get request returned error: %v", err) + } + defer resp.Body.Close() + if resp.StatusCode != http.StatusOK { + return fmt.Errorf("http Get response status not %s: %s", http.StatusText(http.StatusOK), resp.Status) + } + + return nil +} + +func (c *Client) getSalts(username string) ([]string, error) { + u := url.URL{} + q := u.Query() + q.Add("username", username) + body := q.Encode() + resp, err := c.httpClient.Post(urlGetSalts, "application/x-www-form-urlencoded", strings.NewReader(body)) + if err != nil { + return nil, fmt.Errorf("http Post request returned error: %v", err) + } + defer resp.Body.Close() + if resp.StatusCode != http.StatusOK { + return nil, fmt.Errorf("http Post response status not %s: %s", http.StatusText(http.StatusOK), resp.Status) + } + + bodyB, err := io.ReadAll(resp.Body) + if err != nil { + return nil, fmt.Errorf("error reading body bytes: %v", err) + } + fmt.Println("BodyB:", string(bodyB)) + + var gsr GetSaltsResponse + err = json.NewDecoder(strings.NewReader(string(bodyB))).Decode(&gsr) + if err != nil { + return nil, fmt.Errorf("error decoding response body from server: %v", err) + } + + return gsr.Data.Salts, nil +} + +func (c *Client) userLogin(username string, password string, salts []string) error { + hashes, err := generateHashes(password, salts) + if err != nil { + return fmt.Errorf("error generating password hashes: %v", err) + } + + u := url.URL{} + q := u.Query() + q.Add("username", username) + q.Add("password_hashes", hashes) + body := q.Encode() + resp, err := c.httpClient.Post(urlUserLogin, "application/x-www-form-urlencoded", strings.NewReader(body)) + if err != nil { + return fmt.Errorf("http Post request returned error: %v", err) + } + defer resp.Body.Close() + if resp.StatusCode != http.StatusOK { + return fmt.Errorf("http Post response status not %s: %s", http.StatusText(http.StatusOK), resp.Status) + } + bodyB, _ := io.ReadAll(resp.Body) + fmt.Println(string(bodyB)) + + return nil +} + +func generateHashes(password string, salts []string) (string, error) { + vm := otto.New() + + vm.Set("password", password) + vm.Set("salt0", salts[0]) + vm.Set("salt1", salts[1]) + vm.Set("salt2", salts[2]) + + _, err := vm.Run(md5) + if err != nil { + return "", fmt.Errorf("error running md5 javascript: %v", err) + } + + value, err := vm.Get("hashes") + if err != nil { + return "", fmt.Errorf("error getting hashes value: %v", err) + } + + hashes, err := value.ToString() + if err != nil { + return "", fmt.Errorf("error converting hashes value to string: %v", err) + } + + return hashes, nil +} + +func (c *Client) Logout() error { + if c.httpClient == nil { + return pkgErr("", fmt.Errorf("http client is nil")) + } + + err := c.userLogout() + if err != nil { + return pkgErr("error logging out", err) + } + + return nil +} + +func (c *Client) userLogout() error { + resp, err := c.httpClient.Get(urlUserLogout) + if err != nil { + return fmt.Errorf("http Get request returned error: %v", err) + } + defer resp.Body.Close() + if resp.StatusCode != http.StatusOK { + return fmt.Errorf("http Get response status not %s: %s", http.StatusText(http.StatusOK), resp.Status) + } + + return nil +} diff --git a/go.mod b/go.mod index 44eade4..0eca6c1 100644 --- a/go.mod +++ b/go.mod @@ -1,3 +1,9 @@ module github.com/tylertravisty/rumble-livestream-lib-go go 1.19 + +require ( + github.com/robertkrimen/otto v0.2.1 // indirect + golang.org/x/text v0.4.0 // indirect + gopkg.in/sourcemap.v1 v1.0.5 // indirect +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..b3e640f --- /dev/null +++ b/go.sum @@ -0,0 +1,6 @@ +github.com/robertkrimen/otto v0.2.1 h1:FVP0PJ0AHIjC+N4pKCG9yCDz6LHNPCwi/GKID5pGGF0= +github.com/robertkrimen/otto v0.2.1/go.mod h1:UPwtJ1Xu7JrLcZjNWN8orJaM5n5YEtqL//farB5FlRY= +golang.org/x/text v0.4.0 h1:BrVqGRd7+k1DiOgtnFvAkoQEWQvBc25ouMJM6429SFg= +golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= +gopkg.in/sourcemap.v1 v1.0.5 h1:inv58fC9f9J3TK2Y2R1NPntXEn3/wjWHkonhIUODNTI= +gopkg.in/sourcemap.v1 v1.0.5/go.mod h1:2RlvNNSMglmRrcvhfuzp4hQHwOtjxlbjX7UPY/GXb78= diff --git a/livestream.go b/livestream.go index c6094b3..035b43a 100644 --- a/livestream.go +++ b/livestream.go @@ -89,12 +89,8 @@ type LivestreamResponse struct { Livestreams []Livestream `json:"livestreams"` } -type Client struct { - UrlKey string -} - func (c *Client) Request() (*LivestreamResponse, error) { - resp, err := http.Get(c.UrlKey) + resp, err := http.Get(c.StreamKey) if err != nil { return nil, pkgErr("http Get request returned error", err) } diff --git a/md5.js.go b/md5.js.go new file mode 100644 index 0000000..ffa943e --- /dev/null +++ b/md5.js.go @@ -0,0 +1,85 @@ +package rumblelivestreamlib + +const md5 = ` +/* @license + * Version 2.2 Copyright (C) Paul Johnston 1999 - 2009 + * Other contributors: Greg Holt, Andrew Kepert, Ydnar, Lostinet + * Distributed under the BSD License + * See http://pajhome.org.uk/crypt/md5 for more info. + */ + +md5 = function() { + function n(){ + this.hex="0123456789abcdef".split("") + } + return n.prototype={ + hash:function(n){ + var h=this; + return h.binHex(h.binHash(h.strBin(n),n.length<<3)) + }, + hashUTF8:function(n){ + return this.hash(this.encUTF8(n)) + }, + hashRaw:function(n){ + var h=this; + return h.binStr(h.binHash(h.strBin(n),n.length<<3)) + }, + hashRawUTF8:function(n){ + return this.hashRaw(this.encUTF8(n)) + }, + hashStretch:function(n,h,i){ + return this.binHex(this.binHashStretch(n,h,i)) + }, + binHashStretch:function(n,h,i){ + var t,r,f=this,n=f.encUTF8(n),e=h+n,g=32+n.length<<3,o=f.strBin(n),a=o.length,e=f.binHash(f.strBin(e),e.length<<3); + for(i=i||1024,t=0;t>>6&31,128|63&h):h<=65535?t+=String.fromCharCode(224|h>>>12&15,128|h>>>6&63,128|63&h):h<=2097151&&(t+=String.fromCharCode(240|h>>>18&7,128|h>>>12&63,128|h>>>6&63,128|63&h)); + return t + }, + strBin:function(n){ + for(var h=n.length<<3,i=[],t=0;t>5]|=(255&n.charCodeAt(t>>3))<<(31&t); + return i + }, + binHex:function(n){ + for(var h,i,t="",r=n.length<<5,f=0;f>5]>>>(31&f)&255)>>>4&15,t+=this.hex[i]+this.hex[h&=15]; + return t + }, + binStr:function(n){ + for(var h,i="",t=n.length<<5,r=0;r>5]>>>(31&r)&255,i+=String.fromCharCode(h); + return i + }, + binHexBin:function(n){ + for(var h,i,t=n.length<<5,r=[],f=0;f>5]>>>(31&f)&255)>>>4&15,r[f>>4]|=(9>16)+(i>>16)+(r>>16)+(e>>16)+(t>>16)<<16|65535&t)<>>32-f)>>16)+(h>>16)+((t=(65535&i)+(65535&h))>>16)<<16|65535&t + }, + gg:function(n,h,i,t,r,f,e){ + i=h&t|i&~t,t=(65535&n)+(65535&i)+(65535&r)+(65535&e); + return((i=(i=(n>>16)+(i>>16)+(r>>16)+(e>>16)+(t>>16)<<16|65535&t)<>>32-f)>>16)+(h>>16)+((t=(65535&i)+(65535&h))>>16)<<16|65535&t + }, + hh:function(n,h,i,t,r,f,e){ + i=h^i^t,t=(65535&n)+(65535&i)+(65535&r)+(65535&e); + return((i=(i=(n>>16)+(i>>16)+(r>>16)+(e>>16)+(t>>16)<<16|65535&t)<>>32-f)>>16)+(h>>16)+((t=(65535&i)+(65535&h))>>16)<<16|65535&t + }, + ii:function(n,h,i,t,r,f,e){ + i^=h|~t,t=(65535&n)+(65535&i)+(65535&r)+(65535&e); + return((i=(i=(n>>16)+(i>>16)+(r>>16)+(e>>16)+(t>>16)<<16|65535&t)<>>32-f)>>16)+(h>>16)+((t=(65535&i)+(65535&h))>>16)<<16|65535&t + }, + binHash:function(n,h){ + var i,t,r,f,e,g,o=1732584193,a=-271733879,u=-1732584194,s=271733878,c=this;for(n[h>>5]|=128<<(31&h),n[14+(h+64>>>9<<4)]=h,i=n.length,t=0;t>16)+(g>>16)+((g=(65535&o)+(65535&g))>>16)<<16|65535&g,a=(a>>16)+(r>>16)+((g=(65535&a)+(65535&r))>>16)<<16|65535&g,u=(u>>16)+(f>>16)+((g=(65535&u)+(65535&f))>>16)<<16|65535&g,s=(s>>16)+(e>>16)+((g=(65535&s)+(65535&e))>>16)<<16|65535&g; + return[o,a,u,s] + } + }, + new n +}(); + +hashes = [md5.hash(md5.hashStretch(password, salt0, 128) + salt1),md5.hashStretch(password, salt2, 128), salt1] +`