├── .github └── workflows │ ├── build.yml │ ├── release.yml │ └── test.yml ├── .gitignore ├── .golangci.yml ├── LICENSE ├── Makefile ├── README.md ├── cmd ├── ccatctl │ ├── command │ │ ├── basic.go │ │ ├── cookie.go │ │ ├── script.go │ │ ├── token.go │ │ └── value.go │ └── main.go └── cloudcat │ ├── main.go │ └── server │ └── server.go ├── configs └── config.go ├── deploy ├── docker │ └── Dockerfile └── install.sh ├── docs ├── docs.go ├── swagger.json └── swagger.yaml ├── example ├── bing check-in.js ├── value.js └── xhr.js ├── go.mod ├── go.sum ├── internal ├── api │ ├── auth │ │ └── token.go │ ├── router.go │ └── scripts │ │ ├── cookie.go │ │ ├── script.go │ │ └── value.go ├── controller │ ├── auth_ctr │ │ └── token.go │ └── scripts_ctr │ │ ├── cookie.go │ │ ├── script.go │ │ └── value.go ├── model │ └── entity │ │ ├── cookie_entity │ │ ├── cookie.go │ │ ├── cookiejar │ │ │ ├── jar.go │ │ │ ├── print.go │ │ │ ├── punycode.go │ │ │ └── save.go │ │ └── utils.go │ │ ├── resource_entity │ │ └── resource.go │ │ ├── script_entity │ │ ├── script.go │ │ └── storage.go │ │ ├── token_entity │ │ └── token.go │ │ └── value_entity │ │ └── value.go ├── pkg │ └── code │ │ ├── code.go │ │ └── zh_cn.go ├── repository │ ├── cookie_repo │ │ └── cookie.go │ ├── resource_repo │ │ └── resource.go │ ├── script_repo │ │ └── script.go │ ├── token_repo │ │ └── token.go │ └── value_repo │ │ └── value.go ├── service │ ├── auth_svc │ │ └── token.go │ └── scripts_svc │ │ ├── cookie.go │ │ ├── gm.go │ │ ├── script.go │ │ └── value.go └── task │ ├── consumer │ ├── consumer.go │ └── subscribe │ │ ├── resource.go │ │ ├── script.go │ │ └── value.go │ ├── crontab │ ├── crontab.go │ └── handler │ │ └── script.go │ └── producer │ ├── script.go │ └── topic.go ├── migrations ├── 20230904.go └── init.go └── pkg ├── bbolt ├── bolt.go └── migrations.go ├── cloudcat_api ├── client.go ├── cookie.go ├── script.go ├── token.go └── value.go ├── scriptcat ├── model.go ├── options.go ├── plugin.go ├── plugin │ ├── gm.go │ ├── gm_test.go │ ├── value.go │ ├── window │ │ ├── timer.go │ │ └── window.go │ └── xhr.go ├── runtime.go ├── runtime_test.go ├── utils.go └── utils_test.go └── utils ├── aes.go ├── aes_test.go └── utils.go /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | # This workflow will build a golang project 2 | # For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-go 3 | 4 | name: Build 5 | 6 | on: 7 | push: 8 | branches: 9 | - main 10 | - release/* 11 | 12 | pull_request: 13 | branches: 14 | - main 15 | - release/* 16 | 17 | env: 18 | CLOUDCAT: "cloudcat" 19 | BINARY_SUFFIX: "" 20 | CCATCTL: "ccatctl" 21 | COMMIT_ID: "${{ github.sha }}" 22 | 23 | 24 | jobs: 25 | build: 26 | runs-on: ubuntu-latest 27 | strategy: 28 | matrix: 29 | # build and publish in parallel: linux/386, linux/amd64, windows/386, windows/amd64, darwin/amd64, darwin/arm64 30 | goos: [linux, windows, darwin] 31 | goarch: ["386", amd64, arm, arm64] 32 | exclude: 33 | - goos: darwin 34 | goarch: arm 35 | - goos: darwin 36 | goarch: "386" 37 | fail-fast: true 38 | steps: 39 | - uses: actions/checkout@v3 40 | 41 | - name: Set up Go 42 | uses: actions/setup-go@v4 43 | with: 44 | go-version: '1.21' 45 | 46 | - name: Build binary file 47 | env: 48 | GOOS: ${{ matrix.goos }} 49 | GOARCH: ${{ matrix.goarch }} 50 | run: | 51 | if [ $GOOS = "windows" ]; then export BINARY_SUFFIX="$BINARY_SUFFIX.exe"; fi 52 | export CGO_ENABLED=0 53 | export LD_FLAGS="-w -s -X github.com/scriptscat/cloudcat/configs.Version=${COMMIT_ID::7}" 54 | 55 | go build -o "bin/${CLOUDCAT}${BINARY_SUFFIX}" -trimpath -ldflags "$LD_FLAGS" ./cmd/cloudcat 56 | go build -o "bin/${CCATCTL}${BINARY_SUFFIX}" -trimpath -ldflags "$LD_FLAGS" ./cmd/ccatctl 57 | 58 | cd bin 59 | if [ "${{ matrix.goos }}" = "windows" ]; then 60 | zip -j "${CLOUDCAT}_${GOOS}_${GOARCH}.zip" "${CCATCTL}.exe" "${CLOUDCAT}.exe" 61 | else 62 | tar czvf "${CLOUDCAT}_${GOOS}_${GOARCH}.tar.gz" "${CCATCTL}" "${CLOUDCAT}" 63 | fi 64 | 65 | - name: Upload artifact 66 | uses: actions/upload-artifact@v3 67 | if: ${{ matrix.goos != 'windows' }} 68 | with: 69 | name: ${{ matrix.goos }}_${{ matrix.goarch }} 70 | path: bin/*.tar.gz 71 | 72 | - name: Upload windows artifact 73 | uses: actions/upload-artifact@v3 74 | if: ${{ matrix.goos == 'windows' }} 75 | with: 76 | name: ${{ matrix.goos }}_${{ matrix.goarch }} 77 | path: bin/*.zip 78 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Release 2 | 3 | on: 4 | push: 5 | tags: 6 | - 'v*' 7 | workflow_dispatch: 8 | 9 | env: 10 | CLOUDCAT: "cloudcat" 11 | BINARY_SUFFIX: "" 12 | CCATCTL: "ccatctl" 13 | COMMIT_ID: "${{ github.sha }}" 14 | 15 | jobs: 16 | build: 17 | runs-on: ubuntu-latest 18 | strategy: 19 | matrix: 20 | # build and publish in parallel: linux/386, linux/amd64, windows/386, windows/amd64, darwin/amd64, darwin/arm64 21 | goos: [linux, windows, darwin] 22 | goarch: ["386", amd64, arm, arm64] 23 | exclude: 24 | - goos: darwin 25 | goarch: arm 26 | - goos: darwin 27 | goarch: "386" 28 | fail-fast: true 29 | steps: 30 | - uses: actions/checkout@v3 31 | 32 | - name: Set up Go 33 | uses: actions/setup-go@v4 34 | with: 35 | go-version: '1.21' 36 | 37 | - name: Lint 38 | uses: golangci/golangci-lint-action@v3 39 | with: 40 | version: latest 41 | 42 | - name: Tests 43 | run: | 44 | go test $(go list ./...) 45 | 46 | - name: Build binary file 47 | env: 48 | GOOS: ${{ matrix.goos }} 49 | GOARCH: ${{ matrix.goarch }} 50 | run: | 51 | if [ $GOOS = "windows" ]; then export BINARY_SUFFIX="$BINARY_SUFFIX.exe"; fi 52 | export CGO_ENABLED=0 53 | export LD_FLAGS="-w -s -X github.com/scriptscat/cloudcat/configs.Version=${COMMIT_ID::7}" 54 | 55 | go build -o "bin/${CLOUDCAT}${BINARY_SUFFIX}" -trimpath -ldflags "$LD_FLAGS" ./cmd/cloudcat 56 | go build -o "bin/${CCATCTL}${BINARY_SUFFIX}" -trimpath -ldflags "$LD_FLAGS" ./cmd/ccatctl 57 | 58 | cd bin 59 | if [ "${{ matrix.goos }}" = "windows" ]; then 60 | zip -j "${CLOUDCAT}_${GOOS}_${GOARCH}.zip" "${CCATCTL}.exe" "${CLOUDCAT}.exe" 61 | else 62 | tar czvf "${CLOUDCAT}_${GOOS}_${GOARCH}.tar.gz" "${CCATCTL}" "${CLOUDCAT}" 63 | fi 64 | 65 | - name: Upload artifact 66 | uses: actions/upload-artifact@v3 67 | if: ${{ matrix.goos != 'windows' }} 68 | with: 69 | name: ${{ matrix.goos }}_${{ matrix.goarch }} 70 | path: bin/*.tar.gz 71 | 72 | - name: Upload windows artifact 73 | uses: actions/upload-artifact@v3 74 | if: ${{ matrix.goos == 'windows' }} 75 | with: 76 | name: ${{ matrix.goos }}_${{ matrix.goarch }} 77 | path: bin/*.zip 78 | 79 | release: 80 | needs: build 81 | runs-on: ubuntu-latest 82 | steps: 83 | - uses: actions/checkout@v3 84 | # 拿到build产物 85 | - uses: actions/download-artifact@v3 86 | with: 87 | path: bin/ 88 | 89 | - uses: ncipollo/release-action@v1 90 | with: 91 | artifacts: "bin/*/*.tar.gz,bin/*/*.zip" 92 | body: "no describe" 93 | # 判断是否为预发布(包含alpha、beta等关键字) 94 | prerelease: ${{ contains(github.ref, 'alpha') || contains(github.ref, 'beta') }} 95 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | # This workflow will build a golang project 2 | # For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-go 3 | 4 | name: Test 5 | 6 | on: 7 | push: 8 | pull_request: 9 | 10 | jobs: 11 | test: 12 | runs-on: ubuntu-latest 13 | steps: 14 | - uses: actions/checkout@v3 15 | 16 | - name: Set up Go 17 | uses: actions/setup-go@v4 18 | with: 19 | go-version: '1.21' 20 | 21 | - name: Lint 22 | uses: golangci/golangci-lint-action@v3 23 | with: 24 | version: latest 25 | 26 | - name: Tests 27 | run: | 28 | go test $(go list ./...) 29 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Binaries for programs and plugins 2 | *.exe 3 | *.exe~ 4 | *.dll 5 | *.so 6 | *.dylib 7 | 8 | # Test binary, built with `go test -c` 9 | *.test 10 | 11 | # Output of the go coverage tool, specifically when used with LiteIDE 12 | *.out 13 | 14 | # Dependency directories (remove the comment below to include it) 15 | # vendor/ 16 | 17 | .idea 18 | .vscode 19 | .docker 20 | 21 | /configs/config.yaml 22 | /docs 23 | /runtime 24 | /bin 25 | /data 26 | 27 | /example/*.json -------------------------------------------------------------------------------- /.golangci.yml: -------------------------------------------------------------------------------- 1 | linters: 2 | disable-all: false 3 | enable: 4 | - typecheck 5 | - goimports 6 | - misspell 7 | - govet 8 | - ineffassign 9 | - gosimple 10 | - unused 11 | - errcheck 12 | - staticcheck 13 | - gofmt 14 | - bodyclose 15 | - loggercheck 16 | - nilerr 17 | - prealloc 18 | - predeclared 19 | - durationcheck 20 | - exportloopref 21 | - rowserrcheck 22 | - stylecheck 23 | - gosec 24 | - nolintlint 25 | 26 | run: 27 | timeout: 10m 28 | 29 | linters-settings: 30 | stylecheck: 31 | checks: ["-ST1003"] 32 | gosec: 33 | excludes: 34 | - G204 35 | - G306 36 | - G401 37 | - G402 38 | - G404 39 | - G501 40 | - G505 41 | golint: 42 | min-confidence: 0 43 | misspell: 44 | locale: US 45 | 46 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | 2 | check-cago: 3 | ifneq ($(which cago),) 4 | go install github.com/codfrm/cago/cmd/cago@latest 5 | endif 6 | 7 | check-mockgen: 8 | ifneq ($(which mockgen),) 9 | go install github.com/golang/mock/mockgen 10 | endif 11 | 12 | check-golangci-lint: 13 | ifneq ($(which golangci-lint),) 14 | go get -u github.com/golangci/golangci-lint/cmd/golangci-lint 15 | endif 16 | 17 | swagger: check-cago 18 | cago gen swag 19 | 20 | lint: check-golangci-lint 21 | golangci-lint run 22 | 23 | lint-fix: check-golangci-lint 24 | golangci-lint run --fix 25 | 26 | test: lint 27 | go test -v ./... 28 | 29 | coverage.out cover: 30 | go test -coverprofile=coverage.out -covermode=atomic ./... 31 | go tool cover -func=coverage.out 32 | 33 | html-cover: coverage.out 34 | go tool cover -html=coverage.out 35 | go tool cover -func=coverage.out 36 | 37 | generate: check-mockgen swagger 38 | go generate ./... -x 39 | 40 | build: 41 | go build -o bin/cloudcat cmd/cloudcat/main.go 42 | go build -o bin/ccatctl cmd/ccatctl/main.go 43 | 44 | install: 45 | go install ./cmd/cloudcat 46 | go install ./cmd/ccatctl 47 | 48 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # CloudCat 2 | 3 | > 一个用于 **[ScriptCat脚本猫](https://docs.scriptcat.org/)** 扩展云端执行脚本的服务 4 | 5 | ![](https://img.shields.io/github/stars/scriptscat/cloudcat.svg)![](https://img.shields.io/github/v/tag/scriptscat/cloudcat.svg?label=version&sort=semver) 6 | 7 | ## 安装 8 | 9 | ### linux 10 | 11 | ```bash 12 | curl -sSL https://github.com/scriptscat/cloudcat/raw/main/deploy/install.sh | sudo bash 13 | ``` 14 | 15 | ## 使用 16 | 17 | ```bash 18 | # 查看帮助 19 | ccatctl -h 20 | # 安装脚本 21 | ccatctl install -f example/bing\ check-in.js 22 | # 查看脚本列表 23 | ccatctl get script 24 | # 导入cookie/value(storage name) 25 | ccatctl import cookie 6a0bd33 example/cookie.json 26 | # 运行脚本(脚本id) 27 | ccatctl run 6a0bd33 28 | ``` 29 | -------------------------------------------------------------------------------- /cmd/ccatctl/command/basic.go: -------------------------------------------------------------------------------- 1 | package command 2 | 3 | import ( 4 | "github.com/spf13/cobra" 5 | ) 6 | 7 | type Basic struct { 8 | script *Script 9 | value *Value 10 | cookie *Cookie 11 | token *Token 12 | } 13 | 14 | func NewBasic(config string) *Basic { 15 | return &Basic{ 16 | script: NewScript(), 17 | value: NewValue(), 18 | cookie: NewCookie(), 19 | token: NewToken(), 20 | } 21 | } 22 | 23 | func (c *Basic) Command() []*cobra.Command { 24 | create := &cobra.Command{ 25 | Use: "create [resource]", 26 | Short: "创建资源", 27 | } 28 | create.AddCommand(c.token.Create()) 29 | 30 | get := &cobra.Command{ 31 | Use: "get [resource]", 32 | Short: "获取资源信息", 33 | } 34 | get.AddCommand(c.script.Get(), c.value.Get(), c.cookie.Get(), c.token.Get()) 35 | 36 | edit := &cobra.Command{ 37 | Use: "edit [resource]", 38 | Short: "编辑资源信息", 39 | } 40 | edit.AddCommand(c.script.Edit()) 41 | 42 | del := &cobra.Command{ 43 | Use: "delete [resource]", 44 | Short: "删除资源信息", 45 | } 46 | del.AddCommand(c.script.Delete(), c.token.Delete(), c.value.Delete(), c.cookie.Delete()) 47 | 48 | importer := &cobra.Command{ 49 | Use: "import [resource]", 50 | Short: "导入资源信息", 51 | } 52 | importer.AddCommand(c.cookie.Import(), c.value.Import()) 53 | 54 | cmd := []*cobra.Command{create, get, edit, del, importer} 55 | cmd = append(cmd, c.script.Command()...) 56 | cmd = append(cmd, c.value.Command()...) 57 | cmd = append(cmd, c.cookie.Command()...) 58 | return cmd 59 | } 60 | -------------------------------------------------------------------------------- /cmd/ccatctl/command/cookie.go: -------------------------------------------------------------------------------- 1 | package command 2 | 3 | import ( 4 | "context" 5 | "encoding/json" 6 | "net/http" 7 | "os" 8 | "strings" 9 | "time" 10 | 11 | "github.com/scriptscat/cloudcat/internal/api/scripts" 12 | "github.com/scriptscat/cloudcat/internal/model/entity/cookie_entity" 13 | "github.com/scriptscat/cloudcat/pkg/cloudcat_api" 14 | "github.com/scriptscat/cloudcat/pkg/utils" 15 | "github.com/spf13/cobra" 16 | ) 17 | 18 | type Cookie struct { 19 | } 20 | 21 | func NewCookie() *Cookie { 22 | return &Cookie{} 23 | } 24 | 25 | func (c *Cookie) Command() []*cobra.Command { 26 | 27 | return []*cobra.Command{} 28 | } 29 | 30 | func (c *Cookie) Get() *cobra.Command { 31 | ret := &cobra.Command{ 32 | Use: "cookie [storageName] [host]", 33 | Short: "获取cookie信息", 34 | Args: cobra.MinimumNArgs(1), 35 | RunE: func(cmd *cobra.Command, args []string) error { 36 | cli := cloudcat_api.NewCookie(cloudcat_api.DefaultClient()) 37 | storageName := args[0] 38 | // 获取值列表 39 | list, err := cli.CookieList(context.Background(), &scripts.CookieListRequest{ 40 | StorageName: storageName, 41 | }) 42 | if err != nil { 43 | return err 44 | } 45 | if len(args) > 1 { 46 | r := utils.DealTable([]string{ 47 | "NAME", "VALUE", "DOMAIN", "PATH", "EXPIRES", "HTTPONLY", "SECURE", 48 | }, nil, func(i interface{}) []string { 49 | v := i.(*http.Cookie) 50 | return []string{ 51 | v.Name, v.Value, v.Domain, v.Path, 52 | v.Expires.Format("2006-01-02 15:04:05"), 53 | utils.BoolToString(v.HttpOnly), utils.BoolToString(v.Secure), 54 | } 55 | }) 56 | for _, v := range list.List { 57 | if strings.Contains(v.Host, args[1]) { 58 | for _, v := range v.Cookies { 59 | r.WriteLine(v) 60 | } 61 | } 62 | } 63 | r.Render() 64 | return nil 65 | } 66 | utils.DealTable([]string{ 67 | "HOST", 68 | }, list.List, func(i interface{}) []string { 69 | v := i.(*scripts.Cookie) 70 | return []string{ 71 | v.Host, 72 | } 73 | }).Render() 74 | return nil 75 | }, 76 | } 77 | return ret 78 | } 79 | 80 | func (c *Cookie) Delete() *cobra.Command { 81 | ret := &cobra.Command{ 82 | Use: "cookie [storageName] [host]", 83 | Short: "删除cookie信息", 84 | Args: cobra.MinimumNArgs(1), 85 | RunE: func(cmd *cobra.Command, args []string) error { 86 | cli := cloudcat_api.NewCookie(cloudcat_api.DefaultClient()) 87 | storageName := args[0] 88 | // 获取值列表 89 | list, err := cli.CookieList(context.Background(), &scripts.CookieListRequest{ 90 | StorageName: storageName, 91 | }) 92 | if err != nil { 93 | return err 94 | } 95 | if len(args) > 1 { 96 | for _, v := range list.List { 97 | if strings.Contains(v.Host, args[1]) { 98 | if _, err := cli.DeleteCookie(context.Background(), &scripts.DeleteCookieRequest{ 99 | StorageName: storageName, 100 | Host: v.Host, 101 | }); err != nil { 102 | return err 103 | } 104 | } 105 | } 106 | return nil 107 | } 108 | for _, v := range list.List { 109 | if _, err := cli.DeleteCookie(context.Background(), &scripts.DeleteCookieRequest{ 110 | StorageName: storageName, 111 | Host: v.Host, 112 | }); err != nil { 113 | return err 114 | } 115 | } 116 | return nil 117 | }, 118 | } 119 | 120 | return ret 121 | } 122 | 123 | func (c *Cookie) Import() *cobra.Command { 124 | return &cobra.Command{ 125 | Use: "cookie [storageName] [file]", 126 | Short: "导入cookie信息", 127 | Args: cobra.ExactArgs(2), 128 | RunE: func(cmd *cobra.Command, args []string) error { 129 | cli := cloudcat_api.NewCookie(cloudcat_api.DefaultClient()) 130 | data, err := os.ReadFile(args[1]) 131 | if err != nil { 132 | return err 133 | } 134 | storageName := args[0] 135 | // 获取值列表 136 | m := make([]*cookie_entity.HttpCookie, 0) 137 | if err := json.Unmarshal(data, &m); err != nil { 138 | return err 139 | } 140 | for _, v := range m { 141 | if v.Expires.IsZero() && v.ExpirationDate > 0 { 142 | v.Expires = time.Unix(v.ExpirationDate, 0) 143 | } 144 | } 145 | if _, err := cli.SetCookie(context.Background(), &scripts.SetCookieRequest{ 146 | StorageName: storageName, 147 | Cookies: m, 148 | }); err != nil { 149 | return err 150 | } 151 | return nil 152 | }, 153 | } 154 | } 155 | -------------------------------------------------------------------------------- /cmd/ccatctl/command/script.go: -------------------------------------------------------------------------------- 1 | package command 2 | 3 | import ( 4 | "bytes" 5 | "context" 6 | "os" 7 | "os/exec" 8 | "time" 9 | 10 | "github.com/scriptscat/cloudcat/internal/model/entity/script_entity" 11 | 12 | "github.com/scriptscat/cloudcat/internal/api/scripts" 13 | "github.com/scriptscat/cloudcat/pkg/cloudcat_api" 14 | "github.com/scriptscat/cloudcat/pkg/utils" 15 | "github.com/spf13/cobra" 16 | "gopkg.in/yaml.v3" 17 | ) 18 | 19 | type Script struct { 20 | file string 21 | out string 22 | } 23 | 24 | func NewScript() *Script { 25 | return &Script{} 26 | } 27 | 28 | func (s *Script) Command() []*cobra.Command { 29 | install := &cobra.Command{ 30 | Use: "install", 31 | Short: "安装脚本", 32 | RunE: s.install, 33 | } 34 | install.Flags().StringVarP(&s.file, "file", "f", "", "脚本文件") 35 | run := &cobra.Command{ 36 | Use: "run [script]", 37 | Short: "运行脚本", 38 | Args: cobra.ExactArgs(1), 39 | RunE: s.run, 40 | } 41 | stop := &cobra.Command{ 42 | Use: "stop [script]", 43 | Short: "停止脚本", 44 | Args: cobra.ExactArgs(1), 45 | RunE: s.stop, 46 | } 47 | enable := &cobra.Command{ 48 | Use: "enable [script]", 49 | Short: "启用脚本", 50 | Args: cobra.ExactArgs(1), 51 | RunE: s.enable(true), 52 | } 53 | disable := &cobra.Command{ 54 | Use: "disable [script]", 55 | Short: "禁用脚本", 56 | Args: cobra.ExactArgs(1), 57 | RunE: s.enable(false), 58 | } 59 | 60 | return []*cobra.Command{install, run, stop, enable, disable} 61 | } 62 | 63 | func (s *Script) enable(enable bool) func(cmd *cobra.Command, args []string) error { 64 | return func(cmd *cobra.Command, args []string) error { 65 | cli := cloudcat_api.NewScript(cloudcat_api.DefaultClient()) 66 | script, err := cli.Get(context.Background(), &scripts.GetRequest{ 67 | ScriptID: args[0], 68 | }) 69 | if err != nil { 70 | return err 71 | } 72 | if enable { 73 | script.Script.State = script_entity.ScriptStateEnable 74 | } else { 75 | script.Script.State = script_entity.ScriptStateDisable 76 | } 77 | if _, err := cli.Update(context.Background(), &scripts.UpdateRequest{ 78 | ScriptID: args[0], 79 | Script: script.Script, 80 | }); err != nil { 81 | return err 82 | } 83 | return nil 84 | } 85 | } 86 | 87 | func (s *Script) Get() *cobra.Command { 88 | ret := &cobra.Command{ 89 | Use: "script [scriptId]", 90 | Short: "获取脚本信息", 91 | RunE: func(cmd *cobra.Command, args []string) error { 92 | cli := cloudcat_api.NewScript(cloudcat_api.DefaultClient()) 93 | scriptId := "" 94 | if len(args) > 0 { 95 | scriptId = args[0] 96 | } 97 | list, err := cli.List(context.Background(), &scripts.ListRequest{ 98 | ScriptID: scriptId, 99 | }) 100 | if err != nil { 101 | return err 102 | } 103 | if s.out == "yaml" { 104 | for _, v := range list.List { 105 | data, err := yaml.Marshal(v) 106 | if err != nil { 107 | return err 108 | } 109 | _, err = os.Stdout.Write(data) 110 | if err != nil { 111 | return err 112 | } 113 | } 114 | return nil 115 | } 116 | utils.DealTable([]string{ 117 | "ID", "NAME", "STORAGE_NAME", "RUN_AT", "CREATED_AT", 118 | }, list.List, func(i interface{}) []string { 119 | v := i.(*scripts.Script) 120 | sn := script_entity.StorageName(v.ID, v.Metadata) 121 | if len(sn) > 7 { 122 | sn = sn[:7] 123 | } 124 | runAt := "" 125 | if v.State == script_entity.ScriptStateDisable { 126 | runAt = "DISABLE" 127 | } else { 128 | if cron, ok := v.Entity().Crontab(); ok { 129 | runAt = cron 130 | } else { 131 | runAt = "BACKGROUND" 132 | } 133 | } 134 | return []string{ 135 | v.ID[:7], 136 | v.Name, 137 | sn, 138 | runAt, 139 | time.Unix(v.Createtime, 0).Format("2006-01-02 15:04:05"), 140 | } 141 | }).Render() 142 | return nil 143 | }, 144 | } 145 | ret.Flags().StringVarP(&s.out, "out", "o", "table", "输出格式: yaml, table") 146 | return ret 147 | } 148 | 149 | const ( 150 | defaultEditor = "vi" 151 | defaultShell = "/bin/bash" 152 | ) 153 | 154 | func (s *Script) Edit() *cobra.Command { 155 | ret := &cobra.Command{ 156 | Use: "script [scriptId]", 157 | Short: "编辑脚本信息", 158 | Args: cobra.ExactArgs(1), 159 | RunE: func(cmd *cobra.Command, args []string) error { 160 | scriptId := args[0] 161 | cli := cloudcat_api.NewScript(cloudcat_api.DefaultClient()) 162 | resp, err := cli.Get(context.Background(), &scripts.GetRequest{ 163 | ScriptID: scriptId, 164 | }) 165 | if err != nil { 166 | return err 167 | } 168 | data, err := yaml.Marshal(resp.Script) 169 | if err != nil { 170 | return err 171 | } 172 | file, err := os.CreateTemp(os.TempDir(), "") 173 | if err != nil { 174 | return err 175 | } 176 | defer func() { 177 | _ = file.Close() 178 | _ = os.Remove(file.Name()) 179 | }() 180 | // 联合vi编辑 181 | _, err = file.Write(data) 182 | if err != nil { 183 | return err 184 | } 185 | c := exec.Command(defaultShell, "-c", defaultEditor+" "+file.Name()) 186 | c.Stdout = os.Stdout 187 | c.Stderr = os.Stderr 188 | c.Stdin = os.Stdin 189 | err = c.Run() 190 | if err != nil { 191 | return err 192 | } 193 | editData, err := os.ReadFile(file.Name()) 194 | if err != nil { 195 | return err 196 | } 197 | if bytes.Equal(editData, data) { 198 | return nil 199 | } 200 | script := &scripts.Script{} 201 | err = yaml.Unmarshal(editData, script) 202 | if err != nil { 203 | return err 204 | } 205 | if _, err := cli.Update(context.Background(), &scripts.UpdateRequest{ 206 | ScriptID: script.ID, 207 | Script: script, 208 | }); err != nil { 209 | return err 210 | } 211 | return nil 212 | }, 213 | } 214 | return ret 215 | } 216 | 217 | func (s *Script) install(cmd *cobra.Command, args []string) error { 218 | cli := cloudcat_api.NewScript(cloudcat_api.DefaultClient()) 219 | code, err := os.ReadFile(s.file) 220 | if err != nil { 221 | return err 222 | } 223 | _, err = cli.Install(context.Background(), &scripts.InstallRequest{ 224 | Code: string(code), 225 | }) 226 | if err != nil { 227 | return err 228 | } 229 | return nil 230 | } 231 | 232 | func (s *Script) run(cmd *cobra.Command, args []string) error { 233 | cli := cloudcat_api.NewScript(cloudcat_api.DefaultClient()) 234 | if _, err := cli.Run(context.Background(), &scripts.RunRequest{ 235 | ScriptID: args[0], 236 | }); err != nil { 237 | return err 238 | } 239 | return nil 240 | } 241 | 242 | func (s *Script) stop(cmd *cobra.Command, args []string) error { 243 | cli := cloudcat_api.NewScript(cloudcat_api.DefaultClient()) 244 | if _, err := cli.Stop(context.Background(), &scripts.StopRequest{ 245 | ScriptID: args[0], 246 | }); err != nil { 247 | return err 248 | } 249 | return nil 250 | } 251 | 252 | func (s *Script) Delete() *cobra.Command { 253 | ret := &cobra.Command{ 254 | Use: "script [scriptId]", 255 | Short: "删除脚本", 256 | Args: cobra.ExactArgs(1), 257 | RunE: func(cmd *cobra.Command, args []string) error { 258 | scriptId := args[0] 259 | cli := cloudcat_api.NewScript(cloudcat_api.DefaultClient()) 260 | _, err := cli.Delete(context.Background(), &scripts.DeleteRequest{ 261 | ScriptID: scriptId, 262 | }) 263 | if err != nil { 264 | return err 265 | } 266 | return nil 267 | }, 268 | } 269 | return ret 270 | } 271 | -------------------------------------------------------------------------------- /cmd/ccatctl/command/token.go: -------------------------------------------------------------------------------- 1 | package command 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "os" 7 | "time" 8 | 9 | "github.com/scriptscat/cloudcat/internal/api/auth" 10 | "github.com/scriptscat/cloudcat/pkg/cloudcat_api" 11 | "github.com/scriptscat/cloudcat/pkg/utils" 12 | "github.com/spf13/cobra" 13 | "gopkg.in/yaml.v3" 14 | ) 15 | 16 | type Token struct { 17 | out string 18 | } 19 | 20 | func NewToken() *Token { 21 | return &Token{} 22 | } 23 | 24 | func (s *Token) Command() []*cobra.Command { 25 | return []*cobra.Command{} 26 | } 27 | 28 | func (s *Token) Get() *cobra.Command { 29 | ret := &cobra.Command{ 30 | Use: "token [tokenId]", 31 | Short: "获取脚本信息", 32 | RunE: func(cmd *cobra.Command, args []string) error { 33 | cli := cloudcat_api.NewToken(cloudcat_api.DefaultClient()) 34 | tokenId := "" 35 | if len(args) > 0 { 36 | tokenId = args[0] 37 | } 38 | list, err := cli.List(context.Background(), &auth.TokenListRequest{ 39 | TokenID: tokenId, 40 | }) 41 | if err != nil { 42 | return err 43 | } 44 | if s.out == "yaml" { 45 | for _, v := range list.List { 46 | data, err := yaml.Marshal(v) 47 | if err != nil { 48 | return err 49 | } 50 | _, err = os.Stdout.Write(data) 51 | if err != nil { 52 | return err 53 | } 54 | } 55 | return nil 56 | } 57 | utils.DealTable([]string{ 58 | "ID", "CREATED_AT", 59 | }, list.List, func(i interface{}) []string { 60 | v := i.(*auth.Token) 61 | return []string{ 62 | v.ID, 63 | time.Unix(v.Createtime, 0).Format("2006-01-02 15:04:05"), 64 | } 65 | }).Render() 66 | return nil 67 | }, 68 | } 69 | ret.Flags().StringVarP(&s.out, "out", "o", "table", "输出格式: yaml, table") 70 | return ret 71 | } 72 | 73 | func (s *Token) Delete() *cobra.Command { 74 | ret := &cobra.Command{ 75 | Use: "token [tokenId]", 76 | Short: "删除脚本", 77 | Args: cobra.ExactArgs(1), 78 | RunE: func(cmd *cobra.Command, args []string) error { 79 | tokenId := args[0] 80 | cli := cloudcat_api.NewToken(cloudcat_api.DefaultClient()) 81 | _, err := cli.Delete(context.Background(), &auth.TokenDeleteRequest{ 82 | TokenID: tokenId, 83 | }) 84 | if err != nil { 85 | return err 86 | } 87 | return nil 88 | }, 89 | } 90 | return ret 91 | } 92 | 93 | func (s *Token) Create() *cobra.Command { 94 | ret := &cobra.Command{ 95 | Use: "token [tokenId]", 96 | Short: "创建脚本", 97 | Args: cobra.ExactArgs(1), 98 | RunE: func(cmd *cobra.Command, args []string) error { 99 | tokenId := args[0] 100 | cli := cloudcat_api.NewToken(cloudcat_api.DefaultClient()) 101 | resp, err := cli.Create(context.Background(), &auth.TokenCreateRequest{ 102 | TokenID: tokenId, 103 | }) 104 | if err != nil { 105 | return err 106 | } 107 | data, err := yaml.Marshal(resp.Token) 108 | if err != nil { 109 | return err 110 | } 111 | fmt.Println(string(data)) 112 | return nil 113 | }, 114 | } 115 | return ret 116 | } 117 | -------------------------------------------------------------------------------- /cmd/ccatctl/command/value.go: -------------------------------------------------------------------------------- 1 | package command 2 | 3 | import ( 4 | "context" 5 | "encoding/json" 6 | "os" 7 | 8 | "github.com/scriptscat/cloudcat/internal/api/scripts" 9 | "github.com/scriptscat/cloudcat/pkg/cloudcat_api" 10 | "github.com/scriptscat/cloudcat/pkg/utils" 11 | "github.com/spf13/cobra" 12 | ) 13 | 14 | type Value struct { 15 | } 16 | 17 | func NewValue() *Value { 18 | return &Value{} 19 | } 20 | 21 | func (s *Value) Command() []*cobra.Command { 22 | 23 | return []*cobra.Command{} 24 | } 25 | 26 | func (s *Value) Get() *cobra.Command { 27 | ret := &cobra.Command{ 28 | Use: "value [storageName]", 29 | Short: "获取值信息", 30 | Args: cobra.ExactArgs(1), 31 | RunE: func(cmd *cobra.Command, args []string) error { 32 | cli := cloudcat_api.NewValue(cloudcat_api.DefaultClient()) 33 | storageName := args[0] 34 | // 获取值列表 35 | list, err := cli.ValueList(context.Background(), &scripts.ValueListRequest{ 36 | StorageName: storageName, 37 | }) 38 | if err != nil { 39 | return err 40 | } 41 | utils.DealTable([]string{ 42 | "KEY", "VALUE", 43 | }, list.List, func(i interface{}) []string { 44 | v := i.(*scripts.Value) 45 | value, _ := json.Marshal(v.Value.Get()) 46 | return []string{ 47 | v.Key, string(value), 48 | } 49 | }).Render() 50 | return nil 51 | }, 52 | } 53 | return ret 54 | } 55 | 56 | func (s *Value) Import() *cobra.Command { 57 | return &cobra.Command{ 58 | Use: "value [storageName] [file]", 59 | Short: "导入值信息", 60 | Args: cobra.ExactArgs(2), 61 | RunE: func(cmd *cobra.Command, args []string) error { 62 | cli := cloudcat_api.NewValue(cloudcat_api.DefaultClient()) 63 | data, err := os.ReadFile(args[1]) 64 | if err != nil { 65 | return err 66 | } 67 | storageName := args[0] 68 | // 获取值列表 69 | m := make([]*scripts.Value, 0) 70 | if err := json.Unmarshal(data, &m); err != nil { 71 | return err 72 | } 73 | if _, err := cli.SetValue(context.Background(), &scripts.SetValueRequest{ 74 | StorageName: storageName, 75 | Values: m, 76 | }); err != nil { 77 | return err 78 | } 79 | return nil 80 | }, 81 | } 82 | } 83 | 84 | func (s *Value) Delete() *cobra.Command { 85 | return &cobra.Command{ 86 | Use: "value [storageName] [key]", 87 | Short: "删除值信息", 88 | Args: cobra.ExactArgs(2), 89 | RunE: func(cmd *cobra.Command, args []string) error { 90 | cli := cloudcat_api.NewValue(cloudcat_api.DefaultClient()) 91 | storageName := args[0] 92 | key := args[1] 93 | // 获取值列表 94 | if _, err := cli.DeleteValue(context.Background(), &scripts.DeleteValueRequest{ 95 | StorageName: storageName, 96 | Key: key, 97 | }); err != nil { 98 | return err 99 | } 100 | return nil 101 | }, 102 | } 103 | } 104 | -------------------------------------------------------------------------------- /cmd/ccatctl/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "errors" 5 | "log" 6 | "os" 7 | "runtime" 8 | 9 | "github.com/scriptscat/cloudcat/configs" 10 | 11 | "github.com/scriptscat/cloudcat/cmd/ccatctl/command" 12 | "github.com/scriptscat/cloudcat/pkg/cloudcat_api" 13 | "github.com/scriptscat/cloudcat/pkg/utils" 14 | "github.com/spf13/cobra" 15 | "gopkg.in/yaml.v3" 16 | ) 17 | 18 | var configFile = "~/.cloudcat/cloudcat.yaml" 19 | 20 | func init() { 21 | // 判断是否为windows 22 | if runtime.GOOS == "windows" { 23 | configFile = "./cloudcat/cloudcat.yaml" 24 | } 25 | } 26 | 27 | func main() { 28 | config := "" 29 | rootCmd := &cobra.Command{ 30 | Use: "ccatctl", 31 | Short: "ccatctl controls the cloudcat service.", 32 | Version: configs.Version, 33 | PersistentPreRunE: func(cmd *cobra.Command, args []string) error { 34 | config, err := utils.Abs(config) 35 | if err != nil { 36 | return err 37 | } 38 | _, err = os.Stat(config) 39 | if err != nil { 40 | if !os.IsNotExist(err) { 41 | return err 42 | } 43 | // 从环境变量中获取 44 | var ok bool 45 | config, ok = os.LookupEnv("CCATCONFIG") 46 | if !ok { 47 | return errors.New("config file is not exist") 48 | } 49 | } 50 | configData, err := os.ReadFile(config) 51 | if err != nil { 52 | return err 53 | } 54 | cfg := &cloudcat_api.Config{} 55 | if err := yaml.Unmarshal(configData, cfg); err != nil { 56 | return err 57 | } 58 | cli := cloudcat_api.NewClient(cfg) 59 | cloudcat_api.SetDefaultClient(cli) 60 | return nil 61 | }, 62 | } 63 | rootCmd.PersistentFlags().StringVarP(&config, "config", "c", configFile, "config file") 64 | basic := command.NewBasic(config) 65 | rootCmd.AddCommand(basic.Command()...) 66 | 67 | if err := rootCmd.Execute(); err != nil { 68 | log.Fatalf("execute err: %v", err) 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /cmd/cloudcat/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "log" 6 | "runtime" 7 | 8 | "github.com/scriptscat/cloudcat/configs" 9 | 10 | "github.com/scriptscat/cloudcat/cmd/cloudcat/server" 11 | "github.com/scriptscat/cloudcat/pkg/utils" 12 | "github.com/spf13/cobra" 13 | ) 14 | 15 | var ( 16 | configFile = "~/.cloudcat/config.yaml" 17 | ) 18 | 19 | func init() { 20 | // 判断是否为windows 21 | if runtime.GOOS == "windows" { 22 | configFile = "./cloudcat/config.yaml" 23 | } 24 | } 25 | 26 | func main() { 27 | var config string 28 | rootCmd := &cobra.Command{ 29 | Use: "cloudcat", 30 | Short: "cloudcat service.", 31 | Version: configs.Version, 32 | PersistentPreRunE: func(cmd *cobra.Command, args []string) error { 33 | // 转为绝对路径 34 | var err error 35 | config, err = utils.Abs(config) 36 | if err != nil { 37 | return fmt.Errorf("convert to absolute path: %w", err) 38 | } 39 | return nil 40 | }, 41 | } 42 | rootCmd.PersistentFlags().StringVarP(&config, "config", "c", configFile, "config file") 43 | 44 | serverCmd := server.NewServer() 45 | rootCmd.AddCommand(serverCmd.Command(&config)...) 46 | 47 | if err := rootCmd.Execute(); err != nil { 48 | log.Fatalf("execute err: %v", err) 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /cmd/cloudcat/server/server.go: -------------------------------------------------------------------------------- 1 | package server 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "log" 7 | "os" 8 | "path" 9 | "strings" 10 | 11 | "github.com/codfrm/cago" 12 | "github.com/codfrm/cago/configs" 13 | "github.com/codfrm/cago/pkg/broker" 14 | "github.com/codfrm/cago/pkg/logger" 15 | "github.com/codfrm/cago/server/mux" 16 | "github.com/scriptscat/cloudcat/internal/api" 17 | "github.com/scriptscat/cloudcat/internal/api/auth" 18 | "github.com/scriptscat/cloudcat/internal/repository/token_repo" 19 | "github.com/scriptscat/cloudcat/internal/service/auth_svc" 20 | "github.com/scriptscat/cloudcat/internal/task/consumer" 21 | "github.com/scriptscat/cloudcat/migrations" 22 | "github.com/scriptscat/cloudcat/pkg/bbolt" 23 | "github.com/scriptscat/cloudcat/pkg/cloudcat_api" 24 | "github.com/spf13/cobra" 25 | "gopkg.in/yaml.v3" 26 | ) 27 | 28 | type Server struct { 29 | config *string 30 | } 31 | 32 | func NewServer() *Server { 33 | return &Server{} 34 | } 35 | 36 | func (s *Server) Command(config *string) []*cobra.Command { 37 | server := &cobra.Command{ 38 | Use: "server", 39 | Short: "启动服务", 40 | RunE: s.Server, 41 | } 42 | 43 | init := &cobra.Command{ 44 | Use: "init", 45 | Short: "初始化服务", 46 | RunE: s.Init, 47 | } 48 | 49 | s.config = config 50 | return []*cobra.Command{server, init} 51 | } 52 | 53 | func (s *Server) Server(cmd *cobra.Command, args []string) error { 54 | cfg, err := configs.NewConfig("cloudcat", configs.WithConfigFile(*s.config)) 55 | if err != nil { 56 | log.Fatalf("new config err: %v", err) 57 | } 58 | err = cago.New(cmd.Context(), cfg).DisableLogger(). 59 | Registry(cago.FuncComponent(logger.Logger)). 60 | Registry(cago.FuncComponent(broker.Broker)). 61 | Registry(bbolt.Bolt()). 62 | Registry(cago.FuncComponent(consumer.Consumer)). 63 | Registry(cago.FuncComponent(func(ctx context.Context, cfg *configs.Config) error { 64 | return migrations.RunMigrations(bbolt.Default()) 65 | })). 66 | RegistryCancel(mux.HTTP(api.Router)). 67 | Start() 68 | if err != nil { 69 | return err 70 | } 71 | return nil 72 | } 73 | 74 | var configTemplate = `version: 1.0.0 75 | source: file 76 | broker: 77 | type: "event_bus" 78 | debug: false 79 | env: dev 80 | http: 81 | address: 82 | - :8644 83 | logger: 84 | level: "info" 85 | logfile: 86 | enable: true 87 | errorfilename: "{{.configDir}}/cloudcat.error.log" 88 | filename: "{{.configDir}}/cloudcat.log" 89 | db: 90 | path: "{{.configDir}}/data.db" 91 | ` 92 | 93 | func (s *Server) Init(cmd *cobra.Command, args []string) error { 94 | // 判断文件是否存在 95 | _, err := configs.NewConfig("cloudcat", configs.WithConfigFile(*s.config)) 96 | if err == nil { 97 | return fmt.Errorf("config file %s is exist", *s.config) 98 | } else if !os.IsNotExist(err) { 99 | return fmt.Errorf("determine whether the file exists: %w", err) 100 | } 101 | // 写配置文件 102 | configDir := path.Dir(*s.config) 103 | if err := os.MkdirAll(configDir, 0744); err != nil { 104 | return fmt.Errorf("create config dir: %w", err) 105 | } 106 | // 模板渲染 107 | tpl := strings.ReplaceAll(configTemplate, "{{.configDir}}", configDir) 108 | if err := os.WriteFile(*s.config, []byte(tpl), 0644); err != nil { 109 | return fmt.Errorf("write config file: %w", err) 110 | } 111 | 112 | cfg, err := configs.NewConfig("cloudcat", configs.WithConfigFile(*s.config)) 113 | if err != nil { 114 | return fmt.Errorf("new config err: %w", err) 115 | } 116 | err = cago.New(cmd.Context(), cfg).DisableLogger(). 117 | Registry(cago.FuncComponent(logger.Logger)). 118 | Registry(cago.FuncComponent(broker.Broker)). 119 | Registry(bbolt.Bolt()). 120 | Registry(cago.FuncComponent(consumer.Consumer)). 121 | Registry(cago.FuncComponent(func(ctx context.Context, cfg *configs.Config) error { 122 | return migrations.RunMigrations(bbolt.Default()) 123 | })). 124 | RegistryCancel(cago.FuncComponentCancel(func(ctx context.Context, cancel context.CancelFunc, cfg *configs.Config) error { 125 | defer cancel() 126 | token_repo.RegisterToken(token_repo.NewToken()) 127 | // 写client配置文件 128 | config := &cloudcat_api.Config{ 129 | ApiVersion: "v1", 130 | Server: &cloudcat_api.ConfigServer{ 131 | BaseURL: "http://127.0.0.1:8644", 132 | }, 133 | } 134 | token, err := auth_svc.Token().TokenCreate(ctx, &auth.TokenCreateRequest{ 135 | TokenID: "default", 136 | }) 137 | if err != nil { 138 | return fmt.Errorf("create token err: %w", err) 139 | } 140 | config.User = &cloudcat_api.ConfigUser{ 141 | Name: token.Token.ID, 142 | Token: token.Token.Token, 143 | DataEncryptionKey: token.Token.DataEncryptionKey, 144 | } 145 | data, err := yaml.Marshal(config) 146 | if err != nil { 147 | return fmt.Errorf("marshal config err: %w", err) 148 | } 149 | if err := os.WriteFile(path.Join(configDir, "cloudcat.yaml"), data, 0644); err != nil { 150 | return fmt.Errorf("write client config file: %w", err) 151 | } 152 | return nil 153 | })). 154 | Start() 155 | if err != nil { 156 | return err 157 | } 158 | return nil 159 | } 160 | -------------------------------------------------------------------------------- /configs/config.go: -------------------------------------------------------------------------------- 1 | package configs 2 | 3 | var Version = "" 4 | -------------------------------------------------------------------------------- /deploy/docker/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM alpine:3.16 2 | 3 | RUN sed -i 's/dl-cdn.alpinelinux.org/mirrors.tuna.tsinghua.edu.cn/g' /etc/apk/repositories 4 | 5 | RUN apk update && apk add tzdata && \ 6 | cp /usr/share/zoneinfo/Asia/Shanghai /etc/localtime && \ 7 | echo "Asia/Shanghai" > /etc/timezone 8 | 9 | ARG APP_NAME=cago 10 | 11 | ENV APP_NAME=$APP_NAME 12 | 13 | WORKDIR /app 14 | 15 | COPY $APP_NAME . 16 | 17 | RUN ls -l && chmod +x $APP_NAME 18 | 19 | CMD ["sh", "-c", "./$APP_NAME"] 20 | -------------------------------------------------------------------------------- /deploy/install.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # 根据是否有--prerelease参数来获取对应的版本信息 4 | get_release_url() { 5 | if [[ $1 == "--prerelease" ]]; then 6 | curl --silent "https://api.github.com/repos/scriptscat/cloudcat/releases" | 7 | grep "browser_download_url" | 8 | sed -E 's/.*"([^"]+)".*/\1/' | head -n 12 9 | else 10 | curl --silent "https://api.github.com/repos/scriptscat/cloudcat/releases/latest" | 11 | grep "browser_download_url" | 12 | sed -E 's/.*"([^"]+)".*/\1/' 13 | fi 14 | } 15 | 16 | detect_os_and_arch() { 17 | OS=$(uname | tr '[:upper:]' '[:lower:]') 18 | ARCH=$(uname -m) 19 | 20 | case $ARCH in 21 | x86_64) ARCH="amd64" ;; 22 | aarch64) ARCH="arm64" ;; 23 | armv*) ARCH="arm" ;; 24 | *) echo "Unsupported architecture: $ARCH"; exit 1 ;; 25 | esac 26 | 27 | echo "Detected OS: $OS and ARCH: $ARCH" 28 | } 29 | 30 | download_and_extract_binary() { 31 | local release_url="$1" 32 | 33 | detect_os_and_arch 34 | 35 | binary_url=$(echo "$release_url" | grep "${OS}_${ARCH}.tar.gz") 36 | 37 | if [ -z "$binary_url" ]; then 38 | echo "Binary not found for ${OS}_${ARCH}" 39 | exit 1 40 | fi 41 | 42 | echo "Downloading $binary_url..." 43 | curl -L -o cloudcat.tar.gz "$binary_url" 44 | 45 | mkdir -p /usr/local/cloudcat 46 | tar xzf cloudcat.tar.gz -C /usr/local/cloudcat 47 | ln -sf /usr/local/cloudcat/ccatctl /usr/local/bin/ccatctl 48 | chmod +x /usr/local/cloudcat/cloudcat 49 | chmod +x /usr/local/cloudcat/ccatctl 50 | } 51 | 52 | install_as_service() { 53 | /usr/local/cloudcat/cloudcat init 54 | 55 | if [ -f /etc/systemd/system/cloudcat.service ]; then 56 | echo "CloudCat service already exists. Overwriting..." 57 | fi 58 | 59 | cat > /etc/systemd/system/cloudcat.service < { 67 | if (accessKey) { 68 | await push.send(title, content); 69 | } 70 | GM_notification({ 71 | title: title, 72 | text: content, 73 | }); 74 | resolve(); 75 | }) 76 | } 77 | 78 | function getSubstring(inputStr, startStr, endStr) { 79 | const startIndex = inputStr.indexOf(startStr); 80 | if (startIndex == -1) { 81 | return null; 82 | } 83 | const endIndex = inputStr.indexOf(endStr, startIndex + startStr.length); 84 | if (endIndex == -1) { 85 | return null; 86 | } 87 | return inputStr.substring(startIndex + startStr.length, endIndex); 88 | } 89 | 90 | 91 | function getRewardsInfo() { 92 | return new Promise((resolve, reject) => { 93 | // 获取今日签到信息 94 | GM_xmlhttpRequest({ 95 | url: "https://rewards.bing.com", 96 | onload(resp) { 97 | if (resp.status == 200) { 98 | resolve(resp); 99 | } else { 100 | pushSend("必应每日签到失败", "请求返回错误: " + resp.status).then(() => reject()); 101 | } 102 | }, onerror(e) { 103 | pushSend("必应每日签到失败", e || "未知错误").then(() => reject()); 104 | } 105 | }); 106 | }) 107 | } 108 | 109 | function extractKeywords(inputStr) { 110 | const regex = /"indexUrl":"","query":"(.*?)"/g; 111 | const matches = [...inputStr.matchAll(regex)]; 112 | return matches.map(match => match[1]); 113 | } 114 | 115 | let keywordList = []; 116 | let keywordIndex = 0; 117 | 118 | // 获取搜索关键字 119 | function searchKeyword() { 120 | return new Promise((resolve, reject) => { 121 | if (keywordList.length == 0) { 122 | GM_xmlhttpRequest({ 123 | url: "https://top.baidu.com/board?platform=pc&sa=pcindex_entry", 124 | onload(resp) { 125 | if (resp.status == 200) { 126 | keywordList = extractKeywords(resp.responseText); 127 | resolve(keywordList[keywordIndex]); 128 | } else { 129 | pushSend('关键字获取失败', '热门词获取失败'); 130 | reject(new Error('关键字获取失败,' + resp.status)); 131 | } 132 | } 133 | }); 134 | } else { 135 | keywordIndex++; 136 | if (keywordIndex > keywordList.length) { 137 | keywordIndex = 0; 138 | } 139 | resolve(keywordList[keywordIndex]); 140 | } 141 | }).then(k => k + new Date().getTime() % 1000); 142 | } 143 | 144 | let retryNum = 0; 145 | let lastProcess = 0; 146 | let domain = "www.bing.com"; 147 | let firstReq = true; 148 | 149 | 150 | function handler() { 151 | const onload = (resp) => { 152 | const url = new URL(resp.finalUrl); 153 | if (url.host != domain) { 154 | domain = url.host; 155 | } 156 | if (firstReq) { 157 | firstReq = false; 158 | // 处理一下cookie问题 159 | let ig = getSubstring(resp.responseText, "_IG=\"", "\""); 160 | let iid = getSubstring(resp.responseText, "_iid=\"", "\""); 161 | GM_xmlhttpRequest({ 162 | url: "https://" + domain + "/rewardsapp/ncheader?ver=39980043&IID=" + iid + "&IG=" + ig 163 | }); 164 | GM_xmlhttpRequest({ 165 | url: "https://" + domain + "/rewardsapp/reportActivity?IG=" + ig + "&IID=" + iid + "&&src=hp", 166 | }); 167 | } 168 | } 169 | return getRewardsInfo().then(async resp => { 170 | // 获取今日已获取积分 171 | const data = resp.responseText; 172 | const dashboard = JSON.parse(getSubstring(data, "var dashboard = ", ";\r")); 173 | const pcAttributes = dashboard.userStatus.counters.pcSearch[0].attributes; 174 | if (dashboard.userStatus.counters.dailyPoint[0].pointProgress === lastProcess) { 175 | retryNum++; 176 | if (retryNum > 10) { 177 | await pushSend("必应每日签到错误", "请手动检查积分或者重新执行"); 178 | return true; 179 | } 180 | } else { 181 | lastProcess = dashboard.userStatus.counters.dailyPoint[0].pointProgress; 182 | } 183 | if (parseInt(pcAttributes.progress) >= parseInt(pcAttributes.max)) { 184 | // 判断是否有手机 185 | if (dashboard.userStatus.counters.mobileSearch) { 186 | const mobileSearch = dashboard.userStatus.counters.mobileSearch[0].attributes; 187 | if (parseInt(mobileSearch.progress) < parseInt(mobileSearch.max)) { 188 | // 进行一次手机搜索 189 | GM_xmlhttpRequest({ 190 | url: "https://" + domain + "/search?q=" + await searchKeyword(), 191 | onload: onload, 192 | headers: { 193 | "User-Agent": getMobileUA() 194 | } 195 | }); 196 | return false; 197 | } 198 | GM_log("奖励信息", "info", { 199 | pcProcess: pcAttributes.progress, 200 | mobileProcess: mobileSearch.progress 201 | }); 202 | } else { 203 | GM_log("奖励信息", "info", {pcProcess: pcAttributes.progress}); 204 | } 205 | await pushSend("必应每日签到完成", "当前等级: " + dashboard.userStatus.levelInfo.activeLevel + 206 | "(" + dashboard.userStatus.levelInfo.progress + ")" + 207 | "\n可用积分: " + dashboard.userStatus.availablePoints + " 今日积分: " + dashboard.userStatus.counters.dailyPoint[0].pointProgress); 208 | return true; 209 | } else { 210 | // 进行一次搜索 211 | GM_xmlhttpRequest({ 212 | url: "https://" + domain + "/search?q=" + await searchKeyword(), 213 | onload: onload, 214 | }); 215 | return false; 216 | } 217 | }); 218 | } 219 | 220 | return new Promise((resolve, reject) => { 221 | const h = async () => { 222 | try { 223 | const result = await handler(); 224 | if (result) { 225 | resolve(); 226 | } else { 227 | setTimeout(() => { 228 | h(); 229 | }, 1000 * (Math.floor(Math.random() * 4) + 10)); 230 | } 231 | } catch (e) { 232 | pushSend('必应每日签到失败', '请查看错误日志手动重试'); 233 | reject(e); 234 | } 235 | } 236 | h(); 237 | }); 238 | 239 | -------------------------------------------------------------------------------- /example/value.js: -------------------------------------------------------------------------------- 1 | // ==UserScript== 2 | // @name value test 3 | // @namespace https://bbs.tampermonkey.net.cn/ 4 | // @version 0.1.0 5 | // @description try to take over the world! 6 | // @author You 7 | // @crontab */12 * * * * * 8 | // @grant GM_setValue 9 | // @grant GM_getValue 10 | // ==/UserScript== 11 | 12 | return new Promise((resolve) => { 13 | setTimeout(() => { 14 | // Your code here... 15 | GM_setValue("obj", {"test": 1}); 16 | GM_setValue("arr", ["test", "test2"]); 17 | GM_setValue("bool", true); 18 | GM_setValue("num1", 12345); 19 | GM_setValue("num2", 123.45); 20 | GM_setValue("str", "string"); 21 | 22 | GM_log(GM_getValue("obj", 1), "warn", {"test": 1}); 23 | GM_log(GM_getValue("arr", 1), "warn", {"test": 1}); 24 | GM_log(GM_getValue("bool", 1), "warn", {"test": 1}); 25 | GM_log(GM_getValue("num1", 1), "warn", {"test": 1}); 26 | GM_log(GM_getValue("num2", 1), "warn", {"test": 1}); 27 | GM_log(GM_getValue("str", 1), "warn", {"test": 1}); 28 | 29 | resolve(); 30 | }, 2000); 31 | }); 32 | -------------------------------------------------------------------------------- /example/xhr.js: -------------------------------------------------------------------------------- 1 | // ==UserScript== 2 | // @name xhr test 3 | // @namespace https://bbs.tampermonkey.net.cn/ 4 | // @version 0.1.0 5 | // @description try to take over the world! 6 | // @author You 7 | // @crontab */5 * * * * * 8 | // @grant GM_xmlhttpRequest 9 | // @connect bbs.tampermonkey.net.cn 10 | // ==/UserScript== 11 | 12 | return new Promise((resolve) => { 13 | GM_xmlhttpRequest({ 14 | url: "https://bbs.tampermonkey.net.cn/", 15 | method: "POST", 16 | data: "test", 17 | headers: { 18 | "referer": "http://www.example.com/", 19 | "origin": "www.example.com", 20 | // 为空将不会发送此header 21 | "sec-ch-ua-mobile": "", 22 | }, 23 | onload(resp) { 24 | // GM_log("onload", "info", {resp: resp}); 25 | resolve("ok xhr"); 26 | }, 27 | onreadystatechange(resp) { 28 | GM_log("onreadystatechange", "info", {resp: resp}); 29 | }, 30 | onloadend(resp) { 31 | GM_log("onloadend", "info", {resp: resp}); 32 | resolve(); 33 | }, 34 | onerror(e) { 35 | GM_log("onerror", "info", {e: e}); 36 | resolve(); 37 | } 38 | }); 39 | }); 40 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/scriptscat/cloudcat 2 | 3 | go 1.21 4 | 5 | require ( 6 | github.com/codfrm/cago v0.0.0-20230926141048-b0784a0a9f69 7 | github.com/dop251/goja v0.0.0-20230812105242-81d76064690d 8 | github.com/gin-gonic/gin v1.9.1 9 | github.com/goccy/go-json v0.10.2 10 | github.com/golang-jwt/jwt/v5 v5.0.0 11 | github.com/olekukonko/tablewriter v0.0.5 12 | github.com/robfig/cron/v3 v3.0.1 13 | github.com/spf13/cobra v1.7.0 14 | github.com/stretchr/testify v1.8.4 15 | github.com/swaggo/swag v1.8.12 16 | go.etcd.io/bbolt v1.3.7 17 | go.uber.org/zap v1.26.0 18 | gopkg.in/yaml.v3 v3.0.1 19 | ) 20 | 21 | require ( 22 | github.com/KyleBanks/depth v1.2.1 // indirect 23 | github.com/asaskevich/EventBus v0.0.0-20200907212545-49d423059eef // indirect 24 | github.com/bytedance/sonic v1.10.0 // indirect 25 | github.com/chenzhuoyu/base64x v0.0.0-20230717121745-296ad89f973d // indirect 26 | github.com/chenzhuoyu/iasm v0.9.0 // indirect 27 | github.com/davecgh/go-spew v1.1.1 // indirect 28 | github.com/dlclark/regexp2 v1.10.0 // indirect 29 | github.com/dop251/goja_nodejs v0.0.0-20230914102007-198ba9a8b098 // indirect 30 | github.com/gabriel-vasile/mimetype v1.4.2 // indirect 31 | github.com/gin-contrib/sse v0.1.0 // indirect 32 | github.com/go-logr/logr v1.2.4 // indirect 33 | github.com/go-logr/stdr v1.2.2 // indirect 34 | github.com/go-openapi/jsonpointer v0.19.6 // indirect 35 | github.com/go-openapi/jsonreference v0.20.2 // indirect 36 | github.com/go-openapi/spec v0.20.8 // indirect 37 | github.com/go-openapi/swag v0.22.3 // indirect 38 | github.com/go-playground/locales v0.14.1 // indirect 39 | github.com/go-playground/universal-translator v0.18.1 // indirect 40 | github.com/go-playground/validator/v10 v10.15.0 // indirect 41 | github.com/go-sourcemap/sourcemap v2.1.3+incompatible // indirect 42 | github.com/golang/snappy v0.0.1 // indirect 43 | github.com/google/pprof v0.0.0-20230817174616-7a8ec2ada47b // indirect 44 | github.com/inconshreveable/mousetrap v1.1.0 // indirect 45 | github.com/josharian/intern v1.0.0 // indirect 46 | github.com/json-iterator/go v1.1.12 // indirect 47 | github.com/klauspost/cpuid/v2 v2.2.5 // indirect 48 | github.com/leodido/go-urn v1.2.4 // indirect 49 | github.com/mailru/easyjson v0.7.7 // indirect 50 | github.com/mattn/go-isatty v0.0.19 // indirect 51 | github.com/mattn/go-runewidth v0.0.9 // indirect 52 | github.com/mitchellh/mapstructure v1.5.0 // indirect 53 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect 54 | github.com/modern-go/reflect2 v1.0.2 // indirect 55 | github.com/nsqio/go-nsq v1.1.0 // indirect 56 | github.com/pelletier/go-toml/v2 v2.0.9 // indirect 57 | github.com/pmezard/go-difflib v1.0.0 // indirect 58 | github.com/spf13/pflag v1.0.5 // indirect 59 | github.com/twitchyliquid64/golang-asm v0.15.1 // indirect 60 | github.com/ugorji/go/codec v1.2.11 // indirect 61 | go.mongodb.org/mongo-driver v1.11.3 // indirect 62 | go.opentelemetry.io/otel v1.14.0 // indirect 63 | go.opentelemetry.io/otel/sdk v1.14.0 // indirect 64 | go.opentelemetry.io/otel/trace v1.14.0 // indirect 65 | go.uber.org/goleak v1.2.1 // indirect 66 | go.uber.org/multierr v1.11.0 // indirect 67 | golang.org/x/arch v0.4.0 // indirect 68 | golang.org/x/crypto v0.12.0 // indirect 69 | golang.org/x/net v0.14.0 // indirect 70 | golang.org/x/sys v0.11.0 // indirect 71 | golang.org/x/text v0.12.0 // indirect 72 | golang.org/x/tools v0.7.0 // indirect 73 | google.golang.org/protobuf v1.31.0 // indirect 74 | gopkg.in/natefinch/lumberjack.v2 v2.2.1 // indirect 75 | ) 76 | -------------------------------------------------------------------------------- /internal/api/auth/token.go: -------------------------------------------------------------------------------- 1 | package auth 2 | 3 | import ( 4 | "github.com/codfrm/cago/server/mux" 5 | ) 6 | 7 | type Token struct { 8 | ID string `json:"id"` 9 | Token string `json:"token"` 10 | DataEncryptionKey string `json:"data_encryption_key" yaml:"dataEncryptionKey"` 11 | Createtime int64 `json:"createtime"` 12 | Updatetime int64 `json:"updatetime"` 13 | } 14 | 15 | type TokenListRequest struct { 16 | mux.Meta `path:"/tokens" method:"GET"` 17 | TokenID string `form:"token_id"` 18 | } 19 | 20 | type TokenListResponse struct { 21 | List []*Token `json:"list"` 22 | } 23 | 24 | type TokenCreateRequest struct { 25 | mux.Meta `path:"/tokens" method:"POST"` 26 | TokenID string `form:"token_id" json:"token_id"` 27 | } 28 | 29 | type TokenCreateResponse struct { 30 | Token *Token `json:"token"` 31 | } 32 | 33 | type TokenDeleteRequest struct { 34 | mux.Meta `path:"/tokens/:tokenId" method:"DELETE"` 35 | TokenID string `uri:"tokenId"` 36 | } 37 | 38 | type TokenDeleteResponse struct { 39 | } 40 | -------------------------------------------------------------------------------- /internal/api/router.go: -------------------------------------------------------------------------------- 1 | package api 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/scriptscat/cloudcat/internal/controller/auth_ctr" 7 | "github.com/scriptscat/cloudcat/internal/repository/resource_repo" 8 | "github.com/scriptscat/cloudcat/internal/repository/token_repo" 9 | "github.com/scriptscat/cloudcat/internal/service/auth_svc" 10 | 11 | "github.com/codfrm/cago/server/mux" 12 | "github.com/scriptscat/cloudcat/internal/controller/scripts_ctr" 13 | "github.com/scriptscat/cloudcat/internal/repository/cookie_repo" 14 | "github.com/scriptscat/cloudcat/internal/repository/script_repo" 15 | "github.com/scriptscat/cloudcat/internal/repository/value_repo" 16 | "github.com/scriptscat/cloudcat/internal/service/scripts_svc" 17 | ) 18 | 19 | // Router 路由表 20 | // @title 云猫 API 文档 21 | // @version 1.0.0 22 | // @BasePath /api/v1 23 | func Router(ctx context.Context, root *mux.Router) error { 24 | 25 | script_repo.RegisterScript(script_repo.NewScript()) 26 | value_repo.RegisterValue(value_repo.NewValue()) 27 | cookie_repo.RegisterCookie(cookie_repo.NewCookie()) 28 | token_repo.RegisterToken(token_repo.NewToken()) 29 | resource_repo.RegisterResource(resource_repo.NewResource()) 30 | 31 | r := root.Group("/api/v1", auth_svc.Token().Middleware()) 32 | 33 | _, err := scripts_svc.NewScript(ctx) 34 | if err != nil { 35 | return err 36 | } 37 | { 38 | script := scripts_ctr.NewScripts() 39 | r.Bind( 40 | script.List, 41 | script.Install, 42 | script.Update, 43 | script.Get, 44 | script.Delete, 45 | script.Run, 46 | script.Stop, 47 | ) 48 | } 49 | { 50 | value := scripts_ctr.NewValue() 51 | r.Bind( 52 | value.ValueList, 53 | value.SetValue, 54 | ) 55 | } 56 | { 57 | cookie := scripts_ctr.NewCookie() 58 | r.Bind( 59 | cookie.CookieList, 60 | cookie.DeleteCookie, 61 | cookie.SetCookie, 62 | ) 63 | } 64 | { 65 | token := auth_ctr.NewToken() 66 | r.Bind( 67 | token.TokenCreate, 68 | token.TokenList, 69 | token.TokenDelete, 70 | ) 71 | } 72 | 73 | return nil 74 | } 75 | -------------------------------------------------------------------------------- /internal/api/scripts/cookie.go: -------------------------------------------------------------------------------- 1 | package scripts 2 | 3 | import ( 4 | "github.com/codfrm/cago/server/mux" 5 | "github.com/scriptscat/cloudcat/internal/model/entity/cookie_entity" 6 | ) 7 | 8 | type Cookie struct { 9 | StorageName string `json:"storage_name"` 10 | Host string `json:"host"` 11 | Cookies []*cookie_entity.HttpCookie `json:"cookies"` 12 | Createtime int64 `json:"createtime"` 13 | } 14 | 15 | // CookieListRequest 脚本cookie列表 16 | type CookieListRequest struct { 17 | mux.Meta `path:"/cookies/:storageName" method:"GET"` 18 | StorageName string `uri:"storageName"` 19 | } 20 | 21 | // CookieListResponse 脚本cookie列表 22 | type CookieListResponse struct { 23 | List []*Cookie `json:"list"` 24 | } 25 | 26 | // DeleteCookieRequest 删除cookie 27 | type DeleteCookieRequest struct { 28 | mux.Meta `path:"/cookies/:storageName" method:"DELETE"` 29 | StorageName string `uri:"storageName"` 30 | Host string `form:"host"` 31 | } 32 | 33 | type DeleteCookieResponse struct { 34 | } 35 | 36 | // SetCookieRequest 设置cookie 37 | type SetCookieRequest struct { 38 | mux.Meta `path:"/cookies/:storageName" method:"POST"` 39 | StorageName string `uri:"storageName"` 40 | Cookies []*cookie_entity.HttpCookie `form:"cookies"` 41 | } 42 | 43 | // SetCookieResponse 设置cookie 44 | type SetCookieResponse struct { 45 | } 46 | -------------------------------------------------------------------------------- /internal/api/scripts/script.go: -------------------------------------------------------------------------------- 1 | package scripts 2 | 3 | import ( 4 | "github.com/codfrm/cago/server/mux" 5 | "github.com/scriptscat/cloudcat/internal/model/entity/script_entity" 6 | ) 7 | 8 | type Script struct { 9 | ID string `json:"id" yaml:"id,omitempty"` 10 | Name string `json:"name" yaml:"name"` 11 | Code string `json:"code,omitempty" yaml:"code,omitempty"` 12 | Metadata script_entity.Metadata `json:"metadata" yaml:"metadata"` 13 | SelfMetadata script_entity.Metadata `json:"self_metadata" yaml:"selfMetadata"` 14 | Status script_entity.Status `json:"status" yaml:"status"` 15 | State script_entity.ScriptState `json:"state" yaml:"state"` 16 | Createtime int64 `json:"createtime" yaml:"createtime"` 17 | Updatetime int64 `json:"updatetime" yaml:"updatetime"` 18 | } 19 | 20 | func (s *Script) Entity() *script_entity.Script { 21 | return &script_entity.Script{ 22 | ID: s.ID, 23 | Name: s.Name, 24 | Code: s.Code, 25 | Metadata: s.Metadata, 26 | SelfMetadata: s.SelfMetadata, 27 | Status: s.Status, 28 | State: s.State, 29 | Createtime: s.Createtime, 30 | Updatetime: s.Updatetime, 31 | } 32 | } 33 | 34 | // ListRequest 脚本列表 35 | type ListRequest struct { 36 | mux.Meta `path:"/scripts" method:"GET"` 37 | ScriptID string `form:"scriptId"` 38 | } 39 | 40 | // ListResponse 脚本列表 41 | type ListResponse struct { 42 | List []*Script `json:"list"` 43 | } 44 | 45 | // InstallRequest 创建脚本 46 | type InstallRequest struct { 47 | mux.Meta `path:"/scripts" method:"POST"` 48 | Code string `form:"code"` 49 | } 50 | 51 | // InstallResponse 创建脚本 52 | type InstallResponse struct { 53 | Scripts []*Script `json:"scripts"` 54 | } 55 | 56 | // GetRequest 获取脚本 57 | type GetRequest struct { 58 | mux.Meta `path:"/scripts/:scriptId" method:"GET"` 59 | ScriptID string `uri:"scriptId"` 60 | } 61 | 62 | // GetResponse 获取脚本 63 | type GetResponse struct { 64 | Script *Script `json:"script"` 65 | } 66 | 67 | // UpdateRequest 更新脚本 68 | type UpdateRequest struct { 69 | mux.Meta `path:"/scripts/:scriptId" method:"PUT"` 70 | ScriptID string `uri:"scriptId"` 71 | Script *Script `form:"script"` 72 | } 73 | 74 | // UpdateResponse 更新脚本 75 | type UpdateResponse struct { 76 | } 77 | 78 | // DeleteRequest 删除脚本 79 | type DeleteRequest struct { 80 | mux.Meta `path:"/scripts/:scriptId" method:"DELETE"` 81 | ScriptID string `uri:"scriptId"` 82 | } 83 | 84 | // DeleteResponse 删除脚本 85 | type DeleteResponse struct { 86 | } 87 | 88 | type Storage struct { 89 | Name string `json:"name"` 90 | LinkScriptID []string `json:"link_script_id"` 91 | } 92 | 93 | // StorageListRequest 值储存空间列表 94 | type StorageListRequest struct { 95 | mux.Meta `path:"/storages" method:"GET"` 96 | } 97 | 98 | // StorageListResponse 值储存空间列表 99 | type StorageListResponse struct { 100 | List []*Storage `json:"list"` 101 | } 102 | 103 | // RunRequest 运行脚本 104 | type RunRequest struct { 105 | mux.Meta `path:"/scripts/:scriptId/run" method:"POST"` 106 | ScriptID string `uri:"scriptId"` 107 | } 108 | 109 | // RunResponse 运行脚本 110 | type RunResponse struct { 111 | } 112 | 113 | // StopRequest 停止脚本 114 | type StopRequest struct { 115 | mux.Meta `path:"/scripts/:scriptId/stop" method:"POST"` 116 | ScriptID string `uri:"scriptId"` 117 | } 118 | 119 | // StopResponse 停止脚本 120 | type StopResponse struct { 121 | } 122 | 123 | // WatchRequest 监听脚本 124 | type WatchRequest struct { 125 | mux.Meta `path:"/scripts/:scriptId/watch" method:"GET"` 126 | ScriptID string `uri:"scriptId"` 127 | } 128 | 129 | // WatchResponse 监听脚本 130 | type WatchResponse struct { 131 | } 132 | -------------------------------------------------------------------------------- /internal/api/scripts/value.go: -------------------------------------------------------------------------------- 1 | package scripts 2 | 3 | import ( 4 | "github.com/codfrm/cago/server/mux" 5 | "github.com/scriptscat/cloudcat/internal/model/entity/value_entity" 6 | ) 7 | 8 | type Value struct { 9 | StorageName string `json:"storage_name"` 10 | Key string `json:"key"` 11 | Value value_entity.ValueString `json:"value"` 12 | Createtime int64 `json:"createtime"` 13 | } 14 | 15 | // ValueListRequest 脚本值列表 16 | type ValueListRequest struct { 17 | mux.Meta `path:"/values/:storageName" method:"GET"` 18 | StorageName string `uri:"storageName"` 19 | } 20 | 21 | // ValueListResponse 脚本值列表 22 | type ValueListResponse struct { 23 | List []*Value `json:"list"` 24 | } 25 | 26 | // SetValueRequest 设置脚本值 27 | type SetValueRequest struct { 28 | mux.Meta `path:"/values/:storageName" method:"POST"` 29 | StorageName string `uri:"storageName"` 30 | Values []*Value `form:"values"` 31 | } 32 | 33 | // SetValueResponse 设置脚本值 34 | type SetValueResponse struct { 35 | } 36 | 37 | // DeleteValueRequest 删除脚本值 38 | type DeleteValueRequest struct { 39 | mux.Meta `path:"/values/:storageName/:key" method:"DELETE"` 40 | StorageName string `uri:"storageName"` 41 | Key string `uri:"key"` 42 | } 43 | 44 | // DeleteValueResponse 删除脚本值 45 | type DeleteValueResponse struct { 46 | } 47 | -------------------------------------------------------------------------------- /internal/controller/auth_ctr/token.go: -------------------------------------------------------------------------------- 1 | package auth_ctr 2 | 3 | import ( 4 | "context" 5 | 6 | api "github.com/scriptscat/cloudcat/internal/api/auth" 7 | "github.com/scriptscat/cloudcat/internal/service/auth_svc" 8 | ) 9 | 10 | type Token struct { 11 | } 12 | 13 | func NewToken() *Token { 14 | return &Token{} 15 | } 16 | 17 | // TokenList 获取token列表 18 | func (t *Token) TokenList(ctx context.Context, req *api.TokenListRequest) (*api.TokenListResponse, error) { 19 | return auth_svc.Token().TokenList(ctx, req) 20 | } 21 | 22 | // TokenCreate 创建token 23 | func (t *Token) TokenCreate(ctx context.Context, req *api.TokenCreateRequest) (*api.TokenCreateResponse, error) { 24 | return auth_svc.Token().TokenCreate(ctx, req) 25 | } 26 | 27 | // TokenDelete 删除token 28 | func (t *Token) TokenDelete(ctx context.Context, req *api.TokenDeleteRequest) (*api.TokenDeleteResponse, error) { 29 | return auth_svc.Token().TokenDelete(ctx, req) 30 | } 31 | -------------------------------------------------------------------------------- /internal/controller/scripts_ctr/cookie.go: -------------------------------------------------------------------------------- 1 | package scripts_ctr 2 | 3 | import ( 4 | "context" 5 | 6 | api "github.com/scriptscat/cloudcat/internal/api/scripts" 7 | "github.com/scriptscat/cloudcat/internal/service/scripts_svc" 8 | ) 9 | 10 | type Cookie struct { 11 | } 12 | 13 | func NewCookie() *Cookie { 14 | return &Cookie{} 15 | } 16 | 17 | // CookieList 脚本cookie列表 18 | func (c *Cookie) CookieList(ctx context.Context, req *api.CookieListRequest) (*api.CookieListResponse, error) { 19 | return scripts_svc.Cookie().CookieList(ctx, req) 20 | } 21 | 22 | // DeleteCookie 删除cookie 23 | func (c *Cookie) DeleteCookie(ctx context.Context, req *api.DeleteCookieRequest) (*api.DeleteCookieResponse, error) { 24 | return scripts_svc.Cookie().DeleteCookie(ctx, req) 25 | } 26 | 27 | // SetCookie 设置cookie 28 | func (c *Cookie) SetCookie(ctx context.Context, req *api.SetCookieRequest) (*api.SetCookieResponse, error) { 29 | return scripts_svc.Cookie().SetCookie(ctx, req) 30 | } 31 | -------------------------------------------------------------------------------- /internal/controller/scripts_ctr/script.go: -------------------------------------------------------------------------------- 1 | package scripts_ctr 2 | 3 | import ( 4 | "context" 5 | 6 | api "github.com/scriptscat/cloudcat/internal/api/scripts" 7 | "github.com/scriptscat/cloudcat/internal/service/scripts_svc" 8 | ) 9 | 10 | type Script struct { 11 | } 12 | 13 | func NewScripts() *Script { 14 | return &Script{} 15 | } 16 | 17 | // List 脚本列表 18 | func (s *Script) List(ctx context.Context, req *api.ListRequest) (*api.ListResponse, error) { 19 | return scripts_svc.Script().List(ctx, req) 20 | } 21 | 22 | // Install 安装脚本 23 | func (s *Script) Install(ctx context.Context, req *api.InstallRequest) (*api.InstallResponse, error) { 24 | return scripts_svc.Script().Install(ctx, req) 25 | } 26 | 27 | // Get 获取脚本 28 | func (s *Script) Get(ctx context.Context, req *api.GetRequest) (*api.GetResponse, error) { 29 | return scripts_svc.Script().Get(ctx, req) 30 | } 31 | 32 | // Update 更新脚本 33 | func (s *Script) Update(ctx context.Context, req *api.UpdateRequest) (*api.UpdateResponse, error) { 34 | return scripts_svc.Script().Update(ctx, req) 35 | } 36 | 37 | // Delete 删除脚本 38 | func (s *Script) Delete(ctx context.Context, req *api.DeleteRequest) (*api.DeleteResponse, error) { 39 | return scripts_svc.Script().Delete(ctx, req) 40 | } 41 | 42 | // StorageList 值储存空间列表 43 | func (s *Script) StorageList(ctx context.Context, req *api.StorageListRequest) (*api.StorageListResponse, error) { 44 | return scripts_svc.Script().StorageList(ctx, req) 45 | } 46 | 47 | // Run 手动运行脚本 48 | func (s *Script) Run(ctx context.Context, req *api.RunRequest) (*api.RunResponse, error) { 49 | return scripts_svc.Script().Run(ctx, req) 50 | } 51 | 52 | // Watch 监听脚本 53 | func (s *Script) Watch(ctx context.Context, req *api.WatchRequest) (*api.WatchResponse, error) { 54 | return scripts_svc.Script().Watch(ctx, req) 55 | } 56 | 57 | // Stop 停止脚本 58 | func (s *Script) Stop(ctx context.Context, req *api.StopRequest) (*api.StopResponse, error) { 59 | return scripts_svc.Script().Stop(ctx, req) 60 | } 61 | -------------------------------------------------------------------------------- /internal/controller/scripts_ctr/value.go: -------------------------------------------------------------------------------- 1 | package scripts_ctr 2 | 3 | import ( 4 | "context" 5 | 6 | api "github.com/scriptscat/cloudcat/internal/api/scripts" 7 | "github.com/scriptscat/cloudcat/internal/service/scripts_svc" 8 | ) 9 | 10 | type Value struct { 11 | } 12 | 13 | func NewValue() *Value { 14 | return &Value{} 15 | } 16 | 17 | // ValueList 脚本值列表 18 | func (v *Value) ValueList(ctx context.Context, req *api.ValueListRequest) (*api.ValueListResponse, error) { 19 | return scripts_svc.Value().ValueList(ctx, req) 20 | } 21 | 22 | // SetValue 设置脚本值 23 | func (v *Value) SetValue(ctx context.Context, req *api.SetValueRequest) (*api.SetValueResponse, error) { 24 | return scripts_svc.Value().SetValue(ctx, req) 25 | } 26 | 27 | // DeleteValue 删除脚本值 28 | func (v *Value) DeleteValue(ctx context.Context, req *api.DeleteValueRequest) (*api.DeleteValueResponse, error) { 29 | return scripts_svc.Value().DeleteValue(ctx, req) 30 | } 31 | -------------------------------------------------------------------------------- /internal/model/entity/cookie_entity/cookie.go: -------------------------------------------------------------------------------- 1 | package cookie_entity 2 | 3 | import ( 4 | "fmt" 5 | "net/http" 6 | "time" 7 | ) 8 | 9 | type HttpCookie struct { 10 | Name string `json:"name"` 11 | Value string `json:"value"` 12 | 13 | Path string `json:"path"` // optional 14 | Domain string `json:"domain"` // optional 15 | Expires time.Time `json:"expires"` // optional 16 | 17 | ExpirationDate int64 `json:"expirationDate,omitempty"` // optional 到期时间戳 18 | 19 | // MaxAge=0 means no 'Max-Age' attribute specified. 20 | // MaxAge<0 means delete cookie now, equivalently 'Max-Age: 0' 21 | // MaxAge>0 means Max-Age attribute present and given in seconds 22 | MaxAge int `json:"max_age"` 23 | Secure bool `json:"secure"` 24 | HttpOnly bool `json:"http_only"` 25 | SameSite string `json:"same_site"` 26 | } 27 | 28 | type Cookie struct { 29 | StorageName string `json:"storage_name"` 30 | Host string `json:"host"` 31 | Cookies []*HttpCookie `json:"cookies"` 32 | Createtime int64 `json:"createtime"` 33 | } 34 | 35 | func (h *HttpCookie) ToCookie(cookie *http.Cookie) { 36 | h.Name = cookie.Name 37 | h.Value = cookie.Value 38 | h.Path = cookie.Path 39 | h.Domain = cookie.Domain 40 | h.Expires = cookie.Expires 41 | h.MaxAge = cookie.MaxAge 42 | h.Secure = cookie.Secure 43 | h.HttpOnly = cookie.HttpOnly 44 | //switch cookie.SameSite { 45 | //case http.SameSiteDefaultMode: 46 | // h.SameSite = "default" 47 | //case http.SameSiteLaxMode: 48 | // h.SameSite = "lax" 49 | //case http.SameSiteStrictMode: 50 | // h.SameSite = "strict" 51 | //case http.SameSiteNoneMode: 52 | // h.SameSite = "none" 53 | //default: 54 | // h.SameSite = "" 55 | //} 56 | } 57 | 58 | func (h *HttpCookie) ToHttpCookie() *http.Cookie { 59 | return &http.Cookie{ 60 | Name: h.Name, 61 | Value: h.Value, 62 | Path: h.Path, 63 | Domain: h.Domain, 64 | Expires: h.Expires, 65 | MaxAge: h.MaxAge, 66 | Secure: h.Secure, 67 | HttpOnly: h.HttpOnly, 68 | //SameSite: http.SameSiteDefaultMode, 69 | } 70 | } 71 | 72 | // ID returns the domain;path;name triple of e as an ID. 73 | func (e *HttpCookie) ID() string { 74 | return fmt.Sprintf("%s;%s;%s", e.Domain, e.Path, e.Name) 75 | } 76 | -------------------------------------------------------------------------------- /internal/model/entity/cookie_entity/cookiejar/jar.go: -------------------------------------------------------------------------------- 1 | // Copyright 2012 The Go Authors. All rights reserved. 2 | // Use of this source code is governed by a BSD-style 3 | // license that can be found in the LICENSE file. 4 | 5 | // Package cookiejar implements an in-memory RFC 6265-compliant http.CookieJar. 6 | package cookiejar 7 | 8 | import ( 9 | "errors" 10 | "fmt" 11 | "net" 12 | "net/http" 13 | "net/url" 14 | "sort" 15 | "strings" 16 | "sync" 17 | "time" 18 | ) 19 | 20 | // PublicSuffixList provides the public suffix of a domain. For example: 21 | // - the public suffix of "example.com" is "com", 22 | // - the public suffix of "foo1.foo2.foo3.co.uk" is "co.uk", and 23 | // - the public suffix of "bar.pvt.k12.ma.us" is "pvt.k12.ma.us". 24 | // 25 | // Implementations of PublicSuffixList must be safe for concurrent use by 26 | // multiple goroutines. 27 | // 28 | // An implementation that always returns "" is valid and may be useful for 29 | // testing but it is not secure: it means that the HTTP server for foo.com can 30 | // set a cookie for bar.com. 31 | // 32 | // A public suffix list implementation is in the package 33 | // golang.org/x/net/publicsuffix. 34 | type PublicSuffixList interface { 35 | // PublicSuffix returns the public suffix of domain. 36 | // 37 | // TODO: specify which of the caller and callee is responsible for IP 38 | // addresses, for leading and trailing dots, for case sensitivity, and 39 | // for IDN/Punycode. 40 | PublicSuffix(domain string) string 41 | 42 | // String returns a description of the source of this public suffix 43 | // list. The description will typically contain something like a time 44 | // stamp or version number. 45 | String() string 46 | } 47 | 48 | // Options are the options for creating a new Jar. 49 | type Options struct { 50 | // PublicSuffixList is the public suffix list that determines whether 51 | // an HTTP server can set a cookie for a domain. 52 | // 53 | // A nil value is valid and may be useful for testing but it is not 54 | // secure: it means that the HTTP server for foo.co.uk can set a cookie 55 | // for bar.co.uk. 56 | PublicSuffixList PublicSuffixList 57 | } 58 | 59 | // Jar implements the http.CookieJar interface from the net/http package. 60 | type Jar struct { 61 | psList PublicSuffixList 62 | 63 | // mu locks the remaining fields. 64 | mu sync.Mutex 65 | 66 | // entries is a set of entries, keyed by their eTLD+1 and subkeyed by 67 | // their name/domain/path. 68 | entries map[string]map[string]entry 69 | 70 | // nextSeqNum is the next sequence number assigned to a new cookie 71 | // created SetCookies. 72 | nextSeqNum uint64 73 | } 74 | 75 | // New returns a new cookie jar. A nil *Options is equivalent to a zero 76 | // Options. 77 | func New(o *Options) (*Jar, error) { 78 | jar := &Jar{ 79 | entries: make(map[string]map[string]entry), 80 | } 81 | if o != nil { 82 | jar.psList = o.PublicSuffixList 83 | } 84 | return jar, nil 85 | } 86 | 87 | // entry is the internal representation of a cookie. 88 | // 89 | // This struct type is not used outside of this package per se, but the exported 90 | // fields are those of RFC 6265. 91 | type entry struct { 92 | Name string 93 | Value string 94 | Domain string 95 | Path string 96 | SameSite string 97 | Secure bool 98 | HttpOnly bool 99 | Persistent bool 100 | HostOnly bool 101 | Expires time.Time 102 | Creation time.Time 103 | LastAccess time.Time 104 | 105 | // seqNum is a sequence number so that Cookies returns cookies in a 106 | // deterministic order, even for cookies that have equal Path length and 107 | // equal Creation time. This simplifies testing. 108 | seqNum uint64 109 | } 110 | 111 | // id returns the domain;path;name triple of e as an id. 112 | func (e *entry) id() string { 113 | return fmt.Sprintf("%s;%s;%s", e.Domain, e.Path, e.Name) 114 | } 115 | 116 | // shouldSend determines whether e's cookie qualifies to be included in a 117 | // request to host/path. It is the caller's responsibility to check if the 118 | // cookie is expired. 119 | func (e *entry) shouldSend(https bool, host, path string) bool { 120 | return e.domainMatch(host) && e.pathMatch(path) && (https || !e.Secure) 121 | } 122 | 123 | // domainMatch checks whether e's Domain allows sending e back to host. 124 | // It differs from "domain-match" of RFC 6265 section 5.1.3 because we treat 125 | // a cookie with an IP address in the Domain always as a host cookie. 126 | func (e *entry) domainMatch(host string) bool { 127 | if e.Domain == host { 128 | return true 129 | } 130 | return !e.HostOnly && hasDotSuffix(host, e.Domain) 131 | } 132 | 133 | // pathMatch implements "path-match" according to RFC 6265 section 5.1.4. 134 | func (e *entry) pathMatch(requestPath string) bool { 135 | if requestPath == e.Path { 136 | return true 137 | } 138 | if strings.HasPrefix(requestPath, e.Path) { 139 | if e.Path[len(e.Path)-1] == '/' { 140 | return true // The "/any/" matches "/any/path" case. 141 | } else if requestPath[len(e.Path)] == '/' { 142 | return true // The "/any" matches "/any/path" case. 143 | } 144 | } 145 | return false 146 | } 147 | 148 | // hasDotSuffix reports whether s ends in "."+suffix. 149 | func hasDotSuffix(s, suffix string) bool { 150 | return len(s) > len(suffix) && s[len(s)-len(suffix)-1] == '.' && s[len(s)-len(suffix):] == suffix 151 | } 152 | 153 | // Cookies implements the Cookies method of the http.CookieJar interface. 154 | // 155 | // It returns an empty slice if the URL's scheme is not HTTP or HTTPS. 156 | func (j *Jar) Cookies(u *url.URL) (cookies []*http.Cookie) { 157 | return j.cookies(u, time.Now()) 158 | } 159 | 160 | // cookies is like Cookies but takes the current time as a parameter. 161 | func (j *Jar) cookies(u *url.URL, now time.Time) (cookies []*http.Cookie) { 162 | if u.Scheme != "http" && u.Scheme != "https" { 163 | return cookies 164 | } 165 | host, err := canonicalHost(u.Host) 166 | if err != nil { 167 | return cookies 168 | } 169 | key := jarKey(host, j.psList) 170 | 171 | j.mu.Lock() 172 | defer j.mu.Unlock() 173 | 174 | submap := j.entries[key] 175 | if submap == nil { 176 | return cookies 177 | } 178 | 179 | https := u.Scheme == "https" 180 | path := u.Path 181 | if path == "" { 182 | path = "/" 183 | } 184 | 185 | modified := false 186 | selected := make([]entry, 0) 187 | for id, e := range submap { 188 | if e.Persistent && !e.Expires.After(now) { 189 | delete(submap, id) 190 | modified = true 191 | continue 192 | } 193 | if !e.shouldSend(https, host, path) { 194 | continue 195 | } 196 | e.LastAccess = now 197 | submap[id] = e 198 | selected = append(selected, e) 199 | modified = true 200 | } 201 | if modified { 202 | if len(submap) == 0 { 203 | delete(j.entries, key) 204 | } else { 205 | j.entries[key] = submap 206 | } 207 | } 208 | 209 | // sort according to RFC 6265 section 5.4 point 2: by longest 210 | // path and then by earliest creation time. 211 | sort.Slice(selected, func(i, j int) bool { 212 | s := selected 213 | if len(s[i].Path) != len(s[j].Path) { 214 | return len(s[i].Path) > len(s[j].Path) 215 | } 216 | if ret := s[i].Creation.Compare(s[j].Creation); ret != 0 { 217 | return ret < 0 218 | } 219 | return s[i].seqNum < s[j].seqNum 220 | }) 221 | for _, e := range selected { 222 | cookies = append(cookies, &http.Cookie{Name: e.Name, Value: e.Value}) 223 | } 224 | 225 | return cookies 226 | } 227 | 228 | // SetCookies implements the SetCookies method of the http.CookieJar interface. 229 | // 230 | // It does nothing if the URL's scheme is not HTTP or HTTPS. 231 | func (j *Jar) SetCookies(u *url.URL, cookies []*http.Cookie) { 232 | j.setCookies(u, cookies, time.Now()) 233 | } 234 | 235 | // setCookies is like SetCookies but takes the current time as parameter. 236 | func (j *Jar) setCookies(u *url.URL, cookies []*http.Cookie, now time.Time) { 237 | if len(cookies) == 0 { 238 | return 239 | } 240 | if u.Scheme != "http" && u.Scheme != "https" { 241 | return 242 | } 243 | host, err := canonicalHost(u.Host) 244 | if err != nil { 245 | return 246 | } 247 | key := jarKey(host, j.psList) 248 | defPath := defaultPath(u.Path) 249 | 250 | j.mu.Lock() 251 | defer j.mu.Unlock() 252 | 253 | submap := j.entries[key] 254 | 255 | modified := false 256 | for _, cookie := range cookies { 257 | e, remove, err := j.newEntry(cookie, now, defPath, host) 258 | if err != nil { 259 | continue 260 | } 261 | id := e.id() 262 | if remove { 263 | if submap != nil { 264 | if _, ok := submap[id]; ok { 265 | delete(submap, id) 266 | modified = true 267 | } 268 | } 269 | continue 270 | } 271 | if submap == nil { 272 | submap = make(map[string]entry) 273 | } 274 | 275 | if old, ok := submap[id]; ok { 276 | e.Creation = old.Creation 277 | e.seqNum = old.seqNum 278 | } else { 279 | e.Creation = now 280 | e.seqNum = j.nextSeqNum 281 | j.nextSeqNum++ 282 | } 283 | e.LastAccess = now 284 | submap[id] = e 285 | modified = true 286 | } 287 | 288 | if modified { 289 | if len(submap) == 0 { 290 | delete(j.entries, key) 291 | } else { 292 | j.entries[key] = submap 293 | } 294 | } 295 | } 296 | 297 | // canonicalHost strips port from host if present and returns the canonicalized 298 | // host name. 299 | func canonicalHost(host string) (string, error) { 300 | var err error 301 | if hasPort(host) { 302 | host, _, err = net.SplitHostPort(host) 303 | if err != nil { 304 | return "", err 305 | } 306 | } 307 | // Strip trailing dot from fully qualified domain names. 308 | host = strings.TrimSuffix(host, ".") 309 | encoded, err := toASCII(host) 310 | if err != nil { 311 | return "", err 312 | } 313 | // We know this is ascii, no need to check. 314 | lower, _ := ToLower(encoded) 315 | return lower, nil 316 | } 317 | 318 | // hasPort reports whether host contains a port number. host may be a host 319 | // name, an IPv4 or an IPv6 address. 320 | func hasPort(host string) bool { 321 | colons := strings.Count(host, ":") 322 | if colons == 0 { 323 | return false 324 | } 325 | if colons == 1 { 326 | return true 327 | } 328 | return host[0] == '[' && strings.Contains(host, "]:") 329 | } 330 | 331 | // jarKey returns the key to use for a jar. 332 | func jarKey(host string, psl PublicSuffixList) string { 333 | if isIP(host) { 334 | return host 335 | } 336 | 337 | var i int 338 | if psl == nil { 339 | i = strings.LastIndex(host, ".") 340 | if i <= 0 { 341 | return host 342 | } 343 | } else { 344 | suffix := psl.PublicSuffix(host) 345 | if suffix == host { 346 | return host 347 | } 348 | i = len(host) - len(suffix) 349 | if i <= 0 || host[i-1] != '.' { 350 | // The provided public suffix list psl is broken. 351 | // Storing cookies under host is a safe stopgap. 352 | return host 353 | } 354 | // Only len(suffix) is used to determine the jar key from 355 | // here on, so it is okay if psl.PublicSuffix("www.buggy.psl") 356 | // returns "com" as the jar key is generated from host. 357 | } 358 | prevDot := strings.LastIndex(host[:i-1], ".") 359 | return host[prevDot+1:] 360 | } 361 | 362 | // isIP reports whether host is an IP address. 363 | func isIP(host string) bool { 364 | return net.ParseIP(host) != nil 365 | } 366 | 367 | // defaultPath returns the directory part of a URL's path according to 368 | // RFC 6265 section 5.1.4. 369 | func defaultPath(path string) string { 370 | if len(path) == 0 || path[0] != '/' { 371 | return "/" // Path is empty or malformed. 372 | } 373 | 374 | i := strings.LastIndex(path, "/") // Path starts with "/", so i != -1. 375 | if i == 0 { 376 | return "/" // Path has the form "/abc". 377 | } 378 | return path[:i] // Path is either of form "/abc/xyz" or "/abc/xyz/". 379 | } 380 | 381 | // newEntry creates an entry from an http.Cookie c. now is the current time and 382 | // is compared to c.Expires to determine deletion of c. defPath and host are the 383 | // default-path and the canonical host name of the URL c was received from. 384 | // 385 | // remove records whether the jar should delete this cookie, as it has already 386 | // expired with respect to now. In this case, e may be incomplete, but it will 387 | // be valid to call e.id (which depends on e's Name, Domain and Path). 388 | // 389 | // A malformed c.Domain will result in an error. 390 | func (j *Jar) newEntry(c *http.Cookie, now time.Time, defPath, host string) (e entry, remove bool, err error) { 391 | e.Name = c.Name 392 | 393 | if c.Path == "" || c.Path[0] != '/' { 394 | e.Path = defPath 395 | } else { 396 | e.Path = c.Path 397 | } 398 | 399 | e.Domain, e.HostOnly, err = j.domainAndType(host, c.Domain) 400 | if err != nil { 401 | return e, false, err 402 | } 403 | 404 | // MaxAge takes precedence over Expires. 405 | if c.MaxAge < 0 { 406 | return e, true, nil 407 | } else if c.MaxAge > 0 { 408 | e.Expires = now.Add(time.Duration(c.MaxAge) * time.Second) 409 | e.Persistent = true 410 | } else { 411 | if c.Expires.IsZero() { 412 | e.Expires = endOfTime 413 | e.Persistent = false 414 | } else { 415 | if !c.Expires.After(now) { 416 | return e, true, nil 417 | } 418 | e.Expires = c.Expires 419 | e.Persistent = true 420 | } 421 | } 422 | 423 | e.Value = c.Value 424 | e.Secure = c.Secure 425 | e.HttpOnly = c.HttpOnly 426 | 427 | switch c.SameSite { 428 | case http.SameSiteDefaultMode: 429 | e.SameSite = "SameSite" 430 | case http.SameSiteStrictMode: 431 | e.SameSite = "SameSite=Strict" 432 | case http.SameSiteLaxMode: 433 | e.SameSite = "SameSite=Lax" 434 | } 435 | 436 | return e, false, nil 437 | } 438 | 439 | var ( 440 | errIllegalDomain = errors.New("cookiejar: illegal cookie domain attribute") 441 | errMalformedDomain = errors.New("cookiejar: malformed cookie domain attribute") 442 | //errNoHostname = errors.New("cookiejar: no host name available (IP only)") 443 | ) 444 | 445 | // endOfTime is the time when session (non-persistent) cookies expire. 446 | // This instant is representable in most date/time formats (not just 447 | // Go's time.Time) and should be far enough in the future. 448 | var endOfTime = time.Date(9999, 12, 31, 23, 59, 59, 0, time.UTC) 449 | 450 | // domainAndType determines the cookie's domain and hostOnly attribute. 451 | func (j *Jar) domainAndType(host, domain string) (string, bool, error) { 452 | if domain == "" { 453 | // No domain attribute in the SetCookie header indicates a 454 | // host cookie. 455 | return host, true, nil 456 | } 457 | 458 | if isIP(host) { 459 | // RFC 6265 is not super clear here, a sensible interpretation 460 | // is that cookies with an IP address in the domain-attribute 461 | // are allowed. 462 | 463 | // RFC 6265 section 5.2.3 mandates to strip an optional leading 464 | // dot in the domain-attribute before processing the cookie. 465 | // 466 | // Most browsers don't do that for IP addresses, only curl 467 | // (version 7.54) and IE (version 11) do not reject a 468 | // Set-Cookie: a=1; domain=.127.0.0.1 469 | // This leading dot is optional and serves only as hint for 470 | // humans to indicate that a cookie with "domain=.bbc.co.uk" 471 | // would be sent to every subdomain of bbc.co.uk. 472 | // It just doesn't make sense on IP addresses. 473 | // The other processing and validation steps in RFC 6265 just 474 | // collapse to: 475 | if host != domain { 476 | return "", false, errIllegalDomain 477 | } 478 | 479 | // According to RFC 6265 such cookies should be treated as 480 | // domain cookies. 481 | // As there are no subdomains of an IP address the treatment 482 | // according to RFC 6265 would be exactly the same as that of 483 | // a host-only cookie. Contemporary browsers (and curl) do 484 | // allows such cookies but treat them as host-only cookies. 485 | // So do we as it just doesn't make sense to label them as 486 | // domain cookies when there is no domain; the whole notion of 487 | // domain cookies requires a domain name to be well defined. 488 | return host, true, nil 489 | } 490 | 491 | // From here on: If the cookie is valid, it is a domain cookie (with 492 | // the one exception of a public suffix below). 493 | // See RFC 6265 section 5.2.3. 494 | if domain[0] == '.' { 495 | domain = domain[1:] 496 | } 497 | 498 | if len(domain) == 0 || domain[0] == '.' { 499 | // Received either "Domain=." or "Domain=..some.thing", 500 | // both are illegal. 501 | return "", false, errMalformedDomain 502 | } 503 | 504 | domain, isASCII := ToLower(domain) 505 | if !isASCII { 506 | // Received non-ASCII domain, e.g. "perché.com" instead of "xn--perch-fsa.com" 507 | return "", false, errMalformedDomain 508 | } 509 | 510 | if domain[len(domain)-1] == '.' { 511 | // We received stuff like "Domain=www.example.com.". 512 | // Browsers do handle such stuff (actually differently) but 513 | // RFC 6265 seems to be clear here (e.g. section 4.1.2.3) in 514 | // requiring a reject. 4.1.2.3 is not normative, but 515 | // "Domain Matching" (5.1.3) and "Canonicalized Host Names" 516 | // (5.1.2) are. 517 | return "", false, errMalformedDomain 518 | } 519 | 520 | // See RFC 6265 section 5.3 #5. 521 | if j.psList != nil { 522 | if ps := j.psList.PublicSuffix(domain); ps != "" && !hasDotSuffix(domain, ps) { 523 | if host == domain { 524 | // This is the one exception in which a cookie 525 | // with a domain attribute is a host cookie. 526 | return host, true, nil 527 | } 528 | return "", false, errIllegalDomain 529 | } 530 | } 531 | 532 | // The domain must domain-match host: www.mycompany.com cannot 533 | // set cookies for .ourcompetitors.com. 534 | if host != domain && !hasDotSuffix(host, domain) { 535 | return "", false, errIllegalDomain 536 | } 537 | 538 | return domain, false, nil 539 | } 540 | -------------------------------------------------------------------------------- /internal/model/entity/cookie_entity/cookiejar/print.go: -------------------------------------------------------------------------------- 1 | // Copyright 2021 The Go Authors. All rights reserved. 2 | // Use of this source code is governed by a BSD-style 3 | // license that can be found in the LICENSE file. 4 | 5 | package cookiejar 6 | 7 | import ( 8 | "strings" 9 | "unicode" 10 | ) 11 | 12 | // EqualFold is strings.EqualFold, ASCII only. It reports whether s and t 13 | // are equal, ASCII-case-insensitively. 14 | func EqualFold(s, t string) bool { 15 | if len(s) != len(t) { 16 | return false 17 | } 18 | for i := 0; i < len(s); i++ { 19 | if lower(s[i]) != lower(t[i]) { 20 | return false 21 | } 22 | } 23 | return true 24 | } 25 | 26 | // lower returns the ASCII lowercase version of b. 27 | func lower(b byte) byte { 28 | if 'A' <= b && b <= 'Z' { 29 | return b + ('a' - 'A') 30 | } 31 | return b 32 | } 33 | 34 | // IsPrint returns whether s is ASCII and printable according to 35 | // https://tools.ietf.org/html/rfc20#section-4.2. 36 | func IsPrint(s string) bool { 37 | for i := 0; i < len(s); i++ { 38 | if s[i] < ' ' || s[i] > '~' { 39 | return false 40 | } 41 | } 42 | return true 43 | } 44 | 45 | // Is returns whether s is ASCII. 46 | func Is(s string) bool { 47 | for i := 0; i < len(s); i++ { 48 | if s[i] > unicode.MaxASCII { 49 | return false 50 | } 51 | } 52 | return true 53 | } 54 | 55 | // ToLower returns the lowercase version of s if s is ASCII and printable. 56 | func ToLower(s string) (lower string, ok bool) { 57 | if !IsPrint(s) { 58 | return "", false 59 | } 60 | return strings.ToLower(s), true 61 | } 62 | -------------------------------------------------------------------------------- /internal/model/entity/cookie_entity/cookiejar/punycode.go: -------------------------------------------------------------------------------- 1 | // Copyright 2012 The Go Authors. All rights reserved. 2 | // Use of this source code is governed by a BSD-style 3 | // license that can be found in the LICENSE file. 4 | 5 | package cookiejar 6 | 7 | // This file implements the Punycode algorithm from RFC 3492. 8 | 9 | import ( 10 | "fmt" 11 | "strings" 12 | "unicode/utf8" 13 | ) 14 | 15 | // These parameter values are specified in section 5. 16 | // 17 | // All computation is done with int32s, so that overflow behavior is identical 18 | // regardless of whether int is 32-bit or 64-bit. 19 | const ( 20 | base int32 = 36 21 | damp int32 = 700 22 | initialBias int32 = 72 23 | initialN int32 = 128 24 | skew int32 = 38 25 | tmax int32 = 26 26 | tmin int32 = 1 27 | ) 28 | 29 | // encode encodes a string as specified in section 6.3 and prepends prefix to 30 | // the result. 31 | // 32 | // The "while h < length(input)" line in the specification becomes "for 33 | // remaining != 0" in the Go code, because len(s) in Go is in bytes, not runes. 34 | func encode(prefix, s string) (string, error) { 35 | output := make([]byte, len(prefix), len(prefix)+1+2*len(s)) 36 | copy(output, prefix) 37 | delta, n, bias := int32(0), initialN, initialBias 38 | b, remaining := int32(0), int32(0) 39 | for _, r := range s { 40 | if r < utf8.RuneSelf { 41 | b++ 42 | output = append(output, byte(r)) 43 | } else { 44 | remaining++ 45 | } 46 | } 47 | h := b 48 | if b > 0 { 49 | output = append(output, '-') 50 | } 51 | for remaining != 0 { 52 | m := int32(0x7fffffff) 53 | for _, r := range s { 54 | if m > r && r >= n { 55 | m = r 56 | } 57 | } 58 | delta += (m - n) * (h + 1) 59 | if delta < 0 { 60 | return "", fmt.Errorf("cookiejar: invalid label %q", s) 61 | } 62 | n = m 63 | for _, r := range s { 64 | if r < n { 65 | delta++ 66 | if delta < 0 { 67 | return "", fmt.Errorf("cookiejar: invalid label %q", s) 68 | } 69 | continue 70 | } 71 | if r > n { 72 | continue 73 | } 74 | q := delta 75 | for k := base; ; k += base { 76 | t := k - bias 77 | if t < tmin { 78 | t = tmin 79 | } else if t > tmax { 80 | t = tmax 81 | } 82 | if q < t { 83 | break 84 | } 85 | output = append(output, encodeDigit(t+(q-t)%(base-t))) 86 | q = (q - t) / (base - t) 87 | } 88 | output = append(output, encodeDigit(q)) 89 | bias = adapt(delta, h+1, h == b) 90 | delta = 0 91 | h++ 92 | remaining-- 93 | } 94 | delta++ 95 | n++ 96 | } 97 | return string(output), nil 98 | } 99 | 100 | func encodeDigit(digit int32) byte { 101 | switch { 102 | case 0 <= digit && digit < 26: 103 | return byte(digit + 'a') 104 | case 26 <= digit && digit < 36: 105 | return byte(digit + ('0' - 26)) 106 | } 107 | panic("cookiejar: internal error in punycode encoding") 108 | } 109 | 110 | // adapt is the bias adaptation function specified in section 6.1. 111 | func adapt(delta, numPoints int32, firstTime bool) int32 { 112 | if firstTime { 113 | delta /= damp 114 | } else { 115 | delta /= 2 116 | } 117 | delta += delta / numPoints 118 | k := int32(0) 119 | for delta > ((base-tmin)*tmax)/2 { 120 | delta /= base - tmin 121 | k += base 122 | } 123 | return k + (base-tmin+1)*delta/(delta+skew) 124 | } 125 | 126 | // Strictly speaking, the remaining code below deals with IDNA (RFC 5890 and 127 | // friends) and not Punycode (RFC 3492) per se. 128 | 129 | // acePrefix is the ASCII Compatible Encoding prefix. 130 | const acePrefix = "xn--" 131 | 132 | // toASCII converts a domain or domain label to its ASCII form. For example, 133 | // toASCII("bücher.example.com") is "xn--bcher-kva.example.com", and 134 | // toASCII("golang") is "golang". 135 | func toASCII(s string) (string, error) { 136 | if Is(s) { 137 | return s, nil 138 | } 139 | labels := strings.Split(s, ".") 140 | for i, label := range labels { 141 | if !Is(label) { 142 | a, err := encode(acePrefix, label) 143 | if err != nil { 144 | return "", err 145 | } 146 | labels[i] = a 147 | } 148 | } 149 | return strings.Join(labels, "."), nil 150 | } 151 | -------------------------------------------------------------------------------- /internal/model/entity/cookie_entity/cookiejar/save.go: -------------------------------------------------------------------------------- 1 | package cookiejar 2 | 3 | import "github.com/scriptscat/cloudcat/internal/model/entity/cookie_entity" 4 | 5 | func (j *Jar) Import(cookies []*cookie_entity.HttpCookie) { 6 | 7 | } 8 | 9 | func (j *Jar) Export() map[string][]*cookie_entity.HttpCookie { 10 | 11 | return nil 12 | } 13 | -------------------------------------------------------------------------------- /internal/model/entity/cookie_entity/utils.go: -------------------------------------------------------------------------------- 1 | // Copyright 2012 The Go Authors. All rights reserved. 2 | // Use of this source code is governed by a BSD-style 3 | // license that can be found in the LICENSE file. 4 | 5 | // Package cookiejar implements an in-memory RFC 6265-compliant http.CookieJar. 6 | package cookie_entity 7 | 8 | import ( 9 | "fmt" 10 | "net" 11 | "net/http/cookiejar" 12 | "strings" 13 | "unicode" 14 | "unicode/utf8" 15 | ) 16 | 17 | // CanonicalHost strips port from host if present and returns the canonicalized 18 | // host name. 19 | func CanonicalHost(host string) (string, error) { 20 | var err error 21 | if hasPort(host) { 22 | host, _, err = net.SplitHostPort(host) 23 | if err != nil { 24 | return "", err 25 | } 26 | } 27 | // Strip trailing dot from fully qualified domain names. 28 | host = strings.TrimSuffix(host, ".") 29 | encoded, err := toASCII(host) 30 | if err != nil { 31 | return "", err 32 | } 33 | // We know this is ascii, no need to check. 34 | lower, _ := ToLower(encoded) 35 | return lower, nil 36 | } 37 | 38 | // hasPort reports whether host contains a port number. host may be a host 39 | // name, an IPv4 or an IPv6 address. 40 | func hasPort(host string) bool { 41 | colons := strings.Count(host, ":") 42 | if colons == 0 { 43 | return false 44 | } 45 | if colons == 1 { 46 | return true 47 | } 48 | return host[0] == '[' && strings.Contains(host, "]:") 49 | } 50 | 51 | // JarKey returns the key to use for a jar. 52 | func JarKey(host string, psl cookiejar.PublicSuffixList) string { 53 | if isIP(host) { 54 | return host 55 | } 56 | 57 | var i int 58 | if psl == nil { 59 | i = strings.LastIndex(host, ".") 60 | if i <= 0 { 61 | return host 62 | } 63 | } else { 64 | suffix := psl.PublicSuffix(host) 65 | if suffix == host { 66 | return host 67 | } 68 | i = len(host) - len(suffix) 69 | if i <= 0 || host[i-1] != '.' { 70 | // The provided public suffix list psl is broken. 71 | // Storing cookies under host is a safe stopgap. 72 | return host 73 | } 74 | // Only len(suffix) is used to determine the jar key from 75 | // here on, so it is okay if psl.PublicSuffix("www.buggy.psl") 76 | // returns "com" as the jar key is generated from host. 77 | } 78 | prevDot := strings.LastIndex(host[:i-1], ".") 79 | return host[prevDot+1:] 80 | } 81 | 82 | // isIP reports whether host is an IP address. 83 | func isIP(host string) bool { 84 | return net.ParseIP(host) != nil 85 | } 86 | 87 | // DefaultPath returns the directory part of a URL's path according to 88 | // RFC 6265 section 5.1.4. 89 | func DefaultPath(path string) string { 90 | if len(path) == 0 || path[0] != '/' { 91 | return "/" // Path is empty or malformed. 92 | } 93 | 94 | i := strings.LastIndex(path, "/") // Path starts with "/", so i != -1. 95 | if i == 0 { 96 | return "/" // Path has the form "/abc". 97 | } 98 | return path[:i] // Path is either of form "/abc/xyz" or "/abc/xyz/". 99 | } 100 | 101 | // toASCII converts a domain or domain label to its ASCII form. For example, 102 | // toASCII("bücher.example.com") is "xn--bcher-kva.example.com", and 103 | // toASCII("golang") is "golang". 104 | func toASCII(s string) (string, error) { 105 | if Is(s) { 106 | return s, nil 107 | } 108 | labels := strings.Split(s, ".") 109 | for i, label := range labels { 110 | if !Is(label) { 111 | a, err := encode(acePrefix, label) 112 | if err != nil { 113 | return "", err 114 | } 115 | labels[i] = a 116 | } 117 | } 118 | return strings.Join(labels, "."), nil 119 | } 120 | 121 | // Is returns whether s is ASCII. 122 | func Is(s string) bool { 123 | for i := 0; i < len(s); i++ { 124 | if s[i] > unicode.MaxASCII { 125 | return false 126 | } 127 | } 128 | return true 129 | } 130 | 131 | // These parameter values are specified in section 5. 132 | // 133 | // All computation is done with int32s, so that overflow behavior is identical 134 | // regardless of whether int is 32-bit or 64-bit. 135 | const ( 136 | base int32 = 36 137 | damp int32 = 700 138 | initialBias int32 = 72 139 | initialN int32 = 128 140 | skew int32 = 38 141 | tmax int32 = 26 142 | tmin int32 = 1 143 | ) 144 | 145 | // encode encodes a string as specified in section 6.3 and prepends prefix to 146 | // the result. 147 | // 148 | // The "while h < length(input)" line in the specification becomes "for 149 | // remaining != 0" in the Go code, because len(s) in Go is in bytes, not runes. 150 | func encode(prefix, s string) (string, error) { 151 | output := make([]byte, len(prefix), len(prefix)+1+2*len(s)) 152 | copy(output, prefix) 153 | delta, n, bias := int32(0), initialN, initialBias 154 | b, remaining := int32(0), int32(0) 155 | for _, r := range s { 156 | if r < utf8.RuneSelf { 157 | b++ 158 | output = append(output, byte(r)) 159 | } else { 160 | remaining++ 161 | } 162 | } 163 | h := b 164 | if b > 0 { 165 | output = append(output, '-') 166 | } 167 | for remaining != 0 { 168 | m := int32(0x7fffffff) 169 | for _, r := range s { 170 | if m > r && r >= n { 171 | m = r 172 | } 173 | } 174 | delta += (m - n) * (h + 1) 175 | if delta < 0 { 176 | return "", fmt.Errorf("cookiejar: invalid label %q", s) 177 | } 178 | n = m 179 | for _, r := range s { 180 | if r < n { 181 | delta++ 182 | if delta < 0 { 183 | return "", fmt.Errorf("cookiejar: invalid label %q", s) 184 | } 185 | continue 186 | } 187 | if r > n { 188 | continue 189 | } 190 | q := delta 191 | for k := base; ; k += base { 192 | t := k - bias 193 | if t < tmin { 194 | t = tmin 195 | } else if t > tmax { 196 | t = tmax 197 | } 198 | if q < t { 199 | break 200 | } 201 | output = append(output, encodeDigit(t+(q-t)%(base-t))) 202 | q = (q - t) / (base - t) 203 | } 204 | output = append(output, encodeDigit(q)) 205 | bias = adapt(delta, h+1, h == b) 206 | delta = 0 207 | h++ 208 | remaining-- 209 | } 210 | delta++ 211 | n++ 212 | } 213 | return string(output), nil 214 | } 215 | 216 | func encodeDigit(digit int32) byte { 217 | switch { 218 | case 0 <= digit && digit < 26: 219 | return byte(digit + 'a') 220 | case 26 <= digit && digit < 36: 221 | return byte(digit + ('0' - 26)) 222 | } 223 | panic("cookiejar: internal error in punycode encoding") 224 | } 225 | 226 | // adapt is the bias adaptation function specified in section 6.1. 227 | func adapt(delta, numPoints int32, firstTime bool) int32 { 228 | if firstTime { 229 | delta /= damp 230 | } else { 231 | delta /= 2 232 | } 233 | delta += delta / numPoints 234 | k := int32(0) 235 | for delta > ((base-tmin)*tmax)/2 { 236 | delta /= base - tmin 237 | k += base 238 | } 239 | return k + (base-tmin+1)*delta/(delta+skew) 240 | } 241 | 242 | // Strictly speaking, the remaining code below deals with IDNA (RFC 5890 and 243 | // friends) and not Punycode (RFC 3492) per se. 244 | 245 | // acePrefix is the ASCII Compatible Encoding prefix. 246 | const acePrefix = "xn--" 247 | 248 | // ToLower returns the lowercase version of s if s is ASCII and printable. 249 | func ToLower(s string) (lower string, ok bool) { 250 | if !IsPrint(s) { 251 | return "", false 252 | } 253 | return strings.ToLower(s), true 254 | } 255 | 256 | // IsPrint returns whether s is ASCII and printable according to 257 | // https://tools.ietf.org/html/rfc20#section-4.2. 258 | func IsPrint(s string) bool { 259 | for i := 0; i < len(s); i++ { 260 | if s[i] < ' ' || s[i] > '~' { 261 | return false 262 | } 263 | } 264 | return true 265 | } 266 | -------------------------------------------------------------------------------- /internal/model/entity/resource_entity/resource.go: -------------------------------------------------------------------------------- 1 | package resource_entity 2 | 3 | type Resource struct { 4 | URL string `json:"url"` 5 | Content string `json:"content"` 6 | Createtime int64 `json:"createtime"` 7 | Updatetime int64 `json:"updatetime"` 8 | } 9 | -------------------------------------------------------------------------------- /internal/model/entity/script_entity/script.go: -------------------------------------------------------------------------------- 1 | package script_entity 2 | 3 | import ( 4 | "time" 5 | 6 | "github.com/scriptscat/cloudcat/pkg/scriptcat" 7 | ) 8 | 9 | type Runtime string 10 | 11 | type ScriptState string 12 | 13 | const ( 14 | ScriptStateEnable ScriptState = "enable" 15 | ScriptStateDisable ScriptState = "disable" 16 | 17 | RuntimeScriptCat Runtime = "scriptcat" 18 | ) 19 | 20 | type Metadata map[string][]string 21 | 22 | type Status map[string]string 23 | 24 | const ( 25 | RunState = "runState" 26 | RunStateRunning = "running" 27 | RunStateComplete = "complete" 28 | 29 | ErrorMsg = "errorMsg" 30 | ) 31 | 32 | type Script struct { 33 | ID string `json:"id"` 34 | Name string `json:"name"` 35 | Code string `json:"code"` 36 | Runtime Runtime `json:"runtime"` 37 | Metadata Metadata `json:"metadata"` 38 | SelfMetadata Metadata `json:"self_metadata"` 39 | Status Status `json:"status"` 40 | State ScriptState `json:"state"` 41 | Createtime int64 `json:"createtime"` 42 | Updatetime int64 `json:"updatetime"` 43 | } 44 | 45 | func (s *Script) Create(script *scriptcat.Script) error { 46 | s.ID = script.ID 47 | s.Name = script.Metadata["name"][0] 48 | s.Code = script.Code 49 | s.Runtime = RuntimeScriptCat 50 | s.Metadata = Metadata(script.Metadata) 51 | s.Status = nil 52 | s.State = ScriptStateEnable 53 | s.Createtime = time.Now().Unix() 54 | return nil 55 | } 56 | 57 | func (s *Script) Update(script *scriptcat.Script) error { 58 | s.Name = script.Metadata["name"][0] 59 | s.Code = script.Code 60 | s.Runtime = RuntimeScriptCat 61 | s.Metadata = Metadata(script.Metadata) 62 | s.Updatetime = time.Now().Unix() 63 | return nil 64 | } 65 | 66 | func (s *Script) Scriptcat() *scriptcat.Script { 67 | return &scriptcat.Script{ 68 | ID: s.ID, 69 | Code: s.Code, 70 | Metadata: scriptcat.Metadata(s.Metadata), 71 | } 72 | } 73 | 74 | func (s *Script) StorageName() string { 75 | return StorageName(s.ID, s.Metadata) 76 | } 77 | 78 | func (s *Script) Crontab() (string, bool) { 79 | cron, ok := s.Metadata["crontab"] 80 | if ok { 81 | return cron[0], true 82 | } 83 | return "", false 84 | } 85 | 86 | func StorageName(id string, m Metadata) string { 87 | storageNames, ok := m["storageName"] 88 | if !ok { 89 | storageNames = []string{id} 90 | } 91 | return storageNames[0] 92 | } 93 | 94 | func (s Status) GetRunStatus() string { 95 | if s == nil { 96 | return "" 97 | } 98 | status, ok := s["runStatus"] 99 | if !ok { 100 | return RunStateComplete 101 | } 102 | return status 103 | } 104 | 105 | func (s Status) SetRunStatus(status string) { 106 | if s == nil { 107 | return 108 | } 109 | s["runStatus"] = status 110 | } 111 | -------------------------------------------------------------------------------- /internal/model/entity/script_entity/storage.go: -------------------------------------------------------------------------------- 1 | package script_entity 2 | 3 | type Storage struct { 4 | Name string `json:"name"` 5 | LinkScriptID []string `json:"link_script_id"` 6 | } 7 | -------------------------------------------------------------------------------- /internal/model/entity/token_entity/token.go: -------------------------------------------------------------------------------- 1 | package token_entity 2 | 3 | type Token struct { 4 | ID string `json:"id"` 5 | Token string `json:"token"` // jwt 6 | Secret string `json:"secret"` // jwt签名密钥 7 | DataEncryptionKey string `json:"data_encryption_key"` // 数据加密密钥 8 | Createtime int64 `json:"createtime"` 9 | Updatetime int64 `json:"updatetime"` 10 | } 11 | -------------------------------------------------------------------------------- /internal/model/entity/value_entity/value.go: -------------------------------------------------------------------------------- 1 | package value_entity 2 | 3 | import ( 4 | "encoding/json" 5 | ) 6 | 7 | type Value struct { 8 | StorageName string `json:"storage_name"` 9 | Key string `json:"key"` 10 | Value ValueString `json:"value"` 11 | Createtime int64 `json:"createtime"` 12 | } 13 | 14 | type ValueString struct { 15 | value []byte 16 | } 17 | 18 | func (v *ValueString) Set(value interface{}) error { 19 | var err error 20 | v.value, err = json.Marshal(value) 21 | if err != nil { 22 | return err 23 | } 24 | return nil 25 | } 26 | 27 | func (v *ValueString) Get() interface{} { 28 | var value interface{} 29 | err := json.Unmarshal(v.value, &value) 30 | if err != nil { 31 | return nil 32 | } 33 | return value 34 | } 35 | 36 | func (v *ValueString) MarshalJSON() ([]byte, error) { 37 | return v.value, nil 38 | } 39 | 40 | func (v *ValueString) UnmarshalJSON(data []byte) error { 41 | v.value = data 42 | return nil 43 | } 44 | -------------------------------------------------------------------------------- /internal/pkg/code/code.go: -------------------------------------------------------------------------------- 1 | package code 2 | 3 | // auth 4 | const ( 5 | TokenIsEmpty = iota + 100000 6 | TokenIsInvalid 7 | TokenIsExpired 8 | TokenNotFound 9 | ) 10 | 11 | // script 12 | const ( 13 | ErrResourceNotFound = iota + 101000 14 | ErrResourceMustID 15 | ErrResourceArgs 16 | ) 17 | 18 | // script 19 | const ( 20 | ScriptParseFailed = iota + 102000 21 | ScriptNotFound 22 | ScriptRuntimeNotFound 23 | ScriptAlreadyEnable 24 | ScriptAlreadyDisable 25 | ScriptStateError 26 | ScriptRunStateError 27 | 28 | StorageNameNotFound 29 | ) 30 | -------------------------------------------------------------------------------- /internal/pkg/code/zh_cn.go: -------------------------------------------------------------------------------- 1 | package code 2 | 3 | import "github.com/codfrm/cago/pkg/i18n" 4 | 5 | func init() { 6 | i18n.Register(i18n.DefaultLang, zhCN) 7 | } 8 | 9 | var zhCN = map[int]string{ 10 | TokenIsEmpty: "token不能为空", 11 | TokenIsInvalid: "token无效", 12 | TokenIsExpired: "token已过期", 13 | TokenNotFound: "token不存在", 14 | 15 | ErrResourceNotFound: "资源不存在", 16 | ErrResourceMustID: "必须输入资源id", 17 | ErrResourceArgs: "参数错误", 18 | 19 | ScriptParseFailed: "脚本解析失败", 20 | ScriptNotFound: "脚本不存在", 21 | ScriptRuntimeNotFound: "脚本运行时不存在", 22 | ScriptAlreadyEnable: "脚本已经启用", 23 | ScriptAlreadyDisable: "脚本已经禁用", 24 | ScriptStateError: "脚本状态错误", 25 | ScriptRunStateError: "脚本运行状态错误", 26 | 27 | StorageNameNotFound: "存储名称不存在", 28 | } 29 | -------------------------------------------------------------------------------- /internal/repository/cookie_repo/cookie.go: -------------------------------------------------------------------------------- 1 | package cookie_repo 2 | 3 | import ( 4 | "context" 5 | "encoding/json" 6 | "errors" 7 | 8 | "github.com/scriptscat/cloudcat/internal/model/entity/cookie_entity" 9 | "github.com/scriptscat/cloudcat/pkg/bbolt" 10 | bolt "go.etcd.io/bbolt" 11 | ) 12 | 13 | type CookieRepo interface { 14 | Find(ctx context.Context, storageName, host string) (*cookie_entity.Cookie, error) 15 | FindPage(ctx context.Context, storageName string) ([]*cookie_entity.Cookie, int64, error) 16 | Create(ctx context.Context, cookie *cookie_entity.Cookie) error 17 | Update(ctx context.Context, cookie *cookie_entity.Cookie) error 18 | Delete(ctx context.Context, storageName, host string) error 19 | 20 | DeleteByStorage(ctx context.Context, storageName string) error 21 | } 22 | 23 | var defaultCookie CookieRepo 24 | 25 | func Cookie() CookieRepo { 26 | return defaultCookie 27 | } 28 | 29 | func RegisterCookie(i CookieRepo) { 30 | defaultCookie = i 31 | } 32 | 33 | type cookieRepo struct { 34 | } 35 | 36 | func NewCookie() CookieRepo { 37 | return &cookieRepo{} 38 | } 39 | 40 | func (u *cookieRepo) Find(ctx context.Context, storageName, host string) (*cookie_entity.Cookie, error) { 41 | ret := &cookie_entity.Cookie{} 42 | if err := bbolt.Default().Update(func(tx *bolt.Tx) error { 43 | b, err := tx.Bucket([]byte("cookie")).CreateBucketIfNotExists([]byte(storageName)) 44 | if err != nil { 45 | return err 46 | } 47 | data := b.Get([]byte(host)) 48 | if data == nil { 49 | return bbolt.ErrNil 50 | } 51 | return json.Unmarshal(data, ret) 52 | }); err != nil { 53 | if bbolt.IsNil(err) { 54 | return nil, nil 55 | } 56 | return nil, err 57 | } 58 | return ret, nil 59 | } 60 | 61 | func (u *cookieRepo) Create(ctx context.Context, cookie *cookie_entity.Cookie) error { 62 | return bbolt.Default().Update(func(tx *bolt.Tx) error { 63 | b, err := tx.Bucket([]byte("cookie")).CreateBucketIfNotExists([]byte(cookie.StorageName)) 64 | if err != nil { 65 | return err 66 | } 67 | data, err := json.Marshal(cookie) 68 | if err != nil { 69 | return err 70 | } 71 | return b.Put([]byte(cookie.Host), data) 72 | }) 73 | } 74 | 75 | func (u *cookieRepo) Update(ctx context.Context, cookie *cookie_entity.Cookie) error { 76 | return bbolt.Default().Update(func(tx *bolt.Tx) error { 77 | b, err := tx.Bucket([]byte("cookie")).CreateBucketIfNotExists([]byte(cookie.StorageName)) 78 | if err != nil { 79 | return err 80 | } 81 | data, err := json.Marshal(cookie) 82 | if err != nil { 83 | return err 84 | } 85 | return b.Put([]byte(cookie.Host), data) 86 | }) 87 | } 88 | 89 | func (u *cookieRepo) Delete(ctx context.Context, storageName, host string) error { 90 | return bbolt.Default().Update(func(tx *bolt.Tx) error { 91 | b, err := tx.Bucket([]byte("cookie")).CreateBucketIfNotExists([]byte(storageName)) 92 | if err != nil { 93 | return err 94 | } 95 | return b.Delete([]byte(host)) 96 | }) 97 | } 98 | 99 | func (u *cookieRepo) FindPage(ctx context.Context, storageName string) ([]*cookie_entity.Cookie, int64, error) { 100 | var list []*cookie_entity.Cookie 101 | if err := bbolt.Default().Update(func(tx *bolt.Tx) error { 102 | b, err := tx.Bucket([]byte("cookie")).CreateBucketIfNotExists([]byte(storageName)) 103 | if err != nil { 104 | return err 105 | } 106 | c := b.Cursor() 107 | for k, v := c.First(); k != nil; k, v = c.Next() { 108 | var cookie cookie_entity.Cookie 109 | if err := json.Unmarshal(v, &cookie); err != nil { 110 | return err 111 | } 112 | list = append(list, &cookie) 113 | } 114 | return nil 115 | }); err != nil { 116 | return nil, 0, err 117 | } 118 | return list, int64(len(list)), nil 119 | } 120 | 121 | func (u *cookieRepo) DeleteByStorage(ctx context.Context, storageName string) error { 122 | return bbolt.Default().Update(func(tx *bolt.Tx) error { 123 | err := tx.Bucket([]byte("cookie")).DeleteBucket([]byte(storageName)) 124 | if errors.Is(err, bolt.ErrBucketNotFound) { 125 | return nil 126 | } 127 | return err 128 | }) 129 | } 130 | -------------------------------------------------------------------------------- /internal/repository/resource_repo/resource.go: -------------------------------------------------------------------------------- 1 | package resource_repo 2 | 3 | import ( 4 | "context" 5 | "encoding/json" 6 | 7 | "github.com/scriptscat/cloudcat/internal/model/entity/resource_entity" 8 | "github.com/scriptscat/cloudcat/pkg/bbolt" 9 | bolt "go.etcd.io/bbolt" 10 | ) 11 | 12 | type ResourceRepo interface { 13 | Find(ctx context.Context, url string) (*resource_entity.Resource, error) 14 | FindPage(ctx context.Context) ([]*resource_entity.Resource, error) 15 | Create(ctx context.Context, resource *resource_entity.Resource) error 16 | Update(ctx context.Context, resource *resource_entity.Resource) error 17 | Delete(ctx context.Context, url string) error 18 | } 19 | 20 | var defaultResource ResourceRepo 21 | 22 | func Resource() ResourceRepo { 23 | return defaultResource 24 | } 25 | 26 | func RegisterResource(i ResourceRepo) { 27 | defaultResource = i 28 | } 29 | 30 | type resourceRepo struct { 31 | } 32 | 33 | func NewResource() ResourceRepo { 34 | return &resourceRepo{} 35 | } 36 | 37 | func (u *resourceRepo) Find(ctx context.Context, url string) (*resource_entity.Resource, error) { 38 | resource := &resource_entity.Resource{} 39 | if err := bbolt.Default().Update(func(tx *bolt.Tx) error { 40 | b := tx.Bucket([]byte("resource")) 41 | data := b.Get([]byte(url)) 42 | if data == nil { 43 | return bbolt.ErrNil 44 | } 45 | return json.Unmarshal(data, resource) 46 | }); err != nil { 47 | if bbolt.IsNil(err) { 48 | return nil, nil 49 | } 50 | return nil, err 51 | } 52 | return resource, nil 53 | } 54 | 55 | func (u *resourceRepo) Create(ctx context.Context, resource *resource_entity.Resource) error { 56 | return bbolt.Default().Update(func(tx *bolt.Tx) error { 57 | data, err := json.Marshal(resource) 58 | if err != nil { 59 | return err 60 | } 61 | return tx.Bucket([]byte("resource")).Put([]byte(resource.URL), data) 62 | }) 63 | } 64 | 65 | func (u *resourceRepo) Update(ctx context.Context, resource *resource_entity.Resource) error { 66 | return bbolt.Default().Update(func(tx *bolt.Tx) error { 67 | data, err := json.Marshal(resource) 68 | if err != nil { 69 | return err 70 | } 71 | return tx.Bucket([]byte("resource")).Put([]byte(resource.URL), data) 72 | }) 73 | } 74 | 75 | func (u *resourceRepo) Delete(ctx context.Context, url string) error { 76 | return bbolt.Default().Update(func(tx *bolt.Tx) error { 77 | return tx.Bucket([]byte("resource")).Delete([]byte(url)) 78 | }) 79 | } 80 | 81 | func (u *resourceRepo) FindPage(ctx context.Context) ([]*resource_entity.Resource, error) { 82 | list := make([]*resource_entity.Resource, 0) 83 | if err := bbolt.Default().View(func(tx *bolt.Tx) error { 84 | b := tx.Bucket([]byte("resource")) 85 | c := b.Cursor() 86 | for k, v := c.First(); k != nil; k, v = c.Next() { 87 | resource := &resource_entity.Resource{} 88 | if err := json.Unmarshal(v, resource); err != nil { 89 | return err 90 | } 91 | list = append(list, resource) 92 | } 93 | return nil 94 | }); err != nil { 95 | return nil, err 96 | } 97 | return list, nil 98 | } 99 | -------------------------------------------------------------------------------- /internal/repository/script_repo/script.go: -------------------------------------------------------------------------------- 1 | package script_repo 2 | 3 | import ( 4 | "context" 5 | "strings" 6 | 7 | "github.com/goccy/go-json" 8 | "github.com/scriptscat/cloudcat/pkg/bbolt" 9 | bolt "go.etcd.io/bbolt" 10 | 11 | "github.com/scriptscat/cloudcat/internal/model/entity/script_entity" 12 | ) 13 | 14 | type ScriptRepo interface { 15 | Find(ctx context.Context, id string) (*script_entity.Script, error) 16 | FindByPrefixID(ctx context.Context, prefix string) (*script_entity.Script, error) 17 | FindPage(ctx context.Context) ([]*script_entity.Script, error) 18 | Create(ctx context.Context, script *script_entity.Script) error 19 | Update(ctx context.Context, script *script_entity.Script) error 20 | Delete(ctx context.Context, id string) error 21 | 22 | FindByStorage(ctx context.Context, storageName string) ([]*script_entity.Script, error) 23 | StorageList(ctx context.Context) ([]*script_entity.Storage, error) 24 | } 25 | 26 | var defaultScript ScriptRepo 27 | 28 | func Script() ScriptRepo { 29 | return defaultScript 30 | } 31 | 32 | func RegisterScript(i ScriptRepo) { 33 | defaultScript = i 34 | } 35 | 36 | type scriptRepo struct { 37 | } 38 | 39 | func NewScript() ScriptRepo { 40 | return &scriptRepo{} 41 | } 42 | 43 | func (u *scriptRepo) Find(ctx context.Context, id string) (*script_entity.Script, error) { 44 | script := &script_entity.Script{} 45 | if err := bbolt.Default().View(func(tx *bolt.Tx) error { 46 | b := tx.Bucket([]byte("script")) 47 | data := b.Get([]byte(id)) 48 | if data == nil { 49 | return bbolt.ErrNil 50 | } 51 | return json.Unmarshal(data, script) 52 | }); err != nil { 53 | if bbolt.IsNil(err) { 54 | return nil, nil 55 | } 56 | return nil, err 57 | } 58 | return script, nil 59 | } 60 | 61 | func (u *scriptRepo) FindByPrefixID(ctx context.Context, prefix string) (*script_entity.Script, error) { 62 | script := &script_entity.Script{} 63 | if err := bbolt.Default().View(func(tx *bolt.Tx) error { 64 | b := tx.Bucket([]byte("script")) 65 | c := b.Cursor() 66 | for k, v := c.First(); k != nil; k, v = c.Next() { 67 | if strings.HasPrefix(string(k), prefix) { 68 | if err := json.Unmarshal(v, script); err != nil { 69 | return err 70 | } 71 | return nil 72 | } 73 | } 74 | return bbolt.ErrNil 75 | }); err != nil { 76 | if bbolt.IsNil(err) { 77 | return nil, nil 78 | } 79 | return nil, err 80 | } 81 | return script, nil 82 | } 83 | 84 | func (u *scriptRepo) Create(ctx context.Context, script *script_entity.Script) error { 85 | return bbolt.Default().Update(func(tx *bolt.Tx) error { 86 | data, err := json.Marshal(script) 87 | if err != nil { 88 | return err 89 | } 90 | return tx.Bucket([]byte("script")).Put([]byte(script.ID), data) 91 | }) 92 | } 93 | 94 | func (u *scriptRepo) Update(ctx context.Context, script *script_entity.Script) error { 95 | return bbolt.Default().Update(func(tx *bolt.Tx) error { 96 | data, err := json.Marshal(script) 97 | if err != nil { 98 | return err 99 | } 100 | return tx.Bucket([]byte("script")).Put([]byte(script.ID), data) 101 | }) 102 | } 103 | 104 | func (u *scriptRepo) Delete(ctx context.Context, id string) error { 105 | return bbolt.Default().Update(func(tx *bolt.Tx) error { 106 | return tx.Bucket([]byte("script")).Delete([]byte(id)) 107 | }) 108 | } 109 | 110 | func (u *scriptRepo) FindPage(ctx context.Context) ([]*script_entity.Script, error) { 111 | list := make([]*script_entity.Script, 0) 112 | if err := bbolt.Default().View(func(tx *bolt.Tx) error { 113 | b := tx.Bucket([]byte("script")) 114 | c := b.Cursor() 115 | for k, v := c.First(); k != nil; k, v = c.Next() { 116 | script := &script_entity.Script{} 117 | if err := json.Unmarshal(v, script); err != nil { 118 | return err 119 | } 120 | list = append(list, script) 121 | } 122 | return nil 123 | }); err != nil { 124 | return nil, err 125 | } 126 | return list, nil 127 | } 128 | 129 | func (u *scriptRepo) FindByStorage(ctx context.Context, storageName string) ([]*script_entity.Script, error) { 130 | list, err := u.FindPage(ctx) 131 | if err != nil { 132 | return nil, err 133 | } 134 | ret := make([]*script_entity.Script, 0) 135 | for _, v := range list { 136 | if strings.HasPrefix(v.StorageName(), storageName) { 137 | ret = append(ret, v) 138 | } 139 | } 140 | return ret, nil 141 | } 142 | 143 | func (u *scriptRepo) StorageList(ctx context.Context) ([]*script_entity.Storage, error) { 144 | list, err := u.FindPage(ctx) 145 | if err != nil { 146 | return nil, err 147 | } 148 | m := make(map[string][]string) 149 | for _, v := range list { 150 | _, ok := m[v.StorageName()] 151 | if !ok { 152 | m[v.StorageName()] = make([]string, 0) 153 | } 154 | m[v.StorageName()] = append(m[v.StorageName()], v.ID) 155 | } 156 | ret := make([]*script_entity.Storage, 0) 157 | for k, v := range m { 158 | ret = append(ret, &script_entity.Storage{ 159 | Name: k, 160 | LinkScriptID: v, 161 | }) 162 | } 163 | return ret, nil 164 | } 165 | -------------------------------------------------------------------------------- /internal/repository/token_repo/token.go: -------------------------------------------------------------------------------- 1 | package token_repo 2 | 3 | import ( 4 | "context" 5 | "encoding/json" 6 | 7 | "github.com/scriptscat/cloudcat/internal/model/entity/token_entity" 8 | "github.com/scriptscat/cloudcat/pkg/bbolt" 9 | bolt "go.etcd.io/bbolt" 10 | ) 11 | 12 | type TokenRepo interface { 13 | Find(ctx context.Context, id string) (*token_entity.Token, error) 14 | FindPage(ctx context.Context) ([]*token_entity.Token, error) 15 | Create(ctx context.Context, token *token_entity.Token) error 16 | Update(ctx context.Context, token *token_entity.Token) error 17 | Delete(ctx context.Context, id string) error 18 | } 19 | 20 | var defaultToken TokenRepo 21 | 22 | func Token() TokenRepo { 23 | return defaultToken 24 | } 25 | 26 | func RegisterToken(i TokenRepo) { 27 | defaultToken = i 28 | } 29 | 30 | type tokenRepo struct { 31 | } 32 | 33 | func NewToken() TokenRepo { 34 | return &tokenRepo{} 35 | } 36 | 37 | func (u *tokenRepo) Find(ctx context.Context, id string) (*token_entity.Token, error) { 38 | token := &token_entity.Token{} 39 | if err := bbolt.Default().Update(func(tx *bolt.Tx) error { 40 | b := tx.Bucket([]byte("token")) 41 | data := b.Get([]byte(id)) 42 | if data == nil { 43 | return bbolt.ErrNil 44 | } 45 | return json.Unmarshal(data, token) 46 | }); err != nil { 47 | if bbolt.IsNil(err) { 48 | return nil, nil 49 | } 50 | return nil, err 51 | } 52 | return token, nil 53 | } 54 | 55 | func (u *tokenRepo) Create(ctx context.Context, token *token_entity.Token) error { 56 | return bbolt.Default().Update(func(tx *bolt.Tx) error { 57 | data, err := json.Marshal(token) 58 | if err != nil { 59 | return err 60 | } 61 | return tx.Bucket([]byte("token")).Put([]byte(token.ID), data) 62 | }) 63 | } 64 | 65 | func (u *tokenRepo) Update(ctx context.Context, token *token_entity.Token) error { 66 | return bbolt.Default().Update(func(tx *bolt.Tx) error { 67 | data, err := json.Marshal(token) 68 | if err != nil { 69 | return err 70 | } 71 | return tx.Bucket([]byte("token")).Put([]byte(token.ID), data) 72 | }) 73 | } 74 | 75 | func (u *tokenRepo) Delete(ctx context.Context, id string) error { 76 | return bbolt.Default().Update(func(tx *bolt.Tx) error { 77 | return tx.Bucket([]byte("token")).Delete([]byte(id)) 78 | }) 79 | } 80 | 81 | func (u *tokenRepo) FindPage(ctx context.Context) ([]*token_entity.Token, error) { 82 | list := make([]*token_entity.Token, 0) 83 | if err := bbolt.Default().View(func(tx *bolt.Tx) error { 84 | b := tx.Bucket([]byte("token")) 85 | c := b.Cursor() 86 | for k, v := c.First(); k != nil; k, v = c.Next() { 87 | token := &token_entity.Token{} 88 | if err := json.Unmarshal(v, token); err != nil { 89 | return err 90 | } 91 | list = append(list, token) 92 | } 93 | return nil 94 | }); err != nil { 95 | return nil, err 96 | } 97 | return list, nil 98 | } 99 | -------------------------------------------------------------------------------- /internal/repository/value_repo/value.go: -------------------------------------------------------------------------------- 1 | package value_repo 2 | 3 | import ( 4 | "context" 5 | "errors" 6 | 7 | "github.com/goccy/go-json" 8 | "github.com/scriptscat/cloudcat/internal/model/entity/value_entity" 9 | "github.com/scriptscat/cloudcat/pkg/bbolt" 10 | bolt "go.etcd.io/bbolt" 11 | ) 12 | 13 | type ValueRepo interface { 14 | Find(ctx context.Context, storageName, key string) (*value_entity.Value, error) 15 | FindPage(ctx context.Context, storageName string) ([]*value_entity.Value, int64, error) 16 | Create(ctx context.Context, value *value_entity.Value) error 17 | Update(ctx context.Context, value *value_entity.Value) error 18 | Delete(ctx context.Context, storageName, key string) error 19 | 20 | DeleteByStorage(ctx context.Context, storageName string) error 21 | } 22 | 23 | var defaultValue ValueRepo 24 | 25 | func Value() ValueRepo { 26 | return defaultValue 27 | } 28 | 29 | func RegisterValue(i ValueRepo) { 30 | defaultValue = i 31 | } 32 | 33 | type valueRepo struct { 34 | } 35 | 36 | func NewValue() ValueRepo { 37 | return &valueRepo{} 38 | } 39 | 40 | func (u *valueRepo) Find(ctx context.Context, storageName, key string) (*value_entity.Value, error) { 41 | value := &value_entity.Value{} 42 | if err := bbolt.Default().Update(func(tx *bolt.Tx) error { 43 | b, err := tx.Bucket([]byte("value")).CreateBucketIfNotExists([]byte(storageName)) 44 | if err != nil { 45 | return err 46 | } 47 | data := b.Get([]byte(key)) 48 | if data == nil { 49 | return bbolt.ErrNil 50 | } 51 | return json.Unmarshal(data, value) 52 | }); err != nil { 53 | if bbolt.IsNil(err) { 54 | return nil, nil 55 | } 56 | return nil, err 57 | } 58 | return value, nil 59 | } 60 | 61 | func (u *valueRepo) Create(ctx context.Context, value *value_entity.Value) error { 62 | return bbolt.Default().Update(func(tx *bolt.Tx) error { 63 | data, err := json.Marshal(value) 64 | if err != nil { 65 | return err 66 | } 67 | b, err := tx.Bucket([]byte("value")).CreateBucketIfNotExists([]byte(value.StorageName)) 68 | if err != nil { 69 | return err 70 | } 71 | return b.Put([]byte(value.Key), data) 72 | }) 73 | } 74 | 75 | func (u *valueRepo) Update(ctx context.Context, value *value_entity.Value) error { 76 | return bbolt.Default().Update(func(tx *bolt.Tx) error { 77 | data, err := json.Marshal(value) 78 | if err != nil { 79 | return err 80 | } 81 | b, err := tx.Bucket([]byte("value")).CreateBucketIfNotExists([]byte(value.StorageName)) 82 | if err != nil { 83 | return err 84 | } 85 | return b.Put([]byte(value.Key), data) 86 | }) 87 | } 88 | 89 | func (u *valueRepo) Delete(ctx context.Context, storageName, key string) error { 90 | return bbolt.Default().Update(func(tx *bolt.Tx) error { 91 | b, err := tx.Bucket([]byte("value")).CreateBucketIfNotExists([]byte(storageName)) 92 | if err != nil { 93 | return err 94 | } 95 | return b.Delete([]byte(key)) 96 | }) 97 | } 98 | 99 | func (u *valueRepo) DeleteByStorage(ctx context.Context, storageName string) error { 100 | return bbolt.Default().Update(func(tx *bolt.Tx) error { 101 | err := tx.Bucket([]byte("value")).DeleteBucket([]byte(storageName)) 102 | if errors.Is(err, bolt.ErrBucketNotFound) { 103 | return nil 104 | } 105 | return err 106 | }) 107 | } 108 | 109 | func (u *valueRepo) FindPage(ctx context.Context, storageName string) ([]*value_entity.Value, int64, error) { 110 | values := make([]*value_entity.Value, 0) 111 | if err := bbolt.Default().Update(func(tx *bolt.Tx) error { 112 | b, err := tx.Bucket([]byte("value")).CreateBucketIfNotExists([]byte(storageName)) 113 | if err != nil { 114 | return err 115 | } 116 | c := b.Cursor() 117 | for k, v := c.First(); k != nil; k, v = c.Next() { 118 | value := &value_entity.Value{} 119 | if err := json.Unmarshal(v, value); err != nil { 120 | return err 121 | } 122 | values = append(values, value) 123 | } 124 | return nil 125 | }); err != nil { 126 | return nil, 0, err 127 | } 128 | return values, int64(len(values)), nil 129 | } 130 | -------------------------------------------------------------------------------- /internal/service/auth_svc/token.go: -------------------------------------------------------------------------------- 1 | package auth_svc 2 | 3 | import ( 4 | "context" 5 | "io" 6 | "strings" 7 | "time" 8 | 9 | "github.com/codfrm/cago/pkg/i18n" 10 | "github.com/codfrm/cago/pkg/utils" 11 | "github.com/codfrm/cago/pkg/utils/httputils" 12 | "github.com/gin-gonic/gin" 13 | "github.com/golang-jwt/jwt/v5" 14 | api "github.com/scriptscat/cloudcat/internal/api/auth" 15 | "github.com/scriptscat/cloudcat/internal/model/entity/token_entity" 16 | "github.com/scriptscat/cloudcat/internal/pkg/code" 17 | "github.com/scriptscat/cloudcat/internal/repository/token_repo" 18 | utils2 "github.com/scriptscat/cloudcat/pkg/utils" 19 | ) 20 | 21 | type TokenSvc interface { 22 | // TokenList 获取token列表 23 | TokenList(ctx context.Context, req *api.TokenListRequest) (*api.TokenListResponse, error) 24 | // TokenCreate 创建token 25 | TokenCreate(ctx context.Context, req *api.TokenCreateRequest) (*api.TokenCreateResponse, error) 26 | // TokenDelete 删除token 27 | TokenDelete(ctx context.Context, req *api.TokenDeleteRequest) (*api.TokenDeleteResponse, error) 28 | // Middleware 验证token中间件 29 | Middleware() gin.HandlerFunc 30 | } 31 | 32 | type tokenSvc struct { 33 | } 34 | 35 | var defaultToken = &tokenSvc{} 36 | 37 | func Token() TokenSvc { 38 | return defaultToken 39 | } 40 | 41 | // TokenList 获取token列表 42 | func (s *tokenSvc) TokenList(ctx context.Context, req *api.TokenListRequest) (*api.TokenListResponse, error) { 43 | list, err := token_repo.Token().FindPage(ctx) 44 | if err != nil { 45 | return nil, err 46 | } 47 | resp := &api.TokenListResponse{ 48 | List: make([]*api.Token, 0), 49 | } 50 | for _, v := range list { 51 | if req.TokenID != "" && strings.HasPrefix(v.ID, req.TokenID) { 52 | continue 53 | } 54 | resp.List = append(resp.List, &api.Token{ 55 | ID: v.ID, 56 | Token: v.Token, 57 | DataEncryptionKey: v.DataEncryptionKey, 58 | Createtime: v.Createtime, 59 | Updatetime: v.Createtime, 60 | }) 61 | } 62 | return resp, nil 63 | } 64 | 65 | // TokenCreate 创建token 66 | func (s *tokenSvc) TokenCreate(ctx context.Context, req *api.TokenCreateRequest) (*api.TokenCreateResponse, error) { 67 | id := req.TokenID 68 | secret := utils.RandString(32, utils.Letter) 69 | token := jwt.NewWithClaims(jwt.SigningMethodHS256, jwt.RegisteredClaims{ 70 | Issuer: "cloudcat", 71 | Subject: "auth:" + id, 72 | ID: id, 73 | }) 74 | tokenString, err := token.SignedString([]byte(secret)) 75 | if err != nil { 76 | return nil, err 77 | } 78 | m := &token_entity.Token{ 79 | ID: id, 80 | Token: tokenString, 81 | Secret: secret, 82 | DataEncryptionKey: utils.RandString(32, utils.Letter), 83 | Createtime: time.Now().Unix(), 84 | Updatetime: time.Now().Unix(), 85 | } 86 | if err := token_repo.Token().Create(ctx, m); err != nil { 87 | return nil, err 88 | } 89 | return &api.TokenCreateResponse{ 90 | Token: &api.Token{ 91 | ID: m.ID, 92 | Token: m.Token, 93 | DataEncryptionKey: m.DataEncryptionKey, 94 | Createtime: m.Createtime, 95 | Updatetime: m.Createtime, 96 | }}, nil 97 | } 98 | 99 | // TokenDelete 删除token 100 | func (s *tokenSvc) TokenDelete(ctx context.Context, req *api.TokenDeleteRequest) (*api.TokenDeleteResponse, error) { 101 | err := token_repo.Token().Delete(ctx, req.TokenID) 102 | if err != nil { 103 | return nil, err 104 | } 105 | return nil, nil 106 | } 107 | 108 | // Middleware 验证token中间件 109 | func (s *tokenSvc) Middleware() gin.HandlerFunc { 110 | return func(c *gin.Context) { 111 | // 获取token 112 | tokenString := c.GetHeader("Authorization") 113 | if tokenString == "" { 114 | httputils.HandleResp(c, i18n.NewUnauthorizedError(c, code.TokenIsEmpty)) 115 | return 116 | } 117 | tokenString = strings.TrimPrefix(tokenString, "Bearer ") 118 | // 解析token 119 | var m *token_entity.Token 120 | _, err := jwt.ParseWithClaims(tokenString, &jwt.RegisteredClaims{}, func(token *jwt.Token) (interface{}, error) { 121 | _, ok := token.Method.(*jwt.SigningMethodHMAC) 122 | if !ok { 123 | return nil, i18n.NewUnauthorizedError(c, code.TokenIsInvalid) 124 | } 125 | claims := token.Claims.(*jwt.RegisteredClaims) 126 | var err error 127 | m, err = token_repo.Token().Find(c, claims.ID) 128 | if err != nil { 129 | return nil, err 130 | } 131 | if m == nil { 132 | return nil, i18n.NewUnauthorizedError(c, code.TokenIsInvalid) 133 | } 134 | return []byte(m.Secret), nil 135 | }) 136 | if err != nil { 137 | httputils.HandleResp(c, err) 138 | return 139 | } 140 | // 解密body 141 | oldBody := c.Request.Body 142 | r, err := utils2.NewAesDecrypt([]byte(m.DataEncryptionKey), c.Request.Body) 143 | if err != nil { 144 | httputils.HandleResp(c, err) 145 | return 146 | } 147 | c.Request.Body = utils2.WarpCloser(r, oldBody) 148 | c.Request.GetBody = func() (io.ReadCloser, error) { 149 | return c.Request.Body, nil 150 | } 151 | // 包装response 加密body 152 | warpW, err := newWarpWrite(c.Writer, m.DataEncryptionKey) 153 | if err != nil { 154 | httputils.HandleResp(c, err) 155 | return 156 | } 157 | c.Writer = warpW 158 | defer func() { 159 | _ = warpW.Close() 160 | }() 161 | c.Next() 162 | } 163 | } 164 | 165 | type warpWrite struct { 166 | gin.ResponseWriter 167 | done chan struct{} 168 | pr *io.PipeReader 169 | pw *io.PipeWriter 170 | aes io.Reader 171 | } 172 | 173 | func newWarpWrite(w gin.ResponseWriter, key string) (*warpWrite, error) { 174 | pr, pw := io.Pipe() 175 | aes, err := utils2.NewAesEncrypt([]byte(key), pr) 176 | if err != nil { 177 | return nil, err 178 | } 179 | done := make(chan struct{}) 180 | go func() { 181 | defer func() { 182 | close(done) 183 | _ = pr.Close() 184 | }() 185 | _, _ = io.Copy(w, aes) 186 | }() 187 | return &warpWrite{ 188 | ResponseWriter: w, 189 | done: done, 190 | pw: pw, 191 | pr: pr, 192 | aes: aes, 193 | }, nil 194 | } 195 | 196 | func (w *warpWrite) WriteString(s string) (int, error) { 197 | return w.Write([]byte(s)) 198 | } 199 | 200 | func (w *warpWrite) Write(p []byte) (int, error) { 201 | return w.pw.Write(p) 202 | } 203 | 204 | func (w *warpWrite) Close() error { 205 | if err := w.pw.Close(); err != nil { 206 | return err 207 | } 208 | <-w.done 209 | return nil 210 | } 211 | -------------------------------------------------------------------------------- /internal/service/scripts_svc/cookie.go: -------------------------------------------------------------------------------- 1 | package scripts_svc 2 | 3 | import ( 4 | "context" 5 | "time" 6 | 7 | "github.com/codfrm/cago/pkg/i18n" 8 | api "github.com/scriptscat/cloudcat/internal/api/scripts" 9 | "github.com/scriptscat/cloudcat/internal/model/entity/cookie_entity" 10 | "github.com/scriptscat/cloudcat/internal/pkg/code" 11 | "github.com/scriptscat/cloudcat/internal/repository/cookie_repo" 12 | "github.com/scriptscat/cloudcat/internal/repository/script_repo" 13 | ) 14 | 15 | type CookieSvc interface { 16 | // CookieList 脚本cookie列表 17 | CookieList(ctx context.Context, req *api.CookieListRequest) (*api.CookieListResponse, error) 18 | // DeleteCookie 删除cookie 19 | DeleteCookie(ctx context.Context, req *api.DeleteCookieRequest) (*api.DeleteCookieResponse, error) 20 | // SetCookie 设置cookie 21 | SetCookie(ctx context.Context, req *api.SetCookieRequest) (*api.SetCookieResponse, error) 22 | } 23 | 24 | type cookieSvc struct { 25 | } 26 | 27 | var defaultCookie = &cookieSvc{} 28 | 29 | func Cookie() CookieSvc { 30 | return defaultCookie 31 | } 32 | 33 | // CookieList 脚本cookie列表 34 | func (c *cookieSvc) CookieList(ctx context.Context, req *api.CookieListRequest) (*api.CookieListResponse, error) { 35 | scripts, err := script_repo.Script().FindByStorage(ctx, req.StorageName) 36 | if err != nil { 37 | return nil, err 38 | } 39 | if len(scripts) == 0 { 40 | return nil, i18n.NewNotFoundError(ctx, code.StorageNameNotFound) 41 | } 42 | script := scripts[0] 43 | list, _, err := cookie_repo.Cookie().FindPage(ctx, script.StorageName()) 44 | if err != nil { 45 | return nil, err 46 | } 47 | resp := &api.CookieListResponse{ 48 | List: make([]*api.Cookie, 0), 49 | } 50 | for _, v := range list { 51 | resp.List = append(resp.List, &api.Cookie{ 52 | StorageName: v.StorageName, 53 | Host: v.Host, 54 | Cookies: v.Cookies, 55 | Createtime: v.Createtime, 56 | }) 57 | } 58 | return resp, nil 59 | } 60 | 61 | // DeleteCookie 删除cookie 62 | func (c *cookieSvc) DeleteCookie(ctx context.Context, req *api.DeleteCookieRequest) (*api.DeleteCookieResponse, error) { 63 | scripts, err := script_repo.Script().FindByStorage(ctx, req.StorageName) 64 | if err != nil { 65 | return nil, err 66 | } 67 | if len(scripts) == 0 { 68 | return nil, i18n.NewNotFoundError(ctx, code.StorageNameNotFound) 69 | } 70 | if err := cookie_repo.Cookie().Delete(ctx, scripts[0].StorageName(), req.Host); err != nil { 71 | return nil, err 72 | } 73 | return nil, nil 74 | } 75 | 76 | // SetCookie 设置cookie 77 | func (c *cookieSvc) SetCookie(ctx context.Context, req *api.SetCookieRequest) (*api.SetCookieResponse, error) { 78 | scripts, err := script_repo.Script().FindByStorage(ctx, req.StorageName) 79 | if err != nil { 80 | return nil, err 81 | } 82 | if len(scripts) == 0 { 83 | return nil, i18n.NewNotFoundError(ctx, code.StorageNameNotFound) 84 | } 85 | cookiesMap := make(map[string][]*cookie_entity.HttpCookie) 86 | for _, v := range req.Cookies { 87 | host, err := cookie_entity.CanonicalHost(v.Domain) 88 | if err != nil { 89 | return nil, err 90 | } 91 | key := cookie_entity.JarKey(host, nil) 92 | _, ok := cookiesMap[key] 93 | if !ok { 94 | cookiesMap = make(map[string][]*cookie_entity.HttpCookie) 95 | } 96 | cookiesMap[key] = append(cookiesMap[key], v) 97 | } 98 | for key, v := range cookiesMap { 99 | model, err := cookie_repo.Cookie().Find(ctx, scripts[0].StorageName(), key) 100 | if err != nil { 101 | return nil, err 102 | } 103 | if model == nil { 104 | if err := cookie_repo.Cookie().Create(ctx, &cookie_entity.Cookie{ 105 | StorageName: scripts[0].StorageName(), 106 | Host: key, 107 | Cookies: v, 108 | Createtime: time.Now().Unix(), 109 | }); err != nil { 110 | return nil, err 111 | } 112 | } else { 113 | model.Cookies = mergeCookie(model.Cookies, v) 114 | if err := cookie_repo.Cookie().Update(ctx, model); err != nil { 115 | return nil, err 116 | } 117 | } 118 | } 119 | return nil, nil 120 | } 121 | 122 | func mergeCookie(oldCookie, newCookie []*cookie_entity.HttpCookie) []*cookie_entity.HttpCookie { 123 | cookieMap := make(map[string]*cookie_entity.HttpCookie) 124 | for _, v := range oldCookie { 125 | cookieMap[v.ID()] = v 126 | } 127 | for _, v := range newCookie { 128 | cookieMap[v.ID()] = v 129 | } 130 | cookies := make([]*cookie_entity.HttpCookie, 0) 131 | now := time.Now() 132 | for _, v := range cookieMap { 133 | if v.MaxAge < 0 { 134 | continue 135 | } else if v.MaxAge > 0 { 136 | if v.Expires.IsZero() { 137 | v.Expires = now.Add(time.Duration(v.MaxAge) * time.Second) 138 | } 139 | } else { 140 | if !v.Expires.IsZero() { 141 | if !v.Expires.After(now) { 142 | continue 143 | } 144 | } 145 | } 146 | cookies = append(cookies, v) 147 | } 148 | return cookies 149 | } 150 | -------------------------------------------------------------------------------- /internal/service/scripts_svc/gm.go: -------------------------------------------------------------------------------- 1 | package scripts_svc 2 | 3 | import ( 4 | "context" 5 | "io" 6 | "net/http" 7 | "net/http/cookiejar" 8 | "net/url" 9 | "sync" 10 | "time" 11 | 12 | "github.com/codfrm/cago/pkg/logger" 13 | "github.com/scriptscat/cloudcat/internal/model/entity/cookie_entity" 14 | "github.com/scriptscat/cloudcat/internal/model/entity/resource_entity" 15 | "github.com/scriptscat/cloudcat/internal/model/entity/value_entity" 16 | "github.com/scriptscat/cloudcat/internal/repository/cookie_repo" 17 | "github.com/scriptscat/cloudcat/internal/repository/resource_repo" 18 | "github.com/scriptscat/cloudcat/internal/repository/value_repo" 19 | "github.com/scriptscat/cloudcat/pkg/scriptcat" 20 | "github.com/scriptscat/cloudcat/pkg/scriptcat/plugin" 21 | "go.uber.org/zap" 22 | ) 23 | 24 | type GMPluginFunc struct { 25 | } 26 | 27 | func NewGMPluginFunc() plugin.GMPluginFunc { 28 | return &GMPluginFunc{} 29 | } 30 | 31 | func (g *GMPluginFunc) SetValue(ctx context.Context, script *scriptcat.Script, key string, value interface{}) error { 32 | model, err := value_repo.Value().Find(ctx, script.StorageName(), key) 33 | if err != nil { 34 | return err 35 | } 36 | if model == nil { 37 | model = &value_entity.Value{ 38 | StorageName: script.StorageName(), 39 | Key: key, 40 | Createtime: time.Now().Unix(), 41 | } 42 | err := model.Value.Set(value) 43 | if err != nil { 44 | return err 45 | } 46 | return value_repo.Value().Create(ctx, model) 47 | } 48 | if err := model.Value.Set(value); err != nil { 49 | return err 50 | } 51 | return value_repo.Value().Update(ctx, model) 52 | } 53 | 54 | func (g *GMPluginFunc) GetValue(ctx context.Context, script *scriptcat.Script, key string) (interface{}, error) { 55 | model, err := value_repo.Value().Find(ctx, script.StorageName(), key) 56 | if err != nil { 57 | return "", err 58 | } 59 | if model == nil { 60 | return "", nil 61 | } 62 | return model.Value.Get(), nil 63 | } 64 | 65 | func (g *GMPluginFunc) ListValue(ctx context.Context, script *scriptcat.Script) (map[string]interface{}, error) { 66 | list, _, err := value_repo.Value().FindPage(ctx, script.StorageName()) 67 | if err != nil { 68 | return nil, err 69 | } 70 | m := make(map[string]interface{}) 71 | for _, v := range list { 72 | m[v.Key] = v.Value.Get() 73 | } 74 | return m, nil 75 | } 76 | 77 | func (g *GMPluginFunc) DeleteValue(ctx context.Context, script *scriptcat.Script, key string) error { 78 | return value_repo.Value().Delete(ctx, script.StorageName(), key) 79 | } 80 | 81 | type cookieJar struct { 82 | sync.Mutex 83 | *cookiejar.Jar 84 | storageName string 85 | cookies map[string]map[string]*cookie_entity.HttpCookie 86 | } 87 | 88 | func (c *cookieJar) Cookies(u *url.URL) []*http.Cookie { 89 | cookies := c.Jar.Cookies(u) 90 | return cookies 91 | } 92 | 93 | func (c *cookieJar) SetCookies(u *url.URL, cookies []*http.Cookie) { 94 | c.Lock() 95 | defer c.Unlock() 96 | // 记录url 97 | host, err := cookie_entity.CanonicalHost(u.Host) 98 | if err != nil { 99 | return 100 | } 101 | key := cookie_entity.JarKey(host, nil) 102 | defPath := cookie_entity.DefaultPath(u.Path) 103 | submap, ok := c.cookies[key] 104 | if !ok { 105 | submap = make(map[string]*cookie_entity.HttpCookie) 106 | } 107 | for _, v := range cookies { 108 | cookie := &cookie_entity.HttpCookie{} 109 | cookie.ToCookie(v) 110 | if cookie.Path == "" || cookie.Path[0] != '/' { 111 | cookie.Path = defPath 112 | } 113 | if cookie.Domain != "" && cookie.Domain[0] != '.' { 114 | cookie.Domain = host 115 | } 116 | submap[cookie.ID()] = cookie 117 | } 118 | if len(submap) == 0 { 119 | delete(c.cookies, key) 120 | } else { 121 | c.cookies[key] = submap 122 | } 123 | // 设置cookie 124 | c.Jar.SetCookies(u, cookies) 125 | } 126 | 127 | func (c *cookieJar) Save(ctx context.Context) error { 128 | c.Lock() 129 | defer c.Unlock() 130 | for host, v := range c.cookies { 131 | saveCookies := make([]*cookie_entity.HttpCookie, 0) 132 | for _, v := range v { 133 | saveCookies = append(saveCookies, v) 134 | } 135 | model, err := cookie_repo.Cookie().Find(ctx, c.storageName, host) 136 | if err != nil { 137 | return err 138 | } 139 | if model == nil { 140 | if err := cookie_repo.Cookie().Create(ctx, &cookie_entity.Cookie{ 141 | StorageName: c.storageName, 142 | Host: host, 143 | Cookies: saveCookies, 144 | Createtime: time.Now().Unix(), 145 | }); err != nil { 146 | return err 147 | } 148 | } else { 149 | model.Cookies = mergeCookie(model.Cookies, saveCookies) 150 | if err := cookie_repo.Cookie().Update(ctx, model); err != nil { 151 | return err 152 | } 153 | } 154 | } 155 | c.cookies = make(map[string]map[string]*cookie_entity.HttpCookie) 156 | return nil 157 | } 158 | 159 | func (g *GMPluginFunc) Logger(ctx context.Context, script *scriptcat.Script) *zap.Logger { 160 | return logger.Ctx(ctx).With(zap.String("script_id", script.ID), 161 | zap.String("name", script.Metadata["name"][0])) 162 | } 163 | 164 | func (g *GMPluginFunc) LoadCookieJar(ctx context.Context, script *scriptcat.Script) (plugin.CookieJar, error) { 165 | jar, err := cookiejar.New(&cookiejar.Options{}) 166 | if err != nil { 167 | return nil, err 168 | } 169 | cookies, _, err := cookie_repo.Cookie().FindPage(ctx, script.StorageName()) 170 | if err != nil { 171 | return nil, err 172 | } 173 | for _, v := range cookies { 174 | u, err := url.Parse("https://" + v.Host) 175 | if err != nil { 176 | return nil, err 177 | } 178 | cookies := make([]*http.Cookie, 0) 179 | for _, v := range v.Cookies { 180 | if v.Domain != "" && v.Domain[0] != '.' { 181 | u, err := url.Parse("https://" + v.Domain) 182 | if err != nil { 183 | return nil, err 184 | } 185 | jar.SetCookies(u, []*http.Cookie{v.ToHttpCookie()}) 186 | continue 187 | } 188 | cookies = append(cookies, v.ToHttpCookie()) 189 | } 190 | jar.SetCookies(u, cookies) 191 | } 192 | return &cookieJar{ 193 | Jar: jar, 194 | storageName: script.StorageName(), 195 | cookies: make(map[string]map[string]*cookie_entity.HttpCookie), 196 | }, nil 197 | } 198 | 199 | func (g *GMPluginFunc) LoadResource(ctx context.Context, url string) (string, error) { 200 | // 从资源缓存中搜索是否存在 201 | resource, err := resource_repo.Resource().Find(ctx, url) 202 | if err != nil { 203 | return "", err 204 | } 205 | if resource != nil { 206 | return resource.Content, nil 207 | } 208 | // 从远程获取资源 209 | resp, err := http.Get(url) // #nosec 210 | if err != nil { 211 | return "", err 212 | } 213 | defer resp.Body.Close() 214 | // 保存资源 215 | content, err := io.ReadAll(resp.Body) 216 | if err != nil { 217 | return "", err 218 | } 219 | m := &resource_entity.Resource{ 220 | URL: url, 221 | Content: string(content), 222 | Createtime: time.Now().Unix(), 223 | Updatetime: time.Now().Unix(), 224 | } 225 | if err := resource_repo.Resource().Create(ctx, m); err != nil { 226 | return "", err 227 | } 228 | return m.Content, nil 229 | } 230 | -------------------------------------------------------------------------------- /internal/service/scripts_svc/script.go: -------------------------------------------------------------------------------- 1 | package scripts_svc 2 | 3 | import ( 4 | "context" 5 | "errors" 6 | "sync" 7 | "time" 8 | 9 | "github.com/scriptscat/cloudcat/pkg/scriptcat/plugin/window" 10 | "go.uber.org/zap/zapcore" 11 | 12 | "github.com/codfrm/cago/pkg/gogo" 13 | "github.com/scriptscat/cloudcat/internal/task/producer" 14 | 15 | "github.com/codfrm/cago/pkg/i18n" 16 | "github.com/codfrm/cago/pkg/logger" 17 | "github.com/robfig/cron/v3" 18 | api "github.com/scriptscat/cloudcat/internal/api/scripts" 19 | "github.com/scriptscat/cloudcat/internal/model/entity/script_entity" 20 | "github.com/scriptscat/cloudcat/internal/pkg/code" 21 | "github.com/scriptscat/cloudcat/internal/repository/script_repo" 22 | "github.com/scriptscat/cloudcat/pkg/scriptcat" 23 | "github.com/scriptscat/cloudcat/pkg/scriptcat/plugin" 24 | "go.uber.org/zap" 25 | ) 26 | 27 | type ScriptSvc interface { 28 | // List 脚本列表 29 | List(ctx context.Context, req *api.ListRequest) (*api.ListResponse, error) 30 | // Install 安装脚本 31 | Install(ctx context.Context, req *api.InstallRequest) (*api.InstallResponse, error) 32 | // Get 获取脚本 33 | Get(ctx context.Context, req *api.GetRequest) (*api.GetResponse, error) 34 | // Update 更新脚本 35 | Update(ctx context.Context, req *api.UpdateRequest) (*api.UpdateResponse, error) 36 | // Delete 删除脚本 37 | Delete(ctx context.Context, req *api.DeleteRequest) (*api.DeleteResponse, error) 38 | // StorageList 值储存空间列表 39 | StorageList(ctx context.Context, req *api.StorageListRequest) (*api.StorageListResponse, error) 40 | // Run 手动运行脚本 41 | Run(ctx context.Context, req *api.RunRequest) (*api.RunResponse, error) 42 | // Watch 监听脚本 43 | Watch(ctx context.Context, req *api.WatchRequest) (*api.WatchResponse, error) 44 | // Stop 停止脚本 45 | Stop(ctx context.Context, req *api.StopRequest) (*api.StopResponse, error) 46 | } 47 | 48 | type scriptSvc struct { 49 | sync.Mutex 50 | ctx map[string]context.CancelFunc 51 | cron *cron.Cron 52 | cronEntry map[string]cron.EntryID 53 | } 54 | 55 | var defaultScripts ScriptSvc 56 | 57 | func Script() ScriptSvc { 58 | return defaultScripts 59 | } 60 | 61 | func NewScript(ctx context.Context) (ScriptSvc, error) { 62 | svc := &scriptSvc{ 63 | cronEntry: make(map[string]cron.EntryID), 64 | ctx: make(map[string]context.CancelFunc), 65 | } 66 | svc.cron = cron.New(cron.WithSeconds()) 67 | svc.cron.Start() 68 | if err := gogo.Go(func(ctx context.Context) error { 69 | <-ctx.Done() 70 | svc.cron.Stop() 71 | logger.Ctx(ctx).Info("stop cron") 72 | return nil 73 | }, gogo.WithContext(ctx)); err != nil { 74 | return nil, err 75 | } 76 | // 初始化脚本运行环境 77 | scriptcat.RegisterRuntime(scriptcat.NewRuntime( 78 | logger.NewCtxLogger(logger.Default()), 79 | []scriptcat.Plugin{ 80 | window.NewBrowserPlugin(), 81 | plugin.NewGMPlugin(NewGMPluginFunc()), 82 | }, 83 | )) 84 | // 初始化运行脚本 85 | list, err := script_repo.Script().FindPage(ctx) 86 | if err != nil { 87 | return nil, err 88 | } 89 | for _, v := range list { 90 | if v.State == script_entity.ScriptStateEnable { 91 | script := v 92 | _ = gogo.Go(func(ctx context.Context) error { 93 | return svc.run(ctx, script) 94 | }) 95 | } 96 | } 97 | defaultScripts = svc 98 | return svc, nil 99 | } 100 | 101 | func (s *scriptSvc) runScript(ctx context.Context, script *script_entity.Script) error { 102 | s.Lock() 103 | ctx, cancel := context.WithCancel(ctx) 104 | s.ctx[script.ID] = cancel 105 | defer func(scriptId string) { 106 | s.Lock() 107 | defer s.Unlock() 108 | cancel() 109 | delete(s.ctx, scriptId) 110 | }(script.ID) 111 | s.Unlock() 112 | // 更新数据库中的状态 113 | script, err := script_repo.Script().Find(ctx, script.ID) 114 | if err != nil { 115 | return err 116 | } 117 | script.Status.SetRunStatus(script_entity.RunStateRunning) 118 | if err := script_repo.Script().Update(ctx, script); err != nil { 119 | return err 120 | } 121 | defer func() { 122 | script, err := script_repo.Script().Find(ctx, script.ID) 123 | if err == nil && script != nil { 124 | script.Status.SetRunStatus(script_entity.RunStateComplete) 125 | if err := script_repo.Script().Update(ctx, script); err != nil { 126 | logger.Ctx(ctx).Error("update script status error", zap.Error(err)) 127 | } 128 | } else { 129 | logger.Ctx(ctx).Error("find script error", zap.Error(err)) 130 | } 131 | }() 132 | with := logger.Ctx(ctx). 133 | With(zap.String("id", script.ID), zap.String("name", script.Name)). 134 | WithOptions(zap.Hooks(func(entry zapcore.Entry) error { 135 | return nil 136 | })) 137 | if _, err := scriptcat.RuntimeCat().Run(logger.ContextWithLogger(ctx, with), &scriptcat.Script{ 138 | ID: script.ID, 139 | Code: script.Code, 140 | Metadata: scriptcat.Metadata(script.Metadata), 141 | }); err != nil { 142 | with.Error("run script error", zap.Error(err)) 143 | return err 144 | } 145 | return nil 146 | } 147 | 148 | func (s *scriptSvc) run(ctx context.Context, script *script_entity.Script) error { 149 | logger := logger.Ctx(ctx). 150 | With(zap.String("id", script.ID), zap.String("name", script.Name)) 151 | // 判断是什么类型的脚本,如果是后台脚本,直接运行,定时脚本,添加定时任务 152 | if _, ok := script.Metadata["background"]; ok { 153 | if err := s.runScript(ctx, script); err != nil { 154 | logger.Error("run background script error", zap.Error(err)) 155 | return err 156 | } 157 | } else if cron, ok := script.Crontab(); ok { 158 | if err := s.addCron(ctx, script, cron); err != nil { 159 | logger.Error("add cron error", zap.Error(err)) 160 | return err 161 | } 162 | } else { 163 | logger.Error("script type error") 164 | return errors.New("script type error") 165 | } 166 | logger.Info("run script success") 167 | return nil 168 | } 169 | 170 | func (s *scriptSvc) addCron(ctx context.Context, script *script_entity.Script, c string) error { 171 | logger := logger.Ctx(ctx). 172 | With(zap.String("id", script.ID), zap.String("name", script.Name)) 173 | var err error 174 | c, err = scriptcat.ConvCron(c) 175 | if err != nil { 176 | return err 177 | } 178 | cronEntry, err := s.cron.AddFunc(c, func() { 179 | err := s.runScript(ctx, script) 180 | if err != nil { 181 | logger.Error("run cron script error", zap.Error(err)) 182 | } else { 183 | logger.Info("run cron script success") 184 | } 185 | }) 186 | if err != nil { 187 | return err 188 | } 189 | s.Lock() 190 | s.cronEntry[script.ID] = cronEntry 191 | s.Unlock() 192 | return nil 193 | } 194 | 195 | // List 脚本列表 196 | func (s *scriptSvc) List(ctx context.Context, req *api.ListRequest) (*api.ListResponse, error) { 197 | list, err := script_repo.Script().FindPage(ctx) 198 | if err != nil { 199 | return nil, err 200 | } 201 | resp := &api.ListResponse{ 202 | List: make([]*api.Script, 0), 203 | } 204 | for _, v := range list { 205 | resp.List = append(resp.List, &api.Script{ 206 | ID: v.ID, 207 | Name: v.Name, 208 | Metadata: v.Metadata, 209 | SelfMetadata: v.SelfMetadata, 210 | Status: v.Status, 211 | State: v.State, 212 | Createtime: v.Createtime, 213 | Updatetime: v.Updatetime, 214 | }) 215 | } 216 | return resp, nil 217 | } 218 | 219 | // Install 安装脚本 220 | func (s *scriptSvc) Install(ctx context.Context, req *api.InstallRequest) (*api.InstallResponse, error) { 221 | resp := &api.InstallResponse{ 222 | Scripts: make([]*api.Script, 0), 223 | } 224 | script, err := scriptcat.RuntimeCat().Parse(ctx, req.Code) 225 | if err != nil { 226 | return nil, err 227 | } 228 | // 根据id判断是否已经存在 229 | model, err := script_repo.Script().Find(ctx, script.ID) 230 | if err != nil { 231 | return nil, err 232 | } 233 | // 如果存在则更新 234 | if model != nil { 235 | if err := model.Update(script); err != nil { 236 | return nil, err 237 | } 238 | if err := script_repo.Script().Update(ctx, model); err != nil { 239 | return nil, err 240 | } 241 | // 如果已经是开启,那么重新启动 242 | if model.State == script_entity.ScriptStateEnable { 243 | _ = gogo.Go(func(ctx context.Context) error { 244 | s.disable(model) 245 | return s.run(context.Background(), model) 246 | }) 247 | } 248 | } else { 249 | model = &script_entity.Script{} 250 | if err := model.Create(script); err != nil { 251 | return nil, err 252 | } 253 | if err := script_repo.Script().Create(ctx, model); err != nil { 254 | return nil, err 255 | } 256 | // 开启 257 | _ = gogo.Go(func(ctx context.Context) error { 258 | return s.run(ctx, model) 259 | }) 260 | } 261 | if err := producer.PublishScriptUpdate(ctx, model); err != nil { 262 | return nil, err 263 | } 264 | return resp, nil 265 | } 266 | 267 | // Get 获取脚本 268 | func (s *scriptSvc) Get(ctx context.Context, req *api.GetRequest) (*api.GetResponse, error) { 269 | script, err := script_repo.Script().FindByPrefixID(ctx, req.ScriptID) 270 | if err != nil { 271 | return nil, err 272 | } 273 | if script == nil { 274 | return nil, i18n.NewNotFoundError(ctx, code.ScriptNotFound) 275 | } 276 | return &api.GetResponse{ 277 | Script: &api.Script{ 278 | ID: script.ID, 279 | Name: script.Name, 280 | Code: script.Code, 281 | Metadata: script.Metadata, 282 | Status: script.Status, 283 | State: script.State, 284 | Createtime: script.Createtime, 285 | Updatetime: script.Updatetime, 286 | }, 287 | }, nil 288 | } 289 | 290 | func (s *scriptSvc) stop(script *script_entity.Script) { 291 | s.Lock() 292 | defer s.Unlock() 293 | if cancel, ok := s.ctx[script.ID]; ok { 294 | cancel() 295 | delete(s.ctx, script.ID) 296 | } 297 | } 298 | 299 | func (s *scriptSvc) disable(script *script_entity.Script) { 300 | // 停止脚本 301 | s.Lock() 302 | defer s.Unlock() 303 | if cancel, ok := s.ctx[script.ID]; ok { 304 | cancel() 305 | delete(s.ctx, script.ID) 306 | } 307 | if eid, ok := s.cronEntry[script.ID]; ok { 308 | s.cron.Remove(eid) 309 | delete(s.cronEntry, script.ID) 310 | } 311 | } 312 | 313 | // Update 更新脚本 314 | func (s *scriptSvc) Update(ctx context.Context, req *api.UpdateRequest) (*api.UpdateResponse, error) { 315 | // 查出脚本 316 | model, err := script_repo.Script().FindByPrefixID(ctx, req.ScriptID) 317 | if err != nil { 318 | return nil, err 319 | } 320 | if model == nil { 321 | return nil, i18n.NewNotFoundError(ctx, code.ScriptNotFound) 322 | } 323 | if req.Script.Code != "" { 324 | script, err := scriptcat.RuntimeCat().Parse(ctx, req.Script.Code) 325 | if err != nil { 326 | return nil, err 327 | } 328 | if err := model.Update(script); err != nil { 329 | return nil, err 330 | } 331 | } 332 | if model.State != req.Script.State { 333 | model.State = req.Script.State 334 | switch req.Script.State { 335 | case script_entity.ScriptStateEnable: 336 | _ = gogo.Go(func(ctx context.Context) error { 337 | return s.run(ctx, model) 338 | }) 339 | case script_entity.ScriptStateDisable: 340 | go s.disable(model) 341 | default: 342 | return nil, i18n.NewError(ctx, code.ScriptStateError) 343 | } 344 | } else if model.Status.GetRunStatus() != req.Script.Status.GetRunStatus() { 345 | switch req.Script.Status.GetRunStatus() { 346 | case script_entity.RunStateRunning: 347 | _ = gogo.Go(func(ctx context.Context) error { 348 | return s.runScript(ctx, model) 349 | }) 350 | case script_entity.RunStateComplete: 351 | go s.stop(model) 352 | default: 353 | return nil, i18n.NewError(ctx, code.ScriptRunStateError) 354 | } 355 | } 356 | // 更新信息 357 | if req.Script.Metadata != nil { 358 | model.Metadata = req.Script.Metadata 359 | } 360 | if req.Script.SelfMetadata != nil { 361 | model.SelfMetadata = req.Script.SelfMetadata 362 | } 363 | if req.Script.Status != nil { 364 | model.Status = req.Script.Status 365 | } 366 | if req.Script.State != "" { 367 | model.State = req.Script.State 368 | } 369 | model.Updatetime = time.Now().Unix() 370 | if err := script_repo.Script().Update(ctx, model); err != nil { 371 | return nil, err 372 | } 373 | if err := producer.PublishScriptUpdate(ctx, model); err != nil { 374 | return nil, err 375 | } 376 | return nil, nil 377 | } 378 | 379 | // Delete 删除脚本 380 | func (s *scriptSvc) Delete(ctx context.Context, req *api.DeleteRequest) (*api.DeleteResponse, error) { 381 | // 查出脚本 382 | script, err := script_repo.Script().FindByPrefixID(ctx, req.ScriptID) 383 | if err != nil { 384 | return nil, err 385 | } 386 | if script == nil { 387 | return nil, i18n.NewNotFoundError(ctx, code.ScriptNotFound) 388 | } 389 | s.disable(script) 390 | if err := script_repo.Script().Delete(ctx, script.ID); err != nil { 391 | return nil, err 392 | } 393 | if err := producer.PublishScriptDelete(ctx, script); err != nil { 394 | return nil, err 395 | } 396 | return nil, nil 397 | } 398 | 399 | // StorageList 值储存空间列表 400 | func (s *scriptSvc) StorageList(ctx context.Context, req *api.StorageListRequest) (*api.StorageListResponse, error) { 401 | list, err := script_repo.Script().StorageList(ctx) 402 | if err != nil { 403 | return nil, err 404 | } 405 | resp := &api.StorageListResponse{ 406 | List: make([]*api.Storage, 0), 407 | } 408 | for _, v := range list { 409 | resp.List = append(resp.List, &api.Storage{ 410 | Name: v.Name, 411 | LinkScriptID: v.LinkScriptID, 412 | }) 413 | } 414 | return resp, nil 415 | } 416 | 417 | // Run 手动运行脚本 418 | func (s *scriptSvc) Run(ctx context.Context, req *api.RunRequest) (*api.RunResponse, error) { 419 | script, err := script_repo.Script().FindByPrefixID(ctx, req.ScriptID) 420 | if err != nil { 421 | return nil, err 422 | } 423 | if script == nil { 424 | return nil, i18n.NewNotFoundError(ctx, code.ScriptNotFound) 425 | } 426 | go func() { 427 | err = s.runScript(context.Background(), script) 428 | if err != nil { 429 | logger.Ctx(ctx).Error("run script error", zap.Error(err)) 430 | } 431 | }() 432 | return nil, nil 433 | } 434 | 435 | // Watch 监听脚本 436 | func (s *scriptSvc) Watch(ctx context.Context, req *api.WatchRequest) (*api.WatchResponse, error) { 437 | return nil, nil 438 | } 439 | 440 | // Stop 停止脚本 441 | func (s *scriptSvc) Stop(ctx context.Context, req *api.StopRequest) (*api.StopResponse, error) { 442 | script, err := script_repo.Script().FindByPrefixID(ctx, req.ScriptID) 443 | if err != nil { 444 | return nil, err 445 | } 446 | if script == nil { 447 | return nil, i18n.NewNotFoundError(ctx, code.ScriptNotFound) 448 | } 449 | s.stop(script) 450 | return nil, nil 451 | } 452 | -------------------------------------------------------------------------------- /internal/service/scripts_svc/value.go: -------------------------------------------------------------------------------- 1 | package scripts_svc 2 | 3 | import ( 4 | "context" 5 | "time" 6 | 7 | "github.com/scriptscat/cloudcat/internal/model/entity/value_entity" 8 | 9 | "github.com/codfrm/cago/pkg/i18n" 10 | "github.com/scriptscat/cloudcat/internal/pkg/code" 11 | "github.com/scriptscat/cloudcat/internal/repository/script_repo" 12 | 13 | api "github.com/scriptscat/cloudcat/internal/api/scripts" 14 | "github.com/scriptscat/cloudcat/internal/repository/value_repo" 15 | ) 16 | 17 | type ValueSvc interface { 18 | // ValueList 脚本值列表 19 | ValueList(ctx context.Context, req *api.ValueListRequest) (*api.ValueListResponse, error) 20 | // SetValue 设置脚本值 21 | SetValue(ctx context.Context, req *api.SetValueRequest) (*api.SetValueResponse, error) 22 | // DeleteValue 删除脚本值 23 | DeleteValue(ctx context.Context, req *api.DeleteValueRequest) (*api.DeleteValueResponse, error) 24 | } 25 | 26 | type valueSvc struct { 27 | } 28 | 29 | var defaultValue = &valueSvc{} 30 | 31 | func Value() ValueSvc { 32 | return defaultValue 33 | } 34 | 35 | // ValueList 脚本值列表 36 | func (v *valueSvc) ValueList(ctx context.Context, req *api.ValueListRequest) (*api.ValueListResponse, error) { 37 | scripts, err := script_repo.Script().FindByStorage(ctx, req.StorageName) 38 | if err != nil { 39 | return nil, err 40 | } 41 | if len(scripts) == 0 { 42 | return nil, i18n.NewNotFoundError(ctx, code.StorageNameNotFound) 43 | } 44 | script := scripts[0] 45 | list, _, err := value_repo.Value().FindPage(ctx, script.StorageName()) 46 | if err != nil { 47 | return nil, err 48 | } 49 | resp := &api.ValueListResponse{ 50 | List: make([]*api.Value, 0), 51 | } 52 | for _, v := range list { 53 | resp.List = append(resp.List, &api.Value{ 54 | StorageName: v.StorageName, 55 | Key: v.Key, 56 | Value: v.Value, 57 | Createtime: v.Createtime, 58 | }) 59 | } 60 | return resp, nil 61 | } 62 | 63 | // SetValue 设置脚本值 64 | func (v *valueSvc) SetValue(ctx context.Context, req *api.SetValueRequest) (*api.SetValueResponse, error) { 65 | scripts, err := script_repo.Script().FindByStorage(ctx, req.StorageName) 66 | if err != nil { 67 | return nil, err 68 | } 69 | if len(scripts) == 0 { 70 | return nil, i18n.NewNotFoundError(ctx, code.StorageNameNotFound) 71 | } 72 | script := scripts[0] 73 | for _, v := range req.Values { 74 | model, err := value_repo.Value().Find(ctx, script.StorageName(), v.Key) 75 | if err != nil { 76 | return nil, err 77 | } 78 | if model == nil { 79 | if err := value_repo.Value().Create(ctx, &value_entity.Value{ 80 | StorageName: script.StorageName(), 81 | Key: v.Key, 82 | Value: v.Value, 83 | Createtime: time.Now().Unix(), 84 | }); err != nil { 85 | return nil, err 86 | } 87 | } else { 88 | model.Value = v.Value 89 | if err := value_repo.Value().Update(ctx, model); err != nil { 90 | return nil, err 91 | } 92 | } 93 | } 94 | return nil, nil 95 | } 96 | 97 | // DeleteValue 删除脚本值 98 | func (v *valueSvc) DeleteValue(ctx context.Context, req *api.DeleteValueRequest) (*api.DeleteValueResponse, error) { 99 | scripts, err := script_repo.Script().FindByStorage(ctx, req.StorageName) 100 | if err != nil { 101 | return nil, err 102 | } 103 | if len(scripts) == 0 { 104 | return nil, i18n.NewNotFoundError(ctx, code.StorageNameNotFound) 105 | } 106 | script := scripts[0] 107 | if err := value_repo.Value().Delete(ctx, script.StorageName(), req.Key); err != nil { 108 | return nil, err 109 | } 110 | return nil, nil 111 | } 112 | -------------------------------------------------------------------------------- /internal/task/consumer/consumer.go: -------------------------------------------------------------------------------- 1 | package consumer 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/scriptscat/cloudcat/internal/task/consumer/subscribe" 7 | 8 | "github.com/codfrm/cago/configs" 9 | ) 10 | 11 | type Subscribe interface { 12 | Subscribe(ctx context.Context) error 13 | } 14 | 15 | // Consumer 消费者 16 | func Consumer(ctx context.Context, cfg *configs.Config) error { 17 | subscribers := []Subscribe{ 18 | &subscribe.Script{}, &subscribe.Value{}, 19 | } 20 | for _, v := range subscribers { 21 | if err := v.Subscribe(ctx); err != nil { 22 | return err 23 | } 24 | } 25 | return nil 26 | } 27 | -------------------------------------------------------------------------------- /internal/task/consumer/subscribe/resource.go: -------------------------------------------------------------------------------- 1 | package subscribe 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/codfrm/cago/pkg/broker/broker" 7 | "github.com/scriptscat/cloudcat/internal/model/entity/script_entity" 8 | "github.com/scriptscat/cloudcat/internal/repository/resource_repo" 9 | "github.com/scriptscat/cloudcat/internal/task/producer" 10 | ) 11 | 12 | type Resource struct { 13 | } 14 | 15 | func (v *Resource) Subscribe(ctx context.Context) error { 16 | if err := producer.SubscribeScriptUpdate(ctx, v.scriptUpdate, broker.Group("resource")); err != nil { 17 | return err 18 | } 19 | if err := producer.SubscribeScriptDelete(ctx, v.scriptDelete, broker.Group("resource")); err != nil { 20 | return err 21 | } 22 | return nil 23 | } 24 | 25 | // 消费脚本创建消息,根据meta信息进行分类 26 | func (v *Resource) scriptUpdate(ctx context.Context, script *script_entity.Script) error { 27 | // 删除相关resource 28 | return v.scriptDelete(ctx, script) 29 | } 30 | 31 | func (v *Resource) scriptDelete(ctx context.Context, script *script_entity.Script) error { 32 | // 删除相关resource 33 | for _, v := range script.Metadata["require"] { 34 | if err := resource_repo.Resource().Delete(ctx, v); err != nil { 35 | return err 36 | } 37 | } 38 | return nil 39 | } 40 | -------------------------------------------------------------------------------- /internal/task/consumer/subscribe/script.go: -------------------------------------------------------------------------------- 1 | package subscribe 2 | 3 | import ( 4 | "context" 5 | ) 6 | 7 | type Script struct { 8 | } 9 | 10 | func (s *Script) Subscribe(ctx context.Context) error { 11 | return nil 12 | } 13 | -------------------------------------------------------------------------------- /internal/task/consumer/subscribe/value.go: -------------------------------------------------------------------------------- 1 | package subscribe 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/codfrm/cago/pkg/broker/broker" 7 | "github.com/scriptscat/cloudcat/internal/model/entity/script_entity" 8 | "github.com/scriptscat/cloudcat/internal/repository/cookie_repo" 9 | "github.com/scriptscat/cloudcat/internal/repository/script_repo" 10 | "github.com/scriptscat/cloudcat/internal/repository/value_repo" 11 | "github.com/scriptscat/cloudcat/internal/task/producer" 12 | ) 13 | 14 | type Value struct { 15 | } 16 | 17 | func (v *Value) Subscribe(ctx context.Context) error { 18 | if err := producer.SubscribeScriptUpdate(ctx, v.scriptUpdate, broker.Group("value")); err != nil { 19 | return err 20 | } 21 | if err := producer.SubscribeScriptDelete(ctx, v.scriptDelete, broker.Group("value")); err != nil { 22 | return err 23 | } 24 | return nil 25 | } 26 | 27 | // 消费脚本创建消息,根据meta信息进行分类 28 | func (v *Value) scriptUpdate(ctx context.Context, script *script_entity.Script) error { 29 | return nil 30 | } 31 | 32 | func (v *Value) scriptDelete(ctx context.Context, script *script_entity.Script) error { 33 | // 查询还有哪些脚本使用了这个storage 34 | list, err := script_repo.Script().FindByStorage(ctx, script.StorageName()) 35 | if err != nil { 36 | return err 37 | } 38 | if len(list) == 0 { 39 | // 删除storageName下的value 40 | if err := value_repo.Value().DeleteByStorage(ctx, script.StorageName()); err != nil { 41 | return err 42 | } 43 | // 删除storageCookie下的value 44 | if err := cookie_repo.Cookie().DeleteByStorage(ctx, script.StorageName()); err != nil { 45 | return err 46 | } 47 | } 48 | return nil 49 | } 50 | -------------------------------------------------------------------------------- /internal/task/crontab/crontab.go: -------------------------------------------------------------------------------- 1 | package crontab 2 | 3 | import ( 4 | "github.com/codfrm/cago/server/cron" 5 | "github.com/scriptscat/cloudcat/internal/task/crontab/handler" 6 | ) 7 | 8 | type Cron interface { 9 | Crontab(c cron.Crontab) error 10 | } 11 | 12 | // Crontab 定时任务 13 | func Crontab(cron cron.Crontab) error { 14 | crontab := []Cron{&handler.Script{}} 15 | for _, v := range crontab { 16 | if err := v.Crontab(cron); err != nil { 17 | return err 18 | } 19 | } 20 | return nil 21 | } 22 | -------------------------------------------------------------------------------- /internal/task/crontab/handler/script.go: -------------------------------------------------------------------------------- 1 | package handler 2 | 3 | import ( 4 | "github.com/codfrm/cago/server/cron" 5 | ) 6 | 7 | type Script struct { 8 | } 9 | 10 | func (s *Script) Crontab(c cron.Crontab) error { 11 | return nil 12 | } 13 | -------------------------------------------------------------------------------- /internal/task/producer/script.go: -------------------------------------------------------------------------------- 1 | package producer 2 | 3 | import ( 4 | "context" 5 | "encoding/json" 6 | 7 | "github.com/scriptscat/cloudcat/internal/model/entity/script_entity" 8 | 9 | "github.com/codfrm/cago/pkg/broker" 10 | broker2 "github.com/codfrm/cago/pkg/broker/broker" 11 | ) 12 | 13 | // 脚本相关消息生产者 14 | 15 | type ScriptUpdateMsg struct { 16 | Script *script_entity.Script 17 | } 18 | 19 | func PublishScriptUpdate(ctx context.Context, script *script_entity.Script) error { 20 | // code过大, 不传递 21 | script.Code = "" 22 | body, err := json.Marshal(&ScriptUpdateMsg{ 23 | Script: script, 24 | }) 25 | if err != nil { 26 | return err 27 | } 28 | return broker.Default().Publish(ctx, ScriptUpdateTopic, &broker2.Message{ 29 | Body: body, 30 | }) 31 | } 32 | 33 | func ParseScriptUpdateMsg(msg *broker2.Message) (*ScriptUpdateMsg, error) { 34 | ret := &ScriptUpdateMsg{} 35 | if err := json.Unmarshal(msg.Body, ret); err != nil { 36 | return nil, err 37 | } 38 | return ret, nil 39 | } 40 | 41 | func SubscribeScriptUpdate(ctx context.Context, fn func(ctx context.Context, script *script_entity.Script) error, opts ...broker2.SubscribeOption) error { 42 | _, err := broker.Default().Subscribe(ctx, ScriptUpdateTopic, func(ctx context.Context, ev broker2.Event) error { 43 | m, err := ParseScriptUpdateMsg(ev.Message()) 44 | if err != nil { 45 | return err 46 | } 47 | return fn(ctx, m.Script) 48 | }, opts...) 49 | return err 50 | } 51 | 52 | type ScriptDeleteMsg struct { 53 | Script *script_entity.Script 54 | } 55 | 56 | func PublishScriptDelete(ctx context.Context, script *script_entity.Script) error { 57 | // code过大, 不传递 58 | script.Code = "" 59 | body, err := json.Marshal(&ScriptDeleteMsg{ 60 | Script: script, 61 | }) 62 | if err != nil { 63 | return err 64 | } 65 | return broker.Default().Publish(ctx, ScriptDeleteTopic, &broker2.Message{ 66 | Body: body, 67 | }) 68 | } 69 | 70 | func ParseScriptDeleteMsg(msg *broker2.Message) (*ScriptDeleteMsg, error) { 71 | ret := &ScriptDeleteMsg{} 72 | if err := json.Unmarshal(msg.Body, ret); err != nil { 73 | return nil, err 74 | } 75 | return ret, nil 76 | } 77 | 78 | func SubscribeScriptDelete(ctx context.Context, fn func(ctx context.Context, script *script_entity.Script) error, opts ...broker2.SubscribeOption) error { 79 | _, err := broker.Default().Subscribe(ctx, ScriptDeleteTopic, func(ctx context.Context, ev broker2.Event) error { 80 | m, err := ParseScriptDeleteMsg(ev.Message()) 81 | if err != nil { 82 | return err 83 | } 84 | return fn(ctx, m.Script) 85 | }) 86 | return err 87 | } 88 | -------------------------------------------------------------------------------- /internal/task/producer/topic.go: -------------------------------------------------------------------------------- 1 | package producer 2 | 3 | const ( 4 | ScriptUpdateTopic = "script.update" // 创建脚本 5 | ScriptDeleteTopic = "script.delete" // 删除脚本 6 | ) 7 | -------------------------------------------------------------------------------- /migrations/20230904.go: -------------------------------------------------------------------------------- 1 | package migrations 2 | 3 | import ( 4 | bbolt2 "github.com/scriptscat/cloudcat/pkg/bbolt" 5 | "go.etcd.io/bbolt" 6 | ) 7 | 8 | func T20230210() *bbolt2.Migration { 9 | return &bbolt2.Migration{ 10 | ID: "20230904", 11 | Migrate: func(db *bbolt.DB) error { 12 | return db.Update(func(tx *bbolt.Tx) error { 13 | if _, err := tx.CreateBucketIfNotExists([]byte("value")); err != nil { 14 | return err 15 | } 16 | if _, err := tx.CreateBucketIfNotExists([]byte("script")); err != nil { 17 | return err 18 | } 19 | if _, err := tx.CreateBucketIfNotExists([]byte("cookie")); err != nil { 20 | return err 21 | } 22 | if _, err := tx.CreateBucketIfNotExists([]byte("token")); err != nil { 23 | return err 24 | } 25 | if _, err := tx.CreateBucketIfNotExists([]byte("resource")); err != nil { 26 | return err 27 | } 28 | if _, err := tx.CreateBucketIfNotExists([]byte("logger")); err != nil { 29 | return err 30 | } 31 | return nil 32 | }) 33 | }, 34 | Rollback: func(tx *bbolt.DB) error { 35 | return nil 36 | }, 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /migrations/init.go: -------------------------------------------------------------------------------- 1 | package migrations 2 | 3 | import ( 4 | bbolt2 "github.com/scriptscat/cloudcat/pkg/bbolt" 5 | "go.etcd.io/bbolt" 6 | ) 7 | 8 | // RunMigrations 数据库迁移操作 9 | func RunMigrations(db *bbolt.DB) error { 10 | return run(db, 11 | T20230210, 12 | ) 13 | } 14 | 15 | func run(db *bbolt.DB, fs ...func() *bbolt2.Migration) error { 16 | ms := make([]*bbolt2.Migration, 0, len(fs)) 17 | for _, f := range fs { 18 | ms = append(ms, f()) 19 | } 20 | m := bbolt2.NewMigrate(db, ms...) 21 | if err := m.Migrate(); err != nil { 22 | return err 23 | } 24 | return nil 25 | } 26 | -------------------------------------------------------------------------------- /pkg/bbolt/bolt.go: -------------------------------------------------------------------------------- 1 | package bbolt 2 | 3 | import ( 4 | "context" 5 | "errors" 6 | "time" 7 | 8 | "github.com/codfrm/cago" 9 | "github.com/codfrm/cago/configs" 10 | "github.com/codfrm/cago/pkg/gogo" 11 | "github.com/codfrm/cago/pkg/logger" 12 | bolt "go.etcd.io/bbolt" 13 | "go.uber.org/zap" 14 | ) 15 | 16 | var ( 17 | ErrNil = errors.New("nil") 18 | ) 19 | 20 | func IsNil(err error) bool { 21 | return errors.Is(err, ErrNil) 22 | } 23 | 24 | var db *bolt.DB 25 | 26 | type Config struct { 27 | Path string `yaml:"path"` 28 | } 29 | 30 | func Bolt() cago.FuncComponent { 31 | return func(ctx context.Context, cfg *configs.Config) error { 32 | var err error 33 | config := &Config{} 34 | if err := cfg.Scan("db", config); err != nil { 35 | return err 36 | } 37 | db, err = bolt.Open(config.Path, 0600, &bolt.Options{ 38 | Timeout: 5 * time.Second, 39 | }) 40 | if err != nil { 41 | return err 42 | } 43 | if err := gogo.Go(func(ctx context.Context) error { 44 | <-ctx.Done() 45 | err := db.Close() 46 | if err != nil { 47 | logger.Ctx(ctx).Error("close leveldb err: %v", zap.Error(err)) 48 | } 49 | return nil 50 | }, gogo.WithContext(ctx)); err != nil { 51 | return err 52 | } 53 | return nil 54 | } 55 | } 56 | 57 | func Default() *bolt.DB { 58 | return db 59 | } 60 | 61 | type contextKey int 62 | 63 | const ( 64 | transactionKey contextKey = iota + 1 65 | ) 66 | 67 | func TxCtx(ctx context.Context) *bolt.Tx { 68 | tx, ok := ctx.Value(transactionKey).(*bolt.Tx) 69 | if ok { 70 | return tx 71 | } 72 | return nil 73 | } 74 | 75 | func Transaction(ctx context.Context, fn func(ctx context.Context, tx *bolt.Tx) error) error { 76 | return db.Update(func(tx *bolt.Tx) error { 77 | ctx = context.WithValue(ctx, transactionKey, tx) 78 | return fn(ctx, tx) 79 | }) 80 | } 81 | -------------------------------------------------------------------------------- /pkg/bbolt/migrations.go: -------------------------------------------------------------------------------- 1 | package bbolt 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | "sort" 7 | 8 | "go.etcd.io/bbolt" 9 | ) 10 | 11 | // MigrateFunc 数据库迁移函数 12 | type MigrateFunc func(db *bbolt.DB) error 13 | 14 | // RollbackFunc 数据库回滚函数 15 | type RollbackFunc func(db *bbolt.DB) error 16 | 17 | type Migration struct { 18 | ID string 19 | Migrate MigrateFunc 20 | Rollback RollbackFunc 21 | } 22 | 23 | type Migrate struct { 24 | db *bbolt.DB 25 | migrations []*Migration 26 | } 27 | 28 | func NewMigrate(db *bbolt.DB, migrations ...*Migration) *Migrate { 29 | return &Migrate{ 30 | db: db, 31 | migrations: migrations, 32 | } 33 | } 34 | 35 | func (m *Migrate) Migrate() error { 36 | // 获取所有的迁移记录 37 | records := make([]string, 0) 38 | if err := m.db.Update(func(tx *bbolt.Tx) error { 39 | b, err := tx.CreateBucketIfNotExists([]byte("migrations")) 40 | if err != nil { 41 | return err 42 | } 43 | return b.ForEach(func(k, v []byte) error { 44 | records = append(records, string(v)) 45 | return nil 46 | }) 47 | }); err != nil { 48 | return err 49 | } 50 | // 对比迁移记录和迁移函数 51 | if len(records) > len(m.migrations) { 52 | return errors.New("migrate records more than migrate functions") 53 | } 54 | // 排序 55 | sort.Strings(records) 56 | for n, record := range records { 57 | if record != m.migrations[n].ID { 58 | return fmt.Errorf("migrate id not match: %s != %s", record, m.migrations[n].ID) 59 | } 60 | } 61 | // 取出未迁移的函数 62 | migrations := m.migrations[len(records):] 63 | // 执行迁移 64 | for _, migration := range migrations { 65 | if err := migration.Migrate(m.db); err != nil { 66 | return err 67 | } 68 | } 69 | return nil 70 | } 71 | -------------------------------------------------------------------------------- /pkg/cloudcat_api/client.go: -------------------------------------------------------------------------------- 1 | package cloudcat_api 2 | 3 | import ( 4 | "context" 5 | "encoding/json" 6 | "fmt" 7 | "io" 8 | "net/http" 9 | 10 | "github.com/codfrm/cago/pkg/utils/httputils" 11 | "github.com/codfrm/cago/server/mux" 12 | "github.com/scriptscat/cloudcat/pkg/utils" 13 | ) 14 | 15 | type ConfigServer struct { 16 | BaseURL string `yaml:"baseURL"` 17 | } 18 | 19 | type ConfigUser struct { 20 | Name string `yaml:"name"` 21 | Token string `yaml:"token"` 22 | DataEncryptionKey string `yaml:"dataEncryptionKey"` 23 | } 24 | 25 | type Config struct { 26 | ApiVersion string `yaml:"apiVersion"` 27 | Server *ConfigServer `yaml:"server"` 28 | User *ConfigUser `yaml:"user"` 29 | } 30 | 31 | type Client struct { 32 | config *Config 33 | muxCli *mux.Client 34 | } 35 | 36 | func NewClient(config *Config) *Client { 37 | return &Client{ 38 | config: config, 39 | muxCli: mux.NewClient(config.Server.BaseURL + "/api/" + config.ApiVersion), 40 | } 41 | } 42 | 43 | func (c *Client) Do(ctx context.Context, req any, resp any, opts ...mux.ClientOption) error { 44 | httpReq, err := c.muxCli.Request(ctx, req, opts...) 45 | if err != nil { 46 | return err 47 | } 48 | httpReq.ContentLength = 0 49 | httpReq.Header.Add("Authorization", "Bearer "+c.config.User.Token) 50 | encrypt, err := utils.NewAesEncrypt([]byte(c.config.User.DataEncryptionKey), httpReq.Body) 51 | if err != nil { 52 | return err 53 | } 54 | httpReq.Body = io.NopCloser(encrypt) 55 | httpReq.GetBody = func() (io.ReadCloser, error) { 56 | return httpReq.Body, nil 57 | } 58 | // 请求 59 | httpResp, err := http.DefaultClient.Do(httpReq) 60 | if err != nil { 61 | return err 62 | } 63 | defer func() { 64 | _ = httpResp.Body.Close() 65 | }() 66 | // warp body解密 67 | r, err := utils.NewAesDecrypt([]byte(c.config.User.DataEncryptionKey), httpResp.Body) 68 | if err != nil { 69 | return err 70 | } 71 | b, err := io.ReadAll(r) 72 | if err != nil { 73 | return err 74 | } 75 | jsonResp := &httputils.JSONResponse{ 76 | Data: resp, 77 | } 78 | if err := json.Unmarshal(b, jsonResp); err != nil { 79 | return fmt.Errorf("json unmarshal error: %w", err) 80 | } 81 | if jsonResp.Code != 0 { 82 | return httputils.NewError(httpResp.StatusCode, jsonResp.Code, jsonResp.Msg) 83 | } 84 | return nil 85 | } 86 | 87 | var defaultClient *Client 88 | 89 | func SetDefaultClient(cli *Client) { 90 | defaultClient = cli 91 | } 92 | 93 | func DefaultClient() *Client { 94 | return defaultClient 95 | } 96 | -------------------------------------------------------------------------------- /pkg/cloudcat_api/cookie.go: -------------------------------------------------------------------------------- 1 | package cloudcat_api 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/scriptscat/cloudcat/internal/api/scripts" 7 | ) 8 | 9 | type Cookie struct { 10 | cli *Client 11 | } 12 | 13 | func NewCookie(cli *Client) *Cookie { 14 | return &Cookie{ 15 | cli: cli, 16 | } 17 | } 18 | 19 | func (s *Cookie) CookieList(ctx context.Context, req *scripts.CookieListRequest) (*scripts.CookieListResponse, error) { 20 | resp := &scripts.CookieListResponse{} 21 | if err := s.cli.Do(ctx, req, resp); err != nil { 22 | return resp, err 23 | } 24 | return resp, nil 25 | } 26 | 27 | func (s *Cookie) DeleteCookie(ctx context.Context, req *scripts.DeleteCookieRequest) (*scripts.DeleteCookieResponse, error) { 28 | resp := &scripts.DeleteCookieResponse{} 29 | if err := s.cli.Do(ctx, req, resp); err != nil { 30 | return resp, err 31 | } 32 | return resp, nil 33 | } 34 | 35 | func (s *Cookie) SetCookie(ctx context.Context, req *scripts.SetCookieRequest) (*scripts.SetCookieResponse, error) { 36 | resp := &scripts.SetCookieResponse{} 37 | if err := s.cli.Do(ctx, req, resp); err != nil { 38 | return resp, err 39 | } 40 | return resp, nil 41 | } 42 | -------------------------------------------------------------------------------- /pkg/cloudcat_api/script.go: -------------------------------------------------------------------------------- 1 | package cloudcat_api 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/scriptscat/cloudcat/internal/api/scripts" 7 | ) 8 | 9 | type Script struct { 10 | cli *Client 11 | } 12 | 13 | func NewScript(cli *Client) *Script { 14 | return &Script{ 15 | cli: cli, 16 | } 17 | } 18 | 19 | func (s *Script) List(ctx context.Context, req *scripts.ListRequest) (*scripts.ListResponse, error) { 20 | resp := &scripts.ListResponse{} 21 | if err := s.cli.Do(ctx, req, resp); err != nil { 22 | return resp, err 23 | } 24 | return resp, nil 25 | } 26 | 27 | func (s *Script) Install(ctx context.Context, req *scripts.InstallRequest) (*scripts.InstallResponse, error) { 28 | resp := &scripts.InstallResponse{} 29 | if err := s.cli.Do(ctx, req, resp); err != nil { 30 | return resp, err 31 | } 32 | return resp, nil 33 | } 34 | 35 | func (s *Script) Get(ctx context.Context, req *scripts.GetRequest) (*scripts.GetResponse, error) { 36 | resp := &scripts.GetResponse{} 37 | if err := s.cli.Do(ctx, req, resp); err != nil { 38 | return resp, err 39 | } 40 | return resp, nil 41 | } 42 | 43 | func (s *Script) Update(ctx context.Context, req *scripts.UpdateRequest) (*scripts.UpdateResponse, error) { 44 | resp := &scripts.UpdateResponse{} 45 | if err := s.cli.Do(ctx, req, resp); err != nil { 46 | return resp, err 47 | } 48 | return resp, nil 49 | } 50 | 51 | func (s *Script) Delete(ctx context.Context, req *scripts.DeleteRequest) (*scripts.DeleteResponse, error) { 52 | resp := &scripts.DeleteResponse{} 53 | if err := s.cli.Do(ctx, req, resp); err != nil { 54 | return resp, err 55 | } 56 | return resp, nil 57 | } 58 | 59 | func (s *Script) Run(ctx context.Context, req *scripts.RunRequest) (*scripts.RunResponse, error) { 60 | resp := &scripts.RunResponse{} 61 | if err := s.cli.Do(ctx, req, resp); err != nil { 62 | return resp, err 63 | } 64 | return resp, nil 65 | } 66 | 67 | func (s *Script) Stop(ctx context.Context, req *scripts.StopRequest) (*scripts.StopResponse, error) { 68 | resp := &scripts.StopResponse{} 69 | if err := s.cli.Do(ctx, req, resp); err != nil { 70 | return resp, err 71 | } 72 | return resp, nil 73 | } 74 | -------------------------------------------------------------------------------- /pkg/cloudcat_api/token.go: -------------------------------------------------------------------------------- 1 | package cloudcat_api 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/scriptscat/cloudcat/internal/api/auth" 7 | ) 8 | 9 | type Token struct { 10 | cli *Client 11 | } 12 | 13 | func NewToken(cli *Client) *Token { 14 | return &Token{ 15 | cli: cli, 16 | } 17 | } 18 | 19 | func (t *Token) Create(ctx context.Context, req *auth.TokenCreateRequest) (*auth.TokenCreateResponse, error) { 20 | resp := &auth.TokenCreateResponse{} 21 | if err := t.cli.Do(ctx, req, resp); err != nil { 22 | return resp, err 23 | } 24 | return resp, nil 25 | } 26 | 27 | func (t *Token) List(ctx context.Context, req *auth.TokenListRequest) (*auth.TokenListResponse, error) { 28 | resp := &auth.TokenListResponse{} 29 | if err := t.cli.Do(ctx, req, resp); err != nil { 30 | return resp, err 31 | } 32 | return resp, nil 33 | } 34 | 35 | func (t *Token) Delete(ctx context.Context, req *auth.TokenDeleteRequest) (*auth.TokenDeleteResponse, error) { 36 | resp := &auth.TokenDeleteResponse{} 37 | if err := t.cli.Do(ctx, req, resp); err != nil { 38 | return resp, err 39 | } 40 | return resp, nil 41 | } 42 | -------------------------------------------------------------------------------- /pkg/cloudcat_api/value.go: -------------------------------------------------------------------------------- 1 | package cloudcat_api 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/scriptscat/cloudcat/internal/api/scripts" 7 | ) 8 | 9 | type Value struct { 10 | cli *Client 11 | } 12 | 13 | func NewValue(cli *Client) *Value { 14 | return &Value{ 15 | cli: cli, 16 | } 17 | } 18 | 19 | func (s *Value) ValueList(ctx context.Context, req *scripts.ValueListRequest) (*scripts.ValueListResponse, error) { 20 | resp := &scripts.ValueListResponse{} 21 | if err := s.cli.Do(ctx, req, resp); err != nil { 22 | return resp, err 23 | } 24 | return resp, nil 25 | } 26 | 27 | func (s *Value) SetValue(ctx context.Context, req *scripts.SetValueRequest) (*scripts.SetValueResponse, error) { 28 | resp := &scripts.SetValueResponse{} 29 | if err := s.cli.Do(ctx, req, resp); err != nil { 30 | return resp, err 31 | } 32 | return resp, nil 33 | } 34 | 35 | func (s *Value) DeleteValue(ctx context.Context, req *scripts.DeleteValueRequest) (*scripts.DeleteValueResponse, error) { 36 | resp := &scripts.DeleteValueResponse{} 37 | if err := s.cli.Do(ctx, req, resp); err != nil { 38 | return resp, err 39 | } 40 | return resp, nil 41 | } 42 | -------------------------------------------------------------------------------- /pkg/scriptcat/model.go: -------------------------------------------------------------------------------- 1 | package scriptcat 2 | 3 | type Metadata map[string][]string 4 | 5 | type Script struct { 6 | ID string `json:"id"` 7 | Code string `json:"code"` 8 | Metadata Metadata `json:"metadata"` 9 | } 10 | 11 | func (s *Script) StorageName() string { 12 | storageNames, ok := s.Metadata["storageName"] 13 | if !ok { 14 | storageNames = []string{s.ID} 15 | } 16 | return storageNames[0] 17 | } 18 | -------------------------------------------------------------------------------- /pkg/scriptcat/options.go: -------------------------------------------------------------------------------- 1 | package scriptcat 2 | 3 | type RunOptions struct { 4 | resultCallback func(result interface{}, err error) 5 | } 6 | 7 | type RunOption func(*RunOptions) 8 | 9 | func NewRunOptions(opts ...RunOption) *RunOptions { 10 | options := &RunOptions{} 11 | for _, o := range opts { 12 | o(options) 13 | } 14 | return options 15 | } 16 | 17 | func WithResultCallback(callback func(result interface{}, err error)) RunOption { 18 | return func(options *RunOptions) { 19 | options.resultCallback = callback 20 | } 21 | } 22 | 23 | func (o *RunOptions) ResultCallback(result interface{}, err error) { 24 | if o.resultCallback != nil { 25 | o.resultCallback(result, err) 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /pkg/scriptcat/plugin.go: -------------------------------------------------------------------------------- 1 | package scriptcat 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/dop251/goja" 7 | ) 8 | 9 | type Plugin interface { 10 | // BeforeRun 运行前 11 | BeforeRun(ctx context.Context, script *Script, runtime *goja.Runtime) error 12 | // AfterRun 运行后 13 | AfterRun(ctx context.Context, script *Script, runtime *goja.Runtime) error 14 | } 15 | -------------------------------------------------------------------------------- /pkg/scriptcat/plugin/gm.go: -------------------------------------------------------------------------------- 1 | package plugin 2 | 3 | import ( 4 | "context" 5 | "errors" 6 | "net/http" 7 | 8 | "github.com/codfrm/cago/pkg/logger" 9 | "github.com/dop251/goja" 10 | "github.com/goccy/go-json" 11 | "github.com/scriptscat/cloudcat/pkg/scriptcat" 12 | "go.uber.org/zap" 13 | ) 14 | 15 | type CookieJar interface { 16 | http.CookieJar 17 | Save(ctx context.Context) error 18 | } 19 | 20 | type GMPluginFunc interface { 21 | SetValue(ctx context.Context, script *scriptcat.Script, key string, value interface{}) error 22 | GetValue(ctx context.Context, script *scriptcat.Script, key string) (interface{}, error) 23 | ListValue(ctx context.Context, script *scriptcat.Script) (map[string]interface{}, error) 24 | DeleteValue(ctx context.Context, script *scriptcat.Script, key string) error 25 | 26 | Logger(ctx context.Context, script *scriptcat.Script) *zap.Logger 27 | 28 | LoadCookieJar(ctx context.Context, script *scriptcat.Script) (CookieJar, error) 29 | 30 | LoadResource(ctx context.Context, url string) (string, error) 31 | } 32 | 33 | type grantFunc func(ctx context.Context, script *scriptcat.Script, runtime *goja.Runtime) (func(call goja.FunctionCall) goja.Value, error) 34 | 35 | type ctxCancel struct { 36 | ctx context.Context 37 | cancel context.CancelFunc 38 | } 39 | 40 | // GMPlugin gm 函数插件 41 | type GMPlugin struct { 42 | logger *logger.CtxLogger 43 | gmFunc GMPluginFunc 44 | grantMap map[string]grantFunc 45 | ctxMap map[string]*ctxCancel 46 | } 47 | 48 | func NewGMPlugin(storage GMPluginFunc) scriptcat.Plugin { 49 | p := &GMPlugin{ 50 | logger: logger.NewCtxLogger(logger.Default()).With(zap.String("plugin", "GMPlugin")), 51 | gmFunc: storage, 52 | ctxMap: make(map[string]*ctxCancel), 53 | } 54 | p.grantMap = map[string]grantFunc{ 55 | "GM_xmlhttpRequest": p.xmlHttpRequest, 56 | "GM_setValue": p.setValue, 57 | "GM_getValue": p.getValue, 58 | "GM_log": p.log, 59 | "GM_notification": p.empty, 60 | } 61 | return p 62 | } 63 | 64 | func (g *GMPlugin) Name() string { 65 | return "GMPlugin" 66 | } 67 | 68 | func (g *GMPlugin) Version() string { 69 | return "0.0.1" 70 | } 71 | 72 | func (g *GMPlugin) BeforeRun(ctx context.Context, script *scriptcat.Script, runtime *goja.Runtime) error { 73 | // 根据meta注入gm函数 74 | ctx, cancel := context.WithCancel(ctx) 75 | g.ctxMap[script.ID] = &ctxCancel{ 76 | ctx: ctx, 77 | cancel: cancel, 78 | } 79 | // 注入require 80 | for _, v := range script.Metadata["require"] { 81 | s, err := g.gmFunc.LoadResource(ctx, v) 82 | if err != nil { 83 | logger.Ctx(ctx).Error("load resource error", zap.Error(err)) 84 | } else { 85 | _, err := runtime.RunString(s) 86 | if err != nil { 87 | var e *goja.Exception 88 | if errors.As(err, &e) { 89 | logger.Ctx(ctx).Error("run script exception error", 90 | zap.String("error", e.Value().String())) 91 | } else { 92 | logger.Ctx(ctx).Error("run script error", zap.Error(err)) 93 | } 94 | return err 95 | } 96 | } 97 | } 98 | // 默认注入GM_log 99 | defaultGrant := []string{"GM_log"} 100 | for _, v := range defaultGrant { 101 | f, err := g.grantMap[v](ctx, script, runtime) 102 | if err != nil { 103 | return err 104 | } 105 | if err := runtime.Set(v, f); err != nil { 106 | return err 107 | } 108 | } 109 | for _, grant := range script.Metadata["grant"] { 110 | g, ok := g.grantMap[grant] 111 | if !ok { 112 | continue 113 | } 114 | f, err := g(ctx, script, runtime) 115 | if err != nil { 116 | return err 117 | } 118 | if err := runtime.Set(grant, f); err != nil { 119 | return err 120 | } 121 | } 122 | return nil 123 | } 124 | 125 | func (g *GMPlugin) AfterRun(ctx context.Context, script *scriptcat.Script, runtime *goja.Runtime) error { 126 | if v, ok := g.ctxMap[script.ID]; ok { 127 | v.cancel() 128 | delete(g.ctxMap, script.ID) 129 | } 130 | return nil 131 | } 132 | 133 | func (g *GMPlugin) log(ctx context.Context, script *scriptcat.Script, runtime *goja.Runtime) (func(call goja.FunctionCall) goja.Value, error) { 134 | return func(call goja.FunctionCall) goja.Value { 135 | msg := "" 136 | level := "info" 137 | labels := make([]zap.Field, 0) 138 | if len(call.Arguments) >= 1 { 139 | msg = call.Argument(0).String() 140 | if len(call.Arguments) >= 2 { 141 | level = call.Argument(1).String() 142 | if len(call.Arguments) >= 3 { 143 | b, err := json.Marshal(call.Argument(2)) 144 | if err == nil { 145 | labels = append(labels, zap.ByteString("labels", b)) 146 | } 147 | } 148 | } 149 | } 150 | 151 | logger := g.gmFunc.Logger(ctx, script) 152 | 153 | switch level { 154 | case "debug": 155 | logger.Debug(msg, labels...) 156 | case "info": 157 | logger.Info(msg, labels...) 158 | case "warn": 159 | logger.Warn(msg, labels...) 160 | case "error": 161 | logger.Error(msg, labels...) 162 | } 163 | 164 | return goja.Undefined() 165 | }, nil 166 | } 167 | 168 | func (g *GMPlugin) empty(ctx context.Context, script *scriptcat.Script, runtime *goja.Runtime) (func(call goja.FunctionCall) goja.Value, error) { 169 | return func(call goja.FunctionCall) goja.Value { 170 | logger.Ctx(ctx).Debug("empty function") 171 | return goja.Undefined() 172 | }, nil 173 | } 174 | -------------------------------------------------------------------------------- /pkg/scriptcat/plugin/gm_test.go: -------------------------------------------------------------------------------- 1 | package plugin 2 | 3 | import ( 4 | "fmt" 5 | "net/url" 6 | "testing" 7 | ) 8 | 9 | func TestName(t *testing.T) { 10 | t.Logf("A") 11 | a := "https://www.baidu.com/?q=你好&b=%E4%BD%A0%E5%A5%BD" 12 | u, _ := url.Parse(a) 13 | fmt.Println(u, u.String(), u.RequestURI(), u.Query().Encode()) 14 | t.Log(a) 15 | } 16 | -------------------------------------------------------------------------------- /pkg/scriptcat/plugin/value.go: -------------------------------------------------------------------------------- 1 | package plugin 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | 7 | "github.com/dop251/goja" 8 | "github.com/scriptscat/cloudcat/pkg/scriptcat" 9 | ) 10 | 11 | func (g *GMPlugin) setValue(ctx context.Context, script *scriptcat.Script, runtime *goja.Runtime) (func(call goja.FunctionCall) goja.Value, error) { 12 | return func(call goja.FunctionCall) goja.Value { 13 | key := call.Argument(0).String() 14 | arg1 := call.Argument(1) 15 | if err := g.gmFunc.SetValue(ctx, script, key, arg1.Export()); err != nil { 16 | panic(fmt.Errorf("GM_setValue error: %v", err)) 17 | } 18 | return goja.Undefined() 19 | }, nil 20 | } 21 | 22 | func (g *GMPlugin) getValue(ctx context.Context, script *scriptcat.Script, runtime *goja.Runtime) (func(call goja.FunctionCall) goja.Value, error) { 23 | return func(call goja.FunctionCall) goja.Value { 24 | s, err := g.gmFunc.GetValue(ctx, script, call.Argument(0).String()) 25 | if err != nil { 26 | return goja.Undefined() 27 | } 28 | if s == nil { 29 | if len(call.Arguments) > 1 { 30 | return call.Argument(1) 31 | } 32 | return goja.Undefined() 33 | } 34 | return runtime.ToValue(s) 35 | }, nil 36 | } 37 | -------------------------------------------------------------------------------- /pkg/scriptcat/plugin/window/timer.go: -------------------------------------------------------------------------------- 1 | package window 2 | 3 | import ( 4 | "sync" 5 | "time" 6 | 7 | "github.com/dop251/goja" 8 | ) 9 | 10 | type stop interface { 11 | stop() 12 | } 13 | 14 | type Timer struct { 15 | sync.Mutex 16 | vm *goja.Runtime 17 | jobId int 18 | stopCh chan struct{} 19 | jobCh chan func() 20 | 21 | stopMap map[int]stop 22 | } 23 | 24 | func NewTimer(vm *goja.Runtime) *Timer { 25 | return &Timer{ 26 | vm: vm, 27 | stopCh: make(chan struct{}), 28 | jobCh: make(chan func()), 29 | stopMap: make(map[int]stop), 30 | } 31 | } 32 | 33 | func (t *Timer) Start() error { 34 | if err := t.vm.Set("setTimeout", t.setTimeout); err != nil { 35 | return err 36 | } 37 | if err := t.vm.Set("clearTimeout", t.clearTimeout); err != nil { 38 | return err 39 | } 40 | if err := t.vm.Set("setInterval", t.setInterval); err != nil { 41 | return err 42 | } 43 | if err := t.vm.Set("clearInterval", t.clearInterval); err != nil { 44 | return err 45 | } 46 | go func() { 47 | for { 48 | select { 49 | case f := <-t.jobCh: 50 | f() 51 | case <-t.stopCh: 52 | return 53 | } 54 | } 55 | }() 56 | return nil 57 | } 58 | 59 | func (t *Timer) Stop() { 60 | close(t.stopCh) 61 | for _, v := range t.stopMap { 62 | v.stop() 63 | } 64 | } 65 | 66 | func (t *Timer) setTimeout(call goja.FunctionCall) goja.Value { 67 | return t.schedule(call, false) 68 | } 69 | 70 | func (t *Timer) setInterval(call goja.FunctionCall) goja.Value { 71 | return t.schedule(call, true) 72 | } 73 | 74 | func (t *Timer) clearTimeout(tm *timeout) { 75 | if tm != nil && !tm.canceled { 76 | tm.stop() 77 | } 78 | } 79 | 80 | func (t *Timer) clearInterval(i *interval) { 81 | if i != nil && !i.canceled { 82 | i.stop() 83 | } 84 | } 85 | 86 | func (t *Timer) schedule(call goja.FunctionCall, repeating bool) goja.Value { 87 | if fn, ok := goja.AssertFunction(call.Argument(0)); ok { 88 | delay := call.Argument(1).ToInteger() 89 | var args []goja.Value 90 | if len(call.Arguments) > 2 { 91 | args = call.Arguments[2:] 92 | } 93 | f := func() { _, _ = fn(nil, args...) } 94 | t.jobId++ 95 | if repeating { 96 | return t.vm.ToValue(t.addInterval(f, time.Duration(delay)*time.Millisecond)) 97 | } else { 98 | return t.vm.ToValue(t.addTimeout(f, time.Duration(delay)*time.Millisecond)) 99 | } 100 | } 101 | return nil 102 | } 103 | 104 | type job struct { 105 | canceled bool 106 | fn func() 107 | } 108 | 109 | type timeout struct { 110 | job 111 | timer *time.Timer 112 | } 113 | 114 | func (t *timeout) stop() { 115 | t.canceled = true 116 | t.timer.Stop() 117 | } 118 | 119 | func (t *Timer) addTimeout(f func(), duration time.Duration) *timeout { 120 | tm := &timeout{ 121 | job: job{fn: f}, 122 | } 123 | tm.timer = time.AfterFunc(duration, func() { 124 | t.jobCh <- func() { 125 | tm.fn() 126 | } 127 | }) 128 | t.stopMap[t.jobId] = tm 129 | return tm 130 | } 131 | 132 | type interval struct { 133 | job 134 | ticker *time.Ticker 135 | stopChan chan struct{} 136 | } 137 | 138 | func (i *interval) stop() { 139 | i.canceled = true 140 | close(i.stopChan) 141 | } 142 | 143 | func (t *Timer) addInterval(f func(), timeout time.Duration) *interval { 144 | // https://nodejs.org/api/timers.html#timers_setinterval_callback_delay_args 145 | if timeout <= 0 { 146 | timeout = time.Millisecond 147 | } 148 | 149 | i := &interval{ 150 | job: job{fn: f}, 151 | ticker: time.NewTicker(timeout), 152 | stopChan: make(chan struct{}), 153 | } 154 | 155 | t.stopMap[t.jobId] = i 156 | 157 | go func() { 158 | for { 159 | select { 160 | case <-i.stopChan: 161 | i.ticker.Stop() 162 | break 163 | case <-i.ticker.C: 164 | t.jobCh <- i.fn 165 | } 166 | } 167 | }() 168 | return i 169 | } 170 | -------------------------------------------------------------------------------- /pkg/scriptcat/plugin/window/window.go: -------------------------------------------------------------------------------- 1 | package window 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/dop251/goja" 7 | "github.com/dop251/goja_nodejs/require" 8 | "github.com/dop251/goja_nodejs/url" 9 | scriptcat2 "github.com/scriptscat/cloudcat/pkg/scriptcat" 10 | ) 11 | 12 | // Plugin 注册一些基础的函数 13 | type Plugin struct { 14 | } 15 | 16 | func NewBrowserPlugin() scriptcat2.Plugin { 17 | return &Plugin{} 18 | } 19 | 20 | func (w *Plugin) Name() string { 21 | return "NodeJS" 22 | } 23 | 24 | func (w *Plugin) BeforeRun(ctx context.Context, script *scriptcat2.Script, vm *goja.Runtime) error { 25 | // 注册计时器 26 | timer := NewTimer(vm) 27 | if err := timer.Start(); err != nil { 28 | return err 29 | } 30 | if err := vm.Set("window", vm.GlobalObject()); err != nil { 31 | return err 32 | } 33 | // url 34 | nodejs := new(require.Registry) 35 | nodejs.Enable(vm) 36 | url.Enable(vm) 37 | return nil 38 | } 39 | 40 | func (w *Plugin) AfterRun(ctx context.Context, script *scriptcat2.Script, runtime *goja.Runtime) error { 41 | return nil 42 | } 43 | -------------------------------------------------------------------------------- /pkg/scriptcat/plugin/xhr.go: -------------------------------------------------------------------------------- 1 | package plugin 2 | 3 | import ( 4 | "bytes" 5 | "context" 6 | "io" 7 | "net/http" 8 | "net/url" 9 | "time" 10 | 11 | "github.com/dop251/goja" 12 | "github.com/scriptscat/cloudcat/pkg/scriptcat" 13 | "go.uber.org/zap" 14 | ) 15 | 16 | func (g *GMPlugin) xmlHttpRequest(ctx context.Context, script *scriptcat.Script, runtime *goja.Runtime) (func(call goja.FunctionCall) goja.Value, error) { 17 | cookieJar, err := g.gmFunc.LoadCookieJar(ctx, script) 18 | if err != nil { 19 | return nil, err 20 | } 21 | return func(call goja.FunctionCall) goja.Value { 22 | // TODO: 实现代理等 23 | cli := &http.Client{ 24 | Transport: nil, 25 | CheckRedirect: func(req *http.Request, via []*http.Request) error { 26 | return nil 27 | }, 28 | Jar: cookieJar, 29 | Timeout: time.Second * 30, 30 | } 31 | 32 | if len(call.Arguments) != 1 { 33 | g.logger.Ctx(ctx).Warn("GMXHR incorrect number of parameters") 34 | return nil 35 | } 36 | args, ok := call.Arguments[0].Export().(map[string]interface{}) 37 | if !ok { 38 | g.logger.Ctx(ctx).Warn("GMXHR parameter is not an object") 39 | return nil 40 | } 41 | method, _ := args["method"].(string) 42 | u, _ := args["url"].(string) 43 | if u == "" { 44 | g.logger.Ctx(ctx).Warn("GMXHR url cannot be empty") 45 | return nil 46 | } 47 | uu, err := url.Parse(u) 48 | if err != nil { 49 | g.logger.Ctx(ctx).Warn("GMXHR url format error", zap.Error(err)) 50 | g.xhrOnError(ctx, runtime, args, err) 51 | return nil 52 | } 53 | uu.RawQuery = uu.Query().Encode() 54 | var body io.Reader 55 | if method != "GET" { 56 | data, _ := args["data"].(string) 57 | body = bytes.NewBufferString(data) 58 | } 59 | req, err := http.NewRequest(method, uu.String(), body) 60 | if err != nil { 61 | g.logger.Ctx(ctx).Warn("GMXHR create request failed", zap.Error(err)) 62 | g.xhrOnError(ctx, runtime, args, err) 63 | return nil 64 | } 65 | // 默认header 66 | req.Header.Set("User-Agent", "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/117.0.0.0 Safari/537.36 Edg/117.0.2045.40") 67 | req.Header.Set("Host", req.URL.Host) 68 | if headers, ok := args["headers"].(map[string]interface{}); ok { 69 | for k, v := range headers { 70 | req.Header.Set(k, v.(string)) 71 | } 72 | } 73 | 74 | if timeout, _ := args["timeout"].(float64); timeout != 0 { 75 | cli.Timeout = time.Duration(timeout) * time.Millisecond 76 | } 77 | 78 | go func() { 79 | defer func(cookieJar CookieJar, ctx context.Context) { 80 | _ = cookieJar.Save(ctx) 81 | }(cookieJar, context.Background()) 82 | resp, err := cli.Do(req) 83 | if err != nil { 84 | g.logger.Ctx(ctx).Warn("GMXHR request error", zap.Error(err)) 85 | g.xhrOnError(ctx, runtime, args, err) 86 | return 87 | } 88 | defer resp.Body.Close() 89 | body, err := io.ReadAll(resp.Body) 90 | if err != nil { 91 | g.logger.Ctx(ctx).Warn("GMXHR request error", zap.Error(err)) 92 | g.xhrOnError(ctx, runtime, args, err) 93 | return 94 | } 95 | respObj, err := goRespToXhrResp(resp, runtime, body) 96 | if err != nil { 97 | g.logger.Ctx(ctx).Warn("GMXHR request error", zap.Error(err)) 98 | g.xhrOnError(ctx, runtime, args, err) 99 | return 100 | } 101 | onload, ok := goja.AssertFunction(runtime.ToValue(args["onload"])) 102 | if ok { 103 | g.logger.Ctx(ctx).Debug("GMXHR request onload") 104 | _, err := onload(nil, respObj) 105 | if err != nil { 106 | g.logger.Ctx(ctx).Warn("GMXHR onload error", zap.Error(err)) 107 | return 108 | } 109 | } 110 | }() 111 | 112 | return goja.Undefined() 113 | }, nil 114 | } 115 | 116 | func goRespToXhrResp(resp *http.Response, runtime *goja.Runtime, body []byte) (goja.Value, error) { 117 | respObj := runtime.NewObject() 118 | if err := respObj.Set("finalUrl", resp.Request.URL.String()); err != nil { 119 | return nil, err 120 | } 121 | if err := respObj.Set("readyState", 4); err != nil { 122 | return nil, err 123 | } 124 | if err := respObj.Set("status", resp.StatusCode); err != nil { 125 | return nil, err 126 | } 127 | if err := respObj.Set("statusText", resp.Status); err != nil { 128 | return nil, err 129 | } 130 | respHeaders := "" 131 | for k, v := range resp.Header { 132 | respHeaders += k + ": " + v[0] + "\n" 133 | } 134 | if err := respObj.Set("responseHeaders", respHeaders); err != nil { 135 | return nil, err 136 | } 137 | if err := respObj.Set("response", string(body)); err != nil { 138 | return nil, err 139 | } 140 | if err := respObj.Set("responseText", string(body)); err != nil { 141 | return nil, err 142 | } 143 | return respObj, nil 144 | } 145 | 146 | func (g *GMPlugin) xhrOnError(ctx context.Context, runtime *goja.Runtime, args map[string]interface{}, err error) goja.Value { 147 | onerror, ok := goja.AssertFunction(runtime.ToValue(args["onerror"])) 148 | if ok { 149 | g.logger.Debug("GMXHR request onerror") 150 | errObj := runtime.NewObject() 151 | if err := errObj.Set("error", err.Error()); err != nil { 152 | return goja.Undefined() 153 | } 154 | _, err := onerror(nil, errObj) 155 | if err != nil { 156 | g.logger.Ctx(ctx).Warn("GMXHR onerror error", zap.Error(err)) 157 | return goja.Undefined() 158 | } 159 | } 160 | return goja.Undefined() 161 | } 162 | -------------------------------------------------------------------------------- /pkg/scriptcat/runtime.go: -------------------------------------------------------------------------------- 1 | package scriptcat 2 | 3 | import ( 4 | "context" 5 | "crypto/sha256" 6 | "errors" 7 | "fmt" 8 | 9 | "github.com/codfrm/cago/pkg/errs" 10 | "github.com/codfrm/cago/pkg/logger" 11 | "github.com/dop251/goja" 12 | "go.uber.org/zap" 13 | ) 14 | 15 | const ( 16 | ScriptCat = "scriptcat" 17 | ) 18 | 19 | // Runtime 脚本运行时 20 | type Runtime interface { 21 | // Parse 解析脚本 22 | Parse(ctx context.Context, script string) (*Script, error) 23 | // Run 运行脚本 24 | Run(ctx context.Context, script *Script) (interface{}, error) 25 | } 26 | 27 | type scriptcat struct { 28 | plugins []Plugin 29 | logger *logger.CtxLogger 30 | } 31 | 32 | var defaultRuntime Runtime 33 | 34 | func RegisterRuntime(i Runtime) { 35 | defaultRuntime = i 36 | } 37 | 38 | func RuntimeCat() Runtime { 39 | return defaultRuntime 40 | } 41 | 42 | func NewRuntime(logger *logger.CtxLogger, plugins []Plugin) Runtime { 43 | return &scriptcat{ 44 | plugins: plugins, 45 | logger: logger, 46 | } 47 | } 48 | 49 | func (s *scriptcat) Name() string { 50 | return "scriptcat" 51 | } 52 | 53 | func (s *scriptcat) Parse(ctx context.Context, code string) (*Script, error) { 54 | meta := ParseMetaToJson(code) 55 | if len(meta["name"]) == 0 { 56 | return nil, errs.Warn(errors.New("script name is empty")) 57 | } 58 | if len(meta["namespace"]) == 0 { 59 | return nil, errs.Warn(errors.New("script namespace is empty")) 60 | } 61 | if len(meta["version"]) == 0 { 62 | return nil, errs.Warn(errors.New("script version is empty")) 63 | } 64 | id := meta["name"][0] + meta["namespace"][0] 65 | hash := sha256.New() 66 | hash.Write([]byte(id)) 67 | id = fmt.Sprintf("%x", hash.Sum(nil)) 68 | script := &Script{ 69 | ID: id, 70 | Code: code, 71 | Metadata: meta, 72 | } 73 | return script, nil 74 | } 75 | 76 | func (s *scriptcat) Run(ctx context.Context, script *Script) (interface{}, error) { 77 | options := NewRunOptions() 78 | vm := goja.New() 79 | code := ` 80 | function vm` + script.ID + `() { 81 | ` + script.Code + ` 82 | } 83 | ` 84 | for _, p := range s.plugins { 85 | if err := p.BeforeRun(ctx, script, vm); err != nil { 86 | return nil, err 87 | } 88 | } 89 | defer func() { 90 | for _, p := range s.plugins { 91 | if err := p.AfterRun(ctx, script, vm); err != nil { 92 | s.logger.Logger.Error("plugin after run error", zap.Error(err)) 93 | } 94 | } 95 | vm.Interrupt("halt") 96 | }() 97 | _, err := vm.RunString(code) 98 | if err != nil { 99 | var e *goja.Exception 100 | if errors.As(err, &e) { 101 | s.logger.Ctx(ctx).Error("run script exception error", 102 | zap.String("error", e.Value().String())) 103 | } else { 104 | s.logger.Ctx(ctx).Error("run script error", zap.Error(err)) 105 | } 106 | return nil, err 107 | } 108 | vmFun, ok := goja.AssertFunction(vm.Get("vm" + script.ID)) 109 | if !ok { 110 | return nil, errors.New("not a vm function") 111 | } 112 | value, err := vmFun(goja.Undefined(), vm.ToValue(1), vm.ToValue(2)) 113 | if err != nil { 114 | options.ResultCallback(nil, err) 115 | s.logger.Logger.Error("script run error", zap.Error(err)) 116 | vm.Interrupt("halt") 117 | return nil, err 118 | } 119 | ctx, cancel := context.WithCancel(ctx) 120 | defer cancel() 121 | var result interface{} 122 | if err := s.then(vm, value, func() { 123 | result = value 124 | cancel() 125 | }); err != nil { 126 | s.logger.Logger.Error("script then error", zap.Error(err)) 127 | return nil, err 128 | } 129 | if err := s.catch(vm, value, func() { 130 | cancel() 131 | }); err != nil { 132 | s.logger.Logger.Error("script catch error", zap.Error(err)) 133 | return nil, err 134 | } 135 | <-ctx.Done() 136 | return result, nil 137 | } 138 | 139 | func (s *scriptcat) then(vm *goja.Runtime, value goja.Value, resolve func()) error { 140 | oj := value.ToObject(vm) 141 | then, ok := goja.AssertFunction(oj.Get("then")) 142 | if !ok { 143 | return errors.New("not a then function") 144 | } 145 | _, err := then(value, vm.ToValue(func(result interface{}) { 146 | // 任务完成处理 147 | s.logger.Logger.Info("script complete", zap.Any("result", result)) 148 | resolve() 149 | })) 150 | if err != nil { 151 | return err 152 | } 153 | return nil 154 | } 155 | 156 | func (s *scriptcat) catch(vm *goja.Runtime, value goja.Value, reject func()) error { 157 | oj := value.ToObject(vm) 158 | catch, ok := goja.AssertFunction(oj.Get("catch")) 159 | if !ok { 160 | return errors.New("not a catch function") 161 | } 162 | _, err := catch(value, vm.ToValue(func(e interface{}) { 163 | promise, ok := oj.Export().(*goja.Promise) 164 | // 任务错误处理 165 | if ok { 166 | s.logger.Logger.Error("script error", 167 | zap.Any("error", e), 168 | zap.Any("reject", promise.Result().String()), 169 | //zap.Any("js stack",promise.Result()) 170 | ) 171 | } else { 172 | s.logger.Logger.Error("script error", 173 | zap.Any("error", e), 174 | ) 175 | } 176 | reject() 177 | })) 178 | if err != nil { 179 | return err 180 | } 181 | return err 182 | } 183 | -------------------------------------------------------------------------------- /pkg/scriptcat/runtime_test.go: -------------------------------------------------------------------------------- 1 | package scriptcat_test 2 | 3 | import ( 4 | "context" 5 | "testing" 6 | 7 | "github.com/codfrm/cago/pkg/logger" 8 | scriptcat2 "github.com/scriptscat/cloudcat/pkg/scriptcat" 9 | "github.com/scriptscat/cloudcat/pkg/scriptcat/plugin/window" 10 | _ "github.com/scriptscat/cloudcat/pkg/scriptcat/plugin/window" 11 | "github.com/stretchr/testify/assert" 12 | "go.uber.org/zap" 13 | ) 14 | 15 | func TestScriptCat(t *testing.T) { 16 | ctx := context.Background() 17 | logger.SetLogger(zap.L()) 18 | r := scriptcat2.NewRuntime(logger.NewCtxLogger(logger.Default()), []scriptcat2.Plugin{ 19 | window.NewBrowserPlugin(), 20 | }) 21 | script := &scriptcat2.Script{ 22 | ID: "1", 23 | Code: "return new Promise(resolve=>{" + 24 | "resolve('ok')" + 25 | "})", 26 | Metadata: nil, 27 | } 28 | _, err := r.Run(ctx, script) 29 | assert.Nil(t, err) 30 | } 31 | -------------------------------------------------------------------------------- /pkg/scriptcat/utils.go: -------------------------------------------------------------------------------- 1 | package scriptcat 2 | 3 | import ( 4 | "errors" 5 | "regexp" 6 | "strings" 7 | ) 8 | 9 | func ParseMeta(script string) string { 10 | return regexp.MustCompile(`// ==UserScript==([\\s\\S]+?)// ==/UserScript==`).FindString(script) 11 | } 12 | 13 | func ParseMetaToJson(meta string) map[string][]string { 14 | reg := regexp.MustCompile("(?im)^//\\s*@(.+?)([\r\n]+|$|\\s+(.+?)$)") 15 | list := reg.FindAllStringSubmatch(meta, -1) 16 | ret := make(map[string][]string) 17 | for _, v := range list { 18 | v[1] = strings.ToLower(v[1]) 19 | if _, ok := ret[v[1]]; !ok { 20 | ret[v[1]] = make([]string, 0) 21 | } 22 | ret[v[1]] = append(ret[v[1]], strings.TrimSpace(v[3])) 23 | } 24 | return ret 25 | } 26 | 27 | // ConvCron 转换cron表达式 28 | func ConvCron(cron string) (string, error) { 29 | // 对once进行处理 30 | unit := strings.Split(cron, " ") 31 | if len(unit) == 5 { 32 | unit = append([]string{"0"}, unit...) 33 | } 34 | if len(unit) != 6 { 35 | return "", errors.New("cron format error: " + cron) 36 | } 37 | for i, v := range unit { 38 | if v == "once" { 39 | unit[i] = "*" 40 | i -= 1 41 | for ; i >= 0; i-- { 42 | if unit[i] == "*" { 43 | unit[i] = "0" 44 | } 45 | } 46 | break 47 | } else if strings.Contains(v, "-") { 48 | // 取最小的时间 49 | unit[i] = strings.Split(v, "-")[0] 50 | } 51 | } 52 | return strings.Join(unit, " "), nil 53 | } 54 | -------------------------------------------------------------------------------- /pkg/scriptcat/utils_test.go: -------------------------------------------------------------------------------- 1 | package scriptcat 2 | 3 | import ( 4 | "fmt" 5 | "testing" 6 | 7 | "github.com/stretchr/testify/assert" 8 | ) 9 | 10 | func TestConvCron(t *testing.T) { 11 | type args struct { 12 | cron string 13 | } 14 | tests := []struct { 15 | name string 16 | args args 17 | want string 18 | wantErr assert.ErrorAssertionFunc 19 | }{ 20 | {"case1", args{"* * * * *"}, "0 * * * * *", assert.NoError}, 21 | {"case2", args{"* 10-23 once * *"}, "0 0 10 * * *", assert.NoError}, 22 | } 23 | for _, tt := range tests { 24 | t.Run(tt.name, func(t *testing.T) { 25 | got, err := ConvCron(tt.args.cron) 26 | if !tt.wantErr(t, err, fmt.Sprintf("ConvCron(%v)", tt.args.cron)) { 27 | return 28 | } 29 | assert.Equalf(t, tt.want, got, "ConvCron(%v)", tt.args.cron) 30 | }) 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /pkg/utils/aes.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import ( 4 | "bytes" 5 | "crypto/aes" 6 | "crypto/cipher" 7 | "io" 8 | ) 9 | 10 | // AesEncrypt aes-cbc pkcs7padding 11 | type AesEncrypt struct { 12 | buf []byte 13 | mode cipher.BlockMode 14 | body io.Reader 15 | } 16 | 17 | func NewAesEncrypt(key []byte, body io.Reader) (*AesEncrypt, error) { 18 | block, err := aes.NewCipher(key) 19 | if err != nil { 20 | return nil, err 21 | } 22 | mode := cipher.NewCBCEncrypter(block, key[:aes.BlockSize]) 23 | return &AesEncrypt{ 24 | mode: mode, 25 | body: body, 26 | }, nil 27 | } 28 | 29 | func (a *AesEncrypt) Read(p []byte) (int, error) { 30 | buf := make([]byte, len(p)) 31 | n, err := a.body.Read(buf) 32 | if err != nil { 33 | if err == io.EOF { 34 | if len(a.buf) == 0 && n == 0 { 35 | if a.buf != nil { 36 | // 填充16个16 37 | buf = bytes.Repeat([]byte{16}, 16) 38 | if len(p) < len(buf) { 39 | return 0, io.ErrShortBuffer 40 | } 41 | a.mode.CryptBlocks(p, buf) 42 | a.buf = nil 43 | return len(buf), nil 44 | } 45 | return 0, io.EOF 46 | } 47 | // 最后一次加密, 不足blockSize的整数倍的, 补齐 48 | buf = append(a.buf, buf[:n]...) 49 | // 需要补齐的长度 50 | padding := aes.BlockSize - len(buf)%aes.BlockSize 51 | // 补齐 52 | if padding > 0 { 53 | buf = append(buf, bytes.Repeat([]byte{byte(padding)}, padding)...) 54 | } 55 | if len(p) < len(buf) { 56 | return 0, io.ErrShortBuffer 57 | } 58 | a.mode.CryptBlocks(p, buf) 59 | a.buf = nil 60 | return len(buf), nil 61 | } 62 | return n, err 63 | } 64 | // 只加密blockSize的整数倍 65 | buf = append(a.buf, buf[:n]...) 66 | // 有效长度 67 | n = len(buf) - len(buf)%aes.BlockSize 68 | a.buf = buf[n:] 69 | a.mode.CryptBlocks(p[:n], buf[:n]) 70 | return n, nil 71 | } 72 | 73 | type AesDecrypt struct { 74 | buf []byte 75 | mode cipher.BlockMode 76 | body io.Reader 77 | } 78 | 79 | func NewAesDecrypt(key []byte, body io.Reader) (*AesDecrypt, error) { 80 | block, err := aes.NewCipher(key) 81 | if err != nil { 82 | return nil, err 83 | } 84 | mode := cipher.NewCBCDecrypter(block, key[:aes.BlockSize]) 85 | return &AesDecrypt{ 86 | mode: mode, 87 | body: body, 88 | }, nil 89 | } 90 | 91 | func (a *AesDecrypt) Read(p []byte) (int, error) { 92 | buf := make([]byte, len(p)) 93 | n, err := a.body.Read(buf) 94 | if err != nil { 95 | if err == io.EOF { 96 | if len(a.buf) == 0 && n == 0 { 97 | return 0, io.EOF 98 | } 99 | buf = append(a.buf, buf[:n]...) 100 | if len(p) < len(buf) { 101 | a.buf = buf[len(p):] 102 | buf = buf[:len(p)] 103 | } else { 104 | a.buf = nil 105 | } 106 | a.mode.CryptBlocks(buf, buf) 107 | // 去掉填充的数据 108 | padding := int(buf[len(buf)-1]) 109 | if padding > 0 && padding <= aes.BlockSize { 110 | n = len(buf) - padding 111 | } else { 112 | n = len(buf) 113 | } 114 | copy(p, buf[:n]) 115 | 116 | return n, nil 117 | } 118 | return n, err 119 | } 120 | // 只加密blockSize的整数倍 121 | buf = append(a.buf, buf[:n]...) 122 | if len(p) < len(buf) { 123 | a.buf = buf[len(p):] 124 | buf = buf[:len(p)] 125 | } else { 126 | a.buf = nil 127 | } 128 | // 有效长度 129 | n = len(buf) - len(buf)%aes.BlockSize 130 | if len(a.buf) == 0 { 131 | // 再留下最后blockSize的长度 132 | n = n - aes.BlockSize 133 | if n < 0 { 134 | a.buf = buf 135 | return 0, err 136 | } 137 | a.buf = append(a.buf, buf[n:]...) 138 | } 139 | a.mode.CryptBlocks(p[:n], buf[:n]) 140 | return n, nil 141 | } 142 | 143 | type readClose struct { 144 | io.Reader 145 | io.Closer 146 | } 147 | 148 | func WarpCloser(r io.Reader, c io.Closer) io.ReadCloser { 149 | return &readClose{ 150 | Reader: r, 151 | Closer: c, 152 | } 153 | } 154 | -------------------------------------------------------------------------------- /pkg/utils/aes_test.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import ( 4 | "bytes" 5 | "io" 6 | "testing" 7 | 8 | "github.com/codfrm/cago/pkg/utils" 9 | ) 10 | 11 | func TestAes(t *testing.T) { 12 | type args struct { 13 | key []byte 14 | data string 15 | } 16 | tests := []struct { 17 | name string 18 | args args 19 | wantErr bool 20 | }{ 21 | {"case1", args{[]byte("1234567890123456"), "你好"}, false}, 22 | {"case2", args{[]byte("1234567890123456"), "0000000000000000"}, false}, 23 | } 24 | for _, tt := range tests { 25 | t.Run(tt.name, func(t *testing.T) { 26 | buf := bytes.NewBufferString(tt.args.data) 27 | got, err := NewAesEncrypt(tt.args.key, buf) 28 | if (err != nil) != tt.wantErr { 29 | t.Errorf("NewAesEncrypt() error = %v, wantErr %v", err, tt.wantErr) 30 | return 31 | } 32 | encryptData := bytes.NewBuffer(nil) 33 | _, err = io.Copy(encryptData, got) 34 | if (err != nil) != tt.wantErr { 35 | t.Errorf("Copy() error = %v, wantErr %v", err, tt.wantErr) 36 | return 37 | } 38 | decrypt, err := NewAesDecrypt(tt.args.key, encryptData) 39 | if (err != nil) != tt.wantErr { 40 | t.Errorf("NewAesDecrypt() error = %v, wantErr %v", err, tt.wantErr) 41 | return 42 | } 43 | _, err = io.Copy(buf, decrypt) 44 | if (err != nil) != tt.wantErr { 45 | t.Errorf("Copy() error = %v, wantErr %v", err, tt.wantErr) 46 | return 47 | } 48 | if buf.String() != tt.args.data { 49 | t.Errorf("Decrypt() error = %v, wantErr %v", err, tt.wantErr) 50 | return 51 | } 52 | }) 53 | } 54 | } 55 | 56 | func FuzzAes(f *testing.F) { 57 | key := []byte(utils.RandString(16, utils.Mix)) 58 | testcases := [][]byte{ 59 | []byte("Hello, world"), 60 | []byte(" "), 61 | []byte("!12345"), 62 | []byte("0000000000000000"), 63 | []byte(""), 64 | } 65 | for _, tc := range testcases { 66 | f.Add(tc) 67 | } 68 | f.Fuzz(func(t *testing.T, data []byte) { 69 | buf := bytes.NewBuffer(data) 70 | got, err := NewAesEncrypt(key, buf) 71 | if err != nil { 72 | t.Errorf("NewAesEncrypt() error = %v", err) 73 | return 74 | } 75 | encryptData := bytes.NewBuffer(nil) 76 | _, err = io.Copy(encryptData, got) 77 | if err != nil { 78 | t.Errorf("Copy() error = %v", err) 79 | return 80 | } 81 | decrypt, err := NewAesDecrypt(key, encryptData) 82 | if err != nil { 83 | t.Errorf("NewAesDecrypt() error = %v", err) 84 | return 85 | } 86 | _, err = io.Copy(buf, decrypt) 87 | if err != nil { 88 | t.Errorf("Copy() error = %v", err) 89 | return 90 | } 91 | if !bytes.Equal(buf.Bytes(), data) { 92 | t.Errorf("Decrypt() error = %v", err) 93 | return 94 | } 95 | }) 96 | } 97 | -------------------------------------------------------------------------------- /pkg/utils/utils.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | "path/filepath" 7 | "reflect" 8 | "strings" 9 | 10 | "github.com/olekukonko/tablewriter" 11 | ) 12 | 13 | func Table(header []string, data [][]string) *tablewriter.Table { 14 | table := tablewriter.NewWriter(os.Stdout) 15 | table.SetHeader(header) 16 | table.SetHeaderAlignment(tablewriter.ALIGN_LEFT) 17 | table.SetAlignment(tablewriter.ALIGN_LEFT) 18 | table.SetHeaderLine(false) 19 | table.SetBorder(false) 20 | table.SetTablePadding(" ") // pad with tabs 21 | table.SetNoWhiteSpace(true) 22 | table.AppendBulk(data) // Add Bulk Data 23 | return table 24 | } 25 | 26 | type Render interface { 27 | WriteLine(data interface{}) 28 | Render() 29 | } 30 | 31 | type render struct { 32 | table *tablewriter.Table 33 | deal func(interface{}) []string 34 | line int 35 | } 36 | 37 | func newRender(header []string, deal func(interface{}) []string) *render { 38 | table := tablewriter.NewWriter(os.Stdout) 39 | table.SetHeaderAlignment(tablewriter.ALIGN_LEFT) 40 | table.SetAlignment(tablewriter.ALIGN_LEFT) 41 | table.SetHeaderLine(false) 42 | table.SetBorder(false) 43 | table.SetTablePadding(" ") // pad with tabs 44 | table.SetNoWhiteSpace(true) 45 | table.SetHeader(header) 46 | return &render{ 47 | table: table, 48 | deal: deal, 49 | } 50 | } 51 | 52 | func (e *render) WriteLine(data interface{}) { 53 | e.line += 1 54 | e.table.Append(e.deal(data)) 55 | } 56 | 57 | func (e *render) Render() { 58 | if e.line == 0 { 59 | fmt.Println("没有数据") 60 | } else { 61 | e.table.Render() 62 | } 63 | } 64 | 65 | func DealTable(header []string, data interface{}, deal func(interface{}) []string) Render { 66 | r := newRender(header, deal) 67 | reflectionRow := reflect.ValueOf(data) 68 | if reflectionRow.Kind() != reflect.Slice && reflectionRow.Kind() != reflect.Array { 69 | return r 70 | } 71 | for i := 0; i < reflectionRow.Len(); i++ { 72 | r.WriteLine(reflectionRow.Index(i).Interface()) 73 | } 74 | return r 75 | } 76 | 77 | func BoolToString(b bool) string { 78 | if b { 79 | return "true" 80 | } 81 | return "false" 82 | } 83 | 84 | // Abs 转绝对路径,处理了"~" 85 | func Abs(p string) (string, error) { 86 | if strings.HasPrefix(p, "~") { 87 | home, err := os.UserHomeDir() 88 | if err != nil { 89 | return "", err 90 | } 91 | return filepath.Abs(filepath.Join(home, p[1:])) 92 | } 93 | return filepath.Abs(p) 94 | } 95 | --------------------------------------------------------------------------------