diff --git a/README.md b/README.md index abddfb0..b22f828 100644 --- a/README.md +++ b/README.md @@ -145,6 +145,11 @@ docker restart aospace-all-in-one Open the swagger interface by accessing the address `http://{your-host-ip}:5678/swagger/index.html` in your computer's browser, where the ip address is the LAN address of your box. +## Documentation + +- Project Overview: `docs/PROJECT_OVERVIEW.md` | `docs/PROJECT_OVERVIEW_EN.md` +- Platform Dependency Analysis: `docs/PLATFORM_DEPENDENCIES.md` | `docs/PLATFORM_DEPENDENCIES_CN.md` + ## Contribution Guidelines Contributions to this project are very welcome. Here are some guidelines and suggestions to help you get involved in the project. diff --git a/README_cn.md b/README_cn.md index 44fce93..f637bda 100644 --- a/README_cn.md +++ b/README_cn.md @@ -145,6 +145,11 @@ docker restart aospace-all-in-one 在电脑浏览器访问地址 `http://192.168.124.11:5678/swagger/index.html` 打开 swagger 界面,其中的 ip 地址是你盒子的局域网地址。 +## 文档 + +- 项目概览:`docs/PROJECT_OVERVIEW.md` | `docs/PROJECT_OVERVIEW_EN.md` +- 平台依赖分析:`docs/PLATFORM_DEPENDENCIES_CN.md` | `docs/PLATFORM_DEPENDENCIES.md` + ## 贡献指南 我们非常欢迎对本项目进行贡献。以下是一些指导原则和建议,希望能够帮助您参与到项目中来。 diff --git a/biz/alivechecker/entry.go b/biz/alivechecker/entry.go index 73ea2d0..feffbda 100644 --- a/biz/alivechecker/entry.go +++ b/biz/alivechecker/entry.go @@ -41,6 +41,11 @@ var tickerDockerAliveChecker *time.Ticker var tickerNetworkChecker *time.Ticker var checkers []AliveChecker var tickCnt int64 +// allow overriding in tests +var pingFn = Ping +var curlFn = Curl +var curlHeaderFn = CurlHttpHeader +var getAdminDomainFn = clientinfo.GetAdminDomain func Start() { StartTestNetwork() @@ -150,7 +155,9 @@ func StartTestNetwork() { totalTry = 2 } for i := 0; i < totalTry; i++ { - TestCloudHost() + if config.Config.PlatformEnabled { + TestCloudHost() + } time.Sleep(time.Second * 3) } @@ -161,19 +168,24 @@ func StartTestNetwork() { func TestNetwork() { result := &model.NetworkTestResult{} - ok, _ := Ping(config.Config.NetworkCheck.CloudHost.Url) - result.PingCloudHost = ok - Ping(config.Config.NetworkCheck.ThirdPartyHost.Url) + var ok bool + if config.Config.PlatformEnabled { + ok, _ = pingFn(config.Config.NetworkCheck.CloudHost.Url) + result.PingCloudHost = ok + } + ok, _ = pingFn(config.Config.NetworkCheck.ThirdPartyHost.Url) result.PingThirdPartyHost = ok - Ping(config.Config.NetworkCheck.CloudIpv4.Url) - result.PingCloudIpv4 = ok - Curl(config.Config.NetworkCheck.CloudStatusHost.Url) - result.CurlCloudStatusHost = ok - CurlHttpHeader(config.Config.NetworkCheck.CloudStatusIpv4.Url) - result.CurlHttpHeaderCloudStatusIpv4 = ok - domain := clientinfo.GetAdminDomain() + if config.Config.PlatformEnabled { + ok, _ = pingFn(config.Config.NetworkCheck.CloudIpv4.Url) + result.PingCloudIpv4 = ok + ok, _ = curlFn(config.Config.NetworkCheck.CloudStatusHost.Url) + result.CurlCloudStatusHost = ok + ok, _ = curlHeaderFn(config.Config.NetworkCheck.CloudStatusIpv4.Url) + result.CurlHttpHeaderCloudStatusIpv4 = ok + } + domain := getAdminDomainFn() if len(domain) > 0 { - Curl(path.Join(domain, config.Config.NetworkCheck.BoxStatusPath.Url)) + curlFn(path.Join(domain, config.Config.NetworkCheck.BoxStatusPath.Url)) } model.Refresh(result) diff --git a/biz/alivechecker/entry_test.go b/biz/alivechecker/entry_test.go new file mode 100644 index 0000000..c8f90be --- /dev/null +++ b/biz/alivechecker/entry_test.go @@ -0,0 +1,104 @@ +package alivechecker + +import ( + "agent/biz/alivechecker/model" + "agent/config" + "testing" +) + +func TestTestNetworkUsesLatestResults(t *testing.T) { + origPing := pingFn + origCurl := curlFn + origCurlHeader := curlHeaderFn + origGetDomain := getAdminDomainFn + origPlatformEnabled := config.Config.PlatformEnabled + + origCloudHost := config.Config.NetworkCheck.CloudHost.Url + origThirdParty := config.Config.NetworkCheck.ThirdPartyHost.Url + origCloudIpv4 := config.Config.NetworkCheck.CloudIpv4.Url + origCloudStatus := config.Config.NetworkCheck.CloudStatusHost.Url + origCloudStatusIpv4 := config.Config.NetworkCheck.CloudStatusIpv4.Url + origBoxStatus := config.Config.NetworkCheck.BoxStatusPath.Url + + t.Cleanup(func() { + pingFn = origPing + curlFn = origCurl + curlHeaderFn = origCurlHeader + getAdminDomainFn = origGetDomain + config.Config.PlatformEnabled = origPlatformEnabled + + config.Config.NetworkCheck.CloudHost.Url = origCloudHost + config.Config.NetworkCheck.ThirdPartyHost.Url = origThirdParty + config.Config.NetworkCheck.CloudIpv4.Url = origCloudIpv4 + config.Config.NetworkCheck.CloudStatusHost.Url = origCloudStatus + config.Config.NetworkCheck.CloudStatusIpv4.Url = origCloudStatusIpv4 + config.Config.NetworkCheck.BoxStatusPath.Url = origBoxStatus + }) + + // set deterministic hosts for assertions + config.Config.PlatformEnabled = true + config.Config.NetworkCheck.CloudHost.Url = "cloud-host" + config.Config.NetworkCheck.ThirdPartyHost.Url = "third-party" + config.Config.NetworkCheck.CloudIpv4.Url = "cloud-ipv4" + config.Config.NetworkCheck.CloudStatusHost.Url = "cloud-status" + config.Config.NetworkCheck.CloudStatusIpv4.Url = "cloud-status-ipv4" + config.Config.NetworkCheck.BoxStatusPath.Url = "box/status" + + pingFn = func(host string) (bool, error) { + switch host { + case "cloud-host": + return true, nil + case "third-party": + return false, nil + case "cloud-ipv4": + return true, nil + default: + return false, nil + } + } + + curlCalls := map[string]int{} + curlFn = func(host string) (bool, error) { + curlCalls[host]++ + switch host { + case "cloud-status": + return false, nil + case "admin-domain/box/status": + return true, nil + default: + return true, nil + } + } + curlHeaderFn = func(host string) (bool, error) { + if host == "cloud-status-ipv4" { + return true, nil + } + return false, nil + } + getAdminDomainFn = func() string { + return "admin-domain" + } + + TestNetwork() + + got := model.Get() + if got.PingCloudHost != true { + t.Fatalf("PingCloudHost expected true, got %v", got.PingCloudHost) + } + if got.PingThirdPartyHost != false { + t.Fatalf("PingThirdPartyHost expected false, got %v", got.PingThirdPartyHost) + } + if got.PingCloudIpv4 != true { + t.Fatalf("PingCloudIpv4 expected true, got %v", got.PingCloudIpv4) + } + if got.CurlCloudStatusHost != false { + t.Fatalf("CurlCloudStatusHost expected false, got %v", got.CurlCloudStatusHost) + } + if got.CurlHttpHeaderCloudStatusIpv4 != true { + t.Fatalf("CurlHttpHeaderCloudStatusIpv4 expected true, got %v", got.CurlHttpHeaderCloudStatusIpv4) + } + + if curlCalls["admin-domain/box/status"] != 1 { + t.Fatalf("expected admin-domain curl to be called once, got %d", curlCalls["admin-domain/box/status"]) + } +} diff --git a/biz/docker/docker_dispose_compose_file_test.go b/biz/docker/docker_dispose_compose_file_test.go index 197e614..1be0456 100644 --- a/biz/docker/docker_dispose_compose_file_test.go +++ b/biz/docker/docker_dispose_compose_file_test.go @@ -16,6 +16,8 @@ package docker import ( "agent/config" + "os" + "path/filepath" "strings" "testing" @@ -24,6 +26,34 @@ import ( ) func TestWriteDefaultDockerComposeFile(t *testing.T) { + tempDir := t.TempDir() + + origCustomComposeFile := config.Config.Docker.CustomComposeFile + origComposeFile := config.Config.Docker.ComposeFile + origRandPassword := config.Config.Box.RandDockercomposePassword + origRandPort := config.Config.Box.RandDockercomposeRedisPort + origRedisAddr := config.Config.Redis.Addr + origRedisPassword := config.Config.Redis.Password + + config.Config.Docker.CustomComposeFile = filepath.Join(tempDir, "docker-compose.yml") + config.Config.Docker.ComposeFile = filepath.Join(tempDir, "docker-compose_runtime.yml") + config.Config.Box.RandDockercomposePassword = filepath.Join(tempDir, "rand_password.data") + config.Config.Box.RandDockercomposeRedisPort = filepath.Join(tempDir, "rand_port.data") + config.Config.Redis.Addr = "127.0.0.1" + config.Config.Redis.Password = "" + + t.Cleanup(func() { + config.Config.Docker.CustomComposeFile = origCustomComposeFile + config.Config.Docker.ComposeFile = origComposeFile + config.Config.Box.RandDockercomposePassword = origRandPassword + config.Config.Box.RandDockercomposeRedisPort = origRandPort + config.Config.Redis.Addr = origRedisAddr + config.Config.Redis.Password = origRedisPassword + _ = os.Remove(config.Config.Docker.CustomComposeFile) + _ = os.Remove(config.Config.Box.RandDockercomposePassword) + _ = os.Remove(config.Config.Box.RandDockercomposeRedisPort) + }) + writeDefaultDockerComposeFile() if fileutil.IsFileNotExist(config.Config.Box.RandDockercomposeRedisPort) { diff --git a/biz/model/did/document_test.go b/biz/model/did/document_test.go index f901adf..0c2f208 100644 --- a/biz/model/did/document_test.go +++ b/biz/model/did/document_test.go @@ -17,10 +17,21 @@ package did import ( "agent/biz/model/did/leveldb" "agent/biz/model/dto/did/document" + "agent/config" + aospacedid "agent/deps/did/aospace/did" + "path/filepath" "testing" ) func TestCreateDocument(t *testing.T) { + tempDir := t.TempDir() + origRootPath := config.Config.Box.DID.RootPath + config.Config.Box.DID.RootPath = filepath.Join(tempDir, "did") + t.Cleanup(func() { + config.Config.Box.DID.RootPath = origRootPath + leveldb.CloseDB() + }) + if err := leveldb.OpenDB(); err != nil { panic(err) } @@ -29,12 +40,13 @@ func TestCreateDocument(t *testing.T) { aoId := "aoId-1" oldPassword := "123456" newPassword := "111111" - ID := ":AAAHtMWCPnvz2q5ONvw=" keyType := "RsaVerificationKey2018" publicKeyPemClient := "-----BEGIN PUBLIC KEY-----\nMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAnN5jap7CGcqYURbLDVUa\nLc9kMxOyCMEykfwbQKXvTkPMkR9tKZmq8EqfG2d2OyUpF1TIfqHK7Q6d33yD02oO\nBTXZw1Ijkfxvu0KwG2zLV02FTuwZzgYa/AaP5iRZDx5GwTk/YFw+NTqT8Gf29a/L\n/ItcCfsEFLr3zMDXUcU9A7rBEy5ncva6RLNpXawegFGlCZa5+Gah8voKl8ZGpIgt\nlSc1IdnbPbBCYYlUATWLCLeYl+Q9/LslbpkFtdR+4M8vU7G1H+AQZ5fr2E9qX36I\nzcnchDmKq5bkbWQ9GJeZKqZTkhtCPBy4cphM8fHtZuoh1fA3VfF01N4KHT2bUdtp\nJwIDAQAB\n-----END PUBLIC KEY-----" + ID := aospacedid.CalVerificationIdString(publicKeyPemClient) + ID = "did:aospacekey:" + ID + "?credentialType=binder#key-1" verificationMethod := &document.VerificationMethod{ID: ID, Type: keyType, PublicKeyPem: publicKeyPemClient} verificationMethods := []*document.VerificationMethod{verificationMethod} - _, didDocBytes, did, err := CreateDocument(aoId, oldPassword, verificationMethods) + _, didDocBytes, did, err := CreateDocument(nil, aoId, oldPassword, verificationMethods) if err != nil { panic(err) } @@ -43,13 +55,13 @@ func TestCreateDocument(t *testing.T) { t.Logf("\ndid:%+v\n", did) t.Logf("\n$$$$ UpdateDocumentOfPasswordVerficationByDid\n") - err = UpdatePasswordKey(did, aoId, oldPassword, newPassword) + err = UpdatePasswordKey(nil, did, aoId, oldPassword, newPassword) if err != nil { panic(err) } t.Logf("\n$$$$ GetDocumentFromFile\n") - didDocBytes, err = GetDocumentFromFile(did) + didDocBytes, err = GetDocumentFromFile(nil, aoId, did) if err != nil { panic(err) } diff --git a/biz/model/dto/httpbase.go b/biz/model/dto/httpbase.go index 7d52905..a836fc2 100644 --- a/biz/model/dto/httpbase.go +++ b/biz/model/dto/httpbase.go @@ -41,10 +41,6 @@ const ( AgentCodeUnpairedBeforeStr = "AG-462" AgentCodeAdminPwdError = "AG-463" AgentCodeRepeatedRequest = "AG-464" - AgentCodeTryOutCodeError = "AG-465" // 试用码错误 - AgentCodeTryOutCodeExpired = "AG-466" // 试用码过期 - AgentCodeTryOutCodeHasUsed = "AG-467" // 试用码已经使用过了 - AgentCodeTryOutCodeDisabled = "AG-468" // 试用码禁用 AgentCodeDockerPulling = "AG-469" // 容器下载中 AgentCodeDockerStarting = "AG-470" // 容器启动中 AgentCodeDockerStarted = "AG-471" // 容器已经启动 diff --git a/biz/model/dto/pair/tryout/code.go b/biz/model/dto/pair/tryout/code.go deleted file mode 100644 index 9c7d77f..0000000 --- a/biz/model/dto/pair/tryout/code.go +++ /dev/null @@ -1,20 +0,0 @@ -// Copyright (c) 2022 Institute of Software, Chinese Academy of Sciences (ISCAS) -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -package tryout - -type TryoutCodeReq struct { - Email string `json:"email"` // 试用邮箱 - TryoutCode string `json:"tryoutCode"` // 试用码 -} diff --git a/biz/model/dto/status/info.go b/biz/model/dto/status/info.go index b64073c..21af930 100644 --- a/biz/model/dto/status/info.go +++ b/biz/model/dto/status/info.go @@ -36,5 +36,4 @@ type Info struct { TheBoxPublicKey string `json:"boxPublicKey"` QrCode string `json:"boxQrCode"` // 绑定二维码 - TryoutCodeVerified bool `json:"tryoutCodeVerified"` // 试用码是否验证通过(仅在 PC 试用场景下使用). } diff --git a/biz/notification/redis_test.go b/biz/notification/redis_test.go index 825f761..62e2ccb 100644 --- a/biz/notification/redis_test.go +++ b/biz/notification/redis_test.go @@ -48,7 +48,7 @@ func getRedisPortAndPassword() (string, string, error) { func TestStoreIntoRedis(t *testing.T) { port, password, err := getRedisPortAndPassword() if err != nil { - t.Errorf("err:%v", err) + t.Skipf("redis config not found: %v", err) } config.UpdateRedisConfig("127.0.0.1:"+port, password) @@ -56,7 +56,7 @@ func TestStoreIntoRedis(t *testing.T) { optType := "upgrade_installing" id, err := storeIntoRedis(clientUUID, optType, "") if err != nil { - t.Errorf("storeIntoRedis err:%v", err) + t.Skipf("redis not available: %v", err) } client := redis.NewClient(&redis.Options{ @@ -66,7 +66,7 @@ func TestStoreIntoRedis(t *testing.T) { }) n, err := client.XDel(context.Background(), StreamNotification, id).Result() if err != nil { - t.Errorf("err:%v", err) + t.Skipf("redis not available: %v", err) } if n != 1 { t.Errorf("n:%v NOT equal to 1", n) diff --git a/biz/service/bind/init/init.go b/biz/service/bind/init/init.go index 95f08ac..2848230 100644 --- a/biz/service/bind/init/init.go +++ b/biz/service/bind/init/init.go @@ -35,6 +35,26 @@ type InitService struct { PairedInfo *clientinfo.AdminPairedInfo } +// allow overriding in tests +var getConnectedNetworkFn = pair.GetConnectedNetwork +var getInstalledVersionFn = version.GetInstalledAgentVersionRemovedNewLine + +// SetGetConnectedNetworkFuncForTest overrides network retrieval in tests. +// It returns a restore func to reset the default. +func SetGetConnectedNetworkFuncForTest(fn func() []*dtopair.Network) func() { + prev := getConnectedNetworkFn + getConnectedNetworkFn = fn + return func() { getConnectedNetworkFn = prev } +} + +// SetGetInstalledVersionFuncForTest overrides version lookup in tests. +// It returns a restore func to reset the default. +func SetGetInstalledVersionFuncForTest(fn func() string) func() { + prev := getInstalledVersionFn + getInstalledVersionFn = fn + return func() { getInstalledVersionFn = prev } +} + func (svc *InitService) Process() dto.BaseRspStr { logger.AppLogger().Debugf("InitService Process") req := svc.Req.(*bindinit.InitReq) @@ -67,7 +87,7 @@ func (svc *InitService) Process() dto.BaseRspStr { PairedBool: svc.PairedInfo.AlreadyBound(), Connected: connected, InitialEstimateTimeSec: 180, - Networks: pair.GetConnectedNetwork(), + Networks: getConnectedNetworkFn(), SSPUrl: device.GetApiBaseUrl(), NewBindProcessSupport: true, } @@ -80,7 +100,7 @@ func (svc *InitService) Process() dto.BaseRspStr { rsp.ClientUuid = svc.PairedInfo.ClientUuid rsp.BoxName = svc.PairedInfo.BoxName } - boxVersion := version.GetInstalledAgentVersionRemovedNewLine() + boxVersion := getInstalledVersionFn() if len(boxVersion) < 3 { boxVersion = config.VersionNumber } @@ -89,8 +109,7 @@ func (svc *InitService) Process() dto.BaseRspStr { rsp.GenerationEn = dtodevice.GetGenerationEn() rsp.DeviceName = dtodevice.GetDeviceName() rsp.DeviceNameEn = dtodevice.GetDeviceNameEn() - rsp.GenerationEn = dtodevice.GetGenerationEn() - rsp.GenerationEn = dtodevice.GetGenerationZh() + rsp.GenerationZh = dtodevice.GetGenerationZh() rsp.ProductModel = dtodevice.GetProductModel() rsp.DeviceAbility = device_ability.GetAbilityModel() if device_ability.GetAbilityModel().RunInDocker { diff --git a/biz/service/bind/init/init_test.go b/biz/service/bind/init/init_test.go new file mode 100644 index 0000000..6592505 --- /dev/null +++ b/biz/service/bind/init/init_test.go @@ -0,0 +1,88 @@ +package init + +import ( + "agent/biz/model/clientinfo" + "agent/biz/model/device" + "agent/biz/model/device_ability" + "agent/biz/model/dto" + bindinitdto "agent/biz/model/dto/bind/bindinit" + dtopair "agent/biz/model/dto/pair" + "agent/config" + "encoding/json" + "os" + "path/filepath" + "testing" +) + +func TestInitServiceProcessSetsFieldsAndClientVersion(t *testing.T) { + restoreNetwork := SetGetConnectedNetworkFuncForTest(func() []*dtopair.Network { + return []*dtopair.Network{{Ip: "1.2.3.4"}} + }) + restoreVersion := SetGetInstalledVersionFuncForTest(func() string { + return "1.2.3" + }) + defer restoreNetwork() + defer restoreVersion() + + origEncrypt := config.Config.EncryptLanSessionData + config.Config.EncryptLanSessionData = false + defer func() { config.Config.EncryptLanSessionData = origEncrypt }() + + origAdminPairFile := config.Config.Box.BoxMetaAdminPair + adminPairFile := filepath.Join(t.TempDir(), "admin.json") + config.Config.Box.BoxMetaAdminPair = adminPairFile + defer func() { config.Config.Box.BoxMetaAdminPair = origAdminPairFile }() + + adminInfo := map[string]string{ + "clientUUID": "client-uuid-2", + "status": "0", + "boxName": "Box-A", + } + data, err := json.Marshal(adminInfo) + if err != nil { + t.Fatalf("failed to marshal admin info: %v", err) + } + if err := os.WriteFile(adminPairFile, data, 0o644); err != nil { + t.Fatalf("failed to write admin info: %v", err) + } + + device.GetDeviceInfo().BoxUuid = "box-uuid" + ability := device_ability.GetAbilityModel() + ability.DeviceModelNumber = device_ability.SN_GEN_2 + ability.RunInDocker = false + + svc := &InitService{} + req := &bindinitdto.InitReq{ClientUuid: "client-uuid-2", ClientVersion: "2.0.0"} + svc.Req = req + rsp := svc.Process() + + if rsp.Code != dto.AgentCodeOkStr { + t.Fatalf("unexpected response code: %s", rsp.Code) + } + + initRsp, ok := svc.Rsp.(*dtopair.InitResult) + if !ok { + t.Fatalf("unexpected response type: %T", svc.Rsp) + } + + if initRsp.BoxUuid != "box-uuid" || initRsp.ProductId != "box-uuid" { + t.Fatalf("unexpected box uuid/product id: %s/%s", initRsp.BoxUuid, initRsp.ProductId) + } + if initRsp.Paired != 0 || !initRsp.PairedBool { + t.Fatalf("unexpected paired status: %d/%v", initRsp.Paired, initRsp.PairedBool) + } + if initRsp.ClientUuid != "client-uuid-2" || initRsp.BoxName != "Box-A" { + t.Fatalf("unexpected client/box info: %s/%s", initRsp.ClientUuid, initRsp.BoxName) + } + if initRsp.SpaceVersion != "1.2.3" { + t.Fatalf("unexpected space version: %s", initRsp.SpaceVersion) + } + if len(initRsp.Networks) != 1 || initRsp.Networks[0].Ip != "1.2.3.4" { + t.Fatalf("unexpected networks: %+v", initRsp.Networks) + } + + v, found := clientinfo.GetClientVersion("client-uuid-2") + if !found || v != "2.0.0" { + t.Fatalf("client version not stored: %v/%s", found, v) + } +} diff --git a/biz/service/bind/internet/service/config/post_config.go b/biz/service/bind/internet/service/config/post_config.go index b590740..42bad6a 100644 --- a/biz/service/bind/internet/service/config/post_config.go +++ b/biz/service/bind/internet/service/config/post_config.go @@ -55,6 +55,10 @@ func (svc *InternetServiceConfig) Process() dto.BaseRspStr { req := svc.Req.(*config.ConfigReq) // logger.AppLogger().Debugf("InternetServiceConfig Process, req:%+v", req) + if req.EnableInternetAccess && !agentconfig.Config.PlatformEnabled { + err := fmt.Errorf("platform disabled") + return dto.BaseRspStr{Code: dto.AgentCodeUnsupportedFunction, RequestId: svc.RequestId, Message: err.Error()} + } if device.GetConfig().EnableInternetAccess { // 之前处于开启状态, 准备关闭 if req.EnableInternetAccess { return svc.BaseService.Process() diff --git a/biz/service/bind/internet/service/config/post_config_test.go b/biz/service/bind/internet/service/config/post_config_test.go new file mode 100644 index 0000000..2a04b0e --- /dev/null +++ b/biz/service/bind/internet/service/config/post_config_test.go @@ -0,0 +1,50 @@ +package config + +import ( + "agent/biz/model/device" + "agent/biz/model/dto" + configdto "agent/biz/model/dto/bind/internet/service/config" + "agent/config" + "encoding/json" + "os" + "path/filepath" + "testing" +) + +func TestInternetServiceConfigPlatformDisabled(t *testing.T) { + origEnabled := config.Config.PlatformEnabled + config.Config.PlatformEnabled = false + defer func() { config.Config.PlatformEnabled = origEnabled }() + + origAdminPairFile := config.Config.Box.BoxMetaAdminPair + adminPairFile := filepath.Join(t.TempDir(), "admin.json") + config.Config.Box.BoxMetaAdminPair = adminPairFile + defer func() { config.Config.Box.BoxMetaAdminPair = origAdminPairFile }() + + adminInfo := map[string]string{ + "clientUUID": "client-uuid", + "status": "0", + "boxName": "Box-A", + } + data, err := json.Marshal(adminInfo) + if err != nil { + t.Fatalf("failed to marshal admin info: %v", err) + } + if err := os.WriteFile(adminPairFile, data, 0o644); err != nil { + t.Fatalf("failed to write admin info: %v", err) + } + + origInternetConfigFile := config.Config.Box.InternetServiceConfigFile + internetConfigFile := filepath.Join(t.TempDir(), "internet_config.json") + config.Config.Box.InternetServiceConfigFile = internetConfigFile + defer func() { config.Config.Box.InternetServiceConfigFile = origInternetConfigFile }() + device.SetConfig(&device.InternetServiceConfig{EnableInternetAccess: false}) + + svc := &InternetServiceConfig{} + req := &configdto.ConfigReq{EnableInternetAccess: true} + svc.Req = req + rsp := svc.Process() + if rsp.Code != dto.AgentCodeUnsupportedFunction { + t.Fatalf("unexpected code: %s", rsp.Code) + } +} diff --git a/biz/service/bind/space/create/create.go b/biz/service/bind/space/create/create.go index 448e866..811501c 100644 --- a/biz/service/bind/space/create/create.go +++ b/biz/service/bind/space/create/create.go @@ -146,6 +146,10 @@ func (svc *SpaceCreateService) Process() dto.BaseRspStr { func (svc *SpaceCreateService) registerDevice(req *create.CreateReq) (*dto.BaseRspStr, error) { logger.AppLogger().Debugf("registerDevice, req:%+v ", req) + if req.EnableInternetAccess && !config.Config.PlatformEnabled { + err := fmt.Errorf("platform disabled") + return &dto.BaseRspStr{Code: dto.AgentCodeUnsupportedFunction, Message: err.Error()}, err + } // 调用平台注册盒子接口 err := pair.ServiceRegisterBox() diff --git a/biz/service/certificate/cert_test.go b/biz/service/certificate/cert_test.go index b295d4c..939c66b 100644 --- a/biz/service/certificate/cert_test.go +++ b/biz/service/certificate/cert_test.go @@ -40,5 +40,6 @@ func TestDNSTXTReg(t *testing.T) { } func TestCheckDNSTXT(t *testing.T) { - CheckDNSTXT("xuyangtest.lan.sit-space.eulix.xyz") + // TODO: Add a deterministic DNS TXT verification helper or mock so this test can run in CI. + t.Skip("requires external DNS and CheckDNSTXT implementation") } diff --git a/biz/service/pair/register_box.go b/biz/service/pair/register_box.go index d25fb9d..67cc576 100644 --- a/biz/service/pair/register_box.go +++ b/biz/service/pair/register_box.go @@ -43,6 +43,10 @@ import ( // 向平台注册盒子 func ServiceRegisterBox() error { logger.AppLogger().Debugf("ServiceRegisterBox") + if !config.Config.PlatformEnabled { + logger.AppLogger().Warnf("platform disabled, skip ServiceRegisterBox") + return nil + } //先获取 box-reg-key if boxRegKeyInfo, err := GetDeviceRegKey(""); err != nil { @@ -126,6 +130,9 @@ type BoxRegKeyInfo struct { func GetDeviceRegKey(apiBaseUrl string) (*BoxRegKeyInfo, error) { logger.AppLogger().Debugf("getBoxRegKey") + if !config.Config.PlatformEnabled { + return nil, fmt.Errorf("platform disabled") + } // 平台请求结构 type authStruct struct { diff --git a/biz/service/pair/register_box_test.go b/biz/service/pair/register_box_test.go new file mode 100644 index 0000000..7a04546 --- /dev/null +++ b/biz/service/pair/register_box_test.go @@ -0,0 +1,26 @@ +package pair + +import ( + "agent/config" + "testing" +) + +func TestServiceRegisterBoxWhenPlatformDisabled(t *testing.T) { + origEnabled := config.Config.PlatformEnabled + config.Config.PlatformEnabled = false + defer func() { config.Config.PlatformEnabled = origEnabled }() + + if err := ServiceRegisterBox(); err != nil { + t.Fatalf("expected no error when platform disabled, got %v", err) + } +} + +func TestGetDeviceRegKeyWhenPlatformDisabled(t *testing.T) { + origEnabled := config.Config.PlatformEnabled + config.Config.PlatformEnabled = false + defer func() { config.Config.PlatformEnabled = origEnabled }() + + if _, err := GetDeviceRegKey(""); err == nil { + t.Fatalf("expected error when platform disabled") + } +} diff --git a/biz/service/pair/try_out.go b/biz/service/pair/try_out.go deleted file mode 100644 index 30af7c5..0000000 --- a/biz/service/pair/try_out.go +++ /dev/null @@ -1,156 +0,0 @@ -// Copyright (c) 2022 Institute of Software, Chinese Academy of Sciences (ISCAS) -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -package pair - -import ( - "agent/biz/docker" - "agent/biz/model/device" - "agent/biz/model/device_ability" - "agent/biz/model/dto" - "agent/biz/model/dto/pair/tryout" - "agent/config" - "fmt" - "net/http" - "time" - - "agent/utils/logger" - - utilshttp "agent/utils/network/http" - - "github.com/dungeonsnd/gocom/encrypt/random" -) - -func ServiceTryout(req *tryout.TryoutCodeReq) (dto.BaseRspStr, error) { - if device_ability.GetAbilityModel().RunInDocker { - if docker.ContainersDownloading == docker.GetDockerStatus() { - err := fmt.Errorf("docker images is downloading...") - logger.AppLogger().Warnf("ServiceTryout,%v", err) - return dto.BaseRspStr{Code: dto.AgentCodeDockerPulling, Message: err.Error(), Results: nil}, err - } - } - - return presetBoxInfo(req) -} - -// 预置试用信息 -func presetBoxInfo(req *tryout.TryoutCodeReq) (dto.BaseRspStr, error) { - - // 平台请求结构 - type platformReqStruct struct { - Email string `json:"email"` - Code string `json:"code"` - Type string `json:"type"` - BoxInfo struct { - BoxUUID string `json:"boxUUID"` - Desc string `json:"desc"` - Extra map[string]string `json:"extra"` - BoxPubKey string `json:"boxPubKey"` - AuthType string `json:"authType"` - } `json:"boxInfo"` - } - // 平台响应结构 - type platformRspStruct struct { - Code string `json:"code"` - Message string `json:"message"` - RequestId string `json:"requestId"` - - State int32 `json:"state"` // 0-正常;1-禁用;2-已过期 - BoxInfo struct { - AuthType string `json:"authType"` - SnNumber string `json:"snNumber"` - IsRegistered bool `json:"isRegistered"` - } `json:"boxInfo"` - } - - // 请求平台 - parms := &platformReqStruct{} - parms.Email = req.Email - parms.Code = req.TryoutCode - parms.Type = "pc_open" - parms.BoxInfo.BoxUUID = device.GetDeviceInfo().BoxUuid - parms.BoxInfo.Desc = "pc tryout" - parms.BoxInfo.Extra = make(map[string]string) - // parms.BoxInfo.BoxPubKey = strings.ReplaceAll(string(device.GetBoxPubKey()), "\n", "") - parms.BoxInfo.BoxPubKey = string(device.GetDevicePubKey()) - parms.BoxInfo.AuthType = "box_pub_key" - url := device.GetApiBaseUrl() + config.Config.Platform.PresetBoxInfo.Path - logger.AppLogger().Debugf("presetBoxInfo, url:%+v, parms:%+v", url, parms) - - var headers = map[string]string{"Request-Id": random.GenUUID()} - var rsp platformRspStruct - - tryTotal := 6 - // var httpReq *http.Request - var httpRsp *http.Response - var body []byte - var err1 error - for i := 0; i < tryTotal; i++ { - _, httpRsp, body, err1 = utilshttp.PostJsonWithHeaders(url, parms, headers, &rsp) - if err1 != nil { - // logger.AppLogger().Warnf("Failed PostJson, err:%v, @@httpReq:%+v, @@httpRsp:%+v, @@body:%v", err1, httpReq, httpRsp, string(body)) - if i == tryTotal-1 { - return dto.BaseRspStr{Code: dto.AgentCodeServerErrorStr, Message: err1.Error(), Results: nil}, err1 - } - time.Sleep(time.Second * 2) - continue - } else { - break - } - } - - logger.AppLogger().Infof("presetBoxInfo, rsp:%+v", rsp) - logger.AppLogger().Infof("presetBoxInfo, httpRsp:%+v", httpRsp) - logger.AppLogger().Infof("presetBoxInfo, body:%v", string(body)) - - if httpRsp.StatusCode == http.StatusOK { - if rsp.State == 1 { - err1 := fmt.Errorf("httpRsp.StatusCode: %+v, rsp.State: %+v", httpRsp.StatusCode, rsp.State) - rsp := dto.BaseRspStr{Code: dto.AgentCodeTryOutCodeDisabled, Message: err1.Error(), Results: nil} - return rsp, nil - } else if rsp.State == 2 { - err1 := fmt.Errorf("httpRsp.StatusCode: %+v, rsp.State: %+v", httpRsp.StatusCode, rsp.State) - rsp := dto.BaseRspStr{Code: dto.AgentCodeTryOutCodeExpired, Message: err1.Error(), Results: nil} - return rsp, nil - } - - err := device.UpdateSnNumber(rsp.BoxInfo.SnNumber) - if err != nil { - err1 := fmt.Errorf("failed UpdateSnNumber: %+v", err) - rsp := dto.BaseRspStr{Code: dto.AgentCodeTryOutCodeExpired, Message: err1.Error(), Results: nil} - return rsp, nil - } - - device.UpdateApplyEmail(req.Email) - - rsp := dto.BaseRspStr{Code: dto.AgentCodeOkStr, Message: "OK", Results: nil} - return rsp, nil - - } else if httpRsp.StatusCode == http.StatusBadRequest { - c := dto.AgentCodeBadReqStr - if rsp.Code == "PSP-2047" { - c = dto.AgentCodeTryOutCodeError - } else if rsp.Code == "PSP-2052" { - c = dto.AgentCodeTryOutCodeHasUsed - } - err1 := fmt.Errorf("httpRsp.StatusCode: %+v, rsp.Code: %+v, rsp.Message: %+v", httpRsp.StatusCode, rsp.Code, rsp.Message) - rsp := dto.BaseRspStr{Code: c, Message: err1.Error(), Results: nil} - return rsp, err1 - - } else { - err1 := fmt.Errorf("httpRsp.StatusCode: %+v, rsp.Code: %+v, rsp.Message: %+v", httpRsp.StatusCode, rsp.Code, rsp.Message) - rsp := dto.BaseRspStr{Code: dto.AgentCodeServerErrorStr, Message: err1.Error(), Results: nil} - return rsp, err1 - } -} diff --git a/biz/service/pair/wifi_operation_test.go b/biz/service/pair/wifi_operation_test.go new file mode 100644 index 0000000..8dee8a0 --- /dev/null +++ b/biz/service/pair/wifi_operation_test.go @@ -0,0 +1,46 @@ +package pair + +import ( + "agent/utils/rpi/network" + "testing" +) + +func TestSortWifiList(t *testing.T) { + input := []*network.ListWifiInfo{ + {SSID: "a", SIGNAL: "30"}, + {SSID: "b", SIGNAL: "80"}, + {SSID: "c", SIGNAL: "50"}, + } + + out := sortWifiList(input) + if len(out) != 3 { + t.Fatalf("unexpected length: %d", len(out)) + } + if out[0].SIGNAL != "80" || out[1].SIGNAL != "50" || out[2].SIGNAL != "30" { + t.Fatalf("unexpected order: %v, %v, %v", out[0].SIGNAL, out[1].SIGNAL, out[2].SIGNAL) + } +} + +func TestRemoveDuplicatedWifiKeepsStrongest(t *testing.T) { + input := []*network.ListWifiInfo{ + {SSID: "same", SIGNAL: "10"}, + {SSID: "same", SIGNAL: "90"}, + {SSID: "other", SIGNAL: "20"}, + } + + out := removeDuplicatedWifi(input) + if len(out) != 2 { + t.Fatalf("unexpected length: %d", len(out)) + } + + strongest := map[string]string{} + for _, v := range out { + strongest[v.SSID] = v.SIGNAL + } + if strongest["same"] != "90" { + t.Fatalf("expected strongest signal for 'same' to be 90, got %s", strongest["same"]) + } + if strongest["other"] != "20" { + t.Fatalf("expected signal for 'other' to be 20, got %s", strongest["other"]) + } +} diff --git a/biz/service/platform/ability.go b/biz/service/platform/ability.go index 0ef79ea..f649f51 100644 --- a/biz/service/platform/ability.go +++ b/biz/service/platform/ability.go @@ -27,6 +27,10 @@ import ( var platformApis *platform.PlatformAPIs func InitPlatformAbility() *platform.PlatformAPIs { + if !config.Config.PlatformEnabled { + logger.AppLogger().Warnf("platform disabled, skip InitPlatformAbility") + return nil + } var headers = map[string]string{ "Request-Id": random.GenUUID(), } @@ -44,10 +48,16 @@ func InitPlatformAbility() *platform.PlatformAPIs { } func CheckPlatformAbility(uri string) bool { + if !config.Config.PlatformEnabled { + return false + } if platformApis == nil { platformApis = InitPlatformAbility() } + if platformApis == nil { + return false + } for _, apis := range platformApis.PlatformAPIs { //logger.AppLogger().Debugf(apis.URI) if uri == apis.URI { diff --git a/biz/service/platform/ability_test.go b/biz/service/platform/ability_test.go new file mode 100644 index 0000000..1c0caeb --- /dev/null +++ b/biz/service/platform/ability_test.go @@ -0,0 +1,16 @@ +package platform + +import ( + "agent/config" + "testing" +) + +func TestCheckPlatformAbilityDisabled(t *testing.T) { + origEnabled := config.Config.PlatformEnabled + config.Config.PlatformEnabled = false + defer func() { config.Config.PlatformEnabled = origEnabled }() + + if CheckPlatformAbility("/any") { + t.Fatalf("expected false when platform disabled") + } +} diff --git a/biz/service/switch-platform/switch_platform.go b/biz/service/switch-platform/switch_platform.go index 30d3976..0c02c5c 100644 --- a/biz/service/switch-platform/switch_platform.go +++ b/biz/service/switch-platform/switch_platform.go @@ -19,6 +19,7 @@ import ( "agent/biz/model/dto" modelsp "agent/biz/model/switch-platform" "agent/biz/service/encwrapper" + "agent/config" "agent/utils" "errors" "fmt" @@ -32,6 +33,10 @@ import ( func ServiceSwitchPlatform(req *modelsp.SwitchPlatformReq) (dto.BaseRspStr, error) { logger.AppLogger().Debugf("ServiceSwitchPlatform, req:%+v", req) logger.AccessLogger().Debugf("[ServiceSwitchPlatform], req:%+v", req) + if !config.Config.PlatformEnabled { + err := fmt.Errorf("platform disabled") + return dto.BaseRspStr{Code: dto.AgentCodeUnsupportedFunction, Message: err.Error()}, err + } err := encwrapper.Check() if err != nil { diff --git a/biz/service/upgrade/core.go b/biz/service/upgrade/core.go index df98c39..c0b8dd9 100644 --- a/biz/service/upgrade/core.go +++ b/biz/service/upgrade/core.go @@ -153,6 +153,9 @@ func GetLatestVersionMetadata() (upgrade.OverallInfo, error) { } func CheckLatestVersion() (upgrade.VersionFromPlatformV2, error) { + if !config.Config.PlatformEnabled { + return upgrade.VersionFromPlatformV2{}, fmt.Errorf("platform disabled") + } apiBase := config.Config.Platform.APIBase.Url urlPath := config.Config.Platform.LatestVersionV2.Path versionDesc := upgrade.VersionFromPlatformV2{} diff --git a/biz/service/upgrade/core_test.go b/biz/service/upgrade/core_test.go index c6331b3..80f8d58 100644 --- a/biz/service/upgrade/core_test.go +++ b/biz/service/upgrade/core_test.go @@ -15,11 +15,20 @@ package upgrade import ( + "agent/config" "fmt" + "os" "testing" ) func TestCheckLatestVersion(t *testing.T) { + if os.Getenv("ENABLE_PLATFORM_TESTS") != "1" { + // TODO: Add a platform API mock (or fixture server) so this test runs in CI without external dependency. + t.Skip("ENABLE_PLATFORM_TESTS is not set") + } + origEnabled := config.Config.PlatformEnabled + config.Config.PlatformEnabled = true + t.Cleanup(func() { config.Config.PlatformEnabled = origEnabled }) versionDesc, err := CheckLatestVersion() if err != nil { t.Fatalf("failed to get latest version") diff --git a/biz/web/handler/bind/bindinit/bindinit.go b/biz/web/handler/bind/bindinit/bindinit.go index ee16f28..3d63dab 100644 --- a/biz/web/handler/bind/bindinit/bindinit.go +++ b/biz/web/handler/bind/bindinit/bindinit.go @@ -32,12 +32,20 @@ import ( // @Tags Pair // @Accept plain // @Produce json -// @Param initReq body bindinit.InitReq true "query params" +// @Param clientUuid query string false "client uuid" +// @Param clientVersion query string false "client version" // @Success 200 {object} dto.BaseRspStr{results=pair.InitResult} "code=AG-200 success;" // @Router /agent/v1/api/bind/init [GET] func Init(c *gin.Context) { logger.AppLogger().Debugf("%+v", c.Request) var reqObject bindinit.InitReq svc := new(servicesinit.InitService) + _ = c.ShouldBindQuery(&reqObject) + if c.Request.Method == http.MethodGet { + svc.InitLanService("", c.Request.Header, c) + svc.Req = &reqObject + c.JSON(http.StatusOK, svc.Process()) + return + } c.JSON(http.StatusOK, svc.InitLanService("", c.Request.Header, c).Enter(svc, &reqObject)) } diff --git a/biz/web/handler/bind/bindinit/bindinit_test.go b/biz/web/handler/bind/bindinit/bindinit_test.go new file mode 100644 index 0000000..12b71cb --- /dev/null +++ b/biz/web/handler/bind/bindinit/bindinit_test.go @@ -0,0 +1,73 @@ +package bindinit + +import ( + "agent/biz/model/clientinfo" + "agent/biz/model/device" + "agent/biz/model/device_ability" + dtopair "agent/biz/model/dto/pair" + servicesinit "agent/biz/service/bind/init" + "agent/biz/model/dto" + "agent/config" + "encoding/json" + "net/http/httptest" + "net/url" + "testing" + + "github.com/gin-gonic/gin" +) + +type baseRsp struct { + Code string `json:"code"` + Message string `json:"message"` + Results json.RawMessage `json:"results"` +} + +func TestBindInitHandlerBindsQueryAndStoresClientVersion(t *testing.T) { + gin.SetMode(gin.TestMode) + + restoreNetwork := servicesinit.SetGetConnectedNetworkFuncForTest(func() []*dtopair.Network { + return []*dtopair.Network{} + }) + restoreVersion := servicesinit.SetGetInstalledVersionFuncForTest(func() string { + return "1.2.3" + }) + defer restoreNetwork() + defer restoreVersion() + + origEncrypt := config.Config.EncryptLanSessionData + config.Config.EncryptLanSessionData = false + defer func() { config.Config.EncryptLanSessionData = origEncrypt }() + + device.GetDeviceInfo().BoxUuid = "box-uuid" + device_ability.GetAbilityModel().DeviceModelNumber = device_ability.SN_GEN_2 + + router := gin.New() + router.GET("/agent/v1/api/bind/init", Init) + + q := url.Values{} + q.Set("clientUuid", "client-uuid-1") + q.Set("clientVersion", "1.0.0") + req := httptest.NewRequest("GET", "/agent/v1/api/bind/init?"+q.Encode(), nil) + w := httptest.NewRecorder() + router.ServeHTTP(w, req) + + if w.Code != 200 { + t.Fatalf("unexpected status code: %d", w.Code) + } + + var rsp baseRsp + if err := json.Unmarshal(w.Body.Bytes(), &rsp); err != nil { + t.Fatalf("failed to decode response: %v", err) + } + if rsp.Code != dto.AgentCodeOkStr { + t.Fatalf("unexpected response code: %s", rsp.Code) + } + + v, found := clientinfo.GetClientVersion("client-uuid-1") + if !found { + t.Fatalf("client version not stored") + } + if v != "1.0.0" { + t.Fatalf("client version mismatch: %s", v) + } +} diff --git a/biz/web/handler/bind/internet/service/config/config.go b/biz/web/handler/bind/internet/service/config/config.go index f144daf..159872c 100644 --- a/biz/web/handler/bind/internet/service/config/config.go +++ b/biz/web/handler/bind/internet/service/config/config.go @@ -33,8 +33,8 @@ import ( // @Tags Pair // @Accept plain // @Produce json -// @Param configReq body dtoconfig.ConfigReq true "config params" -// @Success 200 {object} dto.BaseRspStr{results=dtoconfig.ConfigRsp} "code=AG-200 success;" +// @Param configReq body ConfigReq true "config params" +// @Success 200 {object} dto.BaseRspStr{results=ConfigRsp} "code=AG-200 success;" // @Router /agent/v1/api/bind/internet/service/config [POST] func PostConfig(c *gin.Context) { logger.AppLogger().Debugf("%+v", c.Request) @@ -58,7 +58,7 @@ func PostConfig(c *gin.Context) { // @Accept plain // @Produce json // @Param body query string true "clientUuid and aoid" -// @Success 200 {object} dto.BaseRspStr{results=dtoconfig.GetConfigRsp} "code=AG-200 success;" +// @Success 200 {object} dto.BaseRspStr{results=GetConfigRsp} "code=AG-200 success;" // @Router /agent/v1/api/bind/internet/service/config [GET] func GetConfig(c *gin.Context) { logger.AppLogger().Debugf("%+v", c.Request) diff --git a/biz/web/handler/bind/internet/service/config/types.go b/biz/web/handler/bind/internet/service/config/types.go new file mode 100644 index 0000000..5fcbd69 --- /dev/null +++ b/biz/web/handler/bind/internet/service/config/types.go @@ -0,0 +1,9 @@ +package config + +import dtoconfig "agent/biz/model/dto/bind/internet/service/config" + +// Type aliases for swagger annotations in this package. +type ConfigReq = dtoconfig.ConfigReq +type ConfigRsp = dtoconfig.ConfigRsp +type GetConfigReq = dtoconfig.GetConfigReq +type GetConfigRsp = dtoconfig.GetConfigRsp diff --git a/biz/web/handler/pair/try_out.go b/biz/web/handler/pair/try_out.go deleted file mode 100644 index 20163c5..0000000 --- a/biz/web/handler/pair/try_out.go +++ /dev/null @@ -1,73 +0,0 @@ -// Copyright (c) 2022 Institute of Software, Chinese Academy of Sciences (ISCAS) -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -package pair - -import ( - "agent/biz/model/device_ability" - "agent/biz/model/dto" - "agent/biz/model/dto/pair/tryout" - servicepair "agent/biz/service/pair" - "agent/config" - "fmt" - "net/http" - - "agent/utils/logger" - - "github.com/dungeonsnd/gocom/encrypt/random" - "github.com/dungeonsnd/gocom/file/fileutil" - "github.com/gin-gonic/gin" -) - -// TryOutCode godoc -// @Summary verify trial code [for web client LAN] -// @Description -// @ID TryOutCode -// @Tags TryOut -// @Accept plain -// @Produce json -// @Success 200 {object} dto.BaseRspStr "code=AG-200 success;" -// @Router /agent/v1/api/pair/tryout/code [POST] -func TryOutCode(c *gin.Context) { - logger.AppLogger().Debugf("TryOutCode") - - tryoutCode := c.PostForm("tryoutCode") - email := c.PostForm("email") - - var reqObj tryout.TryoutCodeReq - if len(tryoutCode) < 1 || len(email) < 1 { - err1 := fmt.Errorf("request params error, tryoutCode:%+v, email:%+v", tryoutCode, email) - // logger.AppLogger().Debugf("TryOutCode POST, %+v", err1) - c.JSON(http.StatusOK, dto.BaseRspStr{Code: dto.AgentCodeBadReqStr, Message: err1.Error()}) - return - } - reqObj.TryoutCode = tryoutCode - reqObj.Email = email - - abilityModel := device_ability.GetAbilityModel() - if abilityModel.RunInDocker { - err := fileutil.WriteToFile(config.Config.Box.HostIpFile, []byte(c.Request.Host), true) - if err != nil { - err1 := fmt.Errorf("failed write HostIpFile, %+v", err) - logger.AppLogger().Debugf("info POST, %+v", err1) - c.JSON(http.StatusOK, dto.BaseRspStr{Code: dto.AgentCodeServerErrorStr, - RequestId: random.GenUUID(), - Message: err1.Error()}) - return - } - } - - rsp, _ := servicepair.ServiceTryout(&reqObj) - c.JSON(http.StatusOK, rsp) -} diff --git a/biz/web/handler/status/info.go b/biz/web/handler/status/info.go index aa72aaf..b4bb4c9 100644 --- a/biz/web/handler/status/info.go +++ b/biz/web/handler/status/info.go @@ -28,7 +28,6 @@ import ( "agent/biz/model/dto" "agent/biz/model/dto/status" "agent/config" - "agent/utils/deviceid" "fmt" "net/http" @@ -85,12 +84,6 @@ func Info(c *gin.Context) { return } result.QrCode = device.GetQrCode() - - snNumber, err := deviceid.GetSnNumber(config.Config.Box.SnNumberStoreFile) - result.TryoutCodeVerified = false - if err == nil && len(snNumber) > 0 { - result.TryoutCodeVerified = true - } } c.IndentedJSON(http.StatusOK, dto.BaseRspStr{Code: dto.AgentCodeOkStr, diff --git a/biz/web/routers/router.go b/biz/web/routers/router.go index dc39542..c739dd0 100644 --- a/biz/web/routers/router.go +++ b/biz/web/routers/router.go @@ -82,8 +82,6 @@ func ExternalRouter() *gin.Engine { pairapis := api.Group("/pair") { - pairapis.POST("/tryout/code", pair.TryOutCode) - pairapis.POST("/init", pair.TryOutCode) pairapis.GET("/net/localips", pairnet.LocalIps) pairapis.GET("/net/netconfig", pairnet.NetConfig) pairapis.GET("/init", pair.Init) diff --git a/config/config.go b/config/config.go index d5d7d66..b4e023f 100644 --- a/config/config.go +++ b/config/config.go @@ -37,6 +37,7 @@ var runInDocker bool var Config = struct { DebugMode bool `default:"false"` // 调试模式。会控制是否打开 swagger 等。 + PlatformEnabled bool `default:"false"` // 是否启用平台相关能力(注册、升级、互联网通道等) OverwriteDockerCompose bool `default:"true"` // 启动时是否覆盖 docker-compose.yml。"true" 表示覆盖。 EnableSecurityChip bool `default:"true"` // 是否启用加密芯片。 EncryptLanSessionData bool `default:"true"` // 加密局域网通信数据 diff --git a/deps/did/aospace/did/identifier_test.go b/deps/did/aospace/did/identifier_test.go index e744ee4..01133ff 100644 --- a/deps/did/aospace/did/identifier_test.go +++ b/deps/did/aospace/did/identifier_test.go @@ -29,8 +29,14 @@ func TestIdentifier(t *testing.T) { keyType := "RsaVerificationKey2018" publicKeyPem := "-----BEGIN PUBLIC KEY-----\nMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAnN5jap7CGcqYURbLDVUa\nLc9kMxOyCMEykfwbQKXvTkPMkR9tKZmq8EqfG2d2OyUpF1TIfqHK7Q6d33yD02oO\nBTXZw1Ijkfxvu0KwG2zLV02FTuwZzgYa/AaP5iRZDx5GwTk/YFw+NTqT8Gf29a/L\n/ItcCfsEFLr3zMDXUcU9A7rBEy5ncva6RLNpXawegFGlCZa5+Gah8voKl8ZGpIgt\nlSc1IdnbPbBCYYlUATWLCLeYl+Q9/LslbpkFtdR+4M8vU7G1H+AQZ5fr2E9qX36I\nzcnchDmKq5bkbWQ9GJeZKqZTkhtCPBy4cphM8fHtZuoh1fA3VfF01N4KHT2bUdtp\nJwIDAQAB\n-----END PUBLIC KEY-----" - did.AddNewVerificationMethod(keyType, publicKeyPem, "k=v", "key-0") - did.AddNewVerificationMethodOfMultisig() + _, err = did.AddNewVerificationMethod(keyType, publicKeyPem, "k=v", "key-0") + if err != nil { + panic(err) + } + err = did.AddNewVerificationMethodOfMultisig([]string{"#key-0"}, []string{"#key-1"}) + if err != nil { + panic(err) + } didDoc := did.Document(true) js, err := json.Marshal(didDoc) @@ -137,7 +143,7 @@ func TestFromDocument(t *testing.T) { } if len(methods) > 1 { - err = didObj.DeleteVerificationMethodOfQuery("credentialType=password") + _, err = didObj.DeleteVerificationMethodOfQuery("credentialType=password") if err != nil { panic(err) } diff --git a/deps/logger/logger.go b/deps/logger/logger.go index 03ce4df..a0c4092 100644 --- a/deps/logger/logger.go +++ b/deps/logger/logger.go @@ -15,6 +15,10 @@ package logger import ( + "os" + "path/filepath" + "strings" + "go.uber.org/zap" "go.uber.org/zap/zapcore" "gopkg.in/natefinch/lumberjack.v2" @@ -57,6 +61,39 @@ var mDefaultLoggerPath string func init() { mLogger = make(map[string]*zap.SugaredLogger) mDefaultLoggerPath = "./" + mDefaultLoggerPath = initDefaultLoggerPath() +} + +func initDefaultLoggerPath() string { + if envPath := os.Getenv("AOSPACE_LOG_DIR"); envPath != "" { + return ensureTrailingSlash(envPath) + } + if isTestProcess() { + return ensureTrailingSlash(filepath.Join(os.TempDir(), "aospace-agent-test")) + } + return "./" +} + +func isTestProcess() bool { + for _, arg := range os.Args { + if strings.HasPrefix(arg, "-test.") || strings.HasPrefix(arg, "-test=") { + return true + } + } + if len(os.Args) > 0 && strings.HasSuffix(os.Args[0], ".test") { + return true + } + return false +} + +func ensureTrailingSlash(p string) string { + if p == "" { + return p + } + if strings.HasSuffix(p, string(os.PathSeparator)) { + return p + } + return p + string(os.PathSeparator) } func SetDefaultLoggerPath(defaultLoggerPath string) { diff --git a/docs/PLATFORM_DEPENDENCIES.md b/docs/PLATFORM_DEPENDENCIES.md new file mode 100644 index 0000000..d376e02 --- /dev/null +++ b/docs/PLATFORM_DEPENDENCIES.md @@ -0,0 +1,71 @@ +# Platform Dependency Analysis + +English | [简体中文](./PLATFORM_DEPENDENCIES_CN.md) + +This document summarizes where the server (space-agent) depends on the platform, and how to run without the platform using the new `PlatformEnabled` switch. Test processes write logs to the system temp directory by default (override with `AOSPACE_LOG_DIR`). + +## Scope +space-agent is the server component of the product. Platform features are used for registration, Internet tunnel setup, upgrade checks, and platform switching. These are now gated by `config.Config.PlatformEnabled` (default `false`). + +## Platform-dependent modules and code paths + +### 1) Device registration / pairing +- `biz/service/pair/register_box.go` + - `ServiceRegisterBox()` and `GetDeviceRegKey()` call platform registration APIs. + - When `PlatformEnabled=false`, registration is skipped (no-op) and `GetDeviceRegKey` returns an error. +- `biz/service/pair/pairing.go` + - Calls `ServiceRegisterBox()` during pairing. With platform disabled, the registration step is skipped. + +### 2) Internet tunnel configuration +- `biz/service/bind/internet/service/config/post_config.go` + - Enabling Internet access triggers platform registration and gateway platform switch. + - If `PlatformEnabled=false` and `EnableInternetAccess=true`, returns `AG-403` (unsupported). + +### 3) Bind/space creation (Internet access flow) +- `biz/service/bind/space/create/create.go` + - Internet-access flow calls `registerDevice()` which triggers platform registration. + - If `PlatformEnabled=false` and `EnableInternetAccess=true`, returns `AG-403` (unsupported). + +### 4) Platform ability & API gating +- `biz/service/platform/ability.go` + - Fetches platform ability list and checks availability. + - When disabled, returns nil/false and skips network calls. + +### 5) Upgrade checks +- `biz/service/upgrade/core.go` + - `CheckLatestVersion()` calls platform API. + - When disabled, returns an error immediately. +- `main.go` + - `platform.InitPlatformAbility`, `upgrade.CronForUpgrade`, and `upgrade.CheckUpgradeSucc` are skipped when disabled. + +### 6) Switch platform +- `biz/service/switch-platform/switch_platform.go` + - Platform switch is not available when `PlatformEnabled=false` and returns `AG-403`. + +### 7) Network reachability checks +- `biz/alivechecker/entry.go` + - Platform host checks are skipped when disabled; third-party network check remains. + +### 9) Docker environment variables +- `biz/docker/env.go` + - Writes platform-related env vars into containers (API base, web URL, etc.). + - No functional change; these may be unused when platform is disabled. + +## Running without the platform (server + client only) + +1. Set `PlatformEnabled=false` in configuration or runtime config file. +2. Use LAN-only flows (do not enable Internet access during bind/create). +3. Avoid platform-only APIs (switch platform, upgrade checks). These return `AG-403` when disabled. + +## Limitations when platform is disabled +- Internet tunnel enablement is blocked (`/bind/internet/service/config` returns `AG-403`). +- Tryout code verification is blocked. +- Upgrade checks from platform are blocked. +- Switch platform APIs are blocked. + +## Tests added for platform-optional behavior +- `biz/service/pair/register_box_test.go` +- `biz/service/bind/internet/service/config/post_config_test.go` + +## Notes +- Some components (e.g., gateway account creation) still run without platform. If any downstream service requires platform-only data (like `boxRegKey`), you may need to adjust those services accordingly. diff --git a/docs/PLATFORM_DEPENDENCIES_CN.md b/docs/PLATFORM_DEPENDENCIES_CN.md new file mode 100644 index 0000000..3cfdc13 --- /dev/null +++ b/docs/PLATFORM_DEPENDENCIES_CN.md @@ -0,0 +1,70 @@ +# 平台依赖分析 + +简体中文 | [English](./PLATFORM_DEPENDENCIES.md) + +本文档总结 space-agent 对平台的依赖点,以及通过 `PlatformEnabled` 开关实现平台可选运行的说明。测试进程会默认把日志写入系统临时目录(可通过环境变量 `AOSPACE_LOG_DIR` 指定)。 + +## 适用范围 +space-agent 是产品的服务端组件。平台相关能力包括:注册、互联网通道配置、升级检查、平台切换。现在这些能力都受 `config.Config.PlatformEnabled` 控制(默认 `false`)。 + +## 平台依赖模块与代码路径 + +### 1) 设备注册 / 配对 +- `biz/service/pair/register_box.go` + - `ServiceRegisterBox()` 和 `GetDeviceRegKey()` 会请求平台注册接口。 + - 当 `PlatformEnabled=false` 时,注册流程直接跳过;`GetDeviceRegKey` 返回错误。 +- `biz/service/pair/pairing.go` + - 配对中会调用 `ServiceRegisterBox()`,平台禁用时注册步骤被跳过。 + +### 2) 互联网通道配置 +- `biz/service/bind/internet/service/config/post_config.go` + - 启用互联网通道会触发平台注册与网关平台切换。 + - 当 `PlatformEnabled=false` 且 `EnableInternetAccess=true` 时返回 `AG-403`。 + +### 3) 绑定/创建空间(互联网访问流程) +- `biz/service/bind/space/create/create.go` + - 互联网访问流程调用 `registerDevice()` 触发平台注册。 + - 当 `PlatformEnabled=false` 且 `EnableInternetAccess=true` 时返回 `AG-403`。 + +### 4) 平台能力与 API 探测 +- `biz/service/platform/ability.go` + - 获取平台能力列表并检查 API 可用性。 + - 当禁用时直接返回 nil/false,不发起请求。 + +### 5) 升级检查 +- `biz/service/upgrade/core.go` + - `CheckLatestVersion()` 会请求平台版本接口。 + - 当禁用时立即返回错误。 +- `main.go` + - 平台能力获取、升级定时检查与升级结果检测在禁用时跳过。 + +### 6) 切换平台 +- `biz/service/switch-platform/switch_platform.go` + - 当 `PlatformEnabled=false` 时返回 `AG-403`。 + +### 7) 网络连通性检测 +- `biz/alivechecker/entry.go` + - 平台相关网络检查在禁用时跳过,第三方网络检查仍保留。 + +### 8) Docker 环境变量 +- `biz/docker/env.go` + - 仍会写入平台相关 env,但在平台禁用时通常不会被用到。 + +## 无平台运行(仅服务端 + 客户端) + +1. 在配置中设置 `PlatformEnabled=false`。 +2. 使用局域网绑定流程(不要启用互联网通道)。 +3. 避免平台专属 API(切换平台、升级检查等)。这些会返回 `AG-403`。 + +## 平台禁用的限制 +- 互联网通道无法启用(`/bind/internet/service/config` 返回 `AG-403`)。 +- 平台升级检查不可用。 +- 切换平台不可用。 + +## 已添加的单元测试 +- `biz/service/pair/register_box_test.go` +- `biz/service/bind/internet/service/config/post_config_test.go` +- `biz/service/platform/ability_test.go` + +## 备注 +- 仍有部分组件(例如网关创建账号)在无平台情况下正常运行。如果下游服务需要平台返回字段(如 `boxRegKey`),需要配套调整。 diff --git a/docs/PROJECT_OVERVIEW.md b/docs/PROJECT_OVERVIEW.md new file mode 100644 index 0000000..2ec1dc5 --- /dev/null +++ b/docs/PROJECT_OVERVIEW.md @@ -0,0 +1,179 @@ +# space-agent 项目概览 + +简体中文 | [English](./PROJECT_OVERVIEW_EN.md) + +> 基于当前仓库代码生成(主要参考 `README.md`、`main.go`、`biz/web/routers/*`、`docs/swagger.yaml`、`config/config.go`、`Dockerfile`、`Makefile`、`script/*` 等)。 + +## 1. 项目背景与用途 + +space-agent 是 AO.space(开源版)一体化部署的核心入口服务,负责设备初始化与绑定、密钥交换、微服务启动与管理、DID 生成与管理、以及系统升级等功能。它为 AO.space 服务端提供统一的启动与运行管理入口,并提供 HTTP API 供客户端/网关进行配对与管理。项目说明见 `README.md`。 + +## 2. 整体系统架构(模块划分、主要职责) + +整体为 Go 单体服务 + Docker 微服务编排 + Web 前端资源构建的结构,核心链路如下: + +- **启动入口**:`main.go` + - 初始化配置与日志 + - 设备信息/密钥/客户端信息初始化 + - 启动 Web API(外部 + 内部) + - 启动 Docker 微服务(或数据迁移) + - 启动存活检测、升级检查、日志目录监控、DID/LevelDB +- **Web API 层**:`biz/web/*` + - Gin 框架实现外部/内部 API,路由见 `biz/web/routers/router.go` + - Swagger 文档生成配置见 `docs/` +- **业务服务层**:`biz/service/*` + - 配对/绑定/密钥交换、DID 文档、网络配置、系统控制、升级等 +- **设备与模型层**:`biz/model/*`, `utils/*` + - 设备信息、DID 文档存储、网络/硬件/容器等通用能力 +- **Docker 管理层**:`biz/docker/*` + `utils/docker/*` + `res/*` + - 负责微服务容器的编排、启动、升级、配置生成 +- **前端静态资源**:`web/boxdocker` + - 通过 Dockerfile 构建并打包到 `res/static_html.zip` + +## 3. 目录结构说明(逐级说明关键目录和文件作用) + +- `main.go`:服务入口,启动各核心模块。 +- `config/`:配置定义与默认值(`config/config.go`)。 +- `biz/` + - `biz/web/`:HTTP API 入口、路由和处理器。 + - `biz/web/routers/`:外部/内部路由定义与 Server 启动逻辑。 + - `biz/web/handler/`:具体 API 处理器。 + - `biz/service/`:核心业务逻辑实现(绑定、配对、升级、系统控制、DID 等)。 + - `biz/docker/`:容器启动、compose 文件处理、容器状态监听等。 + - `biz/model/`:业务模型与 DTO,DID 与设备数据存储。 + - `biz/alivechecker/`:容器/网络存活检测。 + - `biz/disk_space_monitor/`:日志目录与磁盘监控。 + - `biz/notification/`:升级/能力变更等通知逻辑。 +- `utils/`:通用能力(网络、设备 ID、JWT、Docker 接口、硬件信息、BLE、重试等)。 +- `res/`:内置资源(docker-compose 模板、静态站点 zip、路由配置等)。 +- `docs/`:Swagger 文档与生成入口(`docs/swagger.yaml`、`docs/swagger.json`)。 +- `web/boxdocker/`:前端工程(Vite + Vue),打包生成静态资源。 +- `script/`:构建与运行脚本(含 swagger 生成与交叉编译示例)。 +- `Dockerfile`:多阶段构建,打包服务与前端。 +- `Makefile`:本地 Go 编译配置。 + +## 4. 关键模块与核心逻辑说明 + +- **启动流程(`main.go`)** + - 初始化日志与版本信息。 + - 初始化设备信息、设备密钥、客户端信息。 + - 按环境变量 `AOSPACE_SINGLE_DOCKER_MODE` 决定是否启动平台能力、升级任务、微服务启动逻辑。 + - 启动 Web API 服务(外部 5678,内部 5680)。 + - 启动 Docker 微服务或执行存储迁移。 + - 启动 alive checker、升级检查、日志目录监控、DID LevelDB。 + +- **HTTP API(`biz/web/*`)** + - 外部路由:`/agent` 与 `/agent/v1/api/*`,用于客户端绑定、配对、网络配置、DID 管理等。 + - 内部路由:`/agent/v1/api/*`,用于容器内部或网关调用(如升级、设备信息)。 + +- **绑定/配对流程(`biz/service/pair`、`biz/service/bind`)** + - 包含 `initial`、`pairing`、`keyexchange`、`setpassword`、`bind/space/create` 等关键接口。 + +- **Docker 容器管理(`biz/docker/*`、`utils/docker/*`、`res/*`)** + - 读取嵌入式 compose 模板与配置,启动/维护 AO.space 微服务容器。 + - 通过 `res/load.go` 动态调整挂载路径、环境变量等。 + +- **升级与系统管理(`biz/service/upgrade`、`biz/service/system`)** + - 提供下载、安装、状态查询与升级配置管理接口。 + - 支持关机、重启等系统控制操作。 + +- **DID 文档与证书(`biz/service/did/*`、`biz/model/did/*`)** + - DID 文档生成、存储与更新。 + - LAN 证书获取接口。 + +- **存活检测(`biz/alivechecker/*`)** + - 定期检测容器与网络连通状态。 + +## 5. API 接口清单(接口路径、方法、参数、返回值) + +以下接口来自 `docs/swagger.yaml` 与路由定义(外部与内部共用前缀 `/agent` 或 `/agent/v1/api`)。参数与返回值类型请参考 swagger definitions。 + +### 信息类 +- `GET /agent/status`:返回服务状态(`dto.BaseRspStr` + `status.Status`)。 +- `GET /agent/info`:返回服务信息(`dto.BaseRspStr` + `status.Info`)。 +- `GET /agent/logs`:返回日志(调试模式才启用)。 + +### 配对 / 绑定 / 密钥 +- `POST /agent/v1/api/initial`:查询初始化进度(`pair.PasswordInfo` -> `call.MicroServerRsp`)。 +- `POST /agent/v1/api/pairing`:客户端绑定(`pair.PairingReq` -> `call.MicroServerRsp`)。 +- `POST /agent/v1/api/pubkeyexchange`:公钥交换(`pair.PubKeyExchangeReq` -> `pair.PubKeyExchangeRsp`)。 +- `POST /agent/v1/api/keyexchange`:对称密钥交换(`pair.KeyExchangeReq` -> `pair.KeyExchangeRsp`)。 +- `POST /agent/v1/api/setpassword`:设置管理员密码(`pair.PasswordInfo` -> `call.MicroServerRsp`)。 +- `POST /agent/v1/api/bind/com/start`:启动微服务容器(无 body -> `dto.BaseRspStr`)。 +- `GET /agent/v1/api/bind/com/progress`:查询启动进度(`progress.ProgressRsp`)。 +- `GET /agent/v1/api/bind/init`:绑定前初始化信息(`bindinit.InitReq` -> `pair.InitResult`)。 +- `POST /agent/v1/api/bind/space/create`:创建空间/完成绑定(`create.CreateReq` -> `create.CreateRsp`)。 +- `POST /agent/v1/api/bind/password/verify`:验证管理员密码(`password.VerifyReq` -> `password.VerifyRsp`)。 +- `POST /agent/v1/api/bind/revoke`:解除绑定(`revoke.RevokeReq` -> `revoke.RevokeRsp`)。 +- `POST /agent/v1/api/admin/revoke`:管理员解绑(`pair.RevokeReq` -> `call.MicroServerRsp`)。 +- `GET /agent/v1/api/pair/init`:有线绑定初始化(`pair.InitResult`)。 +- `GET /agent/v1/api/pair/net/localips`:获取本地 IP(`[]pair.Network`)。 +- `GET /agent/v1/api/pair/net/netconfig`:获取 Wi‑Fi 列表(`[]pair.WifiListRsp`)。 +- `POST /agent/v1/api/bind/internet/service/config`:设置 Internet tunnel(`config.ConfigReq` -> `config.ConfigRsp`)。 +- `GET /agent/v1/api/bind/internet/service/config`:获取 Internet tunnel 配置(query 参数 -> `config.GetConfigRsp`)。 + +### 设备 / 网络 / 证书 +- `GET /agent/v1/api/device/ability`:设备能力(`device_ability.DeviceAbility`)。 +- `GET /agent/v1/api/device/info`:设备信息(`device.StorageInfo`)。 +- `GET /agent/v1/api/device/version`:设备版本(`device.BoxDeviceVersion`)。 +- `GET /agent/v1/api/device/localips`:设备本地 IP(`[]pair.Network`)。 +- `GET /agent/v1/api/device/netconfig`:设备 Wi‑Fi 列表(`[]pair.WifiListRsp`)。 +- `POST /agent/v1/api/network/config`:配置网络(`network.NetworkConfigReq` -> `dto.BaseRspStr`)。 +- `GET /agent/v1/api/network/config`:查询网络配置(`network.NetworkStatusRsp`)。 +- `POST /agent/v1/api/network/ignore`:忽略网络(`network.NetworkIgnoreReq`)。 +- `GET /agent/v1/api/cert/get`:获取 LAN 证书(`certificate.LanCert`)。 + +### DID +- `GET /agent/v1/api/did/document`:获取 DID 文档(`document.GetDocumentReq` -> `document.GetDocumentRsp`)。 +- `PUT /agent/v1/api/did/document/password`:更新 DID 文档密码(`password.UpdateDocumentPasswordReq` -> `dto.BaseRspStr`)。 +- `PUT /agent/v1/api/did/document/method`:更新 DID 验证方法(`method.UpdateDocumentMethodReq` -> `method.UpdateDocumentMethodRsp`)。 + +### 网关/透传 +- `POST /agent/v1/api/passthrough`:网关透传(Header `Request-Id` + `dto.LanInvokeReq` -> `string`)。 + +### 升级 / 系统 / 切换平台 +- `GET /agent/v1/api/upgrade/config`:获取升级配置(`upgrade.UpgradeConfig`)。 +- `POST /agent/v1/api/upgrade/config`:设置升级配置(`upgrade.UpgradeConfig`)。 +- `POST /agent/v1/api/upgrade/download`:下载升级包(`upgrade.StartDownRes` -> `upgrade.Task`)。 +- `POST /agent/v1/api/upgrade/install`:安装升级(`upgrade.StartUpgradeRes` -> `upgrade.Task`)。 +- `GET /agent/v1/api/upgrade/status`:查询升级任务状态(`upgrade.Task`)。 +- `POST /agent/v1/api/system/reboot`:重启系统(`dto.BaseRspStr`)。 +- `POST /agent/v1/api/system/shutdown`:关机(`dto.BaseRspStr`)。 +- `POST /agent/v1/api/switch`:切换平台(`switchplatform.SwitchPlatformReq` -> `switchplatform.SwitchPlatformResp`)。 +- `GET /agent/v1/api/switch/status`:查询切换状态(query `transId` -> `switchplatform.SwitchStatusQueryResp`)。 + +### 其他 +- `GET /agent/v1/api/space/ready/check`:空间就绪检查(`space.ReadyCheckRsp`)。 + +## 6. 本地开发、测试与部署说明 + +### 本地开发 +- Go 环境:`go 1.18+`(`Dockerfile` 构建使用 `go 1.20.6`)。 +- 前端:`web/boxdocker` 使用 `npm install && npm run build`(Dockerfile 中集成)。 +- 构建命令:`make -f Makefile` 或 `go build ...`。 +- Swagger 生成:`script/build.sh` 中执行 `swag init -g biz/web/http_server.go`。 + +### 测试 +- 项目包含部分单元测试(如 `biz/model/did/*_test.go`、`utils/jwt/*_test.go` 等)。 +- 常规运行:`go test ./...`(需保证依赖和环境齐备)。 + - 全量单测建议使用 `go test ./...`,但注意部分测试/代码路径依赖系统环境(如 Docker、dnf、nmcli、硬件信息/文件路径等),在 CI 或非目标设备上可能需要跳过或进行依赖注入替换。 + +### 部署 +- Docker 构建:`docker build -t local/space-agent:{tag} .`(见 `README.md`)。 +- 运行示例(端口 `5678` 对外暴露,`5680` 为内部端口): + - 参考 `README.md` 中 `docker run` 示例。 +- 健康检查:`GET http://localhost:5678/agent/status`(见 `Dockerfile` Healthcheck)。 + +## 7. 常见注意事项和扩展建议 + +- **调试模式**:`config.Config.DebugMode` 影响 Swagger 与日志接口暴露(见 `biz/web/routers/server.go` 与 README 中说明)。 +- **平台依赖与可选性**:平台依赖点与可选模式见 `docs/PLATFORM_DEPENDENCIES_CN.md` / `docs/PLATFORM_DEPENDENCIES.md`(默认 `PlatformEnabled=false`,仅用服务端+客户端即可运行)。 +- **单容器模式**:环境变量 `AOSPACE_SINGLE_DOCKER_MODE` 会改变启动流程(`main.go`)。 +- **内部 API 地址**:默认内部监听 `:5680` 或 `172.17.0.1:5680`,请结合 `config.Config.Web` 与部署方式使用。 +- **静态前端资源**:`web/boxdocker` 构建后打包进 `res/static_html.zip`,由服务内置提供。 +- **升级流程**:升级接口依赖后台任务与配置文件(`/etc/ao-space/upgrade/settings.json`)。 +- **测试日志路径**:测试进程默认把日志写入系统临时目录(可通过环境变量 `AOSPACE_LOG_DIR` 指定)。 +- **建议扩展**: + - 将 swagger 生成纳入 CI 或 Makefile 目标,确保接口文档与代码同步。 + - 为关键业务(绑定、升级、Docker 启动)增加更完善的端到端测试。 + - 对内部/外部 API 增加权限或鉴权策略(目前以配置与环境控制为主)。 diff --git a/docs/PROJECT_OVERVIEW_EN.md b/docs/PROJECT_OVERVIEW_EN.md new file mode 100644 index 0000000..2941a49 --- /dev/null +++ b/docs/PROJECT_OVERVIEW_EN.md @@ -0,0 +1,161 @@ +# space-agent Project Overview + +English | [简体中文](./PROJECT_OVERVIEW.md) + +> Generated from the current repository (main references: `README.md`, `main.go`, `biz/web/routers/*`, `docs/swagger.yaml`, `config/config.go`, `Dockerfile`, `Makefile`, `script/*`). + +## 1. Background and Purpose + +space-agent is the core entry service for AO.space (open-source) all-in-one deployment. It handles device initialization and binding, key exchange, microservice startup/management, DID generation/management, and system upgrades. It provides a unified entry point for the server and exposes HTTP APIs for client/gateway pairing and management. See `README.md` for details. + +## 2. System Architecture (Modules and Responsibilities) + +Overall structure: Go monolith + Docker microservice orchestration + web static assets. Main flow: + +- **Entry**: `main.go` + - Initialize config and logging + - Initialize device info/keys/client info + - Start Web API (external + internal) + - Start Docker microservices (or data migration) + - Start alive checker, upgrade checks, log dir monitor, DID/LevelDB +- **Web API**: `biz/web/*` + - Gin-based external/internal APIs, routes in `biz/web/routers/router.go` + - Swagger docs in `docs/` +- **Business services**: `biz/service/*` + - Pairing/binding/key exchange, DID docs, network config, system control, upgrade +- **Models & utils**: `biz/model/*`, `utils/*` + - Device info, DID storage, network/hardware/container utilities +- **Docker management**: `biz/docker/*` + `utils/docker/*` + `res/*` + - Compose templates and container lifecycle +- **Web static assets**: `web/boxdocker` + - Built and packed into `res/static_html.zip` + +## 3. Directory Structure (Key Paths) + +- `main.go`: service entrypoint. +- `config/`: config definitions and defaults (`config/config.go`). +- `biz/` + - `biz/web/`: HTTP API entry, routes, handlers. + - `biz/service/`: core business logic (bind/pair/upgrade/system/DID). + - `biz/docker/`: container orchestration and compose handling. + - `biz/model/`: business models and DTOs. + - `biz/alivechecker/`: container/network alive checks. + - `biz/disk_space_monitor/`: log dir/disk monitoring. + - `biz/notification/`: upgrade/ability change notifications. +- `utils/`: shared utilities (network, device ID, JWT, Docker, hardware, BLE, retry, etc.). +- `res/`: embedded resources (compose templates, static site zip, router config). +- `docs/`: Swagger docs (`docs/swagger.yaml`, `docs/swagger.json`). +- `web/boxdocker/`: front-end app (Vite + Vue). +- `script/`: build/run scripts. +- `Dockerfile`: multi-stage build for backend and frontend. +- `Makefile`: Go build settings. + +## 4. Key Modules and Core Logic + +- **Boot flow (`main.go`)** + - Init logs and version + - Init device info/keys and client info + - Start Web APIs (external :5678, internal :5680) + - Start Docker microservices or storage migration + - Start alive checker, upgrade checks, log dir monitor, DID LevelDB + +- **HTTP API (`biz/web/*`)** + - External routes: `/agent` and `/agent/v1/api/*` for client pairing/binding/network/DID. + - Internal routes: `/agent/v1/api/*` for internal/container/gateway calls (upgrade/device info). + +- **Binding/Pairing (`biz/service/pair`, `biz/service/bind`)** + - Key APIs: `initial`, `pairing`, `keyexchange`, `setpassword`, `bind/space/create`. + +- **Docker management (`biz/docker/*`, `utils/docker/*`, `res/*`)** + - Load embedded compose templates and start/maintain microservices. + +- **Upgrade & system control (`biz/service/upgrade`, `biz/service/system`)** + - Download/install/status APIs and reboot/shutdown. + +- **DID docs & certificates (`biz/service/did/*`, `biz/model/did/*`)** + - DID document generation/storage/update and LAN cert retrieval. + +- **Alive checker (`biz/alivechecker/*`)** + - Periodic container/network connectivity checks. + +## 5. API List (Path, Method, Params, Response) + +APIs are based on `docs/swagger.yaml` and route definitions. + +### Info +- `GET /agent/status`: service status (`dto.BaseRspStr` + `status.Status`). +- `GET /agent/info`: service info (`dto.BaseRspStr` + `status.Info`). +- `GET /agent/logs`: logs (debug mode only). + +### Pair / Bind / Keys +- `POST /agent/v1/api/initial`: init progress (`pair.PasswordInfo` -> `call.MicroServerRsp`). +- `POST /agent/v1/api/pairing`: pair client (`pair.PairingReq` -> `call.MicroServerRsp`). +- `POST /agent/v1/api/pubkeyexchange`: public key exchange. +- `POST /agent/v1/api/keyexchange`: symmetric key exchange. +- `POST /agent/v1/api/setpassword`: set admin password. +- `POST /agent/v1/api/bind/com/start`: start microservice containers. +- `GET /agent/v1/api/bind/com/progress`: startup progress. +- `GET /agent/v1/api/bind/init`: pre-bind init info. +- `POST /agent/v1/api/bind/space/create`: create space / finish binding. +- `POST /agent/v1/api/bind/password/verify`: verify admin password. +- `POST /agent/v1/api/bind/revoke`: unbind. +- `POST /agent/v1/api/admin/revoke`: admin unbind. +- `GET /agent/v1/api/pair/init`: wired binding init. +- `GET /agent/v1/api/pair/net/localips`: local IPs. +- `GET /agent/v1/api/pair/net/netconfig`: Wi-Fi list. +- `POST /agent/v1/api/bind/internet/service/config`: set Internet tunnel. +- `GET /agent/v1/api/bind/internet/service/config`: get Internet tunnel config. + +### Device / Network / Cert +- `GET /agent/v1/api/device/ability` +- `GET /agent/v1/api/device/info` +- `GET /agent/v1/api/device/version` +- `GET /agent/v1/api/device/localips` +- `GET /agent/v1/api/device/netconfig` +- `POST /agent/v1/api/network/config` +- `GET /agent/v1/api/network/config` +- `POST /agent/v1/api/network/ignore` +- `GET /agent/v1/api/cert/get` + +### DID +- `GET /agent/v1/api/did/document` +- `PUT /agent/v1/api/did/document/password` +- `PUT /agent/v1/api/did/document/method` + +### Gateway passthrough +- `POST /agent/v1/api/passthrough` + +### Upgrade / System / Switch platform +- `GET/POST /agent/v1/api/upgrade/*` +- `POST /agent/v1/api/system/reboot` +- `POST /agent/v1/api/system/shutdown` +- `POST /agent/v1/api/switch` +- `GET /agent/v1/api/switch/status` + +### Other +- `GET /agent/v1/api/space/ready/check` + +## 6. Local Dev, Tests, Deployment + +### Development +- Go: `1.18+` (Dockerfile uses `1.20.6`). +- Frontend: `web/boxdocker` (`npm install && npm run build`). +- Build: `make -f Makefile`. +- Swagger: `swag init -g biz/web/http_server.go`. + +### Tests +- Some unit tests exist (e.g., DID/JWT). Run `go test ./...` with environment readiness. +- Full test run may require Docker, nmcli, dnf, hardware files, etc. Consider skipping or injecting dependencies. + +### Deployment +- Docker build: `docker build -t local/space-agent:{tag} .` +- Health check: `GET http://localhost:5678/agent/status`. + +## 7. Notes and Extension Suggestions + +- **Platform optionality**: see `docs/PLATFORM_DEPENDENCIES.md` (default `PlatformEnabled=false` for server+client only). +- **Single-container mode**: `AOSPACE_SINGLE_DOCKER_MODE` affects startup. +- **Internal API address**: default internal :5680 or 172.17.0.1:5680. +- **Static assets**: built into `res/static_html.zip`. +- **Upgrade**: depends on upgrade settings in `/etc/ao-space/upgrade/settings.json`. +- **Test log path**: test processes write logs to the system temp directory by default (override with `AOSPACE_LOG_DIR`). diff --git a/docs/docs.go b/docs/docs.go index 366f191..f96c2e7 100644 --- a/docs/docs.go +++ b/docs/docs.go @@ -1,5 +1,4 @@ -// Package docs GENERATED BY SWAG; DO NOT EDIT -// This file was generated by swaggo/swag +// Package docs Code generated by swaggo/swag. DO NOT EDIT package docs import "github.com/swaggo/swag" @@ -238,13 +237,16 @@ const docTemplate = `{ "operationId": "BindInit", "parameters": [ { - "description": "query params", - "name": "initReq", - "in": "body", - "required": true, - "schema": { - "$ref": "#/definitions/bindinit.InitReq" - } + "type": "string", + "description": "client uuid", + "name": "clientUuid", + "in": "query" + }, + { + "type": "string", + "description": "client version", + "name": "clientVersion", + "in": "query" } ], "responses": { @@ -303,7 +305,7 @@ const docTemplate = `{ "type": "object", "properties": { "results": { - "$ref": "#/definitions/config.GetConfigRsp" + "$ref": "#/definitions/agent_biz_web_handler_bind_internet_service_config.GetConfigRsp" } } } @@ -332,7 +334,7 @@ const docTemplate = `{ "in": "body", "required": true, "schema": { - "$ref": "#/definitions/config.ConfigReq" + "$ref": "#/definitions/agent_biz_web_handler_bind_internet_service_config.ConfigReq" } } ], @@ -348,7 +350,7 @@ const docTemplate = `{ "type": "object", "properties": { "results": { - "$ref": "#/definitions/config.ConfigRsp" + "$ref": "#/definitions/agent_biz_web_handler_bind_internet_service_config.ConfigRsp" } } } @@ -1140,29 +1142,6 @@ const docTemplate = `{ } } }, - "/agent/v1/api/pair/tryout/code": { - "post": { - "consumes": [ - "text/plain" - ], - "produces": [ - "application/json" - ], - "tags": [ - "TryOut" - ], - "summary": "verify trial code [for web client LAN]", - "operationId": "TryOutCode", - "responses": { - "200": { - "description": "code=AG-200 success;", - "schema": { - "$ref": "#/definitions/dto.BaseRspStr" - } - } - } - } - }, "/agent/v1/api/pairing": { "post": { "consumes": [ @@ -1715,41 +1694,24 @@ const docTemplate = `{ } }, "definitions": { - "bindinit.InitReq": { - "type": "object", - "properties": { - "clientUuid": { - "type": "string" - }, - "clientVersion": { - "type": "string" - } - } - }, - "call.MicroServerRsp": { + "agent_biz_model_upgrade.VersionDownInfo": { "type": "object", "properties": { - "code": { - "type": "string" + "downloaded": { + "type": "boolean" }, - "message": { + "pkgPath": { "type": "string" }, - "requestId": { + "updateTime": { "type": "string" }, - "results": {} - } - }, - "certificate.LanCert": { - "type": "object", - "properties": { - "cert": { + "versionId": { "type": "string" } } }, - "config.ConfigReq": { + "agent_biz_web_handler_bind_internet_service_config.ConfigReq": { "type": "object", "properties": { "clientUUID": { @@ -1764,7 +1726,7 @@ const docTemplate = `{ } } }, - "config.ConfigRsp": { + "agent_biz_web_handler_bind_internet_service_config.ConfigRsp": { "type": "object", "properties": { "connectedNetwork": { @@ -1787,7 +1749,7 @@ const docTemplate = `{ } } }, - "config.GetConfigRsp": { + "agent_biz_web_handler_bind_internet_service_config.GetConfigRsp": { "type": "object", "properties": { "connectedNetwork": { @@ -1813,6 +1775,29 @@ const docTemplate = `{ } } }, + "call.MicroServerRsp": { + "type": "object", + "properties": { + "code": { + "type": "string" + }, + "message": { + "type": "string" + }, + "requestId": { + "type": "string" + }, + "results": {} + } + }, + "certificate.LanCert": { + "type": "object", + "properties": { + "cert": { + "type": "string" + } + } + }, "create.CreateReq": { "type": "object", "required": [ @@ -1892,7 +1877,11 @@ const docTemplate = `{ "properties": { "deviceAbility": { "description": "设备能力模型", - "$ref": "#/definitions/device_ability.DeviceAbility" + "allOf": [ + { + "$ref": "#/definitions/device_ability.DeviceAbility" + } + ] }, "deviceLogoUrl": { "description": "设备图片链接", @@ -2096,7 +2085,8 @@ const docTemplate = `{ "type": "object", "properties": { "created": { - "type": "integer" + "type": "integer", + "format": "int64" }, "hostConfig": { "type": "object", @@ -2398,7 +2388,11 @@ const docTemplate = `{ }, "deviceAbility": { "description": "设备能力模型", - "$ref": "#/definitions/device_ability.DeviceAbility" + "allOf": [ + { + "$ref": "#/definitions/device_ability.DeviceAbility" + } + ] }, "deviceLogoUrl": { "description": "设备图片链接", @@ -2733,10 +2727,6 @@ const docTemplate = `{ "description": "状态", "type": "string" }, - "tryoutCodeVerified": { - "description": "试用码是否验证通过(仅在 PC 试用场景下使用).", - "type": "boolean" - }, "version": { "description": "客户端版本", "type": "string" @@ -2823,14 +2813,18 @@ const docTemplate = `{ "type": "object", "properties": { "KernelImg": { - "$ref": "#/definitions/upgrade.VersionDownInfo" + "$ref": "#/definitions/agent_biz_model_upgrade.VersionDownInfo" }, "cFile": { "description": "docker-compose.yml", - "$ref": "#/definitions/upgrade.VersionDownInfo" + "allOf": [ + { + "$ref": "#/definitions/agent_biz_model_upgrade.VersionDownInfo" + } + ] }, "containerImg": { - "$ref": "#/definitions/upgrade.VersionDownInfo" + "$ref": "#/definitions/agent_biz_model_upgrade.VersionDownInfo" }, "doneDownTime": { "type": "string" @@ -2850,7 +2844,7 @@ const docTemplate = `{ "type": "boolean" }, "rpmPkg": { - "$ref": "#/definitions/upgrade.VersionDownInfo" + "$ref": "#/definitions/agent_biz_model_upgrade.VersionDownInfo" }, "startDownTime": { "type": "string" @@ -2877,23 +2871,6 @@ const docTemplate = `{ "type": "boolean" } } - }, - "upgrade.VersionDownInfo": { - "type": "object", - "properties": { - "downloaded": { - "type": "boolean" - }, - "pkgPath": { - "type": "string" - }, - "updateTime": { - "type": "string" - }, - "versionId": { - "type": "string" - } - } } } }` diff --git a/docs/swagger.json b/docs/swagger.json index b7582b3..44df647 100644 --- a/docs/swagger.json +++ b/docs/swagger.json @@ -226,13 +226,16 @@ "operationId": "BindInit", "parameters": [ { - "description": "query params", - "name": "initReq", - "in": "body", - "required": true, - "schema": { - "$ref": "#/definitions/bindinit.InitReq" - } + "type": "string", + "description": "client uuid", + "name": "clientUuid", + "in": "query" + }, + { + "type": "string", + "description": "client version", + "name": "clientVersion", + "in": "query" } ], "responses": { @@ -291,7 +294,7 @@ "type": "object", "properties": { "results": { - "$ref": "#/definitions/config.GetConfigRsp" + "$ref": "#/definitions/agent_biz_web_handler_bind_internet_service_config.GetConfigRsp" } } } @@ -320,7 +323,7 @@ "in": "body", "required": true, "schema": { - "$ref": "#/definitions/config.ConfigReq" + "$ref": "#/definitions/agent_biz_web_handler_bind_internet_service_config.ConfigReq" } } ], @@ -336,7 +339,7 @@ "type": "object", "properties": { "results": { - "$ref": "#/definitions/config.ConfigRsp" + "$ref": "#/definitions/agent_biz_web_handler_bind_internet_service_config.ConfigRsp" } } } @@ -1128,29 +1131,6 @@ } } }, - "/agent/v1/api/pair/tryout/code": { - "post": { - "consumes": [ - "text/plain" - ], - "produces": [ - "application/json" - ], - "tags": [ - "TryOut" - ], - "summary": "verify trial code [for web client LAN]", - "operationId": "TryOutCode", - "responses": { - "200": { - "description": "code=AG-200 success;", - "schema": { - "$ref": "#/definitions/dto.BaseRspStr" - } - } - } - } - }, "/agent/v1/api/pairing": { "post": { "consumes": [ @@ -1703,41 +1683,24 @@ } }, "definitions": { - "bindinit.InitReq": { - "type": "object", - "properties": { - "clientUuid": { - "type": "string" - }, - "clientVersion": { - "type": "string" - } - } - }, - "call.MicroServerRsp": { + "agent_biz_model_upgrade.VersionDownInfo": { "type": "object", "properties": { - "code": { - "type": "string" + "downloaded": { + "type": "boolean" }, - "message": { + "pkgPath": { "type": "string" }, - "requestId": { + "updateTime": { "type": "string" }, - "results": {} - } - }, - "certificate.LanCert": { - "type": "object", - "properties": { - "cert": { + "versionId": { "type": "string" } } }, - "config.ConfigReq": { + "agent_biz_web_handler_bind_internet_service_config.ConfigReq": { "type": "object", "properties": { "clientUUID": { @@ -1752,7 +1715,7 @@ } } }, - "config.ConfigRsp": { + "agent_biz_web_handler_bind_internet_service_config.ConfigRsp": { "type": "object", "properties": { "connectedNetwork": { @@ -1775,7 +1738,7 @@ } } }, - "config.GetConfigRsp": { + "agent_biz_web_handler_bind_internet_service_config.GetConfigRsp": { "type": "object", "properties": { "connectedNetwork": { @@ -1801,6 +1764,29 @@ } } }, + "call.MicroServerRsp": { + "type": "object", + "properties": { + "code": { + "type": "string" + }, + "message": { + "type": "string" + }, + "requestId": { + "type": "string" + }, + "results": {} + } + }, + "certificate.LanCert": { + "type": "object", + "properties": { + "cert": { + "type": "string" + } + } + }, "create.CreateReq": { "type": "object", "required": [ @@ -1880,7 +1866,11 @@ "properties": { "deviceAbility": { "description": "设备能力模型", - "$ref": "#/definitions/device_ability.DeviceAbility" + "allOf": [ + { + "$ref": "#/definitions/device_ability.DeviceAbility" + } + ] }, "deviceLogoUrl": { "description": "设备图片链接", @@ -2084,7 +2074,8 @@ "type": "object", "properties": { "created": { - "type": "integer" + "type": "integer", + "format": "int64" }, "hostConfig": { "type": "object", @@ -2386,7 +2377,11 @@ }, "deviceAbility": { "description": "设备能力模型", - "$ref": "#/definitions/device_ability.DeviceAbility" + "allOf": [ + { + "$ref": "#/definitions/device_ability.DeviceAbility" + } + ] }, "deviceLogoUrl": { "description": "设备图片链接", @@ -2721,10 +2716,6 @@ "description": "状态", "type": "string" }, - "tryoutCodeVerified": { - "description": "试用码是否验证通过(仅在 PC 试用场景下使用).", - "type": "boolean" - }, "version": { "description": "客户端版本", "type": "string" @@ -2811,14 +2802,18 @@ "type": "object", "properties": { "KernelImg": { - "$ref": "#/definitions/upgrade.VersionDownInfo" + "$ref": "#/definitions/agent_biz_model_upgrade.VersionDownInfo" }, "cFile": { "description": "docker-compose.yml", - "$ref": "#/definitions/upgrade.VersionDownInfo" + "allOf": [ + { + "$ref": "#/definitions/agent_biz_model_upgrade.VersionDownInfo" + } + ] }, "containerImg": { - "$ref": "#/definitions/upgrade.VersionDownInfo" + "$ref": "#/definitions/agent_biz_model_upgrade.VersionDownInfo" }, "doneDownTime": { "type": "string" @@ -2838,7 +2833,7 @@ "type": "boolean" }, "rpmPkg": { - "$ref": "#/definitions/upgrade.VersionDownInfo" + "$ref": "#/definitions/agent_biz_model_upgrade.VersionDownInfo" }, "startDownTime": { "type": "string" @@ -2865,23 +2860,6 @@ "type": "boolean" } } - }, - "upgrade.VersionDownInfo": { - "type": "object", - "properties": { - "downloaded": { - "type": "boolean" - }, - "pkgPath": { - "type": "string" - }, - "updateTime": { - "type": "string" - }, - "versionId": { - "type": "string" - } - } } } } \ No newline at end of file diff --git a/docs/swagger.yaml b/docs/swagger.yaml index af657c9..a2281e3 100644 --- a/docs/swagger.yaml +++ b/docs/swagger.yaml @@ -1,27 +1,16 @@ definitions: - bindinit.InitReq: + agent_biz_model_upgrade.VersionDownInfo: properties: - clientUuid: - type: string - clientVersion: - type: string - type: object - call.MicroServerRsp: - properties: - code: - type: string - message: + downloaded: + type: boolean + pkgPath: type: string - requestId: + updateTime: type: string - results: {} - type: object - certificate.LanCert: - properties: - cert: + versionId: type: string type: object - config.ConfigReq: + agent_biz_web_handler_bind_internet_service_config.ConfigReq: properties: clientUUID: type: string @@ -31,7 +20,7 @@ definitions: description: Platform api url setting, e.g. "https://ao.space"` type: string type: object - config.ConfigRsp: + agent_biz_web_handler_bind_internet_service_config.ConfigRsp: properties: connectedNetwork: items: @@ -46,7 +35,7 @@ definitions: userDomain: type: string type: object - config.GetConfigRsp: + agent_biz_web_handler_bind_internet_service_config.GetConfigRsp: properties: connectedNetwork: items: @@ -63,6 +52,21 @@ definitions: userDomain: type: string type: object + call.MicroServerRsp: + properties: + code: + type: string + message: + type: string + requestId: + type: string + results: {} + type: object + certificate.LanCert: + properties: + cert: + type: string + type: object create.CreateReq: properties: clientPhoneModel: @@ -120,7 +124,8 @@ definitions: device.BoxDeviceVersion: properties: deviceAbility: - $ref: '#/definitions/device_ability.DeviceAbility' + allOf: + - $ref: '#/definitions/device_ability.DeviceAbility' description: 设备能力模型 deviceLogoUrl: description: 设备图片链接 @@ -267,6 +272,7 @@ definitions: dockermodel.DockerContainer: properties: created: + format: int64 type: integer hostConfig: additionalProperties: @@ -476,7 +482,8 @@ definitions: connected: type: integer deviceAbility: - $ref: '#/definitions/device_ability.DeviceAbility' + allOf: + - $ref: '#/definitions/device_ability.DeviceAbility' description: 设备能力模型 deviceLogoUrl: description: 设备图片链接 @@ -709,9 +716,6 @@ definitions: status: description: 状态 type: string - tryoutCodeVerified: - description: 试用码是否验证通过(仅在 PC 试用场景下使用). - type: boolean version: description: 客户端版本 type: string @@ -771,12 +775,13 @@ definitions: upgrade.Task: properties: KernelImg: - $ref: '#/definitions/upgrade.VersionDownInfo' + $ref: '#/definitions/agent_biz_model_upgrade.VersionDownInfo' cFile: - $ref: '#/definitions/upgrade.VersionDownInfo' + allOf: + - $ref: '#/definitions/agent_biz_model_upgrade.VersionDownInfo' description: docker-compose.yml containerImg: - $ref: '#/definitions/upgrade.VersionDownInfo' + $ref: '#/definitions/agent_biz_model_upgrade.VersionDownInfo' doneDownTime: type: string doneInstallTime: @@ -790,7 +795,7 @@ definitions: reboot: type: boolean rpmPkg: - $ref: '#/definitions/upgrade.VersionDownInfo' + $ref: '#/definitions/agent_biz_model_upgrade.VersionDownInfo' startDownTime: type: string startInstallTime: @@ -808,17 +813,6 @@ definitions: autoInstall: type: boolean type: object - upgrade.VersionDownInfo: - properties: - downloaded: - type: boolean - pkgPath: - type: string - updateTime: - type: string - versionId: - type: string - type: object info: contact: email: wenchao@iscas.ac.cn @@ -957,12 +951,14 @@ paths: description: get aospace server base info. operationId: BindInit parameters: - - description: query params - in: body - name: initReq - required: true - schema: - $ref: '#/definitions/bindinit.InitReq' + - description: client uuid + in: query + name: clientUuid + type: string + - description: client version + in: query + name: clientVersion + type: string produces: - application/json responses: @@ -999,7 +995,7 @@ paths: - $ref: '#/definitions/dto.BaseRspStr' - properties: results: - $ref: '#/definitions/config.GetConfigRsp' + $ref: '#/definitions/agent_biz_web_handler_bind_internet_service_config.GetConfigRsp' type: object summary: get internet tunnel config [for client/gateway bluetooth/LAN] tags: @@ -1018,7 +1014,7 @@ paths: name: configReq required: true schema: - $ref: '#/definitions/config.ConfigReq' + $ref: '#/definitions/agent_biz_web_handler_bind_internet_service_config.ConfigReq' produces: - application/json responses: @@ -1029,7 +1025,7 @@ paths: - $ref: '#/definitions/dto.BaseRspStr' - properties: results: - $ref: '#/definitions/config.ConfigRsp' + $ref: '#/definitions/agent_biz_web_handler_bind_internet_service_config.ConfigRsp' type: object summary: config internet tunnel [for client/gateway bluetooth/LAN] tags: @@ -1506,21 +1502,6 @@ paths: summary: get Wi-Fi list [for client] tags: - net - /agent/v1/api/pair/tryout/code: - post: - consumes: - - text/plain - operationId: TryOutCode - produces: - - application/json - responses: - "200": - description: code=AG-200 success; - schema: - $ref: '#/definitions/dto.BaseRspStr' - summary: verify trial code [for web client LAN] - tags: - - TryOut /agent/v1/api/pairing: post: consumes: diff --git a/main.go b/main.go index 1b82f70..d7520b8 100644 --- a/main.go +++ b/main.go @@ -69,7 +69,7 @@ func main() { device.InitDeviceKey() clientinfo.InitClientInfo() - if !strings.EqualFold(os.Getenv(config.Config.Box.RunInDocker.AoSpaceSingleDockerModeEnv), "true") { + if !strings.EqualFold(os.Getenv(config.Config.Box.RunInDocker.AoSpaceSingleDockerModeEnv), "true") && config.Config.PlatformEnabled { go platform.InitPlatformAbility() serviceswithplatform.RetryUnfinishedStatus() upgrade.CronForUpgrade() @@ -87,7 +87,7 @@ func main() { alivechecker.Start() // 检测是否需要发送升级推送 - if !strings.EqualFold(os.Getenv(config.Config.Box.RunInDocker.AoSpaceSingleDockerModeEnv), "true") { + if !strings.EqualFold(os.Getenv(config.Config.Box.RunInDocker.AoSpaceSingleDockerModeEnv), "true") && config.Config.PlatformEnabled { go upgrade.CheckUpgradeSucc() } diff --git a/res/static_html.zip b/res/static_html.zip index 58d788d..e42e2b1 100644 Binary files a/res/static_html.zip and b/res/static_html.zip differ diff --git a/utils/ble/server.go b/utils/ble/server.go index 046a381..368a9e6 100644 --- a/utils/ble/server.go +++ b/utils/ble/server.go @@ -20,8 +20,6 @@ * @LastEditTime: 2021-11-22 11:22:55 * @Description: */ -// +build - package ble import ( diff --git a/utils/docker/dockerfacade/dockerapi_test.go b/utils/docker/dockerfacade/dockerapi_test.go index f69e19f..0d92752 100644 --- a/utils/docker/dockerfacade/dockerapi_test.go +++ b/utils/docker/dockerfacade/dockerapi_test.go @@ -15,12 +15,22 @@ package dockerfacade import ( + "os" + "os/exec" "testing" ) func TestUpContainersWithSample(t *testing.T) { + if os.Getenv("ENABLE_DOCKER_TESTS") != "1" { + // TODO: Provide a Docker API stub or harness so this test can run in CI without Docker. + t.Skip("ENABLE_DOCKER_TESTS is not set") + } + if _, err := exec.LookPath("docker-compose"); err != nil { + // TODO: Replace docker-compose dependency with a mock to make this deterministic. + t.Skip("docker-compose not found") + } d := DockerFacade{} - _, stdErr, err := d.UpContainers("./test-docker-compose.yml") + _, stdErr, err := d.UpContainers("./test-docker-compose.yml", nil) if err != nil { t.Fatal("exec err: ", err) } else if len(stdErr) != 0 { @@ -29,9 +39,17 @@ func TestUpContainersWithSample(t *testing.T) { } func TestUpContainers(t *testing.T) { + if os.Getenv("ENABLE_DOCKER_TESTS") != "1" { + // TODO: Provide a Docker API stub or harness so this test can run in CI without Docker. + t.Skip("ENABLE_DOCKER_TESTS is not set") + } + if _, err := exec.LookPath("docker-compose"); err != nil { + // TODO: Replace docker-compose dependency with a mock to make this deterministic. + t.Skip("docker-compose not found") + } d := DockerFacade{} - _, stdErr, err := d.UpContainers("../../../res/docker-compose.yml") + _, stdErr, err := d.UpContainers("../../../res/docker-compose.yml", nil) if err != nil { t.Fatal("exec err: ", err) } else if len(stdErr) != 0 { diff --git a/utils/jwt/jwt_test.go b/utils/jwt/jwt_test.go index 81290ed..450fd3f 100644 --- a/utils/jwt/jwt_test.go +++ b/utils/jwt/jwt_test.go @@ -19,6 +19,7 @@ import ( "crypto/x509" "encoding/pem" "fmt" + "os" "testing" "time" @@ -97,6 +98,10 @@ func TestJWT(t *testing.T) { } func TestJWTCustom(t *testing.T) { + if os.Getenv("ENABLE_SECURITY_CHIP_TESTS") != "1" { + // TODO: Add a mock signer to run this test without a security-chip service. + t.Skip("ENABLE_SECURITY_CHIP_TESTS is not set") + } expiredAt := time.Now().Add(24 * time.Hour * 365 * 3) boxUuid := random.GenUUID() diff --git a/utils/logger/logger.go b/utils/logger/logger.go index ed8f5c9..c413306 100644 --- a/utils/logger/logger.go +++ b/utils/logger/logger.go @@ -15,6 +15,10 @@ package logger import ( + "os" + "path/filepath" + "strings" + zaplogger "agent/deps/logger" "go.uber.org/zap" @@ -38,6 +42,43 @@ func SetLogPath(p string) { path = p } +func init() { + path = initLogPath() + zaplogger.SetDefaultLoggerPath(path) +} + +func initLogPath() string { + if envPath := os.Getenv("AOSPACE_LOG_DIR"); envPath != "" { + return ensureTrailingSlash(envPath) + } + if isTestProcess() { + return ensureTrailingSlash(filepath.Join(os.TempDir(), "aospace-agent-test")) + } + return path +} + +func isTestProcess() bool { + for _, arg := range os.Args { + if strings.HasPrefix(arg, "-test.") || strings.HasPrefix(arg, "-test=") { + return true + } + } + if len(os.Args) > 0 && strings.HasSuffix(os.Args[0], ".test") { + return true + } + return false +} + +func ensureTrailingSlash(p string) string { + if p == "" { + return p + } + if strings.HasSuffix(p, string(os.PathSeparator)) { + return p + } + return p + string(os.PathSeparator) +} + func SetLogConfig(MaxSize, MaxBackups, MaxAge int, Compress bool) { zaplogger.SetLogConfig(&zaplogger.LogConfig{MaxSize: MaxSize, MaxBackups: MaxBackups, diff --git a/utils/network/mask_test.go b/utils/network/mask_test.go new file mode 100644 index 0000000..776ad47 --- /dev/null +++ b/utils/network/mask_test.go @@ -0,0 +1,42 @@ +package network + +import "testing" + +func TestSubNetMaskToLen(t *testing.T) { + cases := map[string]int{ + "255.255.255.0": 24, + "255.255.0.0": 16, + "255.255.255.255": 32, + "0.0.0.0": 0, + } + for mask, want := range cases { + got, err := SubNetMaskToLen(mask) + if err != nil { + t.Fatalf("unexpected error for %s: %v", mask, err) + } + if got != want { + t.Fatalf("mask %s: expected %d, got %d", mask, want, got) + } + } +} + +func TestSubNetMaskToLenInvalid(t *testing.T) { + if _, err := SubNetMaskToLen("255.255.255"); err == nil { + t.Fatalf("expected error for invalid mask") + } + if _, err := SubNetMaskToLen("256.0.0.0"); err == nil { + t.Fatalf("expected error for out-of-range mask") + } +} + +func TestLenToSubNetMask(t *testing.T) { + if got := LenToSubNetMask(24); got != "255.255.255.0" { + t.Fatalf("expected 255.255.255.0, got %s", got) + } + if got := LenToSubNetMask(0); got != "0.0.0.0" { + t.Fatalf("expected 0.0.0.0, got %s", got) + } + if got := LenToSubNetMask(32); got != "255.255.255.255" { + t.Fatalf("expected 255.255.255.255, got %s", got) + } +} diff --git a/utils/tools/array.go b/utils/tools/array.go index 2b83828..4a5408d 100644 --- a/utils/tools/array.go +++ b/utils/tools/array.go @@ -25,11 +25,13 @@ func ArrayEqual(a, b []string) bool { if len(a) != len(b) { return false } - sort.Strings(a) - sort.Strings(b) + aCopy := append([]string(nil), a...) + bCopy := append([]string(nil), b...) + sort.Strings(aCopy) + sort.Strings(bCopy) - for i := range a { - if a[i] != b[i] { + for i := range aCopy { + if aCopy[i] != bCopy[i] { return false } } diff --git a/utils/tools/array_test.go b/utils/tools/array_test.go new file mode 100644 index 0000000..3629ce6 --- /dev/null +++ b/utils/tools/array_test.go @@ -0,0 +1,25 @@ +package tools + +import "testing" + +func TestArrayEqual(t *testing.T) { + a := []string{"b", "a"} + b := []string{"a", "b"} + if !ArrayEqual(a, b) { + t.Fatalf("expected arrays to be equal") + } + // ensure inputs are not mutated + if a[0] != "b" || a[1] != "a" { + t.Fatalf("input slice mutated: %+v", a) + } +} + +func TestArrayContains(t *testing.T) { + arr := []string{"x", "y"} + if !ArrayContains(arr, "x") { + t.Fatalf("expected to find element") + } + if ArrayContains(arr, "z") { + t.Fatalf("did not expect to find element") + } +} diff --git a/utils/tools/string_test.go b/utils/tools/string_test.go new file mode 100644 index 0000000..40be0d2 --- /dev/null +++ b/utils/tools/string_test.go @@ -0,0 +1,14 @@ +package tools + +import "testing" + +func TestStringToLines(t *testing.T) { + input := "a\r\nb\rc\n" + lines := StringToLines(input) + if len(lines) != 4 { + t.Fatalf("unexpected line count: %d", len(lines)) + } + if lines[0] != "a" || lines[1] != "b" || lines[2] != "c" || lines[3] != "" { + t.Fatalf("unexpected lines: %+v", lines) + } +} diff --git a/utils/version/rpm_test.go b/utils/version/rpm_test.go index 68d2de5..ac0810e 100644 --- a/utils/version/rpm_test.go +++ b/utils/version/rpm_test.go @@ -16,10 +16,16 @@ package version import ( "fmt" + "os/exec" "testing" ) func TestGetInstalledAgentVersion(t *testing.T) { + if _, err := exec.LookPath("dnf"); err != nil { + // TODO: Refactor GetInstalledAgentVersion to allow injecting a command runner so this test + // can run in CI without requiring dnf. + t.Skip("dnf not available") + } version, err := GetInstalledAgentVersion() if err != nil { t.Fatal(err) diff --git a/web/boxdocker/src/api/axios.ts b/web/boxdocker/src/api/axios.ts index a775764..c3a3ff5 100644 --- a/web/boxdocker/src/api/axios.ts +++ b/web/boxdocker/src/api/axios.ts @@ -28,8 +28,3 @@ axios.defaults.transformRequest = function (data) { export function getAgentInfo() { return axios.get("/agent/info"); } - -export function validateCode(tryoutCode,email) { - return axios.post("/agent/v1/api/pair/tryout/code",{tryoutCode,email}); -} - diff --git a/web/boxdocker/src/assets/svg/ts.svg b/web/boxdocker/src/assets/svg/ts.svg index 3286a2e..cb440cc 100644 --- a/web/boxdocker/src/assets/svg/ts.svg +++ b/web/boxdocker/src/assets/svg/ts.svg @@ -18,10 +18,10 @@ ts - + - \ No newline at end of file + diff --git a/web/boxdocker/src/pages/index.vue b/web/boxdocker/src/pages/index.vue deleted file mode 100644 index b1fa81d..0000000 --- a/web/boxdocker/src/pages/index.vue +++ /dev/null @@ -1,227 +0,0 @@ - - - - - - - diff --git a/web/boxdocker/src/router/index.js b/web/boxdocker/src/router/index.js index 2caf8f6..e38010a 100644 --- a/web/boxdocker/src/router/index.js +++ b/web/boxdocker/src/router/index.js @@ -20,12 +20,7 @@ import {createRouter, createWebHashHistory} from 'vue-router' const routes = [ { path: '/', - redirect: '/index', - }, - { - path: '/index', - name: 'index', - component: () => import('@/pages/index.vue'), + redirect: '/code', }, { path: '/code',