install go2rtc on bob
This commit is contained in:
@@ -0,0 +1,568 @@
|
||||
package xiaomi
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"crypto/md5"
|
||||
"crypto/rand"
|
||||
"crypto/rc4"
|
||||
"crypto/sha1"
|
||||
"crypto/sha256"
|
||||
"encoding/base64"
|
||||
"encoding/binary"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/AlexxIT/go2rtc/pkg/core"
|
||||
)
|
||||
|
||||
type Cloud struct {
|
||||
client *http.Client
|
||||
|
||||
sid string
|
||||
cookies string // for auth
|
||||
ssecurity []byte // for encryption
|
||||
|
||||
userID string
|
||||
passToken string
|
||||
|
||||
auth map[string]string
|
||||
}
|
||||
|
||||
func NewCloud(sid string) *Cloud {
|
||||
return &Cloud{
|
||||
client: &http.Client{Timeout: 15 * time.Second},
|
||||
sid: sid,
|
||||
}
|
||||
}
|
||||
|
||||
func (c *Cloud) Login(username, password string) error {
|
||||
res, err := c.client.Get("https://account.xiaomi.com/pass/serviceLogin?_json=true&sid=" + c.sid)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
var v1 struct {
|
||||
Qs string `json:"qs"`
|
||||
Sign string `json:"_sign"`
|
||||
Sid string `json:"sid"`
|
||||
Callback string `json:"callback"`
|
||||
}
|
||||
if _, err = readLoginResponse(res.Body, &v1); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
hash := fmt.Sprintf("%X", md5.Sum([]byte(password)))
|
||||
|
||||
form := url.Values{
|
||||
"_json": {"true"},
|
||||
"hash": {hash},
|
||||
"sid": {v1.Sid},
|
||||
"callback": {v1.Callback},
|
||||
"_sign": {v1.Sign},
|
||||
"qs": {v1.Qs},
|
||||
"user": {username},
|
||||
}
|
||||
cookies := "deviceId=" + core.RandString(16, 62)
|
||||
|
||||
// login after captcha
|
||||
if c.auth != nil && c.auth["captcha_code"] != "" {
|
||||
form.Set("captCode", c.auth["captcha_code"])
|
||||
cookies += "; ick=" + c.auth["ick"]
|
||||
}
|
||||
|
||||
req := Request{
|
||||
Method: "POST",
|
||||
URL: "https://account.xiaomi.com/pass/serviceLoginAuth2",
|
||||
Body: form,
|
||||
RawCookies: cookies,
|
||||
}.Encode()
|
||||
|
||||
res, err = c.client.Do(req)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
var v2 struct {
|
||||
Ssecurity []byte `json:"ssecurity"`
|
||||
PassToken string `json:"passToken"`
|
||||
Location string `json:"location"`
|
||||
|
||||
CaptchaURL string `json:"captchaURL"`
|
||||
NotificationURL string `json:"notificationUrl"`
|
||||
}
|
||||
body, err := readLoginResponse(res.Body, &v2)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// save auth for two-step verification
|
||||
c.auth = map[string]string{
|
||||
"username": username,
|
||||
"password": password,
|
||||
}
|
||||
|
||||
if v2.CaptchaURL != "" {
|
||||
return c.getCaptcha(v2.CaptchaURL)
|
||||
}
|
||||
|
||||
if v2.NotificationURL != "" {
|
||||
return c.authStart(v2.NotificationURL)
|
||||
}
|
||||
|
||||
if v2.Location == "" {
|
||||
return fmt.Errorf("xiaomi: %s", body)
|
||||
}
|
||||
|
||||
c.auth = nil
|
||||
c.ssecurity = v2.Ssecurity
|
||||
c.passToken = v2.PassToken
|
||||
|
||||
return c.finishAuth(v2.Location)
|
||||
}
|
||||
|
||||
func (c *Cloud) LoginWithCaptcha(captcha string) error {
|
||||
if c.auth == nil || c.auth["ick"] == "" {
|
||||
panic("wrong login step")
|
||||
}
|
||||
|
||||
c.auth["captcha_code"] = captcha
|
||||
|
||||
// check if captcha after verify
|
||||
if c.auth["flag"] != "" {
|
||||
return c.sendTicket()
|
||||
}
|
||||
|
||||
return c.Login(c.auth["username"], c.auth["password"])
|
||||
}
|
||||
|
||||
func (c *Cloud) LoginWithVerify(ticket string) error {
|
||||
if c.auth == nil || c.auth["flag"] == "" {
|
||||
panic("wrong login step")
|
||||
}
|
||||
|
||||
req := Request{
|
||||
Method: "POST",
|
||||
URL: "https://account.xiaomi.com/identity/auth/verify" + c.verifyName(),
|
||||
RawParams: "_flag" + c.auth["flag"] + "&ticket=" + ticket + "&trust=false&_json=true",
|
||||
RawCookies: "identity_session=" + c.auth["identity_session"],
|
||||
}.Encode()
|
||||
|
||||
res, err := c.client.Do(req)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
var v1 struct {
|
||||
Location string `json:"location"`
|
||||
}
|
||||
body, err := readLoginResponse(res.Body, &v1)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if v1.Location == "" {
|
||||
return fmt.Errorf("xiaomi: %s", body)
|
||||
}
|
||||
|
||||
return c.finishAuth(v1.Location)
|
||||
}
|
||||
|
||||
func (c *Cloud) getCaptcha(captchaURL string) error {
|
||||
res, err := c.client.Get("https://account.xiaomi.com" + captchaURL)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer res.Body.Close()
|
||||
|
||||
body, err := io.ReadAll(res.Body)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
c.auth["ick"] = findCookie(res, "ick")
|
||||
|
||||
return &LoginError{
|
||||
Captcha: body,
|
||||
}
|
||||
}
|
||||
|
||||
func (c *Cloud) authStart(notificationURL string) error {
|
||||
rawURL := strings.Replace(notificationURL, "/fe/service/identity/authStart", "/identity/list", 1)
|
||||
res, err := c.client.Get(rawURL)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
var v1 struct {
|
||||
Code int `json:"code"`
|
||||
Flag int `json:"flag"`
|
||||
}
|
||||
if _, err = readLoginResponse(res.Body, &v1); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
c.auth["flag"] = strconv.Itoa(v1.Flag)
|
||||
c.auth["identity_session"] = findCookie(res, "identity_session")
|
||||
|
||||
return c.sendTicket()
|
||||
}
|
||||
|
||||
func findCookie(res *http.Response, name string) string {
|
||||
for _, cookie := range res.Cookies() {
|
||||
if cookie.Name == name {
|
||||
return cookie.Value
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func (c *Cloud) verifyName() string {
|
||||
switch c.auth["flag"] {
|
||||
case "4":
|
||||
return "Phone"
|
||||
case "8":
|
||||
return "Email"
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func (c *Cloud) sendTicket() error {
|
||||
name := c.verifyName()
|
||||
cookies := "identity_session=" + c.auth["identity_session"]
|
||||
|
||||
req := Request{
|
||||
URL: "https://account.xiaomi.com/identity/auth/verify" + name,
|
||||
RawParams: "_flag=" + c.auth["flag"] + "&_json=true",
|
||||
RawCookies: cookies,
|
||||
}.Encode()
|
||||
|
||||
res, err := c.client.Do(req)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
var v1 struct {
|
||||
Code int `json:"code"`
|
||||
MaskedPhone string `json:"maskedPhone"`
|
||||
MaskedEmail string `json:"maskedEmail"`
|
||||
}
|
||||
if _, err = readLoginResponse(res.Body, &v1); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// verify after captcha
|
||||
captCode := c.auth["captcha_code"]
|
||||
if captCode != "" {
|
||||
cookies += "; ick=" + c.auth["ick"]
|
||||
}
|
||||
|
||||
form := url.Values{
|
||||
"_json": {"true"},
|
||||
"icode": {captCode},
|
||||
"retry": {"0"},
|
||||
}
|
||||
|
||||
req = Request{
|
||||
Method: "POST",
|
||||
URL: "https://account.xiaomi.com/identity/auth/send" + name + "Ticket",
|
||||
Body: form,
|
||||
RawCookies: cookies,
|
||||
}.Encode()
|
||||
|
||||
res, err = c.client.Do(req)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
var v2 struct {
|
||||
Code int `json:"code"`
|
||||
CaptchaURL string `json:"captchaURL"`
|
||||
}
|
||||
body, err := readLoginResponse(res.Body, &v2)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if v2.CaptchaURL != "" {
|
||||
return c.getCaptcha(v2.CaptchaURL)
|
||||
}
|
||||
|
||||
if v2.Code != 0 {
|
||||
return fmt.Errorf("xiaomi: %s", body)
|
||||
}
|
||||
|
||||
return &LoginError{
|
||||
VerifyPhone: v1.MaskedPhone,
|
||||
VerifyEmail: v1.MaskedEmail,
|
||||
}
|
||||
}
|
||||
|
||||
type LoginError struct {
|
||||
Captcha []byte `json:"captcha,omitempty"`
|
||||
VerifyPhone string `json:"verify_phone,omitempty"`
|
||||
VerifyEmail string `json:"verify_email,omitempty"`
|
||||
}
|
||||
|
||||
func (l *LoginError) Error() string {
|
||||
return ""
|
||||
}
|
||||
|
||||
func (c *Cloud) finishAuth(location string) error {
|
||||
res, err := c.client.Get(location)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer res.Body.Close()
|
||||
|
||||
// LoginWithVerify
|
||||
// - userId, cUserId, serviceToken from cookies
|
||||
// - passToken from redirect cookies
|
||||
// - ssecurity from extra header
|
||||
// LoginWithToken
|
||||
// - userId, cUserId, serviceToken from cookies
|
||||
var cUserId, serviceToken string
|
||||
|
||||
for res != nil {
|
||||
for _, cookie := range res.Cookies() {
|
||||
switch cookie.Name {
|
||||
case "userId":
|
||||
c.userID = cookie.Value
|
||||
case "cUserId":
|
||||
cUserId = cookie.Value
|
||||
case "serviceToken":
|
||||
serviceToken = cookie.Value
|
||||
case "passToken":
|
||||
c.passToken = cookie.Value
|
||||
}
|
||||
}
|
||||
|
||||
if s := res.Header.Get("Extension-Pragma"); s != "" {
|
||||
var v1 struct {
|
||||
Ssecurity []byte `json:"ssecurity"`
|
||||
}
|
||||
if err = json.Unmarshal([]byte(s), &v1); err != nil {
|
||||
return err
|
||||
}
|
||||
c.ssecurity = v1.Ssecurity
|
||||
}
|
||||
|
||||
res = res.Request.Response
|
||||
}
|
||||
|
||||
c.cookies = fmt.Sprintf("userId=%s; cUserId=%s; serviceToken=%s", c.userID, cUserId, serviceToken)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *Cloud) LoginWithToken(userID, passToken string) error {
|
||||
req, err := http.NewRequest("GET", "https://account.xiaomi.com/pass/serviceLogin?_json=true&sid="+c.sid, nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
req.Header.Set("Cookie", fmt.Sprintf("userId=%s; passToken=%s", userID, passToken))
|
||||
|
||||
res, err := c.client.Do(req)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
var v1 struct {
|
||||
Ssecurity []byte `json:"ssecurity"`
|
||||
PassToken string `json:"passToken"`
|
||||
Location string `json:"location"`
|
||||
}
|
||||
if _, err = readLoginResponse(res.Body, &v1); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
c.ssecurity = v1.Ssecurity
|
||||
c.passToken = v1.PassToken
|
||||
|
||||
return c.finishAuth(v1.Location)
|
||||
}
|
||||
|
||||
func (c *Cloud) UserToken() (string, string) {
|
||||
return c.userID, c.passToken
|
||||
}
|
||||
|
||||
func (c *Cloud) Request(baseURL, apiURL, params string, headers map[string]string) ([]byte, error) {
|
||||
form := url.Values{"data": {params}}
|
||||
|
||||
nonce := genNonce()
|
||||
signedNonce := genSignedNonce(c.ssecurity, nonce)
|
||||
|
||||
// 1. gen hash for data param
|
||||
form.Set("rc4_hash__", genSignature64("POST", apiURL, form, signedNonce))
|
||||
|
||||
// 2. encrypt data and hash params
|
||||
for _, v := range form {
|
||||
ciphertext, err := crypt(signedNonce, []byte(v[0]))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
v[0] = base64.StdEncoding.EncodeToString(ciphertext)
|
||||
}
|
||||
|
||||
// 3. add signature for encrypted data and hash params
|
||||
form.Set("signature", genSignature64("POST", apiURL, form, signedNonce))
|
||||
|
||||
// 4. add nonce
|
||||
form.Set("_nonce", base64.StdEncoding.EncodeToString(nonce))
|
||||
|
||||
req, err := http.NewRequest("POST", baseURL+apiURL, strings.NewReader(form.Encode()))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
req.Header.Set("Cookie", c.cookies)
|
||||
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
|
||||
|
||||
for k, v := range headers {
|
||||
req.Header.Set(k, v)
|
||||
}
|
||||
|
||||
res, err := c.client.Do(req)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer res.Body.Close()
|
||||
|
||||
if res.StatusCode != http.StatusOK {
|
||||
return nil, errors.New(res.Status)
|
||||
}
|
||||
|
||||
body, err := io.ReadAll(res.Body)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
ciphertext, err := base64.StdEncoding.DecodeString(string(body))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
plaintext, err := crypt(signedNonce, ciphertext)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var res1 struct {
|
||||
Code int `json:"code"`
|
||||
Message string `json:"message"`
|
||||
Result json.RawMessage `json:"result"`
|
||||
}
|
||||
if err = json.Unmarshal(plaintext, &res1); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if res1.Code != 0 {
|
||||
return nil, errors.New("xiaomi: " + res1.Message)
|
||||
}
|
||||
|
||||
return res1.Result, nil
|
||||
}
|
||||
|
||||
func readLoginResponse(rc io.ReadCloser, v any) ([]byte, error) {
|
||||
defer rc.Close()
|
||||
|
||||
body, err := io.ReadAll(rc)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
body, ok := bytes.CutPrefix(body, []byte("&&&START&&&"))
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("xiaomi: %s", body)
|
||||
}
|
||||
|
||||
return body, json.Unmarshal(body, &v)
|
||||
}
|
||||
|
||||
func genNonce() []byte {
|
||||
ts := time.Now().Unix() / 60
|
||||
|
||||
nonce := make([]byte, 12)
|
||||
_, _ = rand.Read(nonce[:8])
|
||||
binary.BigEndian.PutUint32(nonce[8:], uint32(ts))
|
||||
return nonce
|
||||
}
|
||||
|
||||
func genSignedNonce(ssecurity, nonce []byte) []byte {
|
||||
hasher := sha256.New()
|
||||
hasher.Write(ssecurity)
|
||||
hasher.Write(nonce)
|
||||
return hasher.Sum(nil)
|
||||
}
|
||||
|
||||
func crypt(key, plaintext []byte) ([]byte, error) {
|
||||
cipher, err := rc4.NewCipher(key)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
tmp := make([]byte, 1024)
|
||||
cipher.XORKeyStream(tmp, tmp)
|
||||
|
||||
ciphertext := make([]byte, len(plaintext))
|
||||
cipher.XORKeyStream(ciphertext, plaintext)
|
||||
|
||||
return ciphertext, nil
|
||||
}
|
||||
|
||||
func genSignature64(method, path string, values url.Values, signedNonce []byte) string {
|
||||
s := method + "&" + path + "&data=" + values.Get("data")
|
||||
if values.Has("rc4_hash__") {
|
||||
s += "&rc4_hash__=" + values.Get("rc4_hash__")
|
||||
}
|
||||
s += "&" + base64.StdEncoding.EncodeToString(signedNonce)
|
||||
|
||||
hasher := sha1.New()
|
||||
hasher.Write([]byte(s))
|
||||
signature := hasher.Sum(nil)
|
||||
|
||||
return base64.StdEncoding.EncodeToString(signature)
|
||||
}
|
||||
|
||||
type Request struct {
|
||||
Method string
|
||||
URL string
|
||||
RawParams string
|
||||
Body url.Values
|
||||
Headers url.Values
|
||||
RawCookies string
|
||||
}
|
||||
|
||||
func (r Request) Encode() *http.Request {
|
||||
if r.RawParams != "" {
|
||||
r.URL += "?" + r.RawParams
|
||||
}
|
||||
|
||||
var body io.Reader
|
||||
if r.Body != nil {
|
||||
body = strings.NewReader(r.Body.Encode())
|
||||
}
|
||||
|
||||
req, err := http.NewRequest(r.Method, r.URL, body)
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
if r.Headers != nil {
|
||||
req.Header = http.Header(r.Headers)
|
||||
}
|
||||
if r.Body != nil {
|
||||
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
|
||||
}
|
||||
if r.RawCookies != "" {
|
||||
req.Header.Set("Cookie", r.RawCookies)
|
||||
}
|
||||
|
||||
return req
|
||||
}
|
||||
Reference in New Issue
Block a user