Files
go2rtc/pkg/xiaomi/cloud.go
T

569 lines
12 KiB
Go

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
}