From eaf6d29fd8613c0de77b0f9458880fc03bffb4e9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=BA=90=E6=96=87=E9=9B=A8?= <41315874+fumiama@users.noreply.github.com> Date: Mon, 24 Nov 2025 17:35:20 +0800 Subject: [PATCH] feat: add more apis & cmd tyaliv --- .gitignore | 6 + api/auth/client.go | 13 +- api/auth/client_test.go | 39 +++++ api/desktop/client.go | 323 ++++++++++++++++++++++++++++++++++++ api/desktop/client_test.go | 101 +++++++++++ cfg.go | 26 ++- cmd/tyaliv/main.go | 198 ++++++++++++++++++++++ go.mod | 3 + go.sum | 5 + hcli/cli.go | 53 ++++-- internal/horm/req.go | 8 +- internal/textio/interact.go | 17 ++ wasm.go | 198 ++++++++++++++++++++++ wasm_test.go | 20 +++ 14 files changed, 990 insertions(+), 20 deletions(-) create mode 100644 api/desktop/client.go create mode 100644 api/desktop/client_test.go create mode 100644 cmd/tyaliv/main.go create mode 100644 internal/textio/interact.go create mode 100644 wasm.go create mode 100644 wasm_test.go diff --git a/.gitignore b/.gitignore index 5e326e6..6b315ac 100644 --- a/.gitignore +++ b/.gitignore @@ -33,3 +33,9 @@ go.work.sum # MacOS .DS_Store + +# WASM +*.wasm + +# configs +*.yaml diff --git a/api/auth/client.go b/api/auth/client.go index 03b39df..5e2face 100644 --- a/api/auth/client.go +++ b/api/auth/client.go @@ -94,11 +94,11 @@ type RequestLogin struct { ChallengeID string `form:"challengeId"` DeviceCode string `form:"deviceCode"` DeviceName string `form:"deviceName"` - DeviceType string `form:"deviceType"` + DeviceType uint64 `form:"deviceType"` DeviceModel string `form:"deviceModel"` AppVersion string `form:"appVersion"` SysVersion string `form:"sysVersion"` - ClientVersion string `form:"clientVersion"` + ClientVersion uint64 `form:"clientVersion"` } type ResponseLogin struct { @@ -135,6 +135,8 @@ type ResponseLogin struct { func (r *ResponseLogin) SetClient(cli *hcli.Client) { cli.Tenantid = strconv.FormatInt(r.TenantID, 10) cli.Usereid = r.UserEid + cli.SetSecretKey(r.SecretKey) + cli.SetTimestamp(r.Timestamp) } func Login(tya *tienyik.AES, cli *hcli.Client, r *RequestLogin) (*ResponseLogin, error) { @@ -148,3 +150,10 @@ func Login(tya *tienyik.AES, cli *hcli.Client, r *RequestLogin) (*ResponseLogin, defer resp.Body.Close() return hson.Unmarshal[*ResponseLogin](tya, resp.Body) } + +func Logout(tya *tienyik.AES, cli *hcli.Client) error { + _, err := cli.Post( + textio.API(), "", nil, + ) + return err +} diff --git a/api/auth/client_test.go b/api/auth/client_test.go index c67c443..06ed640 100644 --- a/api/auth/client_test.go +++ b/api/auth/client_test.go @@ -3,9 +3,12 @@ package auth import ( "crypto/rand" "crypto/rsa" + "os" "testing" "github.com/fumiama/tienyik" + "github.com/fumiama/tienyik/api/cdserv" + "github.com/fumiama/tienyik/hcli" "github.com/sirupsen/logrus" ) @@ -36,3 +39,39 @@ func TestNegotiationEncKey(t *testing.T) { t.Fatal(err) } } + +func TestLogin(t *testing.T) { + cli := hcli.NewClient() + sd, err := cdserv.GetServData() + if err != nil { + t.Fatal(err) + } + t.Log("get serv data:", sd) + x, err := GenChallengeData(nil, cli) + if err != nil { + t.Fatal(err) + } + sd.SetClient(cli) + rsp, err := Login(nil, cli, &RequestLogin{ + UserAccount: os.Getenv("TYUSR"), + Password: tienyik.ChallengePassword(os.Getenv("TYPWD"), x.ChallengeCode), + SHA256Password: tienyik.ChallengeSHA256Password(os.Getenv("TYPWD"), x.ChallengeCode), + ChallengeID: x.ChallengeID, + DeviceCode: cli.Devicecode, + DeviceName: tienyik.DeviceNameEdge, + DeviceType: cli.Devicetype, + DeviceModel: tienyik.DeviceModelMacOS, + AppVersion: tienyik.AppVersion, + SysVersion: tienyik.DeviceModelMacOS, + ClientVersion: cli.Version, + }) + if err != nil { + t.Fatal(err) + } + t.Log(rsp) + rsp.SetClient(cli) + err = Logout(nil, cli) + if err != nil { + t.Fatal(err) + } +} diff --git a/api/desktop/client.go b/api/desktop/client.go new file mode 100644 index 0000000..11e7a20 --- /dev/null +++ b/api/desktop/client.go @@ -0,0 +1,323 @@ +package desktop + +import ( + "bytes" + "net/url" + "strconv" + + "github.com/fumiama/tienyik" + "github.com/fumiama/tienyik/hcli" + "github.com/fumiama/tienyik/internal/horm" + "github.com/fumiama/tienyik/internal/hson" + "github.com/fumiama/tienyik/internal/textio" +) + +const ( + DefaultRequestPageDesktopSortType = "createTimeV1" +) + +type RequestPageDesktop struct { + GetCnt int `json:"getCnt"` + DesktopTypes []string `json:"desktopTypes"` + SortType string `json:"sortType"` +} + +type ResponsePageDesktop struct { + Timestamp int64 `json:"timestamp"` + SortList []struct { + ObjID string `json:"objId"` + ObjType int `json:"objType"` + ObjValue string `json:"objValue"` + DesktopTypes []string `json:"desktopTypes"` + } `json:"sortList"` + DesktopPoolList []any `json:"desktopPoolList"` + DesktopList []struct { + ObjType int `json:"objType"` + TenantID int `json:"tenantId"` + ObjID string `json:"objId"` + ConnectURL []string `json:"connectUrl"` + ObjName string `json:"objName"` + Backupurl []string `json:"backupurl"` + OsType string `json:"osType"` + OsName string `json:"osName"` + ConnectMaster int `json:"connectMaster"` + NeedLineUp bool `json:"needLineUp"` + UserDesktopGroupID any `json:"userDesktopGroupId"` + Strategy struct { + ReconnectMsg any `json:"reconnectMsg"` + RebootMsg any `json:"rebootMsg"` + ShutoffMsg any `json:"shutoffMsg"` + ShutdownStrategy any `json:"shutdownStrategy"` + RebootStrategy any `json:"rebootStrategy"` + ModifyComputerAllas string `json:"modifyComputerAllas"` + CheckBeforeConnect any `json:"checkBeforeConnect"` + } `json:"strategy"` + CloudMobileType any `json:"cloudMobileType"` + DesktopID string `json:"desktopId"` + DesktopName string `json:"desktopName"` + FlavorName any `json:"flavorName"` + ImageName string `json:"imageName"` + OsBit string `json:"osBit"` + CPUCore any `json:"cpuCore"` + MemoryGB any `json:"memoryGB"` + RootDiskGB any `json:"rootDiskGB"` + DataDiskGB any `json:"dataDiskGB"` + Status string `json:"status"` + Summary any `json:"summary"` + TanentCode string `json:"tanentCode"` + TanentName string `json:"tanentName"` + UseStatus string `json:"useStatus"` + DesktopCode string `json:"desktopCode"` + ForeignDesktopID string `json:"foreignDesktopId"` + ForbiddenConnect bool `json:"forbiddenConnect"` + GpuType bool `json:"gpuType"` + GpuVirtualMethod any `json:"gpuVirtualMethod"` + UserMode int `json:"userMode"` + DefaultDesktop bool `json:"defaultDesktop"` + ExpireDate any `json:"expireDate"` + CreateDate int64 `json:"createDate"` + NowDate any `json:"nowDate"` + NoticeInterval int `json:"noticeInterval"` + BandExpireDate any `json:"bandExpireDate"` + BandNoticeInterval int `json:"bandNoticeInterval"` + UpperResolution any `json:"upperResolution"` + ProdType string `json:"prodType"` + ProdGroupType int `json:"prodGroupType"` + ProdInstID string `json:"prodInstId"` + ProdGroupName string `json:"prodGroupName"` + DesktopMirrorTagSet []any `json:"desktopMirrorTagSet"` + LicenseExpireDate int64 `json:"licenseExpireDate"` + LicenseNoticeInterval int `json:"licenseNoticeInterval"` + AllowConnStartTime any `json:"allowConnStartTime"` + AllowConnEndTime any `json:"allowConnEndTime"` + OperationAuditSupported bool `json:"operationAuditSupported"` + ProjectionScreenState any `json:"projectionScreenState"` + NickName string `json:"nickName"` + VMType int `json:"vmType"` + PayType string `json:"payType"` + ProdSubType any `json:"prodSubType"` + InstStatus int `json:"instStatus"` + UseTimeVO any `json:"useTimeVO"` + UseStatusShowActions any `json:"useStatusShowActions"` + UseStatusText string `json:"useStatusText"` + UseStatusColor string `json:"useStatusColor"` + ModifyComputerAllas any `json:"modifyComputerAllas"` + UsePrivateImageFile bool `json:"usePrivateImageFile"` + ImageID int `json:"imageId"` + ImageCategoryID int `json:"imageCategoryId"` + HaProdType int `json:"haProdType"` + CtrlTypes []any `json:"ctrlTypes"` + OrderProductData struct { + TimeLimitTotal any `json:"timeLimitTotal"` + TimeLimitUsed any `json:"timeLimitUsed"` + NextAcctTime any `json:"nextAcctTime"` + BusiChannelType any `json:"busiChannelType"` + ManageData any `json:"manageData"` + ActiveDate any `json:"activeDate"` + KeepTime any `json:"keepTime"` + } `json:"orderProductData"` + LicenseID int `json:"licenseId"` + RegionID int `json:"regionId"` + TenantCode string `json:"tenantCode"` + ConnectAPIURL struct { + ConnectPath string `json:"connectPath"` + StatusPath string `json:"statusPath"` + StatePath string `json:"statePath"` + AppendData any `json:"appendData"` + } `json:"connectApiUrl"` + } `json:"desktopList"` + PreemptionDesktopList []any `json:"preemptionDesktopList"` +} + +func PageDesktop(tya *tienyik.AES, cli *hcli.Client, r *RequestPageDesktop) (*ResponsePageDesktop, error) { + resp, err := cli.Post( + textio.API(), textio.ContenTypeJSON, + bytes.NewReader(hson.Marshal(tya, r)), + ) + if err != nil { + return nil, err + } + defer resp.Body.Close() + return hson.Unmarshal[*ResponsePageDesktop](tya, resp.Body) +} + +type ResponseFeature struct { + CPUCore int `json:"cpuCore"` + MemoryGB int `json:"memoryGB"` + SystemDiskGB int `json:"systemDiskGB"` + DataDiskGB any `json:"dataDiskGB"` + TotalDiskGB int `json:"totalDiskGB"` + SysDisk struct { + Size int `json:"size"` + Path string `json:"path"` + Code string `json:"code"` + } `json:"sysDisk"` + DataDiskList []any `json:"dataDiskList"` + MirrorVersion string `json:"mirrorVersion"` + MirrorCategoryName string `json:"mirrorCategoryName"` + DesktopName string `json:"desktopName"` + GpuSliceRAM any `json:"gpuSliceRam"` + GpuSliceRAMDesc any `json:"gpuSliceRamDesc"` + ExpireDate any `json:"expireDate"` + CreateDate int64 `json:"createDate"` + NowDate int64 `json:"nowDate"` + LinkInfo any `json:"linkInfo"` + OrderProduct struct { + TimeLimitTotal any `json:"timeLimitTotal"` + TimeLimitUsed any `json:"timeLimitUsed"` + NextAcctTime any `json:"nextAcctTime"` + BusiChannelType string `json:"busiChannelType"` + ManageData any `json:"manageData"` + ActiveDate any `json:"activeDate"` + KeepTime any `json:"keepTime"` + } `json:"orderProduct"` + MirrorID int `json:"mirrorId"` + MirrorCategoryID int `json:"mirrorCategoryId"` + ProductName string `json:"productName"` + PayType string `json:"payType"` + ProdType string `json:"prodType"` + ProdSubType any `json:"prodSubType"` + TimePkgVOS []any `json:"timePkgVOS"` + Os string `json:"os"` + MirrorType string `json:"mirrorType"` +} + +func Feature(tya *tienyik.AES, cli *hcli.Client, desktopId string, objType int, objId string) (*ResponseFeature, error) { + u, err := url.Parse(textio.API()) + if err != nil { + return nil, err + } + q := u.Query() + q.Set("desktopId", desktopId) + q.Set("objType", strconv.Itoa(objType)) + q.Set("objId", objId) + u.RawQuery = tya.EUrlParams(q) + + resp, err := cli.Get(u.String()) + if err != nil { + return nil, err + } + defer resp.Body.Close() + return hson.Unmarshal[*ResponseFeature](tya, resp.Body) +} + +type ResponseGetDesktopExtraInfo struct { + Strategy struct { + ReconnectMsg any `json:"reconnectMsg"` + RebootMsg any `json:"rebootMsg"` + ShutoffMsg any `json:"shutoffMsg"` + ShutdownStrategy string `json:"shutdownStrategy"` + RebootStrategy string `json:"rebootStrategy"` + ModifyComputerAllas any `json:"modifyComputerAllas"` + CheckBeforeConnect any `json:"checkBeforeConnect"` + } `json:"strategy"` + UpperResolution string `json:"upperResolution"` + TimeLimitProductData any `json:"timeLimitProductData"` + HaProdType int `json:"haProdType"` +} + +func GetDesktopExtraInfo(tya *tienyik.AES, cli *hcli.Client, objId string, objType int) (*ResponseGetDesktopExtraInfo, error) { + u, err := url.Parse(textio.API()) + if err != nil { + return nil, err + } + q := u.Query() + q.Set("objId", objId) + q.Set("objType", strconv.Itoa(objType)) + u.RawQuery = tya.EUrlParams(q) + + resp, err := cli.Get(u.String()) + if err != nil { + return nil, err + } + defer resp.Body.Close() + return hson.Unmarshal[*ResponseGetDesktopExtraInfo](tya, resp.Body) +} + +type RequestConnect struct { + ObjID string `form:"objId"` + ObjType int `form:"objType"` + OsType string `form:"osType"` + DeviceID int `form:"deviceId"` + DeviceCode string `form:"deviceCode"` + DeviceName string `form:"deviceName"` + SysVersion string `form:"sysVersion"` + AppVersion string `form:"appVersion"` + HostName string `form:"hostName"` + VdCommand string `form:"vdCommand"` + IPAddress string `form:"ipAddress"` + MacAddress string `form:"macAddress"` + HardwareFeatureCode string `form:"hardwareFeatureCode"` + SpecifiedCertCategory int `form:"specifiedCertCategory"` +} + +type ResponseConnect struct { + GoingRetry bool `json:"goingRetry"` + DesktopInfo any `json:"desktopInfo"` + ShadowDesktopInfo struct { + InHaMode int `json:"inHaMode"` + HaDesktopID any `json:"haDesktopId"` + HaConnectingTips any `json:"haConnectingTips"` + HaConnectSucTips any `json:"haConnectSucTips"` + } `json:"shadowDesktopInfo"` + DesktopAnywhereInfo struct { + AnywhereStatus int `json:"anywhereStatus"` + MigrateStatus any `json:"migrateStatus"` + AnywhereDesktopID any `json:"anywhereDesktopId"` + SrcResPoolName any `json:"srcResPoolName"` + TargetResPoolName any `json:"targetResPoolName"` + EstimatedTime int `json:"estimatedTime"` + ReminderDays any `json:"reminderDays"` + RoamingDays any `json:"roamingDays"` + NeedReserveRemind int `json:"needReserveRemind"` + ShadowInfoDTO any `json:"shadowInfoDTO"` + AnywhereOpen bool `json:"anywhereOpen"` + ConnectTargetDesktop bool `json:"connectTargetDesktop"` + } `json:"desktopAnywhereInfo"` + DesktopID string `json:"desktopId"` + PollingKey string `json:"pollingKey"` + AuthInfo any `json:"authInfo"` +} + +func Connect(tya *tienyik.AES, cli *hcli.Client, r *RequestConnect) (*ResponseConnect, error) { + resp, err := cli.Post( + textio.API(), textio.ContenTypeForm, + bytes.NewReader(horm.Marshal(tya, r)), + ) + if err != nil { + return nil, err + } + defer resp.Body.Close() + return hson.Unmarshal[*ResponseConnect](tya, resp.Body) +} + +type RequestState struct { + ObjID string `json:"objId"` + ObjType int `json:"objType"` +} + +type ResponseState struct { + ObjType int `json:"objType"` + ObjID string `json:"objId"` + DesktopID int `json:"desktopId"` + DesktopState string `json:"desktopState"` + RunningTask int `json:"runningTask"` + RunningTaskName string `json:"runningTaskName"` + TaskStartTime int64 `json:"taskStartTime"` + MirrorReady any `json:"mirrorReady"` + UseStatus string `json:"useStatus"` + UseStatusText string `json:"useStatusText"` + UseStatusColor string `json:"useStatusColor"` +} + +func State(tya *tienyik.AES, cli *hcli.Client, r []RequestState) ([]ResponseState, error) { + resp, err := cli.Post( + textio.API(), textio.ContenTypeJSON, + bytes.NewReader(hson.Marshal(tya, &r)), + ) + if err != nil { + return nil, err + } + defer resp.Body.Close() + return hson.Unmarshal[[]ResponseState](tya, resp.Body) +} diff --git a/api/desktop/client_test.go b/api/desktop/client_test.go new file mode 100644 index 0000000..2e7e034 --- /dev/null +++ b/api/desktop/client_test.go @@ -0,0 +1,101 @@ +package desktop + +import ( + "os" + "testing" + + "github.com/fumiama/tienyik" + "github.com/fumiama/tienyik/api/auth" + "github.com/fumiama/tienyik/api/cdserv" + "github.com/fumiama/tienyik/hcli" +) + +func TestDesktop(t *testing.T) { + cli := hcli.NewClient() + sd, err := cdserv.GetServData() + if err != nil { + t.Fatal(err) + } + t.Log("get serv data:", sd) + x, err := auth.GenChallengeData(nil, cli) + if err != nil { + t.Fatal(err) + } + sd.SetClient(cli) + rsp, err := auth.Login(nil, cli, &auth.RequestLogin{ + UserAccount: os.Getenv("TYUSR"), + Password: tienyik.ChallengePassword(os.Getenv("TYPWD"), x.ChallengeCode), + SHA256Password: tienyik.ChallengeSHA256Password(os.Getenv("TYPWD"), x.ChallengeCode), + ChallengeID: x.ChallengeID, + DeviceCode: cli.Devicecode, + DeviceName: tienyik.DeviceNameEdge, + DeviceType: cli.Devicetype, + DeviceModel: tienyik.DeviceModelMacOS, + AppVersion: tienyik.AppVersion, + SysVersion: tienyik.DeviceModelMacOS, + ClientVersion: cli.Version, + }) + if err != nil { + t.Fatal(err) + } + t.Log(rsp) + rsp.SetClient(cli) + pd, err := PageDesktop(nil, cli, &RequestPageDesktop{ + GetCnt: 1, + DesktopTypes: []string{"1", tienyik.ArchX86, tienyik.ArchARM, tienyik.ArchHW}, + SortType: DefaultRequestPageDesktopSortType, + }) + if err != nil { + t.Fatal(err) + } + t.Log(pd) + for _, x := range pd.DesktopList { + feat, err := Feature(nil, cli, x.DesktopID, x.ObjType, x.ObjID) + if err != nil { + t.Fatal(err) + } + t.Log("feat:", feat) + ext, err := GetDesktopExtraInfo(nil, cli, x.ObjID, x.ObjType) + if err != nil { + t.Fatal(err) + } + t.Log("ext:", ext) + s, err := State(nil, cli, []RequestState{{ + ObjID: x.ObjID, + ObjType: x.ObjType, + }}) + if err != nil { + t.Fatal(err) + } + t.Log("s1:", s) + con, err := Connect(nil, cli, &RequestConnect{ + ObjID: x.ObjID, + ObjType: x.ObjType, + OsType: x.OsType, + DeviceID: int(cli.Devicetype), + DeviceCode: cli.Devicecode, + DeviceName: tienyik.DeviceNameEdge, + SysVersion: tienyik.DeviceModelMacOS, + AppVersion: tienyik.AppVersion, + HostName: tienyik.DeviceNameEdge, + HardwareFeatureCode: cli.Devicecode, + SpecifiedCertCategory: 1, + }) + if err != nil { + t.Fatal(err) + } + t.Log("con:", con) + s, err = State(nil, cli, []RequestState{{ + ObjID: x.ObjID, + ObjType: x.ObjType, + }}) + if err != nil { + t.Fatal(err) + } + t.Log("s2:", s) + } + err = auth.Logout(nil, cli) + if err != nil { + t.Fatal(err) + } +} diff --git a/cfg.go b/cfg.go index 26f2aa6..cb97732 100644 --- a/cfg.go +++ b/cfg.go @@ -7,12 +7,15 @@ import ( "strings" ) -const Version = "103010001" +const ( + Version = 103010001 + AppVersion = "3.1.0" +) const ( - DeviceTypePC = "25" - DeviceTypeMAC = "45" - DeviceTypeWEB = "60" + DeviceTypePC = 25 + DeviceTypeMAC = 45 + DeviceTypeWEB = 60 ) const ( @@ -21,6 +24,21 @@ const ( AppModelPHONE = "3" ) +const ( + DeviceNameEdge = "Edge浏览器" +) + +// alos sysVersion +const ( + DeviceModelMacOS = "Macintosh; Intel Mac OS X 10_15_7" +) + +const ( + ArchX86 = "2001" + ArchARM = "2002" + ArchHW = "2003" +) + func NewDeviceCode() string { sb := &strings.Builder{} sb.WriteString("web_") diff --git a/cmd/tyaliv/main.go b/cmd/tyaliv/main.go new file mode 100644 index 0000000..49a497b --- /dev/null +++ b/cmd/tyaliv/main.go @@ -0,0 +1,198 @@ +package main + +import ( + "flag" + "fmt" + "os" + "os/signal" + "strings" + "syscall" + "time" + + "gopkg.in/yaml.v3" + + base14 "github.com/fumiama/go-base16384" + "github.com/fumiama/tienyik" + "github.com/fumiama/tienyik/api/auth" + "github.com/fumiama/tienyik/api/cdserv" + "github.com/fumiama/tienyik/api/desktop" + "github.com/fumiama/tienyik/hcli" + "github.com/fumiama/tienyik/internal/log" + "github.com/fumiama/tienyik/internal/textio" +) + +type config struct { + UserAccount string `yaml:"UserAccount"` + PasswordB14 string `yaml:"PasswordB14"` // PasswordB14 is a little bit more secure + CheckIntervalSec int `yaml:"CheckIntervalSec"` // CheckIntervalSec default 60 + GetDeviceCount int `yaml:"GetDeviceCount"` // GetDeviceCount default 20 +} + +func main() { + c := flag.String("c", "config.yaml", "load config file") + s := flag.String("s", "", "save config file template") + flag.Parse() + + cfg := config{} + + if *s != "" { + fmt.Print("UserAccount: ") + fmt.Scanln(&cfg.UserAccount) + pwd := "" + fmt.Print("Password: ") + textio.NoEchoScanln(&pwd) + cfg.PasswordB14 = base14.EncodeString(pwd) + cfg.CheckIntervalSec = 60 + cfg.GetDeviceCount = 20 + data, err := yaml.Marshal(&cfg) + if err != nil { + log.Fatalln(err) + } + err = os.WriteFile(*s, data, 0644) + if err != nil { + log.Fatalln(err) + } + return + } + + f, err := os.Open(*c) + if err != nil { + log.Fatalln(err) + } + err = yaml.NewDecoder(f).Decode(&cfg) + _ = f.Close() + if err != nil { + log.Fatalln(err) + } + if cfg.UserAccount == "" { + log.Fatalln("user account must be set") + } + if cfg.PasswordB14 == "" { + log.Fatalln("password must be set (in b14 format)") + } + if cfg.CheckIntervalSec <= 0 { + cfg.CheckIntervalSec = 60 + } + if cfg.GetDeviceCount <= 0 { + cfg.GetDeviceCount = 20 + } + +RECONN: + cli := hcli.NewClient() + sd, err := cdserv.GetServData() + if err != nil { + log.Fatalln(err) + } + x, err := auth.GenChallengeData(nil, cli) + if err != nil { + log.Fatalln(err) + } + sd.SetClient(cli) + pwd := base14.DecodeString(cfg.PasswordB14) + rsp, err := auth.Login(nil, cli, &auth.RequestLogin{ + UserAccount: cfg.UserAccount, + Password: tienyik.ChallengePassword(pwd, x.ChallengeCode), + SHA256Password: tienyik.ChallengeSHA256Password(pwd, x.ChallengeCode), + ChallengeID: x.ChallengeID, + DeviceCode: cli.Devicecode, + DeviceName: tienyik.DeviceNameEdge, + DeviceType: cli.Devicetype, + DeviceModel: tienyik.DeviceModelMacOS, + AppVersion: tienyik.AppVersion, + SysVersion: tienyik.DeviceModelMacOS, + ClientVersion: cli.Version, + }) + if err != nil { + log.Fatalln(err) + } + rsp.SetClient(cli) + defer auth.Logout(nil, cli) + + pd, err := desktop.PageDesktop(nil, cli, &desktop.RequestPageDesktop{ + GetCnt: 20, + DesktopTypes: []string{"1", tienyik.ArchX86}, + SortType: desktop.DefaultRequestPageDesktopSortType, + }) + if err != nil { + log.Fatalln(err) + } + + mp := make(map[string][2]string, len(pd.DesktopList)*4) + sb := strings.Builder{} + sb.WriteString("available desktops:") + for _, x := range pd.DesktopList { + if x.UseStatusText == "运行中" { + sb.WriteString(" |●") + } else { + sb.WriteString(" |○") + } + sb.WriteString("[") + sb.WriteString(x.ObjID) + sb.WriteString("]") + sb.WriteString(x.ObjName) + sb.WriteString("(") + sb.WriteString(x.OsName) + sb.WriteString(")|") + mp[x.ObjID] = [2]string{x.ObjName, x.OsType} + } + log.Infoln(&sb) + + t := time.NewTicker(time.Second * time.Duration(cfg.CheckIntervalSec)) + defer t.Stop() + + mainStopCh := make(chan struct{}) + mc := make(chan os.Signal, 4) + signal.Notify(mc, os.Interrupt, syscall.SIGQUIT, syscall.SIGTERM) + go func() { + for { + <-mc + close(mainStopCh) + } + }() + + for { + select { + case <-t.C: + log.Infoln("start refreshing...") + + reqs := make([]desktop.RequestState, len(pd.DesktopList)) + for i, x := range pd.DesktopList { + reqs[i].ObjID = x.ObjID + reqs[i].ObjType = x.ObjType + } + s, err := desktop.State(nil, cli, reqs) + if err != nil { + log.Warnln("get state err:", err) + goto RECONN + } + for _, x := range s { + log.Infof("%s [%s]%s status is %s", x.ObjID, mp[x.ObjID][0], x.DesktopState) + if x.DesktopState == "ACTIVE" { + continue + } + log.Infof("%s [%s]%s do refresh", x.ObjID, mp[x.ObjID][0]) + _, err = desktop.Connect(nil, cli, &desktop.RequestConnect{ + ObjID: x.ObjID, + ObjType: x.ObjType, + OsType: mp[x.ObjID][1], + DeviceID: int(cli.Devicetype), + DeviceCode: cli.Devicecode, + DeviceName: tienyik.DeviceNameEdge, + SysVersion: tienyik.DeviceModelMacOS, + AppVersion: tienyik.AppVersion, + HostName: tienyik.DeviceNameEdge, + HardwareFeatureCode: cli.Devicecode, + SpecifiedCertCategory: 1, + }) + if err != nil { + log.Warnln("connect err:", err) + goto RECONN + } + } + case <-mainStopCh: + log.Warnln("quit loop") + return + } + } + +} diff --git a/go.mod b/go.mod index 614567a..4b29fed 100644 --- a/go.mod +++ b/go.mod @@ -5,7 +5,10 @@ go 1.24.4 require ( github.com/fumiama/go-base16384 v1.7.1 github.com/sirupsen/logrus v1.9.3 + github.com/tetratelabs/wazero v1.9.0 golang.org/x/net v0.28.0 + golang.org/x/term v0.23.0 + gopkg.in/yaml.v3 v3.0.1 ) require ( diff --git a/go.sum b/go.sum index af8ec3d..963bbf5 100644 --- a/go.sum +++ b/go.sum @@ -13,15 +13,20 @@ github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/ github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.8.0 h1:pSgiaMZlXftHpm5L7V1+rVB+AZJydKsMxsQBIJw4PKk= github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= +github.com/tetratelabs/wazero v1.9.0 h1:IcZ56OuxrtaEz8UYNRHBrUa9bYeX9oVY93KspZZBf/I= +github.com/tetratelabs/wazero v1.9.0/go.mod h1:TSbcXCfFP0L2FGkRPxHphadXPjo1T6W+CseNNY7EkjM= golang.org/x/net v0.28.0 h1:a9JDOJc5GMUJ0+UDqmLT86WiEy7iWyIhz8gz8E4e5hE= golang.org/x/net v0.28.0/go.mod h1:yqtgsTWOOnlGLG9GFRrK3++bGOUEkNBoHZc8MEDWPNg= golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.23.0 h1:YfKFowiIMvtgl1UERQoTPPToxltDeZfbj4H7dVUCwmM= golang.org/x/sys v0.23.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/term v0.23.0 h1:F6D4vR+EHoL9/sWAWgAR1H2DcHr4PareCbAaCo1RpuU= +golang.org/x/term v0.23.0/go.mod h1:DgV24QBUrK6jhZXl+20l6UWznPlwAHm1Q1mGHtydmSk= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= golang.org/x/text v0.17.0 h1:XtiM5bkSOt+ewxlOE/aE/AKEHibwj/6gvWMl9Rsh0Qc= golang.org/x/text v0.17.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= diff --git a/hcli/cli.go b/hcli/cli.go index 6eb17b7..54cc7ae 100644 --- a/hcli/cli.go +++ b/hcli/cli.go @@ -1,6 +1,7 @@ package hcli import ( + "context" "net/http" "reflect" "strconv" @@ -21,17 +22,22 @@ var DefaultClient = http.Client{ type Client struct { rcnt uintptr + sg tienyik.Signer + secretKey string + offsetTime int64 + Appmodel string Devicecode string - Devicetype string + Devicetype uint64 Servernode string Tenantid string Usereid string - Version string + Version uint64 } func NewClient() *Client { return &Client{ + sg: tienyik.NewSigner(context.TODO()), Appmodel: tienyik.AppModelTOB, Devicecode: tienyik.NewDeviceCode(), Devicetype: tienyik.DeviceTypeWEB, @@ -39,6 +45,15 @@ func NewClient() *Client { } } +func (c *Client) SetSecretKey(k string) { + c.secretKey = k +} + +func (c *Client) SetTimestamp(ts int64) { + n := time.Now().UnixMilli() + c.offsetTime = n - ts +} + func (c *Client) setHeaders(req *http.Request) { if c == nil { return @@ -54,27 +69,39 @@ func (c *Client) setHeaders(req *http.Request) { } fieldValue := v.Field(i) + if fieldValue.IsZero() { + continue + } + k := base14.DecodeString("廝呲舀㴄") + field.Name - if fieldValue.Kind() == reflect.String { - req.Header.Set( - base14.DecodeString("廝呲舀㴄")+field.Name, - fieldValue.String(), - ) + switch fieldValue.Kind() { + case reflect.String: + req.Header.Set(k, fieldValue.String()) + case reflect.Uint64, reflect.Uint32, reflect.Uint16, reflect.Uint8, reflect.Uint, reflect.Uintptr: + req.Header.Set(k, strconv.FormatUint(fieldValue.Uint(), 10)) + case reflect.Int64, reflect.Int32, reflect.Int16, reflect.Int8, reflect.Int: + req.Header.Set(k, strconv.FormatInt(fieldValue.Int(), 10)) + default: + panic("unsupported field " + field.Name + " value type " + fieldValue.Type().String()) } } if c.Appmodel != "" { ts := time.Now().UnixMilli() + rid := uint64(atomic.AddUintptr(&c.rcnt, 1)) + uint64(ts) + requestid := strconv.FormatUint(rid, 10) + + ts -= c.offsetTime timestamp := strconv.FormatInt(ts, 10) - requestid := strconv.FormatUint( - uint64(atomic.AddUintptr(&c.rcnt, 1))+uint64(ts), 10, - ) req.Header.Set(base14.DecodeString("廝呲草獱歙攷徥爀㴆"), requestid) req.Header.Set(base14.DecodeString("廝呲荑睭杜蕆厵縀㴆"), timestamp) - } - if c.Servernode != "" { - //TODO: gensign + if c.secretKey != "" { + req.Header.Set(base14.DecodeString("廝呲荍睧榘敇揉獳欜渀㴂"), c.sg.GenKeyNew( + context.TODO(), c.Devicetype, uint64(ts), rid, c.secretKey, + c.Usereid, req.URL.EscapedPath(), c.Servernode, c.Version, + )) + } } } diff --git a/internal/horm/req.go b/internal/horm/req.go index dc5c01f..7a7bd92 100644 --- a/internal/horm/req.go +++ b/internal/horm/req.go @@ -3,6 +3,7 @@ package horm import ( "net/url" "reflect" + "strconv" "github.com/fumiama/tienyik" "github.com/fumiama/tienyik/internal/textio" @@ -22,8 +23,13 @@ func Marshal(tya *tienyik.AES, x any) []byte { continue } - if field.Kind() == reflect.String { + switch field.Kind() { + case reflect.String: q.Set(formTag, field.String()) + case reflect.Uint64, reflect.Uint32, reflect.Uint16, reflect.Uint8, reflect.Uint, reflect.Uintptr: + q.Set(formTag, strconv.FormatUint(field.Uint(), 10)) + case reflect.Int64, reflect.Int32, reflect.Int16, reflect.Int8, reflect.Int: + q.Set(formTag, strconv.FormatInt(field.Int(), 10)) } } diff --git a/internal/textio/interact.go b/internal/textio/interact.go new file mode 100644 index 0000000..47b0ce0 --- /dev/null +++ b/internal/textio/interact.go @@ -0,0 +1,17 @@ +package textio + +import ( + "fmt" + "os" + + "golang.org/x/term" +) + +func NoEchoScanln(a ...any) (n int, err error) { + fd := int(os.Stdin.Fd()) + bytePassword, err := term.ReadPassword(fd) + if err != nil { + return 0, err + } + return fmt.Sscanln(string(bytePassword)+"\n", a...) +} diff --git a/wasm.go b/wasm.go new file mode 100644 index 0000000..c10753a --- /dev/null +++ b/wasm.go @@ -0,0 +1,198 @@ +package tienyik + +import ( + "context" + "fmt" + "strings" + + _ "embed" + + "github.com/fumiama/tienyik/internal/op" + "github.com/tetratelabs/wazero" + "github.com/tetratelabs/wazero/api" +) + +//go:embed main.1755740488270.wasm +var wasmdata []byte + +type Signer struct { + rt wazero.Runtime + md api.Module + genKey api.Function + genKeyNew api.Function + genKeyWithoutURI api.Function + _malloc api.Function + _free api.Function +} + +func NewSigner(ctx context.Context) (sg Signer) { + rt := wazero.NewRuntimeWithConfig(ctx, wazero.NewRuntimeConfigInterpreter()) + _, err := rt.NewHostModuleBuilder("a"). + // ___cxa_throw + NewFunctionBuilder().WithFunc(func(ctx context.Context, e, n, t uint32) { + panic(fmt.Sprintf("___cxa_throw: ptr(e)=%08x, type(n)=%08x, destructor(t)=%08x", e, n, t)) + }).Export("c"). + // __abort_js + NewFunctionBuilder().WithFunc(func(ctx context.Context) { + panic("wasm aborted") + }).Export("a"). + // _emscripten_resize_heap + NewFunctionBuilder().WithFunc(func(ctx context.Context, _ uint32) uint32 { + panic("wasm oom") + }).Export("b"). + Instantiate(ctx) + if err != nil { + panic(err) + } + + md, err := rt.InstantiateWithConfig(ctx, wasmdata, + wazero.NewModuleConfig()) + if err != nil { + _ = rt.Close(ctx) + panic(err) + } + + sg.rt = rt + sg.md = md + + sg.genKey = md.ExportedFunction("f") + sg.genKeyNew = md.ExportedFunction("g") + sg.genKeyWithoutURI = md.ExportedFunction("h") + sg._malloc = md.ExportedFunction("i") + sg._free = md.ExportedFunction("j") + + return +} + +// GenKey is the go repr of js func +// +// generatorSign(e) { +// const t = Module.lengthBytesUTF8(e.secretKey) + 1 +// , n = Module._malloc(t); +// Module.stringToUTF8(e.secretKey, n, t); +// const r = Module._gen_key(e.deviceType, BigInt(e.timestamp), BigInt(e.requestId), n, e.tenantId, e.userId, e.version) +// , o = Module.UTF8ToString(r); +// return Module._free(n), +// Module._free(r), +// o +// } +func (sg *Signer) GenKey( + ctx context.Context, deviceType, timestamp, requestID uint64, + secretKey string, tenantID, userID, version uint64, +) string { + t := len(secretKey) + 1 + n := sg.malloc(ctx, uint64(t)) + if !sg.md.Memory().WriteString(uint32(n), secretKey+"\x00") { + panic("write out-of-bound") + } + defer sg.free(ctx, n) + + return sg.string(op.Must(sg.genKey.Call( + ctx, deviceType, timestamp, requestID, + n, tenantID, userID, version), + )[0]) +} + +// GenKeyNew is the go repr of js func +// +// generatorSignNew(e) { +// const t = Module.lengthBytesUTF8(e.secretKey) + 1 +// , n = Module._malloc(t); +// Module.stringToUTF8(e.secretKey, n, t); +// const r = Module.lengthBytesUTF8(e.userEid) + 1 +// , o = Module._malloc(r); +// Module.stringToUTF8(e.userEid, o, r); +// const i = Module.lengthBytesUTF8(e.requestUri) + 1 +// , a = Module._malloc(i); +// Module.stringToUTF8(e.requestUri, a, i); +// const s = Module.lengthBytesUTF8(this.serverNode) + 1 +// , l = Module._malloc(s); +// Module.stringToUTF8(this.serverNode, l, s); +// const c = Module._gen_key_new(e.deviceType, BigInt(e.timestamp), BigInt(e.requestId), n, o, a, l, e.version) +// , u = Module.UTF8ToString(c); +// return Module._free(n), +// Module._free(o), +// Module._free(a), +// Module._free(l), +// Module._free(c), +// u +// } +func (sg *Signer) GenKeyNew( + ctx context.Context, deviceType, timestamp, requestID uint64, + secretKey, userEID, requestURI, serverNode string, version uint64, +) string { + t := len(secretKey) + 1 + n := sg.malloc(ctx, uint64(t)) + if !sg.md.Memory().WriteString(uint32(n), secretKey+"\x00") { + panic("write out-of-bound") + } + defer sg.free(ctx, n) + + r := len(userEID) + 1 + o := sg.malloc(ctx, uint64(r)) + if !sg.md.Memory().WriteString(uint32(o), userEID+"\x00") { + panic("write out-of-bound") + } + defer sg.free(ctx, o) + + i := len(requestURI) + 1 + a := sg.malloc(ctx, uint64(i)) + if !sg.md.Memory().WriteString(uint32(a), requestURI+"\x00") { + panic("write out-of-bound") + } + defer sg.free(ctx, a) + + s := len(serverNode) + 1 + l := sg.malloc(ctx, uint64(s)) + if !sg.md.Memory().WriteString(uint32(l), serverNode+"\x00") { + panic("write out-of-bound") + } + defer sg.free(ctx, l) + + return sg.string(op.Must(sg.genKeyNew.Call( + ctx, deviceType, timestamp, requestID, + n, o, a, l, version), + )[0]) +} + +func (sg *Signer) malloc(ctx context.Context, n uint64) uint64 { + return op.Must(sg._malloc.Call(ctx, n))[0] +} + +func (sg *Signer) free(ctx context.Context, n uint64) { + op.Must(sg._free.Call(ctx, n)) +} + +func (sg *Signer) string(ptr uint64) string { + buf := strings.Builder{} + x := uint32(ptr) + for { + b, ok := sg.md.Memory().ReadByte(x) + x++ + if !ok { + panic("read out-of-bound") + } + if b == 0 { + break + } + buf.WriteByte(b) + } + return buf.String() +} + +func (sg *Signer) IsClosed() bool { + return sg.md == nil || sg.rt == nil || sg.md.IsClosed() || + sg.genKey == nil || sg.genKeyNew == nil || sg.genKeyWithoutURI == nil || + sg._malloc == nil || sg._free == nil +} + +func (sg *Signer) Close(ctx context.Context) { + if sg.md != nil && !sg.md.IsClosed() { + sg.md.Close(ctx) + sg.md = nil + } + if sg.rt != nil { + sg.rt.Close(ctx) + sg.rt = nil + } +} diff --git a/wasm_test.go b/wasm_test.go new file mode 100644 index 0000000..05ca8c9 --- /dev/null +++ b/wasm_test.go @@ -0,0 +1,20 @@ +package tienyik + +import ( + "context" + "testing" +) + +func TestSigner(t *testing.T) { + ctx := context.Background() + sg := NewSigner(ctx) + sigstr := sg.GenKeyNew( + ctx, 60, 1763891935806, 1763891937568, "9c047f01dfab388a3ef7c4ae34855e3a", + "6806caebc2cdaef5f10987a94d21cf1f", "/api/cdserv/client/msgcenter/page", + "8aff800b3c96e620043bb7deb4a0258d", 103010001, + ) + if sigstr != "66443F2BF714E1D2B4AC8D3B3CBC3913" { + t.Fatal("got", sigstr) + } + t.Fail() +}