diff --git a/.github/workflows/backend-ci-cd.yml b/.github/workflows/backend-ci-cd.yml index 178bb93..117a84e 100644 --- a/.github/workflows/backend-ci-cd.yml +++ b/.github/workflows/backend-ci-cd.yml @@ -87,6 +87,18 @@ jobs: VERSION=${GITHUB_REF#refs/tags/} echo "VERSION=${VERSION}" >> $GITHUB_OUTPUT + - name: Get build time + id: get_build_time + run: | + BUILD_TIME=$(date -u +'%Y-%m-%dT%H:%M:%SZ') + echo "BUILD_TIME=${BUILD_TIME}" >> $GITHUB_OUTPUT + + - name: Get git commit + id: get_git_commit + run: | + GIT_COMMIT=$(git rev-parse HEAD) + echo "GIT_COMMIT=${GIT_COMMIT}" >> $GITHUB_OUTPUT + - name: Set up QEMU uses: docker/setup-qemu-action@v3 @@ -114,5 +126,8 @@ jobs: GOCACHE=/tmp/go-build GOMODCACHE=/tmp/go-mod REPO_COMMIT=${{ github.sha }} + VERSION=${{ steps.get_version.outputs.VERSION }} + BUILD_TIME=${{ steps.get_build_time.outputs.BUILD_TIME }} + GIT_COMMIT=${{ steps.get_git_commit.outputs.GIT_COMMIT }} cache-from: type=gha cache-to: type=gha,mode=max \ No newline at end of file diff --git a/backend/Makefile b/backend/Makefile index 1ad0385..cfe0e7d 100644 --- a/backend/Makefile +++ b/backend/Makefile @@ -4,6 +4,9 @@ OUTPUT=type=docker,dest=${HOME}/tmp/monkeycode_server.tar GOCACHE=${HOME}/.cache/go-build GOMODCACHE=${HOME}/go/pkg/mod REGISTRY=monkeycode.chaitin.cn/monkeycode +VERSION=dev-${shell git rev-parse HEAD} +BUILD_TIME=${shell date -u +"%Y-%m-%dT%H:%M:%SZ"} +GIT_COMMIT=${shell git rev-parse HEAD} # make build PLATFORM= TAG= OUTPUT= GOCACHE= image: @@ -12,6 +15,9 @@ image: --build-arg GOCACHE=${GOCACHE} \ --build-arg GOMODCACHE=${GOMODCACHE} \ --build-arg REPO_COMMIT=$(shell git rev-parse HEAD) \ + --build-arg VERSION=${VERSION} \ + --build-arg BUILD_TIME=${BUILD_TIME} \ + --build-arg GIT_COMMIT=${GIT_COMMIT} \ --platform ${PLATFORM} \ --tag ${REGISTRY}/backend:${TAG} \ --output ${OUTPUT} \ diff --git a/backend/build/Dockerfile b/backend/build/Dockerfile index ac9f8a8..050865c 100644 --- a/backend/build/Dockerfile +++ b/backend/build/Dockerfile @@ -9,10 +9,17 @@ RUN --mount=type=cache,target=${GOMODCACHE} \ go mod download ARG TARGETOS TARGETARCH GOCACHE +ARG VERSION +ARG BUILD_TIME +ARG GIT_COMMIT RUN --mount=type=bind,target=. \ --mount=type=cache,target=${GOMODCACHE} \ --mount=type=cache,target=${GOCACHE} \ -GOOS=$TARGETOS GOARCH=$TARGETARCH go build -o /out/main cmd/server/main.go cmd/server/wire_gen.go +GOOS=$TARGETOS GOARCH=$TARGETARCH \ +go build \ +-ldflags "-w -s -X 'github.com/chaitin/MonkeyCode/backend/pkg/version.Version=${VERSION}' -X 'github.com/chaitin/MonkeyCode/backend/pkg/version.BuildTime=${BUILD_TIME}' -X 'github.com/chaitin/MonkeyCode/backend/pkg/version.GitCommit=${GIT_COMMIT}'" \ +-o /out/main \ +cmd/server/main.go cmd/server/wire_gen.go FROM alpine:3.21 as binary diff --git a/backend/cmd/server/main.go b/backend/cmd/server/main.go index c8fbf60..ee5bb7b 100644 --- a/backend/cmd/server/main.go +++ b/backend/cmd/server/main.go @@ -24,6 +24,7 @@ func main() { panic(err) } + s.version.Print() s.logger.With("config", s.config).Debug("config") if s.config.Debug { @@ -40,6 +41,10 @@ func main() { panic(err) } + if err := s.report.ReportInstallation(); err != nil { + panic(err) + } + svc := service.NewService(service.WithPprof()) svc.Add(s) if err := svc.Run(); err != nil { diff --git a/backend/cmd/server/wire.go b/backend/cmd/server/wire.go index 723c958..35c7685 100644 --- a/backend/cmd/server/wire.go +++ b/backend/cmd/server/wire.go @@ -17,6 +17,8 @@ import ( v1 "github.com/chaitin/MonkeyCode/backend/internal/model/handler/http/v1" openaiV1 "github.com/chaitin/MonkeyCode/backend/internal/openai/handler/v1" userV1 "github.com/chaitin/MonkeyCode/backend/internal/user/handler/v1" + "github.com/chaitin/MonkeyCode/backend/pkg/report" + "github.com/chaitin/MonkeyCode/backend/pkg/version" ) type Server struct { @@ -29,6 +31,8 @@ type Server struct { userV1 *userV1.UserHandler dashboardV1 *dashv1.DashboardHandler billingV1 *billingv1.BillingHandler + version *version.VersionInfo + report *report.Reporter } func newServer() (*Server, error) { diff --git a/backend/cmd/server/wire_gen.go b/backend/cmd/server/wire_gen.go index ab0016a..49a410c 100644 --- a/backend/cmd/server/wire_gen.go +++ b/backend/cmd/server/wire_gen.go @@ -34,8 +34,10 @@ import ( "github.com/chaitin/MonkeyCode/backend/pkg" "github.com/chaitin/MonkeyCode/backend/pkg/ipdb" "github.com/chaitin/MonkeyCode/backend/pkg/logger" + "github.com/chaitin/MonkeyCode/backend/pkg/report" "github.com/chaitin/MonkeyCode/backend/pkg/session" "github.com/chaitin/MonkeyCode/backend/pkg/store" + "github.com/chaitin/MonkeyCode/backend/pkg/version" "log/slog" ) @@ -82,6 +84,8 @@ func newServer() (*Server, error) { billingRepo := repo7.NewBillingRepo(client) billingUsecase := usecase6.NewBillingUsecase(billingRepo) billingHandler := v1_5.NewBillingHandler(web, billingUsecase, authMiddleware, activeMiddleware) + versionInfo := version.NewVersionInfo() + reporter := report.NewReport(slogLogger, configConfig, versionInfo) server := &Server{ config: configConfig, web: web, @@ -92,6 +96,8 @@ func newServer() (*Server, error) { userV1: userHandler, dashboardV1: dashboardHandler, billingV1: billingHandler, + version: versionInfo, + report: reporter, } return server, nil } @@ -108,4 +114,6 @@ type Server struct { userV1 *v1_3.UserHandler dashboardV1 *v1_4.DashboardHandler billingV1 *v1_5.BillingHandler + version *version.VersionInfo + report *report.Reporter } diff --git a/backend/config/config.go b/backend/config/config.go index fec89bf..4bab73e 100644 --- a/backend/config/config.go +++ b/backend/config/config.go @@ -67,6 +67,10 @@ type Config struct { LimitSecond int `mapstructure:"limit_second"` Limit int `mapstructure:"limit"` } `mapstructure:"extension"` + + DataReport struct { + Key string `mapstructure:"key"` + } `mapstructure:"data_report"` } func Init() (*Config, error) { @@ -103,6 +107,7 @@ func Init() (*Config, error) { v.SetDefault("extension.baseurl", "https://release.baizhi.cloud") v.SetDefault("extension.limit", 1) v.SetDefault("extension.limit_second", 10) + v.SetDefault("data_report.key", "") c := Config{} if err := v.Unmarshal(&c); err != nil { diff --git a/backend/internal/provider.go b/backend/internal/provider.go index e523709..299aa3e 100644 --- a/backend/internal/provider.go +++ b/backend/internal/provider.go @@ -24,6 +24,7 @@ import ( userV1 "github.com/chaitin/MonkeyCode/backend/internal/user/handler/v1" userrepo "github.com/chaitin/MonkeyCode/backend/internal/user/repo" userusecase "github.com/chaitin/MonkeyCode/backend/internal/user/usecase" + "github.com/chaitin/MonkeyCode/backend/pkg/version" ) var Provider = wire.NewSet( @@ -50,4 +51,5 @@ var Provider = wire.NewSet( billingusecase.NewBillingUsecase, erepo.NewExtensionRepo, eusecase.NewExtensionUsecase, + version.NewVersionInfo, ) diff --git a/backend/pkg/aes/aes.go b/backend/pkg/aes/aes.go new file mode 100644 index 0000000..76255d4 --- /dev/null +++ b/backend/pkg/aes/aes.go @@ -0,0 +1,60 @@ +package aes + +import ( + "crypto/aes" + "crypto/cipher" + "crypto/rand" + "encoding/base64" + "errors" +) + +func Encrypt(key []byte, plaintext string) (string, error) { + block, err := aes.NewCipher(key) + if err != nil { + return "", err + } + + gcm, err := cipher.NewGCM(block) + if err != nil { + return "", err + } + + nonce := make([]byte, gcm.NonceSize()) + if _, err := rand.Read(nonce); err != nil { + return "", err + } + + ciphertext := gcm.Seal(nonce, nonce, []byte(plaintext), nil) + + return base64.StdEncoding.EncodeToString(ciphertext), nil +} + +func Decrypt(key []byte, ciphertext string) (string, error) { + data, err := base64.StdEncoding.DecodeString(ciphertext) + if err != nil { + return "", err + } + + block, err := aes.NewCipher(key) + if err != nil { + return "", err + } + + gcm, err := cipher.NewGCM(block) + if err != nil { + return "", err + } + + nonceSize := gcm.NonceSize() + if len(data) < nonceSize { + return "", errors.New("ciphertext too short") + } + + nonce, text := data[:nonceSize], data[nonceSize:] + plaintext, err := gcm.Open(nil, nonce, text, nil) + if err != nil { + return "", err + } + + return string(plaintext), nil +} diff --git a/backend/pkg/machine/machine.go b/backend/pkg/machine/machine.go new file mode 100644 index 0000000..98e8e6a --- /dev/null +++ b/backend/pkg/machine/machine.go @@ -0,0 +1,102 @@ +package machine + +import ( + "crypto/md5" + "crypto/sha256" + "fmt" + "net" + "os" + "runtime" + "sort" + "strings" +) + +type MachineInfo struct { + Hostname string `json:"hostname"` + MACAddresses []string `json:"mac_addresses"` + OSType string `json:"os_type"` + OSRelease string `json:"os_release"` + CPUInfo string `json:"cpu_info"` +} + +func GetMachineInfo() (*MachineInfo, error) { + hostname, err := os.Hostname() + if err != nil { + return nil, fmt.Errorf("failed to get hostname: %w", err) + } + + macAddresses, err := getMACAddresses() + if err != nil { + return nil, fmt.Errorf("failed to get MAC addresses: %w", err) + } + + return &MachineInfo{ + Hostname: hostname, + MACAddresses: macAddresses, + OSType: runtime.GOOS, + OSRelease: getOSRelease(), + CPUInfo: getCPUInfo(), + }, nil +} + +func GenerateMachineID() (string, error) { + machineInfo, err := GetMachineInfo() + if err != nil { + return "", err + } + + var parts []string + parts = append(parts, machineInfo.Hostname) + parts = append(parts, strings.Join(machineInfo.MACAddresses, ",")) + parts = append(parts, machineInfo.OSType) + parts = append(parts, machineInfo.OSRelease) + parts = append(parts, machineInfo.CPUInfo) + + combined := strings.Join(parts, "|") + hash := sha256.Sum256([]byte(combined)) + return fmt.Sprintf("%x", hash), nil +} + +func GenerateShortMachineID() (string, error) { + machineInfo, err := GetMachineInfo() + if err != nil { + return "", err + } + + var parts []string + parts = append(parts, machineInfo.Hostname) + if len(machineInfo.MACAddresses) > 0 { + parts = append(parts, machineInfo.MACAddresses[0]) + } + parts = append(parts, machineInfo.OSType) + + combined := strings.Join(parts, "|") + hash := md5.Sum([]byte(combined)) + return fmt.Sprintf("%x", hash), nil +} + +func getMACAddresses() ([]string, error) { + interfaces, err := net.Interfaces() + if err != nil { + return nil, err + } + + var macAddresses []string + for _, iface := range interfaces { + if iface.Flags&net.FlagLoopback != 0 || len(iface.HardwareAddr) == 0 { + continue + } + macAddresses = append(macAddresses, iface.HardwareAddr.String()) + } + + sort.Strings(macAddresses) + return macAddresses, nil +} + +func getOSRelease() string { + return runtime.GOARCH +} + +func getCPUInfo() string { + return fmt.Sprintf("%s_%d", runtime.GOARCH, runtime.NumCPU()) +} diff --git a/backend/pkg/machine/machine_test.go b/backend/pkg/machine/machine_test.go new file mode 100644 index 0000000..b07ff69 --- /dev/null +++ b/backend/pkg/machine/machine_test.go @@ -0,0 +1,13 @@ +package machine + +import ( + "testing" +) + +func TestGenerateShortMachineID(t *testing.T) { + machineID, err := GenerateMachineID() + if err != nil { + t.Fatal(err) + } + t.Log(machineID) +} diff --git a/backend/pkg/provider.go b/backend/pkg/provider.go index 744396c..97614ac 100644 --- a/backend/pkg/provider.go +++ b/backend/pkg/provider.go @@ -13,6 +13,7 @@ import ( mid "github.com/chaitin/MonkeyCode/backend/internal/middleware" "github.com/chaitin/MonkeyCode/backend/pkg/ipdb" "github.com/chaitin/MonkeyCode/backend/pkg/logger" + "github.com/chaitin/MonkeyCode/backend/pkg/report" "github.com/chaitin/MonkeyCode/backend/pkg/session" "github.com/chaitin/MonkeyCode/backend/pkg/store" ) @@ -24,6 +25,7 @@ var Provider = wire.NewSet( store.NewRedisCli, session.NewSession, ipdb.NewIPDB, + report.NewReport, ) func NewWeb(cfg *config.Config) *web.Web { diff --git a/backend/pkg/report/report.go b/backend/pkg/report/report.go new file mode 100644 index 0000000..a67f5ca --- /dev/null +++ b/backend/pkg/report/report.go @@ -0,0 +1,111 @@ +package report + +import ( + "encoding/json" + "log/slog" + "net/url" + "os" + "time" + + "github.com/google/uuid" + + "github.com/chaitin/MonkeyCode/backend/config" + "github.com/chaitin/MonkeyCode/backend/pkg/aes" + "github.com/chaitin/MonkeyCode/backend/pkg/machine" + "github.com/chaitin/MonkeyCode/backend/pkg/request" + "github.com/chaitin/MonkeyCode/backend/pkg/version" +) + +type Reporter struct { + client *request.Client + version *version.VersionInfo + logger *slog.Logger + IDFile string + machineID string + cfg *config.Config +} + +func NewReport(logger *slog.Logger, cfg *config.Config, version *version.VersionInfo) *Reporter { + raw := "https://baizhi.cloud" + u, _ := url.Parse(raw) + client := request.NewClient(u.Scheme, u.Host, 30*time.Second) + + r := &Reporter{ + client: client, + logger: logger.With("module", "reporter"), + IDFile: "/app/static/.machine_id", + cfg: cfg, + version: version, + } + if _, err := r.readMachineID(); err != nil { + r.logger.With("error", err).Warn("read machine id file failed") + } + return r +} + +func (r *Reporter) readMachineID() (string, error) { + data, err := os.ReadFile(r.IDFile) + if err != nil { + return "", err + } + r.machineID = string(data) + return r.machineID, nil +} + +func (r *Reporter) report(data any) error { + b, err := json.Marshal(data) + if err != nil { + return err + } + + encrypt, err := aes.Encrypt([]byte(r.cfg.DataReport.Key), string(b)) + if err != nil { + return err + } + + req := map[string]any{ + "index": "monkeycode-installation", + "id": uuid.NewString(), + "data": encrypt, + } + + if _, err := request.Post[map[string]any](r.client, "/api/public/data/report", req); err != nil { + r.logger.With("error", err).Warn("report installation failed") + return err + } + + return nil +} + +func (r *Reporter) ReportInstallation() error { + if r.machineID != "" { + return nil + } + + id, err := machine.GenerateMachineID() + if err != nil { + r.logger.With("error", err).Warn("generate machine id failed") + return err + } + r.machineID = id + + f, err := os.Create(r.IDFile) + if err != nil { + r.logger.With("error", err).Warn("create machine id file failed") + return err + } + defer f.Close() + + _, err = f.WriteString(id) + if err != nil { + r.logger.With("error", err).Warn("write machine id file failed") + return err + } + + return r.report(InstallData{ + MachineID: id, + Version: r.version.Version(), + Timestamp: time.Now().Format(time.RFC3339), + Type: "installation", + }) +} diff --git a/backend/pkg/report/types.go b/backend/pkg/report/types.go new file mode 100644 index 0000000..db9881a --- /dev/null +++ b/backend/pkg/report/types.go @@ -0,0 +1,8 @@ +package report + +type InstallData struct { + MachineID string `json:"machine_id"` + Version string `json:"version"` + Timestamp string `json:"timestamp"` + Type string `json:"type"` +} diff --git a/backend/pkg/version/version.go b/backend/pkg/version/version.go new file mode 100644 index 0000000..7cf97bb --- /dev/null +++ b/backend/pkg/version/version.go @@ -0,0 +1,34 @@ +package version + +import "fmt" + +var ( + Version = "v0.0.0" + BuildTime = "" + GitCommit = "" +) + +type VersionInfo struct{} + +func NewVersionInfo() *VersionInfo { + return &VersionInfo{} +} + +func (v *VersionInfo) Print() { + fmt.Printf("🚀 Starting MonkeyCode Server\n") + fmt.Printf("📦 Version: %s\n", Version) + fmt.Printf("⏰ BuildTime: %s\n", BuildTime) + fmt.Printf("📝 GitCommit: %s\n", GitCommit) +} + +func (v *VersionInfo) Version() string { + return Version +} + +func (v *VersionInfo) BuildTime() string { + return BuildTime +} + +func (v *VersionInfo) GitCommit() string { + return GitCommit +}