mirror of
https://github.com/AlexxIT/go2rtc.git
synced 2026-04-22 23:57:20 +08:00
569 lines
12 KiB
Go
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
|
|
}
|