268 lines
5.3 KiB
Go
268 lines
5.3 KiB
Go
package xiaomi
|
|
|
|
import (
|
|
"encoding/hex"
|
|
"encoding/json"
|
|
"errors"
|
|
"fmt"
|
|
"net/http"
|
|
"net/url"
|
|
"strings"
|
|
"sync"
|
|
|
|
"github.com/AlexxIT/go2rtc/internal/api"
|
|
"github.com/AlexxIT/go2rtc/internal/app"
|
|
"github.com/AlexxIT/go2rtc/internal/streams"
|
|
"github.com/AlexxIT/go2rtc/pkg/core"
|
|
"github.com/AlexxIT/go2rtc/pkg/xiaomi"
|
|
"github.com/AlexxIT/go2rtc/pkg/xiaomi/miss"
|
|
)
|
|
|
|
func Init() {
|
|
var v struct {
|
|
Cfg map[string]string `yaml:"xiaomi"`
|
|
}
|
|
app.LoadConfig(&v)
|
|
|
|
tokens = v.Cfg
|
|
|
|
log := app.GetLogger("xiaomi")
|
|
|
|
streams.HandleFunc("xiaomi", func(rawURL string) (core.Producer, error) {
|
|
u, err := url.Parse(rawURL)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
if u.User != nil {
|
|
rawURL, err = getCameraURL(u)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
}
|
|
|
|
log.Debug().Msgf("xiaomi: dial %s", rawURL)
|
|
|
|
return xiaomi.Dial(rawURL)
|
|
})
|
|
|
|
api.HandleFunc("api/xiaomi", apiXiaomi)
|
|
}
|
|
|
|
var tokens map[string]string
|
|
var tokensMu sync.Mutex
|
|
|
|
func getCloud(userID string) (*xiaomi.Cloud, error) {
|
|
tokensMu.Lock()
|
|
defer tokensMu.Unlock()
|
|
|
|
token := tokens[userID]
|
|
cloud := xiaomi.NewCloud(AppXiaomiHome)
|
|
if err := cloud.LoginWithToken(userID, token); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return cloud, nil
|
|
}
|
|
|
|
func getCameraURL(url *url.URL) (string, error) {
|
|
clientPublic, clientPrivate, err := miss.GenerateKey()
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
|
|
query := url.Query()
|
|
|
|
params := fmt.Sprintf(
|
|
`{"app_pubkey":"%x","did":"%s","support_vendors":"CS2"}`,
|
|
clientPublic, query.Get("did"),
|
|
)
|
|
|
|
cloud, err := getCloud(url.User.Username())
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
|
|
region, _ := url.User.Password()
|
|
|
|
res, err := cloud.Request(GetBaseURL(region), "/v2/device/miss_get_vendor", params, nil)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
|
|
var v struct {
|
|
Vendor struct {
|
|
VendorID byte `json:"vendor"`
|
|
} `json:"vendor"`
|
|
PublicKey string `json:"public_key"`
|
|
Sign string `json:"sign"`
|
|
}
|
|
if err = json.Unmarshal(res, &v); err != nil {
|
|
return "", err
|
|
}
|
|
|
|
query.Set("client_public", hex.EncodeToString(clientPublic))
|
|
query.Set("client_private", hex.EncodeToString(clientPrivate))
|
|
query.Set("device_public", v.PublicKey)
|
|
query.Set("sign", v.Sign)
|
|
query.Set("vendor", getVendorName(v.Vendor.VendorID))
|
|
|
|
url.RawQuery = query.Encode()
|
|
return url.String(), nil
|
|
}
|
|
|
|
func getVendorName(i byte) string {
|
|
switch i {
|
|
case 1:
|
|
return "tutk"
|
|
case 3:
|
|
return "agora"
|
|
case 4:
|
|
return "cs2"
|
|
case 6:
|
|
return "mtp"
|
|
}
|
|
return fmt.Sprintf("%d", i)
|
|
}
|
|
|
|
func apiXiaomi(w http.ResponseWriter, r *http.Request) {
|
|
switch r.Method {
|
|
case "GET":
|
|
apiDeviceList(w, r)
|
|
case "POST":
|
|
apiAuth(w, r)
|
|
}
|
|
}
|
|
|
|
func apiDeviceList(w http.ResponseWriter, r *http.Request) {
|
|
query := r.URL.Query()
|
|
|
|
user := query.Get("id")
|
|
if user == "" {
|
|
tokensMu.Lock()
|
|
users := make([]string, 0, len(tokens))
|
|
for s := range tokens {
|
|
users = append(users, s)
|
|
}
|
|
tokensMu.Unlock()
|
|
|
|
api.ResponseJSON(w, users)
|
|
return
|
|
}
|
|
|
|
err := func() error {
|
|
cloud, err := getCloud(user)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
region := query.Get("region")
|
|
|
|
res, err := cloud.Request(GetBaseURL(region), "/v2/home/device_list_page", "{}", nil)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
var v struct {
|
|
List []*Device `json:"list"`
|
|
}
|
|
|
|
if err = json.Unmarshal(res, &v); err != nil {
|
|
return err
|
|
}
|
|
|
|
var items []*api.Source
|
|
|
|
for _, device := range v.List {
|
|
if !strings.Contains(device.Model, ".camera.") && !strings.Contains(device.Model, ".cateye.") {
|
|
continue
|
|
}
|
|
items = append(items, &api.Source{
|
|
Name: device.Name,
|
|
Info: fmt.Sprintf("ip: %s, mac: %s", device.IP, device.MAC),
|
|
URL: fmt.Sprintf("xiaomi://%s:%s@%s?did=%s&model=%s", user, region, device.IP, device.Did, device.Model),
|
|
})
|
|
}
|
|
|
|
api.ResponseSources(w, items)
|
|
return nil
|
|
}()
|
|
|
|
if err != nil {
|
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
|
}
|
|
}
|
|
|
|
type Device struct {
|
|
Did string `json:"did"`
|
|
Name string `json:"name"`
|
|
Model string `json:"model"`
|
|
MAC string `json:"mac"`
|
|
IP string `json:"localip"`
|
|
}
|
|
|
|
var auth *xiaomi.Cloud
|
|
|
|
func apiAuth(w http.ResponseWriter, r *http.Request) {
|
|
if err := r.ParseForm(); err != nil {
|
|
http.Error(w, err.Error(), http.StatusBadRequest)
|
|
return
|
|
}
|
|
|
|
username := r.Form.Get("username")
|
|
password := r.Form.Get("password")
|
|
captcha := r.Form.Get("captcha")
|
|
verify := r.Form.Get("verify")
|
|
|
|
var err error
|
|
|
|
switch {
|
|
case username != "" || password != "":
|
|
auth = xiaomi.NewCloud(AppXiaomiHome)
|
|
err = auth.Login(username, password)
|
|
case captcha != "":
|
|
err = auth.LoginWithCaptcha(captcha)
|
|
case verify != "":
|
|
err = auth.LoginWithVerify(verify)
|
|
default:
|
|
http.Error(w, "wrong request", http.StatusBadRequest)
|
|
return
|
|
}
|
|
|
|
if err == nil {
|
|
userID, token := auth.UserToken()
|
|
auth = nil
|
|
|
|
tokensMu.Lock()
|
|
if tokens == nil {
|
|
tokens = map[string]string{userID: token}
|
|
} else {
|
|
tokens[userID] = token
|
|
}
|
|
tokensMu.Unlock()
|
|
|
|
err = app.PatchConfig([]string{"xiaomi", userID}, token)
|
|
}
|
|
|
|
if err != nil {
|
|
var login *xiaomi.LoginError
|
|
if errors.As(err, &login) {
|
|
w.Header().Set("Content-Type", api.MimeJSON)
|
|
w.WriteHeader(http.StatusUnauthorized)
|
|
_ = json.NewEncoder(w).Encode(err)
|
|
return
|
|
}
|
|
|
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
|
}
|
|
}
|
|
|
|
const AppXiaomiHome = "xiaomiio"
|
|
|
|
func GetBaseURL(region string) string {
|
|
switch region {
|
|
case "de", "i2", "ru", "sg", "us":
|
|
return "https://" + region + ".api.io.mi.com/app"
|
|
}
|
|
return "https://api.io.mi.com/app"
|
|
}
|