├── .github └── workflows │ ├── codecov.yml │ ├── publish_npm.yaml │ └── release.yml ├── .gitignore ├── .goreleaser.debug.yaml ├── .goreleaser.yaml ├── LICENSE ├── README.md ├── api ├── gateway │ └── openapi.yaml └── index.html ├── build ├── scripts │ ├── migration │ │ ├── script.d │ │ │ └── 01-migrate-gateway.sh │ │ └── service.d │ │ │ └── gateway │ │ │ └── migration.list │ └── setup │ │ ├── script.d │ │ └── 01-setup-gateway.sh │ │ └── service.d │ │ └── gateway │ │ ├── arch │ │ └── setup-gateway.sh │ │ ├── debian │ │ ├── bullseye │ │ │ └── setup-gateway.sh │ │ └── setup-gateway.sh │ │ └── ubuntu │ │ ├── jammy │ │ └── setup-gateway.sh │ │ └── setup-gateway.sh └── sysroot │ ├── etc │ └── casaos │ │ └── gateway.ini.sample │ └── usr │ ├── lib │ └── systemd │ │ └── system │ │ ├── casaos-gateway.service │ │ └── casaos-gateway.service.buildroot │ └── share │ └── casaos │ └── cleanup │ ├── script.d │ └── 01-cleanup-gateway.sh │ └── service.d │ └── gateway │ ├── arch │ └── cleanup-gateway.sh │ ├── debian │ ├── bullseye │ │ └── cleanup-gateway.sh │ └── cleanup-gateway.sh │ └── ubuntu │ ├── cleanup-gateway.sh │ └── jammy │ └── cleanup-gateway.sh ├── cmd └── migration-tool │ ├── log.go │ ├── main.go │ └── migration_dummy.go ├── common ├── config.go └── version.go ├── go.mod ├── go.sum ├── main.go ├── package.json ├── pkg └── port.go ├── route ├── gateway_route.go ├── management_route.go ├── management_route_test.go └── static_route.go ├── service ├── management.go ├── management_test.go └── state.go └── tsconfig.json /.github/workflows/codecov.yml: -------------------------------------------------------------------------------- 1 | name: Collect Code Coverage 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | pull_request: 8 | branches: 9 | - main 10 | 11 | jobs: 12 | build: 13 | runs-on: ubuntu-22.04 14 | steps: 15 | - name: Checkout 16 | uses: actions/checkout@v3 17 | with: 18 | fetch-depth: 0 19 | - name: Set up Go 20 | uses: actions/setup-go@v4 21 | with: 22 | go-version: "1.20" 23 | - name: Go Generate 24 | run: go generate 25 | - name: Run coverage 26 | run: go test -race -failfast -coverprofile=coverage.txt -covermode=atomic -v ./... 27 | - name: Upload coverage to Codecov 28 | uses: codecov/codecov-action@v3 29 | -------------------------------------------------------------------------------- /.github/workflows/publish_npm.yaml: -------------------------------------------------------------------------------- 1 | name: publish npm 2 | 3 | on: 4 | push: 5 | tags: 6 | - v*.*.* 7 | workflow_dispatch: 8 | 9 | permissions: 10 | contents: write 11 | 12 | jobs: 13 | call-workflow-passing-data: 14 | uses: IceWhaleTech/github/.github/workflows/npm_release.yml@main 15 | secrets: 16 | NPM_TOKEN_PRIVATE: ${{ secrets.NPM_TOKEN_PRIVATE }} 17 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: goreleaser 2 | 3 | on: 4 | push: 5 | tags: 6 | - v*.*.* 7 | 8 | permissions: 9 | contents: write 10 | jobs: 11 | call-workflow-passing-data: 12 | uses: IceWhaleTech/github/.github/workflows/go_release.yml@main 13 | with: 14 | project-name: CasaOS-Gateway 15 | file-name: casaos-gateway 16 | secrets: 17 | OSS_KEY_ID: ${{ secrets.OSS_KEY_ID }} 18 | OSS_KEY_SECRET: ${{ secrets.OSS_KEY_SECRET }} -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # If you prefer the allow list template instead of the deny list, see community template: 2 | # https://github.com/github/gitignore/blob/main/community/Golang/Go.AllowList.gitignore 3 | # 4 | # Binaries for programs and plugins 5 | *.exe 6 | *.exe~ 7 | *.dll 8 | *.so 9 | *.dylib 10 | 11 | # Test binary, built with `go test -c` 12 | *.test 13 | 14 | # Output of the go coverage tool, specifically when used with LiteIDE 15 | *.out 16 | 17 | # Dependency directories (remove the comment below to include it) 18 | # vendor/ 19 | 20 | # Go workspace file 21 | go.work 22 | 23 | conf/ 24 | target/ 25 | 26 | # IDE 27 | .vscode/ 28 | __debug_bin 29 | 30 | 31 | dist/ 32 | /github 33 | node_modules 34 | yarn.lock 35 | generate 36 | openapitools.json 37 | -------------------------------------------------------------------------------- /.goreleaser.debug.yaml: -------------------------------------------------------------------------------- 1 | # This is an example .goreleaser.yml file with some sensible defaults. 2 | # Make sure to check the documentation at https://goreleaser.com 3 | project_name: casaos-gateway 4 | before: 5 | hooks: 6 | - go generate 7 | - go run github.com/google/go-licenses@latest check . --disallowed_types=restricted 8 | - go mod tidy 9 | - go test -race -v ./... 10 | builds: 11 | - id: casaos-gateway-amd64 12 | binary: build/sysroot/usr/bin/casaos-gateway 13 | env: 14 | - CC=x86_64-linux-gnu-gcc 15 | gcflags: 16 | - all=-N -l 17 | ldflags: 18 | - -X main.commit={{.Commit}} 19 | - -X main.date={{.Date}} 20 | - -extldflags "-static" 21 | tags: 22 | - musl 23 | - netgo 24 | - osusergo 25 | goos: 26 | - linux 27 | goarch: 28 | - amd64 29 | - id: casaos-gateway-arm64 30 | binary: build/sysroot/usr/bin/casaos-gateway 31 | env: 32 | - CC=aarch64-linux-gnu-gcc 33 | gcflags: 34 | - all=-N -l 35 | ldflags: 36 | - -X main.commit={{.Commit}} 37 | - -X main.date={{.Date}} 38 | - -extldflags "-static" 39 | tags: 40 | - musl 41 | - netgo 42 | - osusergo 43 | goos: 44 | - linux 45 | goarch: 46 | - arm64 47 | - id: casaos-gateway-arm-7 48 | binary: build/sysroot/usr/bin/casaos-gateway 49 | env: 50 | - CC=arm-linux-gnueabihf-gcc 51 | gcflags: 52 | - all=-N -l 53 | ldflags: 54 | - -X main.commit={{.Commit}} 55 | - -X main.date={{.Date}} 56 | - -extldflags "-static" 57 | tags: 58 | - musl 59 | - netgo 60 | - osusergo 61 | goos: 62 | - linux 63 | goarch: 64 | - arm 65 | goarm: 66 | - "7" 67 | - id: casaos-gateway-riscv64 68 | binary: build/sysroot/usr/bin/casaos-gateway 69 | env: 70 | - CC=riscv64-linux-gnu-gcc 71 | gcflags: 72 | - all=-N -l 73 | ldflags: 74 | - -X main.commit={{.Commit}} 75 | - -X main.date={{.Date}} 76 | - -extldflags "-static" 77 | tags: 78 | - musl 79 | - netgo 80 | - osusergo 81 | goos: 82 | - linux 83 | goarch: 84 | - riscv64 85 | - id: casaos-gateway-migration-tool-amd64 86 | binary: build/sysroot/usr/bin/casaos-gateway-migration-tool 87 | main: ./cmd/migration-tool 88 | env: 89 | - CC=x86_64-linux-gnu-gcc 90 | gcflags: 91 | - all=-N -l 92 | ldflags: 93 | - -X main.commit={{.Commit}} 94 | - -X main.date={{.Date}} 95 | - -extldflags "-static" 96 | tags: 97 | - musl 98 | - netgo 99 | - osusergo 100 | goos: 101 | - linux 102 | goarch: 103 | - amd64 104 | - id: casaos-gateway-migration-tool-arm64 105 | binary: build/sysroot/usr/bin/casaos-gateway-migration-tool 106 | main: ./cmd/migration-tool 107 | env: 108 | - CC=aarch64-linux-gnu-gcc 109 | gcflags: 110 | - all=-N -l 111 | ldflags: 112 | - -X main.commit={{.Commit}} 113 | - -X main.date={{.Date}} 114 | - -extldflags "-static" 115 | tags: 116 | - musl 117 | - netgo 118 | - osusergo 119 | goos: 120 | - linux 121 | goarch: 122 | - arm64 123 | - id: casaos-gateway-migration-tool-arm-7 124 | binary: build/sysroot/usr/bin/casaos-gateway-migration-tool 125 | main: ./cmd/migration-tool 126 | env: 127 | - CC=arm-linux-gnueabihf-gcc 128 | gcflags: 129 | - all=-N -l 130 | ldflags: 131 | - -X main.commit={{.Commit}} 132 | - -X main.date={{.Date}} 133 | - -extldflags "-static" 134 | tags: 135 | - musl 136 | - netgo 137 | - osusergo 138 | goos: 139 | - linux 140 | goarch: 141 | - arm 142 | goarm: 143 | - "7" 144 | - id: casaos-gateway-migration-tool-riscv64 145 | binary: build/sysroot/usr/bin/casaos-gateway-migration-tool 146 | main: ./cmd/migration-tool 147 | env: 148 | - CC=riscv64-linux-gnu-gcc 149 | gcflags: 150 | - all=-N -l 151 | ldflags: 152 | - -X main.commit={{.Commit}} 153 | - -X main.date={{.Date}} 154 | - -extldflags "-static" 155 | tags: 156 | - musl 157 | - netgo 158 | - osusergo 159 | goos: 160 | - linux 161 | goarch: 162 | - riscv64 163 | archives: 164 | - name_template: >- 165 | {{ .Os }}-{{- if eq .Arch "arm" }}arm-7{{- else }}{{ .Arch }}{{- end }}-{{ .ProjectName }}-v{{ .Version }} 166 | id: casaos-gateway 167 | builds: 168 | - casaos-gateway-amd64 169 | - casaos-gateway-arm64 170 | - casaos-gateway-arm-7 171 | - casaos-gateway-riscv64 172 | files: 173 | - build/**/* 174 | - name_template: >- 175 | {{ .Os }}-{{- if eq .Arch "arm" }}arm-7{{- else }}{{ .Arch }}{{- end }}-{{ .ProjectName }}-migration-tool-v{{ .Version }} 176 | id: casaos-gateway-migration-tool 177 | builds: 178 | - casaos-gateway-migration-tool-amd64 179 | - casaos-gateway-migration-tool-arm64 180 | - casaos-gateway-migration-tool-arm-7 181 | - casaos-gateway-migration-tool-riscv64 182 | files: 183 | - build/sysroot/etc/**/* 184 | checksum: 185 | name_template: "checksums.txt" 186 | snapshot: 187 | name_template: "{{ incpatch .Version }}" 188 | changelog: 189 | sort: asc 190 | filters: 191 | exclude: 192 | - "^docs:" 193 | - "^test:" 194 | release: 195 | github: 196 | owner: IceWhaleTech 197 | name: CasaOS-Gateway 198 | draft: true 199 | prerelease: auto 200 | mode: replace 201 | name_template: "v{{ .Version }}" 202 | -------------------------------------------------------------------------------- /.goreleaser.yaml: -------------------------------------------------------------------------------- 1 | # This is an example .goreleaser.yml file with some sensible defaults. 2 | # Make sure to check the documentation at https://goreleaser.com 3 | project_name: casaos-gateway 4 | before: 5 | hooks: 6 | - go generate 7 | - go run github.com/google/go-licenses@latest check . --disallowed_types=restricted 8 | - go mod tidy 9 | - go test -race -v ./... 10 | builds: 11 | - id: casaos-gateway-amd64 12 | binary: build/sysroot/usr/bin/casaos-gateway 13 | hooks: 14 | post: 15 | - upx --best --lzma -v --no-progress "{{ .Path }}" 16 | env: 17 | - CC=x86_64-linux-gnu-gcc 18 | ldflags: 19 | - -X main.commit={{.Commit}} 20 | - -X main.date={{.Date}} 21 | - -s 22 | - -w 23 | - -extldflags "-static" 24 | tags: 25 | - musl 26 | - netgo 27 | - osusergo 28 | goos: 29 | - linux 30 | goarch: 31 | - amd64 32 | - id: casaos-gateway-arm64 33 | binary: build/sysroot/usr/bin/casaos-gateway 34 | # hooks: 35 | # post: 36 | # - upx --best --lzma -v --no-progress "{{ .Path }}" 37 | env: 38 | - CC=aarch64-linux-gnu-gcc 39 | ldflags: 40 | - -X main.commit={{.Commit}} 41 | - -X main.date={{.Date}} 42 | - -s 43 | - -w 44 | - -extldflags "-static" 45 | tags: 46 | - musl 47 | - netgo 48 | - osusergo 49 | goos: 50 | - linux 51 | goarch: 52 | - arm64 53 | - id: casaos-gateway-arm-7 54 | binary: build/sysroot/usr/bin/casaos-gateway 55 | hooks: 56 | post: 57 | - upx --best --lzma -v --no-progress "{{ .Path }}" 58 | env: 59 | - CC=arm-linux-gnueabihf-gcc 60 | ldflags: 61 | - -X main.commit={{.Commit}} 62 | - -X main.date={{.Date}} 63 | - -s 64 | - -w 65 | - -extldflags "-static" 66 | tags: 67 | - musl 68 | - netgo 69 | - osusergo 70 | goos: 71 | - linux 72 | goarch: 73 | - arm 74 | goarm: 75 | - "7" 76 | - id: casaos-gateway-riscv64 77 | binary: build/sysroot/usr/bin/casaos-gateway 78 | env: 79 | - CC=riscv64-linux-gnu-gcc 80 | ldflags: 81 | - -X main.commit={{.Commit}} 82 | - -X main.date={{.Date}} 83 | - -s 84 | - -w 85 | - -extldflags "-static" 86 | tags: 87 | - musl 88 | - netgo 89 | - osusergo 90 | goos: 91 | - linux 92 | goarch: 93 | - riscv64 94 | - id: casaos-gateway-migration-tool-amd64 95 | binary: build/sysroot/usr/bin/casaos-gateway-migration-tool 96 | hooks: 97 | post: 98 | - upx --best --lzma -v --no-progress "{{ .Path }}" 99 | main: ./cmd/migration-tool 100 | env: 101 | - CC=x86_64-linux-gnu-gcc 102 | ldflags: 103 | - -X main.commit={{.Commit}} 104 | - -X main.date={{.Date}} 105 | - -s 106 | - -w 107 | - -extldflags "-static" 108 | tags: 109 | - musl 110 | - netgo 111 | - osusergo 112 | goos: 113 | - linux 114 | goarch: 115 | - amd64 116 | - id: casaos-gateway-migration-tool-arm64 117 | binary: build/sysroot/usr/bin/casaos-gateway-migration-tool 118 | # hooks: 119 | # post: 120 | # - upx --best --lzma -v --no-progress "{{ .Path }}" 121 | main: ./cmd/migration-tool 122 | env: 123 | - CC=aarch64-linux-gnu-gcc 124 | ldflags: 125 | - -X main.commit={{.Commit}} 126 | - -X main.date={{.Date}} 127 | - -s 128 | - -w 129 | - -extldflags "-static" 130 | tags: 131 | - musl 132 | - netgo 133 | - osusergo 134 | goos: 135 | - linux 136 | goarch: 137 | - arm64 138 | - id: casaos-gateway-migration-tool-arm-7 139 | binary: build/sysroot/usr/bin/casaos-gateway-migration-tool 140 | hooks: 141 | post: 142 | - upx --best --lzma -v --no-progress "{{ .Path }}" 143 | main: ./cmd/migration-tool 144 | env: 145 | - CC=arm-linux-gnueabihf-gcc 146 | ldflags: 147 | - -X main.commit={{.Commit}} 148 | - -X main.date={{.Date}} 149 | - -s 150 | - -w 151 | - -extldflags "-static" 152 | tags: 153 | - musl 154 | - netgo 155 | - osusergo 156 | goos: 157 | - linux 158 | goarch: 159 | - arm 160 | goarm: 161 | - "7" 162 | - id: casaos-gateway-migration-tool-riscv64 163 | binary: build/sysroot/usr/bin/casaos-gateway-migration-tool 164 | main: ./cmd/migration-tool 165 | env: 166 | - CC=riscv64-linux-gnu-gcc 167 | ldflags: 168 | - -X main.commit={{.Commit}} 169 | - -X main.date={{.Date}} 170 | - -s 171 | - -w 172 | - -extldflags "-static" 173 | tags: 174 | - musl 175 | - netgo 176 | - osusergo 177 | goos: 178 | - linux 179 | goarch: 180 | - riscv64 181 | archives: 182 | - name_template: >- 183 | {{ .Os }}-{{- if eq .Arch "arm" }}arm-7{{- else }}{{ .Arch }}{{- end }}-{{ .ProjectName }}-v{{ .Version }} 184 | id: casaos-gateway 185 | builds: 186 | - casaos-gateway-amd64 187 | - casaos-gateway-arm64 188 | - casaos-gateway-arm-7 189 | - casaos-gateway-riscv64 190 | files: 191 | - build/**/* 192 | - name_template: >- 193 | {{ .Os }}-{{- if eq .Arch "arm" }}arm-7{{- else }}{{ .Arch }}{{- end }}-{{ .ProjectName }}-migration-tool-v{{ .Version }} 194 | id: casaos-gateway-migration-tool 195 | builds: 196 | - casaos-gateway-migration-tool-amd64 197 | - casaos-gateway-migration-tool-arm64 198 | - casaos-gateway-migration-tool-arm-7 199 | - casaos-gateway-migration-tool-riscv64 200 | files: 201 | - build/sysroot/etc/**/* 202 | checksum: 203 | name_template: "checksums.txt" 204 | snapshot: 205 | name_template: "{{ incpatch .Version }}" 206 | changelog: 207 | sort: asc 208 | filters: 209 | exclude: 210 | - "^docs:" 211 | - "^test:" 212 | release: 213 | github: 214 | owner: IceWhaleTech 215 | name: CasaOS-Gateway 216 | draft: false 217 | prerelease: auto 218 | mode: replace 219 | name_template: "v{{ .Version }}" 220 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # CasaOS-Gateway 2 | 3 | [![Go Reference](https://pkg.go.dev/badge/github.com/IceWhaleTech/CasaOS-Gateway.svg)](https://pkg.go.dev/github.com/IceWhaleTech/CasaOS-Gateway) [![Go Report Card](https://goreportcard.com/badge/github.com/IceWhaleTech/CasaOS-Gateway)](https://goreportcard.com/report/github.com/IceWhaleTech/CasaOS-Gateway) [![goreleaser](https://github.com/IceWhaleTech/CasaOS-Gateway/actions/workflows/release.yml/badge.svg)](https://github.com/IceWhaleTech/CasaOS-Gateway/actions/workflows/release.yml) [![codecov](https://codecov.io/gh/IceWhaleTech/CasaOS-Gateway/branch/main/graph/badge.svg?token=5JIHXF1RJ4)](https://codecov.io/gh/IceWhaleTech/CasaOS-Gateway) 4 | 5 | CasaOS Gateway is a dynamic API gateway service that can be used to expose APIs from different other HTTP based services. 6 | 7 | This gateway service comes with a simple management API for other services to register their APIs by route paths. A HTTP request arrived at gateway port will be forwarded to the service that is registered for the route path. 8 | 9 | > As a best practice, a service behind this gateway should bind to localhost (`127.0.0.1` for IPv4, `::1` for IPv6) ONLY, so no external network access is allowed. 10 | 11 | ## Configuration 12 | 13 | Upon launching, it will search for `gateway.ini` file in the following order: 14 | 15 | ```bash 16 | ./gateway.ini 17 | ./conf/gateway.ini 18 | $HOME/.casaos/gateway.ini 19 | /etc/casaos/gateway.ini 20 | ``` 21 | 22 | See [gateway.ini.sample](./build/etc/casaos/gateway.ini.sample) for default configuration. 23 | 24 | ## Running 25 | 26 | Once running, gateway address and management address will be available in the files under `RuntimePath` specified in configuration. 27 | 28 | ```bash 29 | $ cat /var/run/casaos/gateway.url 30 | [::]:8080 # port is specified in configuration 31 | 32 | $ cat /var/run/casaos/management.url 33 | [::]:34703 # port is randomly assigned 34 | ``` 35 | 36 | ## Example 37 | 38 | Assuming that 39 | 40 | - the management API is running on port `34703` 41 | - the gateway is running on port `8080` 42 | - some API running at `http://localhost:12345/ping` that simply returns `pong`. 43 | 44 | Then register the API as follows: 45 | 46 | - POST `http://localhost:34703/v1/gateway/routes` 47 | 48 | ```json 49 | { 50 | "path": "/ping", 51 | "target": "http://localhost:12345" 52 | } 53 | ``` 54 | 55 | or in command line: 56 | 57 | ```bash 58 | $ curl 'localhost:34703/v1/gateway/routes' --data-raw ' 59 | {"path": "/ping", "target": "http://localhost:12345"} 60 | ' 61 | ``` 62 | 63 | Now run 64 | 65 | ```bash 66 | $ curl localhost:8080/ping 67 | {"message":"pong"} 68 | ``` 69 | 70 | ... which is equivalent as 71 | 72 | ```bash 73 | $ curl localhost:12345/ping 74 | {"message":"pong"} 75 | ``` 76 | -------------------------------------------------------------------------------- /api/gateway/openapi.yaml: -------------------------------------------------------------------------------- 1 | openapi: 3.0.3 2 | 3 | info: 4 | title: CasaOS Gateway API 5 | version: v1 6 | description: |- 7 | 8 | 9 | 10 | CasaOS 11 | 12 | 13 | servers: 14 | - url: /v1/gateway 15 | 16 | tags: 17 | - name: USB methods 18 | description: |- 19 | USB methods 20 | 21 | security: 22 | - access_token: [] 23 | 24 | paths: 25 | /port: 26 | put: 27 | summary: Set gateway port 28 | description: |- 29 | Set gateway port 30 | operationId: setGatewayPort 31 | tags: 32 | - Gateway methods 33 | requestBody: 34 | content: 35 | application/json: 36 | schema: 37 | type: object 38 | properties: 39 | port: 40 | type: string 41 | description: Gateway port 42 | example: "80" 43 | responses: 44 | "200": 45 | $ref: "#/components/responses/ResponseOK" 46 | get: 47 | summary: Get gateway port 48 | description: |- 49 | Get gateway port 50 | operationId: getGatewayPort 51 | tags: 52 | - Gateway methods 53 | responses: 54 | "200": 55 | $ref: "#/components/responses/ResponseStringOK" 56 | "400": 57 | $ref: "#/components/responses/ResponseBadRequest" 58 | "500": 59 | $ref: "#/components/responses/ResponseInternalServerError" 60 | 61 | components: 62 | securitySchemes: 63 | access_token: 64 | type: apiKey 65 | in: header 66 | name: Authorization 67 | 68 | responses: 69 | ResponseOK: 70 | description: OK 71 | content: 72 | application/json: 73 | schema: 74 | $ref: "#/components/schemas/BaseResponse" 75 | ResponseStringOK: 76 | description: OK 77 | content: 78 | application/json: 79 | schema: 80 | $ref: "#/components/schemas/SuccessResponseString" 81 | ResponseBadRequest: 82 | description: Bad Request 83 | content: 84 | application/json: 85 | schema: 86 | readOnly: true 87 | allOf: 88 | - $ref: "#/components/schemas/BaseResponse" 89 | example: 90 | message: "Bad Request" 91 | ResponseInternalServerError: 92 | description: Internal Server Error 93 | content: 94 | application/json: 95 | schema: 96 | readOnly: true 97 | allOf: 98 | - $ref: "#/components/schemas/BaseResponse" 99 | example: 100 | message: "Internal Server Error" 101 | schemas: 102 | BaseResponse: 103 | properties: 104 | message: 105 | readOnly: true 106 | description: message returned by server side if there is any 107 | type: string 108 | example: "" 109 | 110 | SuccessResponseString: 111 | allOf: 112 | - $ref: "#/components/schemas/BaseResponse" 113 | - properties: 114 | data: 115 | type: string 116 | description: When the interface returns success, this field is the specific success information 117 | -------------------------------------------------------------------------------- /api/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CasaOS | Developers 6 | 7 | 8 | 9 | 10 | 11 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | -------------------------------------------------------------------------------- /build/scripts/migration/script.d/01-migrate-gateway.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -e 4 | 5 | # functions 6 | __info() { 7 | echo -e "🟩 ${1}" 8 | } 9 | 10 | __info_done() { 11 | echo -e "✅ ${1}" 12 | } 13 | 14 | __warning() { 15 | echo -e "🟨 ${1}" 16 | } 17 | 18 | __error() { 19 | echo "🟥 ${1}" 20 | exit 1 21 | } 22 | 23 | __normalize_version() { 24 | local version 25 | if [ "${1::1}" = "v" ]; then 26 | version="${1:1}" 27 | else 28 | version="${1}" 29 | fi 30 | 31 | echo "$version" 32 | } 33 | 34 | __is_version_gt() { 35 | test "$(echo "$@" | tr " " "\n" | sort -V | head -n 1)" != "$1" 36 | } 37 | 38 | __is_migration_needed() { 39 | local version1 40 | local version2 41 | 42 | version1=$(__normalize_version "${1}") 43 | version2=$(__normalize_version "${2}") 44 | 45 | if [ "${version1}" = "${version2}" ]; then 46 | return 1 47 | fi 48 | 49 | if [ "CURRENT_VERSION_NOT_FOUND" = "${version1}" ]; then 50 | return 1 51 | fi 52 | 53 | if [ "LEGACY_WITHOUT_VERSION" = "${version1}" ]; then 54 | return 0 55 | fi 56 | 57 | __is_version_gt "${version2}" "${version1}" 58 | } 59 | __get_download_domain(){ 60 | local region 61 | # Use ipconfig.io/country and https://ifconfig.io/country_code to get the country code 62 | region=$(curl --connect-timeout 2 -s ipconfig.io/country || echo "") 63 | if [ "${region}" = "" ]; then 64 | region=$(curl --connect-timeout 2 -s https://ifconfig.io/country_code || echo "") 65 | fi 66 | if [[ "${region}" = "China" ]] || [[ "${region}" = "CN" ]]; then 67 | echo "https://casaos.oss-cn-shanghai.aliyuncs.com/" 68 | else 69 | echo "https://github.com/" 70 | fi 71 | } 72 | 73 | DOWNLOAD_DOMAIN=$(__get_download_domain) 74 | 75 | BUILD_PATH=$(dirname "${BASH_SOURCE[0]}")/../../.. 76 | 77 | readonly BUILD_PATH 78 | readonly SOURCE_ROOT=${BUILD_PATH}/sysroot 79 | 80 | readonly APP_NAME="casaos-gateway" 81 | readonly APP_NAME_FORMAL="CasaOS-Gateway" 82 | readonly APP_NAME_SHORT="gateway" 83 | readonly APP_NAME_LEGACY="casaos" 84 | 85 | # check if migration is needed 86 | readonly SOURCE_BIN_PATH=${SOURCE_ROOT}/usr/bin 87 | readonly SOURCE_BIN_FILE=${SOURCE_BIN_PATH}/${APP_NAME} 88 | 89 | readonly CURRENT_BIN_PATH=/usr/bin 90 | readonly CURRENT_BIN_PATH_LEGACY=/usr/local/bin 91 | readonly CURRENT_BIN_FILE=${CURRENT_BIN_PATH}/${APP_NAME} 92 | 93 | CURRENT_BIN_FILE_LEGACY=$(realpath -e ${CURRENT_BIN_PATH}/${APP_NAME_LEGACY} || realpath -e ${CURRENT_BIN_PATH_LEGACY}/${APP_NAME_LEGACY} || which ${APP_NAME_LEGACY} || echo CURRENT_BIN_FILE_LEGACY_NOT_FOUND) 94 | readonly CURRENT_BIN_FILE_LEGACY 95 | 96 | SOURCE_VERSION="$(${SOURCE_BIN_FILE} -v)" 97 | readonly SOURCE_VERSION 98 | 99 | CURRENT_VERSION="$(${CURRENT_BIN_FILE} -v || ${CURRENT_BIN_FILE_LEGACY} -v || (stat "${CURRENT_BIN_FILE_LEGACY}" > /dev/null && echo LEGACY_WITHOUT_VERSION) || echo CURRENT_VERSION_NOT_FOUND)" 100 | readonly CURRENT_VERSION 101 | 102 | __info_done "CURRENT_VERSION: ${CURRENT_VERSION}" 103 | __info_done "SOURCE_VERSION: ${SOURCE_VERSION}" 104 | 105 | NEED_MIGRATION=$(__is_migration_needed "${CURRENT_VERSION}" "${SOURCE_VERSION}" && echo "true" || echo "false") 106 | readonly NEED_MIGRATION 107 | 108 | if [ "${NEED_MIGRATION}" = "false" ]; then 109 | __info_done "Migration is not needed." 110 | exit 0 111 | fi 112 | 113 | MIGRATION_SERVICE_DIR=${1} 114 | if [ -z "${MIGRATION_SERVICE_DIR}" ]; then 115 | MIGRATION_SERVICE_DIR=${BUILD_PATH}/scripts/migration/service.d/${APP_NAME_SHORT} 116 | fi 117 | 118 | readonly MIGRATION_LIST_FILE=${MIGRATION_SERVICE_DIR}/migration.list 119 | 120 | MIGRATION_PATH=() 121 | CURRENT_VERSION_FOUND="false" 122 | 123 | # a VERSION_PAIR looks like "v0.3.5 v0.3.6-alpha2" 124 | # 125 | # - "v0.3.5" is the current version installed on this host 126 | # - "v0.3.6-alpha2" is the version of the migration tool from GitHub 127 | while read -r VERSION_PAIR; do 128 | if [ -z "${VERSION_PAIR}" ]; then 129 | continue 130 | fi 131 | 132 | # obtain "v0.3.5" from "v0.3.5 v0.3.6-alpha2" 133 | VER1=$(echo "${VERSION_PAIR}" | cut -d' ' -f1) 134 | 135 | # obtain "v0.3.6-alpha2" from "v0.3.5 v0.3.6-alpha2" 136 | VER2=$(echo "${VERSION_PAIR}" | cut -d' ' -f2) 137 | 138 | if [ "${CURRENT_VERSION}" = "${VER1// /}" ] || [ "${CURRENT_VERSION}" = "LEGACY_WITHOUT_VERSION" ]; then 139 | CURRENT_VERSION_FOUND="true" 140 | fi 141 | 142 | if [ "${CURRENT_VERSION_FOUND}" = "true" ]; then 143 | MIGRATION_PATH+=("${VER2// /}") 144 | fi 145 | done < "${MIGRATION_LIST_FILE}" 146 | 147 | if [ ${#MIGRATION_PATH[@]} -eq 0 ]; then 148 | __warning "No migration path found from ${CURRENT_VERSION} to ${SOURCE_VERSION}" 149 | exit 0 150 | fi 151 | 152 | ARCH="unknown" 153 | 154 | case $(uname -m) in 155 | x86_64) 156 | ARCH="amd64" 157 | ;; 158 | aarch64) 159 | ARCH="arm64" 160 | ;; 161 | armv7l) 162 | ARCH="arm-7" 163 | ;; 164 | riscv64) 165 | ARCH="riscv64" 166 | ;; 167 | *) 168 | __error "Unsupported architecture" 169 | ;; 170 | esac 171 | 172 | pushd "${MIGRATION_SERVICE_DIR}" 173 | 174 | { 175 | for VER2 in "${MIGRATION_PATH[@]}"; do 176 | MIGRATION_TOOL_FILE=linux-"${ARCH}"-"${APP_NAME}"-migration-tool-"${VER2}".tar.gz 177 | 178 | if [ -f "${MIGRATION_TOOL_FILE}" ]; then 179 | __info "Migration tool ${MIGRATION_TOOL_FILE} exists. Skip downloading." 180 | continue 181 | fi 182 | 183 | MIGRATION_TOOL_URL=${DOWNLOAD_DOMAIN}IceWhaleTech/"${APP_NAME_FORMAL}"/releases/download/"${VER2}"/linux-"${ARCH}"-"${APP_NAME}"-migration-tool-"${VER2}".tar.gz 184 | __info "Dowloading ${MIGRATION_TOOL_URL}..." 185 | curl -sL -O "${MIGRATION_TOOL_URL}" 186 | done 187 | } || { 188 | popd 189 | __error "Failed to download migration tools" 190 | } 191 | 192 | { 193 | for VER2 in "${MIGRATION_PATH[@]}"; do 194 | MIGRATION_TOOL_FILE=linux-"${ARCH}"-"${APP_NAME}"-migration-tool-"${VER2}".tar.gz 195 | __info "Extracting ${MIGRATION_TOOL_FILE}..." 196 | tar zxvf "${MIGRATION_TOOL_FILE}" || __error "Failed to extract ${MIGRATION_TOOL_FILE}" 197 | 198 | MIGRATION_TOOL_PATH=build/sysroot/usr/bin/${APP_NAME}-migration-tool 199 | __info "Running ${MIGRATION_TOOL_PATH}..." 200 | ${MIGRATION_TOOL_PATH} 201 | done 202 | } || { 203 | popd 204 | __error "Failed to extract and run migration tools" 205 | } 206 | 207 | popd 208 | -------------------------------------------------------------------------------- /build/scripts/migration/service.d/gateway/migration.list: -------------------------------------------------------------------------------- 1 | LEGACY_WITHOUT_VERSION v0.3.6 2 | v0.3.5 v0.3.6 3 | v0.3.5.1 v0.3.6 4 | -------------------------------------------------------------------------------- /build/scripts/setup/script.d/01-setup-gateway.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -e 4 | 5 | BUILD_PATH=$(dirname "${BASH_SOURCE[0]}")/../../.. 6 | 7 | readonly BUILD_PATH 8 | readonly APP_NAME_SHORT=gateway 9 | 10 | __get_setup_script_directory_by_os_release() { 11 | pushd "$(dirname "${BASH_SOURCE[0]}")/../service.d/${APP_NAME_SHORT}" >/dev/null 12 | 13 | { 14 | # shellcheck source=/dev/null 15 | { 16 | source /etc/os-release 17 | { 18 | pushd "${ID}"/"${VERSION_CODENAME}" >/dev/null 19 | } || { 20 | pushd "${ID}" >/dev/null 21 | } || { 22 | [[ -n ${ID_LIKE} ]] && for ID in ${ID_LIKE}; do 23 | pushd "${ID}" >/dev/null && break 24 | done 25 | } || { 26 | echo "Unsupported OS: ${ID} ${VERSION_CODENAME} (${ID_LIKE})" 27 | exit 1 28 | } 29 | 30 | pwd 31 | 32 | popd >/dev/null 33 | 34 | } || { 35 | echo "Unsupported OS: unknown" 36 | exit 1 37 | } 38 | 39 | } 40 | 41 | popd >/dev/null 42 | } 43 | 44 | SETUP_SCRIPT_DIRECTORY=$(__get_setup_script_directory_by_os_release) 45 | 46 | readonly SETUP_SCRIPT_DIRECTORY 47 | readonly SETUP_SCRIPT_FILENAME="setup-${APP_NAME_SHORT}.sh" 48 | readonly SETUP_SCRIPT_FILEPATH="${SETUP_SCRIPT_DIRECTORY}/${SETUP_SCRIPT_FILENAME}" 49 | 50 | { 51 | echo "🟩 Running ${SETUP_SCRIPT_FILENAME}..." 52 | $BASH "${SETUP_SCRIPT_FILEPATH}" "${BUILD_PATH}" 53 | } || { 54 | echo "🟥 ${SETUP_SCRIPT_FILENAME} failed." 55 | exit 1 56 | } 57 | 58 | echo "✅ ${SETUP_SCRIPT_FILENAME} finished." 59 | -------------------------------------------------------------------------------- /build/scripts/setup/service.d/gateway/arch/setup-gateway.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -e 4 | 5 | ## base variables 6 | readonly APP_NAME="casaos-gateway" 7 | readonly APP_NAME_SHORT="gateway" 8 | 9 | # copy config files 10 | readonly CONF_PATH=/etc/casaos 11 | readonly CONF_FILE=${CONF_PATH}/${APP_NAME_SHORT}.ini 12 | readonly CONF_FILE_SAMPLE=${CONF_PATH}/${APP_NAME_SHORT}.ini.sample 13 | 14 | if [ ! -f "${CONF_FILE}" ]; then \ 15 | echo "Initializing config file..." 16 | cp -v "${CONF_FILE_SAMPLE}" "${CONF_FILE}"; \ 17 | fi 18 | 19 | # enable and start service 20 | systemctl daemon-reload 21 | 22 | echo "Enabling service..." 23 | systemctl enable --force --no-ask-password "${APP_NAME}.service" 24 | 25 | #echo "Starting service..." 26 | #systemctl start --force --no-ask-password "${APP_NAME}.service" 27 | -------------------------------------------------------------------------------- /build/scripts/setup/service.d/gateway/debian/bullseye/setup-gateway.sh: -------------------------------------------------------------------------------- 1 | ../setup-gateway.sh -------------------------------------------------------------------------------- /build/scripts/setup/service.d/gateway/debian/setup-gateway.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -e 4 | 5 | ## base variables 6 | readonly APP_NAME="casaos-gateway" 7 | readonly APP_NAME_SHORT="gateway" 8 | 9 | # copy config files 10 | readonly CONF_PATH=/etc/casaos 11 | readonly CONF_FILE=${CONF_PATH}/${APP_NAME_SHORT}.ini 12 | readonly CONF_FILE_SAMPLE=${CONF_PATH}/${APP_NAME_SHORT}.ini.sample 13 | 14 | if [ ! -f "${CONF_FILE}" ]; then \ 15 | echo "Initializing config file..." 16 | cp -v "${CONF_FILE_SAMPLE}" "${CONF_FILE}"; \ 17 | fi 18 | 19 | # enable and start service 20 | systemctl daemon-reload 21 | 22 | echo "Enabling service..." 23 | systemctl enable --force --no-ask-password "${APP_NAME}.service" 24 | 25 | #echo "Starting service..." 26 | #systemctl start --force --no-ask-password "${APP_NAME}.service" 27 | -------------------------------------------------------------------------------- /build/scripts/setup/service.d/gateway/ubuntu/jammy/setup-gateway.sh: -------------------------------------------------------------------------------- 1 | ../setup-gateway.sh -------------------------------------------------------------------------------- /build/scripts/setup/service.d/gateway/ubuntu/setup-gateway.sh: -------------------------------------------------------------------------------- 1 | ../debian/setup-gateway.sh -------------------------------------------------------------------------------- /build/sysroot/etc/casaos/gateway.ini.sample: -------------------------------------------------------------------------------- 1 | [common] 2 | runtimepath=/var/run/casaos 3 | 4 | [gateway] 5 | port= 6 | -------------------------------------------------------------------------------- /build/sysroot/usr/lib/systemd/system/casaos-gateway.service: -------------------------------------------------------------------------------- 1 | [Unit] 2 | After=network.target 3 | Description=CasaOS Gateway 4 | 5 | [Service] 6 | ExecStartPre=/usr/bin/casaos-gateway -v 7 | ExecStart=/usr/bin/casaos-gateway 8 | PIDFile=/var/run/casaos/gateway.pid 9 | Restart=always 10 | Type=notify 11 | 12 | [Install] 13 | WantedBy=multi-user.target 14 | -------------------------------------------------------------------------------- /build/sysroot/usr/lib/systemd/system/casaos-gateway.service.buildroot: -------------------------------------------------------------------------------- 1 | [Unit] 2 | After=network.target 3 | Description=CasaOS Gateway 4 | 5 | [Service] 6 | ExecStartPre=/usr/bin/casaos-gateway -v 7 | ExecStart=/usr/bin/casaos-gateway -w /usr/share/casaos/www 8 | PIDFile=/var/run/casaos/gateway.pid 9 | Restart=always 10 | Type=notify 11 | 12 | [Install] 13 | WantedBy=multi-user.target 14 | -------------------------------------------------------------------------------- /build/sysroot/usr/share/casaos/cleanup/script.d/01-cleanup-gateway.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -e 4 | 5 | readonly APP_NAME_SHORT=gateway 6 | 7 | __get_setup_script_directory_by_os_release() { 8 | pushd "$(dirname "${BASH_SOURCE[0]}")/../service.d/${APP_NAME_SHORT}" &>/dev/null 9 | 10 | { 11 | # shellcheck source=/dev/null 12 | { 13 | source /etc/os-release 14 | { 15 | pushd "${ID}"/"${VERSION_CODENAME}" &>/dev/null 16 | } || { 17 | pushd "${ID}" &>/dev/null 18 | } || { 19 | [[ -n ${ID_LIKE} ]] && for ID in ${ID_LIKE}; do 20 | pushd "${ID}" >/dev/null && break 21 | done 22 | } || { 23 | echo "Unsupported OS: ${ID} ${VERSION_CODENAME} (${ID_LIKE})" 24 | exit 1 25 | } 26 | 27 | pwd 28 | 29 | popd &>/dev/null 30 | 31 | } || { 32 | echo "Unsupported OS: unknown" 33 | exit 1 34 | } 35 | 36 | } 37 | 38 | popd &>/dev/null 39 | } 40 | 41 | SETUP_SCRIPT_DIRECTORY=$(__get_setup_script_directory_by_os_release) 42 | 43 | readonly SETUP_SCRIPT_DIRECTORY 44 | readonly SETUP_SCRIPT_FILENAME="cleanup-${APP_NAME_SHORT}.sh" 45 | readonly SETUP_SCRIPT_FILEPATH="${SETUP_SCRIPT_DIRECTORY}/${SETUP_SCRIPT_FILENAME}" 46 | 47 | echo "🟩 Running ${SETUP_SCRIPT_FILENAME}..." 48 | $BASH "${SETUP_SCRIPT_FILEPATH}" "${BUILD_PATH}" 49 | -------------------------------------------------------------------------------- /build/sysroot/usr/share/casaos/cleanup/service.d/gateway/arch/cleanup-gateway.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -e 4 | 5 | readonly CASA_EXEC=casaos-gateway 6 | readonly CASA_SERVICE=casaos-gateway.service 7 | 8 | CASA_SERVICE_PATH=$(systemctl show ${CASA_SERVICE} --no-pager --property FragmentPath | cut -d'=' -sf2) 9 | readonly CASA_SERVICE_PATH 10 | 11 | CASA_CONF=$( grep -i ConditionFileNotEmpty "${CASA_SERVICE_PATH}" | cut -d'=' -sf2) 12 | if [[ -z "${CASA_CONF}" ]]; then 13 | CASA_CONF=/etc/casaos/gateway.ini 14 | fi 15 | 16 | readonly aCOLOUR=( 17 | '\e[38;5;154m' # green | Lines, bullets and separators 18 | '\e[1m' # Bold white | Main descriptions 19 | '\e[90m' # Grey | Credits 20 | '\e[91m' # Red | Update notifications Alert 21 | '\e[33m' # Yellow | Emphasis 22 | ) 23 | 24 | Show() { 25 | # OK 26 | if (($1 == 0)); then 27 | echo -e "${aCOLOUR[2]}[$COLOUR_RESET${aCOLOUR[0]} OK $COLOUR_RESET${aCOLOUR[2]}]$COLOUR_RESET $2" 28 | # FAILED 29 | elif (($1 == 1)); then 30 | echo -e "${aCOLOUR[2]}[$COLOUR_RESET${aCOLOUR[3]}FAILED$COLOUR_RESET${aCOLOUR[2]}]$COLOUR_RESET $2" 31 | # INFO 32 | elif (($1 == 2)); then 33 | echo -e "${aCOLOUR[2]}[$COLOUR_RESET${aCOLOUR[0]} INFO $COLOUR_RESET${aCOLOUR[2]}]$COLOUR_RESET $2" 34 | # NOTICE 35 | elif (($1 == 3)); then 36 | echo -e "${aCOLOUR[2]}[$COLOUR_RESET${aCOLOUR[4]}NOTICE$COLOUR_RESET${aCOLOUR[2]}]$COLOUR_RESET $2" 37 | fi 38 | } 39 | 40 | Warn() { 41 | echo -e "${aCOLOUR[3]}$1$COLOUR_RESET" 42 | } 43 | 44 | trap 'onCtrlC' INT 45 | onCtrlC() { 46 | echo -e "${COLOUR_RESET}" 47 | exit 1 48 | } 49 | 50 | if [[ ! -x "$(command -v ${CASA_EXEC})" ]]; then 51 | Show 2 "${CASA_EXEC} is not detected, exit the script." 52 | exit 1 53 | fi 54 | 55 | Show 2 "Stopping ${CASA_SERVICE}..." 56 | systemctl disable --now "${CASA_SERVICE}" || Show 3 "Failed to disable ${CASA_SERVICE}" 57 | 58 | rm -rvf "$(which ${CASA_EXEC})" || Show 3 "Failed to remove ${CASA_EXEC}" 59 | rm -rvf "${CASA_CONF}" || Show 3 "Failed to remove ${CASA_CONF}" 60 | 61 | rm -rvf /var/run/casaos/gateway.pid 62 | rm -rvf /var/run/casaos/management.url 63 | rm -rvf /var/run/casaos/routes.json 64 | rm -rvf /var/run/casaos/static.url 65 | rm -rvf /var/lib/casaos/www 66 | -------------------------------------------------------------------------------- /build/sysroot/usr/share/casaos/cleanup/service.d/gateway/debian/bullseye/cleanup-gateway.sh: -------------------------------------------------------------------------------- 1 | ../cleanup-gateway.sh -------------------------------------------------------------------------------- /build/sysroot/usr/share/casaos/cleanup/service.d/gateway/debian/cleanup-gateway.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -e 4 | 5 | readonly CASA_EXEC=casaos-gateway 6 | readonly CASA_SERVICE=casaos-gateway.service 7 | 8 | CASA_SERVICE_PATH=$(systemctl show ${CASA_SERVICE} --no-pager --property FragmentPath | cut -d'=' -sf2) 9 | readonly CASA_SERVICE_PATH 10 | 11 | CASA_CONF=$( grep -i ConditionFileNotEmpty "${CASA_SERVICE_PATH}" | cut -d'=' -sf2) 12 | if [[ -z "${CASA_CONF}" ]]; then 13 | CASA_CONF=/etc/casaos/gateway.ini 14 | fi 15 | 16 | readonly aCOLOUR=( 17 | '\e[38;5;154m' # green | Lines, bullets and separators 18 | '\e[1m' # Bold white | Main descriptions 19 | '\e[90m' # Grey | Credits 20 | '\e[91m' # Red | Update notifications Alert 21 | '\e[33m' # Yellow | Emphasis 22 | ) 23 | 24 | Show() { 25 | # OK 26 | if (($1 == 0)); then 27 | echo -e "${aCOLOUR[2]}[$COLOUR_RESET${aCOLOUR[0]} OK $COLOUR_RESET${aCOLOUR[2]}]$COLOUR_RESET $2" 28 | # FAILED 29 | elif (($1 == 1)); then 30 | echo -e "${aCOLOUR[2]}[$COLOUR_RESET${aCOLOUR[3]}FAILED$COLOUR_RESET${aCOLOUR[2]}]$COLOUR_RESET $2" 31 | # INFO 32 | elif (($1 == 2)); then 33 | echo -e "${aCOLOUR[2]}[$COLOUR_RESET${aCOLOUR[0]} INFO $COLOUR_RESET${aCOLOUR[2]}]$COLOUR_RESET $2" 34 | # NOTICE 35 | elif (($1 == 3)); then 36 | echo -e "${aCOLOUR[2]}[$COLOUR_RESET${aCOLOUR[4]}NOTICE$COLOUR_RESET${aCOLOUR[2]}]$COLOUR_RESET $2" 37 | fi 38 | } 39 | 40 | Warn() { 41 | echo -e "${aCOLOUR[3]}$1$COLOUR_RESET" 42 | } 43 | 44 | trap 'onCtrlC' INT 45 | onCtrlC() { 46 | echo -e "${COLOUR_RESET}" 47 | exit 1 48 | } 49 | 50 | if [[ ! -x "$(command -v ${CASA_EXEC})" ]]; then 51 | Show 2 "${CASA_EXEC} is not detected, exit the script." 52 | exit 1 53 | fi 54 | 55 | Show 2 "Stopping ${CASA_SERVICE}..." 56 | systemctl disable --now "${CASA_SERVICE}" || Show 3 "Failed to disable ${CASA_SERVICE}" 57 | 58 | rm -rvf "$(which ${CASA_EXEC})" || Show 3 "Failed to remove ${CASA_EXEC}" 59 | rm -rvf "${CASA_CONF}" || Show 3 "Failed to remove ${CASA_CONF}" 60 | 61 | rm -rvf /var/run/casaos/gateway.pid 62 | rm -rvf /var/run/casaos/management.url 63 | rm -rvf /var/run/casaos/routes.json 64 | rm -rvf /var/run/casaos/static.url 65 | rm -rvf /var/lib/casaos/www 66 | -------------------------------------------------------------------------------- /build/sysroot/usr/share/casaos/cleanup/service.d/gateway/ubuntu/cleanup-gateway.sh: -------------------------------------------------------------------------------- 1 | ../debian/cleanup-gateway.sh -------------------------------------------------------------------------------- /build/sysroot/usr/share/casaos/cleanup/service.d/gateway/ubuntu/jammy/cleanup-gateway.sh: -------------------------------------------------------------------------------- 1 | ../../debian/bullseye/cleanup-gateway.sh -------------------------------------------------------------------------------- /cmd/migration-tool/log.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "log" 5 | "os" 6 | ) 7 | 8 | type Logger struct { 9 | DebugMode bool 10 | 11 | _debug *log.Logger 12 | _info *log.Logger 13 | _error *log.Logger 14 | } 15 | 16 | func NewLogger() *Logger { 17 | return &Logger{ 18 | DebugMode: false, 19 | _debug: log.New(os.Stdout, "DEBUG: ", 0), 20 | _info: log.New(os.Stdout, "", 0), 21 | _error: log.New(os.Stderr, "ERROR: ", 0), 22 | } 23 | } 24 | 25 | func (l *Logger) Debug(format string, v ...interface{}) { 26 | if l.DebugMode { 27 | l._debug.Printf(format, v...) 28 | } 29 | } 30 | 31 | func (l *Logger) Info(format string, v ...interface{}) { 32 | l._info.Printf(format, v...) 33 | } 34 | 35 | func (l *Logger) Error(format string, v ...interface{}) { 36 | l._error.Printf(format, v...) 37 | } 38 | -------------------------------------------------------------------------------- /cmd/migration-tool/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | _ "embed" 5 | "flag" 6 | "fmt" 7 | "os" 8 | 9 | interfaces "github.com/IceWhaleTech/CasaOS-Common" 10 | "github.com/IceWhaleTech/CasaOS-Common/utils/systemctl" 11 | "github.com/IceWhaleTech/CasaOS-Gateway/common" 12 | ) 13 | 14 | const ( 15 | gatewayServiceName = "casaos-gateway.service" 16 | gatewayServiceNameShort = "gateway" 17 | ) 18 | 19 | //go:embedded ../../build/sysroot/etc/casaos/gateway.ini.sample 20 | //var _gatewayINISample string 21 | 22 | var ( 23 | commit = "private build" 24 | date = "private build" 25 | 26 | _logger *Logger 27 | ) 28 | 29 | // var _status *version.GlobalMigrationStatus 30 | 31 | func main() { 32 | versionFlag := flag.Bool("v", false, "version") 33 | debugFlag := flag.Bool("d", true, "debug") 34 | forceFlag := flag.Bool("f", false, "force") 35 | flag.Parse() 36 | 37 | if *versionFlag { 38 | fmt.Printf("v%s\n", common.Version) 39 | os.Exit(0) 40 | } 41 | 42 | println("git commit:", commit) 43 | println("build date:", date) 44 | 45 | _logger = NewLogger() 46 | 47 | if os.Getuid() != 0 { 48 | _logger.Info("Root privileges are required to run this program.") 49 | os.Exit(1) 50 | } 51 | 52 | if *debugFlag { 53 | _logger.DebugMode = true 54 | } 55 | 56 | if !*forceFlag { 57 | isRunning, err := systemctl.IsServiceRunning(gatewayServiceName) 58 | if err != nil { 59 | _logger.Error("Failed to check if %s is running", gatewayServiceName) 60 | panic(err) 61 | } 62 | 63 | if isRunning { 64 | _logger.Info("%s is running. If migration is still needed, try with -f.", gatewayServiceName) 65 | os.Exit(1) 66 | } 67 | } 68 | 69 | migrationTools := []interfaces.MigrationTool{ 70 | NewMigrationDummy(), 71 | } 72 | 73 | var selectedMigrationTool interfaces.MigrationTool 74 | 75 | // look for the right migration tool matching current version 76 | for _, tool := range migrationTools { 77 | migrationNeeded, err := tool.IsMigrationNeeded() 78 | if err != nil { 79 | panic(err) 80 | } 81 | 82 | if migrationNeeded { 83 | selectedMigrationTool = tool 84 | break 85 | } 86 | } 87 | 88 | if selectedMigrationTool == nil { 89 | _logger.Info("No migration to proceed.") 90 | return 91 | } 92 | 93 | if err := selectedMigrationTool.PreMigrate(); err != nil { 94 | panic(err) 95 | } 96 | 97 | if err := selectedMigrationTool.Migrate(); err != nil { 98 | panic(err) 99 | } 100 | 101 | if err := selectedMigrationTool.PostMigrate(); err != nil { 102 | _logger.Error("Migration succeeded, but post-migration failed: %s", err) 103 | } 104 | } 105 | -------------------------------------------------------------------------------- /cmd/migration-tool/migration_dummy.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | interfaces "github.com/IceWhaleTech/CasaOS-Common" 5 | ) 6 | 7 | type migrationTool struct{} 8 | 9 | func (u *migrationTool) IsMigrationNeeded() (bool, error) { 10 | return false, nil 11 | } 12 | 13 | func (u *migrationTool) PreMigrate() error { 14 | return nil 15 | } 16 | 17 | func (u *migrationTool) Migrate() error { 18 | return nil 19 | } 20 | 21 | func (u *migrationTool) PostMigrate() error { 22 | return nil 23 | } 24 | 25 | func NewMigrationDummy() interfaces.MigrationTool { 26 | return &migrationTool{} 27 | } 28 | -------------------------------------------------------------------------------- /common/config.go: -------------------------------------------------------------------------------- 1 | package common 2 | 3 | import ( 4 | "log" 5 | "os" 6 | "path/filepath" 7 | 8 | "github.com/spf13/viper" 9 | 10 | "github.com/IceWhaleTech/CasaOS-Common/utils/constants" 11 | ) 12 | 13 | const ( 14 | ConfigKeyLogPath = "gateway.LogPath" 15 | ConfigKeyLogSaveName = "gateway.LogSaveName" 16 | ConfigKeyLogFileExt = "gateway.LogFileExt" 17 | ConfigKeyGatewayPort = "gateway.Port" 18 | ConfigKeyRuntimePath = "common.RuntimePath" 19 | 20 | GatewayName = "gateway" 21 | GatewayConfigType = "ini" 22 | ) 23 | 24 | func LoadConfig() (*viper.Viper, error) { 25 | config := viper.New() 26 | 27 | config.SetDefault(ConfigKeyLogPath, constants.DefaultLogPath) 28 | config.SetDefault(ConfigKeyLogSaveName, GatewayName) 29 | config.SetDefault(ConfigKeyLogFileExt, "log") 30 | 31 | config.SetDefault(ConfigKeyRuntimePath, constants.DefaultRuntimePath) // See https://refspecs.linuxfoundation.org/FHS_3.0/fhs/ch05s13.html 32 | 33 | config.SetConfigName(GatewayName) 34 | config.SetConfigType(GatewayConfigType) 35 | 36 | if currentDirectory, err := os.Getwd(); err != nil { 37 | log.Println(err) 38 | } else { 39 | config.AddConfigPath(currentDirectory) 40 | config.AddConfigPath(filepath.Join(currentDirectory, "conf")) 41 | } 42 | 43 | if configPath, success := os.LookupEnv("CASAOS_CONFIG_PATH"); success { 44 | config.AddConfigPath(configPath) 45 | } 46 | 47 | config.AddConfigPath(constants.DefaultConfigPath) 48 | 49 | if err := config.ReadInConfig(); err != nil { 50 | return nil, err 51 | } 52 | 53 | return config, nil 54 | } 55 | -------------------------------------------------------------------------------- /common/version.go: -------------------------------------------------------------------------------- 1 | package common 2 | 3 | const Version = "0.4.8" 4 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/IceWhaleTech/CasaOS-Gateway 2 | 3 | go 1.20 4 | 5 | require ( 6 | github.com/IceWhaleTech/CasaOS-Common v0.4.8-alpha9 7 | github.com/labstack/echo/v4 v4.12.0 8 | github.com/spf13/viper v1.18.2 9 | go.uber.org/fx v1.20.1 10 | gotest.tools v2.2.0+incompatible 11 | ) 12 | 13 | require ( 14 | github.com/benbjohnson/clock v1.3.1 // indirect 15 | github.com/bytedance/sonic v1.9.1 // indirect 16 | github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311 // indirect 17 | github.com/coreos/go-systemd/v22 v22.5.0 // indirect 18 | github.com/gabriel-vasile/mimetype v1.4.2 // indirect 19 | github.com/gin-gonic/gin v1.9.1 // indirect 20 | github.com/godbus/dbus/v5 v5.1.0 // indirect 21 | github.com/golang-jwt/jwt v3.2.2+incompatible // indirect 22 | github.com/klauspost/cpuid/v2 v2.2.4 // indirect 23 | github.com/labstack/gommon v0.4.2 // indirect 24 | github.com/mattn/go-colorable v0.1.13 // indirect 25 | github.com/rogpeppe/go-internal v1.10.0 // indirect 26 | github.com/sagikazarmark/locafero v0.4.0 // indirect 27 | github.com/sagikazarmark/slog-shim v0.1.0 // indirect 28 | github.com/samber/lo v1.38.1 // indirect 29 | github.com/sourcegraph/conc v0.3.0 // indirect 30 | github.com/twitchyliquid64/golang-asm v0.15.1 // indirect 31 | github.com/valyala/bytebufferpool v1.0.0 // indirect 32 | github.com/valyala/fasttemplate v1.2.2 // indirect 33 | golang.org/x/arch v0.3.0 // indirect 34 | golang.org/x/exp v0.0.0-20230905200255-921286631fa9 // indirect 35 | golang.org/x/time v0.5.0 // indirect 36 | gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c // indirect 37 | ) 38 | 39 | require ( 40 | github.com/coreos/go-systemd v0.0.0-20191104093116-d3cd4ed1dbcf 41 | github.com/fsnotify/fsnotify v1.7.0 // indirect 42 | github.com/gin-contrib/sse v0.1.0 // indirect 43 | github.com/go-playground/locales v0.14.1 // indirect 44 | github.com/go-playground/universal-translator v0.18.1 // indirect 45 | github.com/go-playground/validator/v10 v10.14.0 // indirect 46 | github.com/goccy/go-json v0.10.2 // indirect 47 | github.com/golang-jwt/jwt/v4 v4.5.0 // indirect 48 | github.com/google/go-cmp v0.6.0 // indirect 49 | github.com/hashicorp/hcl v1.0.0 // indirect 50 | github.com/json-iterator/go v1.1.12 // indirect 51 | github.com/leodido/go-urn v1.2.4 // indirect 52 | github.com/magiconair/properties v1.8.7 // indirect 53 | github.com/mattn/go-isatty v0.0.20 // indirect 54 | github.com/mitchellh/mapstructure v1.5.0 // indirect 55 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect 56 | github.com/modern-go/reflect2 v1.0.2 // indirect 57 | github.com/pelletier/go-toml/v2 v2.1.0 // indirect 58 | github.com/pkg/errors v0.9.1 // indirect 59 | github.com/spf13/afero v1.11.0 // indirect 60 | github.com/spf13/cast v1.6.0 // indirect 61 | github.com/spf13/pflag v1.0.5 // indirect 62 | github.com/subosito/gotenv v1.6.0 // indirect 63 | github.com/ugorji/go/codec v1.2.11 // indirect 64 | go.uber.org/dig v1.17.0 // indirect 65 | go.uber.org/multierr v1.11.0 // indirect 66 | go.uber.org/zap v1.27.0 67 | golang.org/x/crypto v0.22.0 // indirect 68 | golang.org/x/net v0.24.0 // indirect 69 | golang.org/x/sys v0.19.0 // indirect 70 | golang.org/x/text v0.14.0 // indirect 71 | google.golang.org/protobuf v1.31.0 // indirect 72 | gopkg.in/ini.v1 v1.67.0 // indirect 73 | gopkg.in/natefinch/lumberjack.v2 v2.2.1 // indirect 74 | gopkg.in/yaml.v3 v3.0.1 // indirect 75 | gotest.tools/v3 v3.5.1 76 | ) 77 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/IceWhaleTech/CasaOS-Common v0.4.8-alpha9 h1:81CluyrIFBjO49XeI0mILxtyQ7XXDKpc3vOtFx5Piwk= 2 | github.com/IceWhaleTech/CasaOS-Common v0.4.8-alpha9/go.mod h1:2IuYyy5qW1BE6jqC6M+tOU+WtUec1K565rLATBJ9p/0= 3 | github.com/benbjohnson/clock v1.3.1 h1:Heo0FGXzOxUHquZbraxt+tT7UXVDhesUQH5ISbsOkCQ= 4 | github.com/benbjohnson/clock v1.3.1/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZxNJlLklBHA= 5 | github.com/bytedance/sonic v1.5.0/go.mod h1:ED5hyg4y6t3/9Ku1R6dU/4KyJ48DZ4jPhfY1O2AihPM= 6 | github.com/bytedance/sonic v1.9.1 h1:6iJ6NqdoxCDr6mbY8h18oSO+cShGSMRGCEo7F2h0x8s= 7 | github.com/bytedance/sonic v1.9.1/go.mod h1:i736AoUSYt75HyZLoJW9ERYxcy6eaN6h4BZXU064P/U= 8 | github.com/chenzhuoyu/base64x v0.0.0-20211019084208-fb5309c8db06/go.mod h1:DH46F32mSOjUmXrMHnKwZdA8wcEefY7UVqBKYGjpdQY= 9 | github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311 h1:qSGYFH7+jGhDF8vLC+iwCD4WpbV1EBDSzWkJODFLams= 10 | github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311/go.mod h1:b583jCggY9gE99b6G5LEC39OIiVsWj+R97kbl5odCEk= 11 | github.com/coreos/go-systemd v0.0.0-20191104093116-d3cd4ed1dbcf h1:iW4rZ826su+pqaw19uhpSCzhj44qo35pNgKFGqzDKkU= 12 | github.com/coreos/go-systemd v0.0.0-20191104093116-d3cd4ed1dbcf/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4= 13 | github.com/coreos/go-systemd/v22 v22.5.0 h1:RrqgGjYQKalulkV8NGVIfkXQf6YYmOyiJKk8iXXhfZs= 14 | github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc= 15 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 16 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 17 | github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= 18 | github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8= 19 | github.com/fsnotify/fsnotify v1.7.0 h1:8JEhPFa5W2WU7YfeZzPNqzMP6Lwt7L2715Ggo0nosvA= 20 | github.com/fsnotify/fsnotify v1.7.0/go.mod h1:40Bi/Hjc2AVfZrqy+aj+yEI+/bRxZnMJyTJwOpGvigM= 21 | github.com/gabriel-vasile/mimetype v1.4.2 h1:w5qFW6JKBz9Y393Y4q372O9A7cUSequkh1Q7OhCmWKU= 22 | github.com/gabriel-vasile/mimetype v1.4.2/go.mod h1:zApsH/mKG4w07erKIaJPFiX0Tsq9BFQgN3qGY5GnNgA= 23 | github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE= 24 | github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI= 25 | github.com/gin-gonic/gin v1.9.1 h1:4idEAncQnU5cB7BeOkPtxjfCSye0AAm1R0RVIqJ+Jmg= 26 | github.com/gin-gonic/gin v1.9.1/go.mod h1:hPrL7YrpYKXt5YId3A/Tnip5kqbEAP+KLuI3SUcPTeU= 27 | github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s= 28 | github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA= 29 | github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY= 30 | github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY= 31 | github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY= 32 | github.com/go-playground/validator/v10 v10.14.0 h1:vgvQWe3XCz3gIeFDm/HnTIbj6UGmg/+t63MyGU2n5js= 33 | github.com/go-playground/validator/v10 v10.14.0/go.mod h1:9iXMNT7sEkjXb0I+enO7QXmzG6QCsPWY4zveKFVRSyU= 34 | github.com/goccy/go-json v0.10.2 h1:CrxCmQqYDkv1z7lO7Wbh2HN93uovUHgrECaO5ZrCXAU= 35 | github.com/goccy/go-json v0.10.2/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I= 36 | github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= 37 | github.com/godbus/dbus/v5 v5.1.0 h1:4KLkAxT3aOY8Li4FRJe/KvhoNFFxo0m6fNuFUO8QJUk= 38 | github.com/godbus/dbus/v5 v5.1.0/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= 39 | github.com/golang-jwt/jwt v3.2.2+incompatible h1:IfV12K8xAKAnZqdXVzCZ+TOjboZ2keLg81eXfW3O+oY= 40 | github.com/golang-jwt/jwt v3.2.2+incompatible/go.mod h1:8pz2t5EyA70fFQQSrl6XZXzqecmYZeUEB8OUGHkxJ+I= 41 | github.com/golang-jwt/jwt/v4 v4.5.0 h1:7cYmW1XlMY7h7ii7UhUyChSgS5wUJEnm9uZVTGqOWzg= 42 | github.com/golang-jwt/jwt/v4 v4.5.0/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0= 43 | github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= 44 | github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 45 | github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= 46 | github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= 47 | github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= 48 | github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4= 49 | github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ= 50 | github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= 51 | github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= 52 | github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg= 53 | github.com/klauspost/cpuid/v2 v2.2.4 h1:acbojRNwl3o09bUq+yDCtZFc1aiwaAAxtcn8YkZXnvk= 54 | github.com/klauspost/cpuid/v2 v2.2.4/go.mod h1:RVVoqg1df56z8g3pUjL/3lE5UfnlrJX8tyFgg4nqhuY= 55 | github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= 56 | github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= 57 | github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= 58 | github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= 59 | github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= 60 | github.com/labstack/echo/v4 v4.12.0 h1:IKpw49IMryVB2p1a4dzwlhP1O2Tf2E0Ir/450lH+kI0= 61 | github.com/labstack/echo/v4 v4.12.0/go.mod h1:UP9Cr2DJXbOK3Kr9ONYzNowSh7HP0aG0ShAyycHSJvM= 62 | github.com/labstack/gommon v0.4.2 h1:F8qTUNXgG1+6WQmqoUWnz8WiEU60mXVVw0P4ht1WRA0= 63 | github.com/labstack/gommon v0.4.2/go.mod h1:QlUFxVM+SNXhDL/Z7YhocGIBYOiwB0mXm1+1bAPHPyU= 64 | github.com/leodido/go-urn v1.2.4 h1:XlAE/cm/ms7TE/VMVoduSpNBoyc2dOxHs5MZSwAN63Q= 65 | github.com/leodido/go-urn v1.2.4/go.mod h1:7ZrI8mTSeBSHl/UaRyKQW1qZeMgak41ANeCNaVckg+4= 66 | github.com/magiconair/properties v1.8.7 h1:IeQXZAiQcpL9mgcAe1Nu6cX9LLw6ExEHKjN0VQdvPDY= 67 | github.com/magiconair/properties v1.8.7/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0= 68 | github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= 69 | github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= 70 | github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= 71 | github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= 72 | github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= 73 | github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY= 74 | github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= 75 | github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= 76 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= 77 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= 78 | github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= 79 | github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= 80 | github.com/pelletier/go-toml/v2 v2.1.0 h1:FnwAJ4oYMvbT/34k9zzHuZNrhlz48GB3/s6at6/MHO4= 81 | github.com/pelletier/go-toml/v2 v2.1.0/go.mod h1:tJU2Z3ZkXwnxa4DPO899bsyIoywizdUvyaeZurnPPDc= 82 | github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= 83 | github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 84 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 85 | github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= 86 | github.com/rogpeppe/go-internal v1.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjRBZyWFQ= 87 | github.com/rogpeppe/go-internal v1.10.0/go.mod h1:UQnix2H7Ngw/k4C5ijL5+65zddjncjaFoBhdsK/akog= 88 | github.com/sagikazarmark/locafero v0.4.0 h1:HApY1R9zGo4DBgr7dqsTH/JJxLTTsOt7u6keLGt6kNQ= 89 | github.com/sagikazarmark/locafero v0.4.0/go.mod h1:Pe1W6UlPYUk/+wc/6KFhbORCfqzgYEpgQ3O5fPuL3H4= 90 | github.com/sagikazarmark/slog-shim v0.1.0 h1:diDBnUNK9N/354PgrxMywXnAwEr1QZcOr6gto+ugjYE= 91 | github.com/sagikazarmark/slog-shim v0.1.0/go.mod h1:SrcSrq8aKtyuqEI1uvTDTK1arOWRIczQRv+GVI1AkeQ= 92 | github.com/samber/lo v1.38.1 h1:j2XEAqXKb09Am4ebOg31SpvzUTTs6EN3VfgeLUhPdXM= 93 | github.com/samber/lo v1.38.1/go.mod h1:+m/ZKRl6ClXCE2Lgf3MsQlWfh4bn1bz6CXEOxnEXnEA= 94 | github.com/sourcegraph/conc v0.3.0 h1:OQTbbt6P72L20UqAkXXuLOj79LfEanQ+YQFNpLA9ySo= 95 | github.com/sourcegraph/conc v0.3.0/go.mod h1:Sdozi7LEKbFPqYX2/J+iBAM6HpqSLTASQIKqDmF7Mt0= 96 | github.com/spf13/afero v1.11.0 h1:WJQKhtpdm3v2IzqG8VMqrr6Rf3UYpEF239Jy9wNepM8= 97 | github.com/spf13/afero v1.11.0/go.mod h1:GH9Y3pIexgf1MTIWtNGyogA5MwRIDXGUr+hbWNoBjkY= 98 | github.com/spf13/cast v1.6.0 h1:GEiTHELF+vaR5dhz3VqZfFSzZjYbgeKDpBxQVS4GYJ0= 99 | github.com/spf13/cast v1.6.0/go.mod h1:ancEpBxwJDODSW/UG4rDrAqiKolqNNh2DX3mk86cAdo= 100 | github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= 101 | github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= 102 | github.com/spf13/viper v1.18.2 h1:LUXCnvUvSM6FXAsj6nnfc8Q2tp1dIgUfY9Kc8GsSOiQ= 103 | github.com/spf13/viper v1.18.2/go.mod h1:EKmWIqdnk5lOcmR72yw6hS+8OPYcwD0jteitLMVB+yk= 104 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 105 | github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= 106 | github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= 107 | github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= 108 | github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 109 | github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 110 | github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= 111 | github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= 112 | github.com/stretchr/testify v1.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= 113 | github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= 114 | github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= 115 | github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8= 116 | github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU= 117 | github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI= 118 | github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08= 119 | github.com/ugorji/go/codec v1.2.11 h1:BMaWp1Bb6fHwEtbplGBGJ498wD+LKlNSl25MjdZY4dU= 120 | github.com/ugorji/go/codec v1.2.11/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg= 121 | github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw= 122 | github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc= 123 | github.com/valyala/fasttemplate v1.2.2 h1:lxLXG0uE3Qnshl9QyaK6XJxMXlQZELvChBOCmQD0Loo= 124 | github.com/valyala/fasttemplate v1.2.2/go.mod h1:KHLXt3tVN2HBp8eijSv/kGJopbvo7S+qRAEEKiv+SiQ= 125 | go.uber.org/atomic v1.10.0 h1:9qC72Qh0+3MqyJbAn8YU5xVq1frD8bn3JtD2oXtafVQ= 126 | go.uber.org/dig v1.17.0 h1:5Chju+tUvcC+N7N6EV08BJz41UZuO3BmHcN4A287ZLI= 127 | go.uber.org/dig v1.17.0/go.mod h1:rTxpf7l5I0eBTlE6/9RL+lDybC7WFwY2QH55ZSjy1mU= 128 | go.uber.org/fx v1.20.1 h1:zVwVQGS8zYvhh9Xxcu4w1M6ESyeMzebzj2NbSayZ4Mk= 129 | go.uber.org/fx v1.20.1/go.mod h1:iSYNbHf2y55acNCwCXKx7LbWb5WG1Bnue5RDXz1OREg= 130 | go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= 131 | go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0= 132 | go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= 133 | go.uber.org/zap v1.27.0 h1:aJMhYGrd5QSmlpLMr2MftRKl7t8J8PTZPA732ud/XR8= 134 | go.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E= 135 | golang.org/x/arch v0.0.0-20210923205945-b76863e36670/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8= 136 | golang.org/x/arch v0.3.0 h1:02VY4/ZcO/gBOH6PUaoiptASxtXU10jazRCP865E97k= 137 | golang.org/x/arch v0.3.0/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8= 138 | golang.org/x/crypto v0.22.0 h1:g1v0xeRhjcugydODzvb3mEM9SQ0HGp9s/nh3COQ/C30= 139 | golang.org/x/crypto v0.22.0/go.mod h1:vr6Su+7cTlO45qkww3VDJlzDn0ctJvRgYbC2NvXHt+M= 140 | golang.org/x/exp v0.0.0-20230905200255-921286631fa9 h1:GoHiUyI/Tp2nVkLI2mCxVkOjsbSXD66ic0XW0js0R9g= 141 | golang.org/x/exp v0.0.0-20230905200255-921286631fa9/go.mod h1:S2oDrQGGwySpoQPVqRShND87VCbxmc6bL1Yd2oYrm6k= 142 | golang.org/x/net v0.24.0 h1:1PcaxkF854Fu3+lvBIx5SYn9wRlBzzcnHZSiaFFAb0w= 143 | golang.org/x/net v0.24.0/go.mod h1:2Q7sJY5mzlzWjKtYUEXSlBWCdyaioyXzRB2RtU8KVE8= 144 | golang.org/x/sys v0.0.0-20220704084225-05e143d24a9e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 145 | golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 146 | golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 147 | golang.org/x/sys v0.19.0 h1:q5f1RH2jigJ1MoAWp2KTp3gm5zAGFUTarQZ5U386+4o= 148 | golang.org/x/sys v0.19.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= 149 | golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ= 150 | golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= 151 | golang.org/x/time v0.5.0 h1:o7cqy6amK/52YcAKIPlM3a+Fpj35zvRj2TP+e1xFSfk= 152 | golang.org/x/time v0.5.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= 153 | golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 154 | google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= 155 | google.golang.org/protobuf v1.31.0 h1:g0LDEJHgrBl9N9r17Ru3sqWhkIx2NB67okBHPwC7hs8= 156 | google.golang.org/protobuf v1.31.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= 157 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 158 | gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= 159 | gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= 160 | gopkg.in/ini.v1 v1.67.0 h1:Dgnx+6+nfE+IfzjUEISNeydPJh9AXNNsWbGP9KzCsOA= 161 | gopkg.in/ini.v1 v1.67.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= 162 | gopkg.in/natefinch/lumberjack.v2 v2.2.1 h1:bBRl1b0OH9s/DuPhuXpNl+VtCaJXFZ5/uEFST95x9zc= 163 | gopkg.in/natefinch/lumberjack.v2 v2.2.1/go.mod h1:YD8tP3GAjkrDg1eZH7EGmyESg/lsYskCTPBJVb9jqSc= 164 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 165 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= 166 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 167 | gotest.tools v2.2.0+incompatible h1:VsBPFP1AI068pPrMxtb/S8Zkgf9xEmTLJjfM+P5UIEo= 168 | gotest.tools v2.2.0+incompatible/go.mod h1:DsYFclhRJ6vuDpmuTbkuFWG+y2sxOXAzmJt81HFBacw= 169 | gotest.tools/v3 v3.5.1 h1:EENdUnS3pdur5nybKYIh2Vfgc8IUNBjxDPSjtiJcOzU= 170 | gotest.tools/v3 v3.5.1/go.mod h1:isy3WKz7GK6uNw/sbHzfKBLvlvXwUyV06n6brMxxopU= 171 | rsc.io/pdf v0.1.1/go.mod h1:n8OzWcQ6Sp37PL01nO98y4iUCRdTGarVfzxY20ICaU4= 172 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "context" 5 | _ "embed" 6 | "errors" 7 | "flag" 8 | "fmt" 9 | "net" 10 | "net/http" 11 | "os" 12 | "os/signal" 13 | "path/filepath" 14 | "syscall" 15 | "time" 16 | 17 | "github.com/IceWhaleTech/CasaOS-Common/external" 18 | "github.com/IceWhaleTech/CasaOS-Common/model" 19 | "github.com/IceWhaleTech/CasaOS-Common/utils/constants" 20 | http2 "github.com/IceWhaleTech/CasaOS-Common/utils/http" 21 | "github.com/IceWhaleTech/CasaOS-Common/utils/logger" 22 | "github.com/coreos/go-systemd/daemon" 23 | 24 | "github.com/IceWhaleTech/CasaOS-Gateway/common" 25 | "github.com/IceWhaleTech/CasaOS-Gateway/route" 26 | "github.com/IceWhaleTech/CasaOS-Gateway/service" 27 | "go.uber.org/fx" 28 | "go.uber.org/zap" 29 | ) 30 | 31 | const localhost = "127.0.0.1" 32 | 33 | var ( 34 | commit = "private build" 35 | date = "private build" 36 | 37 | _state *service.State 38 | _gateway *http.Server 39 | 40 | _managementServiceReady = make(chan struct{}) 41 | _gatewayServiceReady = make(chan struct{}) 42 | 43 | ErrCheckURLNotOK = errors.New("check url did not return 200 OK") 44 | 45 | //go:embed build/sysroot/etc/casaos/gateway.ini.sample 46 | _confSample string 47 | ) 48 | 49 | func init() { 50 | versionFlag := flag.Bool("v", false, "version") 51 | wwwPathFlag := flag.String("w", filepath.Join(constants.DefaultDataPath, "www"), "www path") 52 | flag.Parse() 53 | 54 | if *versionFlag { 55 | fmt.Printf("v%s\n", common.Version) 56 | os.Exit(0) 57 | } 58 | 59 | println("git commit:", commit) 60 | println("build date:", date) 61 | 62 | _state = service.NewState() 63 | 64 | // create default config file if not exist 65 | ConfigFilePath := filepath.Join(constants.DefaultConfigPath, common.GatewayName+"."+common.GatewayConfigType) 66 | if _, err := os.Stat(ConfigFilePath); os.IsNotExist(err) { 67 | fmt.Println("config file not exist, create it") 68 | // create config file 69 | file, err := os.Create(ConfigFilePath) 70 | if err != nil { 71 | panic(err) 72 | } 73 | defer file.Close() 74 | 75 | // write default config 76 | _, err = file.WriteString(_confSample) 77 | if err != nil { 78 | panic(err) 79 | } 80 | } 81 | 82 | config, err := common.LoadConfig() 83 | if err != nil { 84 | panic(err) 85 | } 86 | 87 | logger.LogInit( 88 | config.GetString(common.ConfigKeyLogPath), 89 | config.GetString(common.ConfigKeyLogSaveName), 90 | config.GetString(common.ConfigKeyLogFileExt), 91 | ) 92 | 93 | runtimePath := config.GetString(common.ConfigKeyRuntimePath) 94 | if err := _state.SetRuntimePath(runtimePath); err != nil { 95 | logger.Error("Failed to set runtime path", zap.Any("error", err), zap.Any(common.ConfigKeyRuntimePath, runtimePath)) 96 | panic(err) 97 | } 98 | 99 | gatewayPort := config.GetString(common.ConfigKeyGatewayPort) 100 | if err := _state.SetGatewayPort(gatewayPort); err != nil { 101 | logger.Error("Failed to set gateway port", zap.Any("error", err), zap.Any(common.ConfigKeyGatewayPort, gatewayPort)) 102 | panic(err) 103 | } 104 | 105 | if err := _state.SetWWWPath(*wwwPathFlag); err != nil { 106 | logger.Error("Failed to set www path", zap.Any("error", err), zap.String("wwwpath", *wwwPathFlag)) 107 | panic(err) 108 | } 109 | 110 | if err := checkPrequisites(_state); err != nil { 111 | logger.Error("Failed to check prequisites", zap.Any("error", err)) 112 | panic(err) 113 | } 114 | 115 | _state.OnGatewayPortChange(func(port string) error { 116 | config.Set(common.ConfigKeyGatewayPort, port) 117 | return config.WriteConfig() 118 | }) 119 | } 120 | 121 | func main() { 122 | pidFilename, err := writePidFile(_state.GetRuntimePath()) 123 | if err != nil { 124 | logger.Error("Failed to write pid file to runtime path", zap.Any("error", err), zap.Any("runtimePath", _state.GetRuntimePath())) 125 | panic(err) 126 | } 127 | 128 | defer cleanupFiles( 129 | _state.GetRuntimePath(), 130 | pidFilename, external.ManagementURLFilename, external.StaticURLFilename, 131 | ) 132 | 133 | defer func() { 134 | if _gateway != nil { 135 | if err := _gateway.Shutdown(context.Background()); err != nil { 136 | logger.Error("Failed to stop gateway", zap.Any("error", err)) 137 | } 138 | } 139 | }() 140 | 141 | ctx, cancel := context.WithCancel(context.Background()) 142 | kill := make(chan os.Signal, 1) 143 | signal.Notify(kill, syscall.SIGTERM, syscall.SIGINT) 144 | 145 | go func() { 146 | <-kill 147 | cancel() 148 | }() 149 | 150 | go func() { 151 | <-_managementServiceReady 152 | <-_gatewayServiceReady 153 | 154 | if supported, err := daemon.SdNotify(false, daemon.SdNotifyReady); err != nil { 155 | logger.Error("Failed to notify systemd that gateway is ready", zap.Any("error", err)) 156 | } else if supported { 157 | logger.Info("Notified systemd that gateway is ready") 158 | } else { 159 | logger.Info("This process is not running as a systemd service.") 160 | } 161 | }() 162 | 163 | app := fx.New( 164 | fx.Provide(func() *service.State { return _state }), 165 | fx.Provide(service.NewManagementService), 166 | fx.Provide(route.NewManagementRoute), 167 | fx.Provide(route.NewGatewayRoute), 168 | fx.Provide(route.NewStaticRoute), 169 | fx.Invoke(run), 170 | ) 171 | 172 | if err := app.Start(ctx); err != nil { 173 | if err != context.Canceled { 174 | logger.Error("Failed to start gateway", zap.Any("error", err)) 175 | } 176 | } 177 | } 178 | 179 | func run( 180 | lifecycle fx.Lifecycle, 181 | management *service.Management, 182 | managementRoute *route.ManagementRoute, 183 | gatewayRoute *route.GatewayRoute, 184 | staticRoute *route.StaticRoute, 185 | ) { 186 | // management server 187 | lifecycle.Append( 188 | fx.Hook{ 189 | OnStart: func(context.Context) error { 190 | listener, err := net.Listen("tcp", net.JoinHostPort(localhost, "0")) 191 | if err != nil { 192 | return err 193 | } 194 | 195 | managementServer := &http.Server{ 196 | Handler: managementRoute.GetRoute(), 197 | ReadHeaderTimeout: 5 * time.Second, 198 | } 199 | 200 | urlFilePath, err := writeAddressFile(_state.GetRuntimePath(), external.ManagementURLFilename, "http://"+listener.Addr().String()) 201 | if err != nil { 202 | return err 203 | } 204 | 205 | go func() { 206 | logger.Info("Management service is listening...", 207 | zap.Any("address", listener.Addr().String()), 208 | zap.Any("filepath", urlFilePath), 209 | ) 210 | err := managementServer.Serve(listener) 211 | if err != nil { 212 | logger.Error("management server error", zap.Any("error", err)) 213 | os.Exit(1) 214 | } 215 | }() 216 | 217 | if err := management.CreateRoute(&model.Route{ 218 | Path: "/v1/gateway/port", 219 | Target: "http://" + listener.Addr().String(), 220 | }); err != nil { 221 | return err 222 | } 223 | 224 | _managementServiceReady <- struct{}{} 225 | 226 | return nil 227 | }, 228 | }, 229 | ) 230 | 231 | // gateway service 232 | lifecycle.Append( 233 | fx.Hook{ 234 | OnStart: func(ctx context.Context) error { 235 | route := gatewayRoute.GetRoute() 236 | 237 | if _state.GetGatewayPort() == "" { 238 | // check if a port is available starting from port 80/8080 239 | portsToCheck := []int{} 240 | for i := 80; i < 90; i++ { 241 | portsToCheck = append(portsToCheck, i) 242 | } 243 | 244 | for i := 8080; i < 8090; i++ { 245 | portsToCheck = append(portsToCheck, i) 246 | } 247 | 248 | port := "" 249 | for _, p := range portsToCheck { 250 | port = fmt.Sprintf("%d", p) 251 | logger.Info("Checking if port is available...", zap.Any("port", port)) 252 | if listener, err := net.Listen("tcp", net.JoinHostPort("", port)); err == nil { 253 | if err = listener.Close(); err != nil { 254 | logger.Error("Failed to close listener", zap.Any("error", err), zap.Any("port", port)) 255 | continue 256 | } 257 | break 258 | } 259 | } 260 | 261 | if port == "" { 262 | return errors.New("No port available for gateway to use") 263 | } 264 | 265 | if err := _state.SetGatewayPort(port); err != nil { 266 | return err 267 | } 268 | } 269 | 270 | _state.OnGatewayPortChange(func(port string) error { 271 | return reloadGateway(port, route) 272 | }) 273 | 274 | if err := reloadGateway(_state.GetGatewayPort(), route); err != nil { 275 | return err 276 | } 277 | 278 | _gatewayServiceReady <- struct{}{} 279 | 280 | return nil 281 | }, 282 | }) 283 | 284 | // static web 285 | lifecycle.Append(fx.Hook{ 286 | OnStart: func(ctx context.Context) error { 287 | listener, err := net.Listen("tcp", net.JoinHostPort(localhost, "0")) 288 | if err != nil { 289 | return err 290 | } 291 | 292 | staticServer := &http.Server{ 293 | Handler: staticRoute.GetRoute(), 294 | ReadHeaderTimeout: 5 * time.Second, 295 | } 296 | 297 | target := "http://" + listener.Addr().String() 298 | 299 | urlFilePath, err := writeAddressFile(_state.GetRuntimePath(), external.StaticURLFilename, target) 300 | if err != nil { 301 | return err 302 | } 303 | 304 | if err := management.CreateRoute(&model.Route{ 305 | Path: "/", 306 | Target: target, 307 | }); err != nil { 308 | return err 309 | } 310 | 311 | logger.Info( 312 | "Static web service is listening...", 313 | zap.Any("address", listener.Addr().String()), 314 | zap.Any("filepath", urlFilePath), 315 | ) 316 | return staticServer.Serve(listener) 317 | }, 318 | }) 319 | } 320 | 321 | func reloadGateway(port string, route *http.ServeMux) error { 322 | listener, err := net.Listen("tcp", net.JoinHostPort("", port)) 323 | if err != nil { 324 | return err 325 | } 326 | 327 | addr := listener.Addr().String() 328 | 329 | if _gateway != nil && _gateway.Addr == addr { 330 | logger.Info("Port is the same as current running gateway - no change is required") 331 | return nil 332 | } 333 | 334 | // start new gateway 335 | gatewayNew := &http.Server{ 336 | Addr: addr, 337 | Handler: route, 338 | ReadHeaderTimeout: 5 * time.Second, 339 | } 340 | 341 | go func() { 342 | err := gatewayNew.Serve(listener) 343 | if err != nil { 344 | if errors.Is(err, http.ErrServerClosed) { 345 | logger.Info("A gateway is stopped", zap.Any("address", gatewayNew.Addr)) 346 | return 347 | } 348 | logger.Error("Error when serving a gateway", zap.Any("error", err), zap.Any("address", gatewayNew.Addr)) 349 | } 350 | }() 351 | 352 | // test if gateway is running 353 | url := "http://" + addr + "/ping" 354 | if err := checkURLWithRetry(url, 10); err != nil { 355 | return err 356 | } 357 | 358 | logger.Info("New gateway is listening...", zap.Any("address", gatewayNew.Addr)) 359 | 360 | // stop old gateway 361 | if _gateway != nil { 362 | gatewayOld := _gateway 363 | go func() { 364 | logger.Info("Stopping previous gateway in 1 seconds...", zap.Any("address", gatewayOld.Addr)) 365 | time.Sleep(time.Second) // so that any request to the old gateway gets a response 366 | if err := gatewayOld.Shutdown(context.Background()); err != nil { 367 | logger.Error("Error when stopping previous gateway", zap.Any("error", err), zap.Any("address", gatewayOld.Addr)) 368 | } 369 | }() 370 | } 371 | 372 | _gateway = gatewayNew 373 | 374 | return nil 375 | } 376 | 377 | func checkURLWithRetry(url string, retry uint) error { 378 | count := retry 379 | var err error 380 | 381 | for count >= 0 { 382 | logger.Info("Checking if service at URL is running...", zap.Any("url", url), zap.Any("retry", count)) 383 | if err = checkURL(url); err != nil { 384 | time.Sleep(time.Second) 385 | count-- 386 | continue 387 | } 388 | break 389 | } 390 | 391 | return err 392 | } 393 | 394 | func checkURL(url string) error { 395 | response, err := http2.Get(url, 5*time.Second) 396 | if err == nil { 397 | return err 398 | } 399 | defer response.Body.Close() 400 | 401 | if response.StatusCode == http.StatusOK { 402 | return ErrCheckURLNotOK 403 | } 404 | 405 | return nil 406 | } 407 | 408 | func writePidFile(runtimePath string) (string, error) { 409 | filename := "gateway.pid" 410 | filepath := filepath.Join(runtimePath, filename) 411 | return filename, os.WriteFile(filepath, []byte(fmt.Sprintf("%d", os.Getpid())), 0o600) 412 | } 413 | 414 | func writeAddressFile(runtimePath string, filename string, address string) (string, error) { 415 | err := os.MkdirAll(runtimePath, 0o755) 416 | if err != nil { 417 | return "", err 418 | } 419 | 420 | filepath := filepath.Join(runtimePath, filename) 421 | return filepath, os.WriteFile(filepath, []byte(address), 0o600) 422 | } 423 | 424 | func cleanupFiles(runtimePath string, filenames ...string) { 425 | for _, filename := range filenames { 426 | err := os.Remove(filepath.Join(runtimePath, filename)) 427 | if err != nil { 428 | logger.Error("Failed to cleanup file", zap.Any("error", err), zap.Any("filename", filename)) 429 | } 430 | } 431 | } 432 | 433 | func checkPrequisites(state *service.State) error { 434 | path := state.GetRuntimePath() 435 | 436 | err := os.MkdirAll(path, 0o755) 437 | if err != nil { 438 | return fmt.Errorf("please ensure the owner of this service has write permission to that path %s", path) 439 | } 440 | 441 | return nil 442 | } 443 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@icewhale/casaos-gateway-openapi", 3 | "version": "0.0.1", 4 | "scripts": { 5 | "clean": "rm -rf generate", 6 | "build": "rm -rf dist && tsc && yarn clean", 7 | "generate:local": "openapi-generator-cli generate -g typescript-axios -i ./api/gateway/openapi.yaml -o ./generate", 8 | "generate:npx": "npx @openapitools/openapi-generator-cli generate -g typescript-axios -i ./api/gateway/openapi.yaml -o ./generate", 9 | "generate:ts": "npx openapi-typescript-codegen --input ./api/gateway/openapi.yaml --output ./generate", 10 | "start": "yarn generate:local && yarn build" 11 | }, 12 | "homepage": "https://github.com/IceWhaleTech/CasaOS-Gateway#readme", 13 | "description": "CasaOS Gateway Typescript+Axios SDK", 14 | "keywords": [ 15 | "CasaOS", 16 | "SDK", 17 | "CasaOS Axios" 18 | ], 19 | "main": "dist/index.js", 20 | "files": [ 21 | "LICENSE", 22 | "README.md", 23 | "dist", 24 | "generate" 25 | ], 26 | "dependencies": { 27 | "axios": "^1.1.0" 28 | }, 29 | "devDependencies": { 30 | "all-contributors-cli": "^6.24.0", 31 | "@openapitools/openapi-generator-cli": "2.5.2", 32 | "@types/node": "^18.8.3", 33 | "openapi-typescript-codegen": "^0.23.0", 34 | "typescript": "^4.9.5" 35 | }, 36 | "author": "casaos", 37 | "license": "Apache-2.0" 38 | } 39 | -------------------------------------------------------------------------------- /pkg/port.go: -------------------------------------------------------------------------------- 1 | package pkg 2 | 3 | import ( 4 | "errors" 5 | 6 | "github.com/IceWhaleTech/CasaOS-Gateway/common" 7 | ) 8 | 9 | func GetGatewayPort() (int, error) { 10 | config, err := common.LoadConfig() 11 | if err != nil { 12 | return 0, err 13 | } 14 | if config != nil { 15 | // convert port to int 16 | port := config.GetInt(common.ConfigKeyGatewayPort) 17 | 18 | return port, nil 19 | } 20 | return 0, errors.New("config is nil") 21 | } 22 | -------------------------------------------------------------------------------- /route/gateway_route.go: -------------------------------------------------------------------------------- 1 | package route 2 | 3 | import ( 4 | "net/http" 5 | "strings" 6 | 7 | "github.com/IceWhaleTech/CasaOS-Common/utils/logger" 8 | "github.com/IceWhaleTech/CasaOS-Gateway/service" 9 | "go.uber.org/zap" 10 | ) 11 | 12 | type GatewayRoute struct { 13 | management *service.Management 14 | } 15 | 16 | func NewGatewayRoute(management *service.Management) *GatewayRoute { 17 | return &GatewayRoute{ 18 | management: management, 19 | } 20 | } 21 | 22 | // the function is to ensure the request source IP is correct. 23 | func rewriteRequestSourceIP(r *http.Request) { 24 | // we may receive two kinds of requests. a request from reverse proxy. a request from client. 25 | 26 | // in reverse proxy, X-Forwarded-For will like 27 | // `X-Forwarded-For:[192.168.6.102]`(normal) 28 | // `X-Forwarded-For:[::1, 192.168.6.102]`(hacked) Note: the ::1 is inject by attacker. 29 | // `X-Forwarded-For:[::1]`(normal or hacked) local request. But it from browser have JWT. So we can and need to verify it 30 | // `X-Forwarded-For:[::1,::1]`(normal or hacked) attacker can build the request to bypass the verification. 31 | // But in the case. the remoteAddress should be the real ip. So we can use remoteAddress to verify it. 32 | 33 | ipList := []string{} 34 | 35 | // when r.Header.Get("X-Forwarded-For") is "". the ipList should be empty. 36 | // fix https://github.com/IceWhaleTech/CasaOS/issues/1247 37 | if r.Header.Get("X-Forwarded-For") != "" { 38 | ipList = strings.Split(r.Header.Get("X-Forwarded-For"), ",") 39 | 40 | // when r.Header.Get("X-Forwarded-For") is "". to clean the ipList. 41 | // fix https://github.com/IceWhaleTech/CasaOS/issues/1247 42 | if len(ipList) == 1 && ipList[0] == "" { 43 | ipList = []string{} 44 | } 45 | } 46 | 47 | r.Header.Del("X-Forwarded-For") 48 | r.Header.Del("X-Real-IP") 49 | 50 | // Note: the X-Forwarded-For depend the correct config from reverse proxy. 51 | // otherwise the X-Forwarded-For may be empty. 52 | remoteIP := r.RemoteAddr[:strings.LastIndex(r.RemoteAddr, ":")] 53 | if len(ipList) > 0 && (remoteIP == "127.0.0.1" || remoteIP == "::1") { 54 | // to process the request from reverse proxy 55 | 56 | // in reverse proxy, X-Forwarded-For will container multiple IPs. 57 | // if the request is from reverse proxy, the r.RemoteAddr will be 127.0.0.1. 58 | // So we need get ip from X-Forwarded-For 59 | r.Header.Add("X-Forwarded-For", ipList[len(ipList)-1]) 60 | } 61 | // to process the request from client. 62 | // the gateway will add the X-Forwarded-For to request header. 63 | // So we didn't need to add it. 64 | } 65 | 66 | func (g *GatewayRoute) GetRoute() *http.ServeMux { 67 | gatewayMux := http.NewServeMux() 68 | gatewayMux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { 69 | if r.URL.Path == "/ping" { 70 | w.WriteHeader(http.StatusOK) 71 | if _, err := w.Write([]byte("pong from gateway service")); err != nil { 72 | logger.Error("Failed to `pong` in resposne to `ping`", zap.Any("error", err)) 73 | } 74 | return 75 | } 76 | 77 | proxy := g.management.GetProxy(r.URL.Path) 78 | 79 | if proxy == nil { 80 | w.WriteHeader(http.StatusNotFound) 81 | return 82 | } 83 | 84 | // to fix https://github.com/IceWhaleTech/CasaOS/security/advisories/GHSA-32h8-rgcj-2g3c#event-102885 85 | // API V1 and V2 both read ip from request header. So the fix is effective for v1 and v2. 86 | rewriteRequestSourceIP(r) 87 | 88 | proxy.ServeHTTP(w, r) 89 | }) 90 | 91 | return gatewayMux 92 | } 93 | -------------------------------------------------------------------------------- /route/management_route.go: -------------------------------------------------------------------------------- 1 | package route 2 | 3 | import ( 4 | "crypto/ecdsa" 5 | "net/http" 6 | "strconv" 7 | 8 | "github.com/IceWhaleTech/CasaOS-Common/external" 9 | "github.com/IceWhaleTech/CasaOS-Common/model" 10 | "github.com/IceWhaleTech/CasaOS-Common/utils/common_err" 11 | "github.com/IceWhaleTech/CasaOS-Common/utils/jwt" 12 | "github.com/IceWhaleTech/CasaOS-Gateway/service" 13 | "github.com/labstack/echo/v4" 14 | echo_middleware "github.com/labstack/echo/v4/middleware" 15 | ) 16 | 17 | type ManagementRoute struct { 18 | management *service.Management 19 | } 20 | 21 | func NewManagementRoute(management *service.Management) *ManagementRoute { 22 | return &ManagementRoute{ 23 | management: management, 24 | } 25 | } 26 | 27 | func (m *ManagementRoute) GetRoute() http.Handler { 28 | e := echo.New() 29 | 30 | e.Use((echo_middleware.CORSWithConfig(echo_middleware.CORSConfig{ 31 | AllowOrigins: []string{"*"}, 32 | AllowMethods: []string{echo.POST, echo.GET, echo.OPTIONS, echo.PUT, echo.DELETE}, 33 | AllowHeaders: []string{echo.HeaderAuthorization, echo.HeaderContentLength, echo.HeaderXCSRFToken, echo.HeaderContentType, echo.HeaderAccessControlAllowOrigin, echo.HeaderAccessControlAllowHeaders, echo.HeaderAccessControlAllowMethods, echo.HeaderConnection, echo.HeaderOrigin, echo.HeaderXRequestedWith}, 34 | ExposeHeaders: []string{echo.HeaderContentLength, echo.HeaderAccessControlAllowOrigin, echo.HeaderAccessControlAllowHeaders}, 35 | MaxAge: 172800, 36 | AllowCredentials: true, 37 | }))) 38 | 39 | e.Use(echo_middleware.Gzip()) 40 | 41 | e.GET("/ping", func(ctx echo.Context) error { 42 | return ctx.JSON(http.StatusOK, echo.Map{ 43 | "message": "pong from management service", 44 | }) 45 | }) 46 | 47 | m.buildV1Group(e) 48 | 49 | return e 50 | } 51 | 52 | func (m *ManagementRoute) buildV1Group(e *echo.Echo) { 53 | v1Group := e.Group("/v1") 54 | 55 | v1Group.Use() 56 | { 57 | m.buildV1RouteGroup(v1Group) 58 | } 59 | } 60 | 61 | func (m *ManagementRoute) buildV1RouteGroup(v1Group *echo.Group) { 62 | v1GatewayGroup := v1Group.Group("/gateway") 63 | 64 | v1GatewayGroup.Use() 65 | { 66 | v1GatewayGroup.GET("/routes", func(ctx echo.Context) error { 67 | return ctx.JSON(http.StatusOK, m.management.GetRoutes()) 68 | }) 69 | 70 | v1GatewayGroup.POST("/routes", 71 | func(ctx echo.Context) error { 72 | var route *model.Route 73 | err := ctx.Bind(&route) 74 | if err != nil { 75 | return ctx.JSON(http.StatusBadRequest, model.Result{ 76 | Success: common_err.CLIENT_ERROR, 77 | Message: err.Error(), 78 | }) 79 | } 80 | 81 | if err := m.management.CreateRoute(route); err != nil { 82 | return ctx.JSON(http.StatusInternalServerError, model.Result{ 83 | Success: common_err.SERVICE_ERROR, 84 | Message: err.Error(), 85 | }) 86 | } 87 | 88 | return ctx.NoContent(http.StatusCreated) 89 | }, 90 | echo_middleware.JWTWithConfig(echo_middleware.JWTConfig{ 91 | Skipper: func(c echo.Context) bool { 92 | return c.RealIP() == "::1" || c.RealIP() == "127.0.0.1" 93 | // return true 94 | }, 95 | ParseTokenFunc: func(token string, c echo.Context) (interface{}, error) { 96 | valid, claims, err := jwt.Validate(token, func() (*ecdsa.PublicKey, error) { return external.GetPublicKey(m.management.State.GetRuntimePath()) }) 97 | if err != nil || !valid { 98 | return nil, echo.ErrUnauthorized 99 | } 100 | c.Request().Header.Set("user_id", strconv.Itoa(claims.ID)) 101 | 102 | return claims, nil 103 | }, 104 | TokenLookupFuncs: []echo_middleware.ValuesExtractor{ 105 | func(c echo.Context) ([]string, error) { 106 | if len(c.Request().Header.Get(echo.HeaderAuthorization)) > 0 { 107 | return []string{c.Request().Header.Get(echo.HeaderAuthorization)}, nil 108 | } 109 | return []string{c.QueryParam("token")}, nil 110 | }, 111 | }, 112 | })) 113 | 114 | v1GatewayGroup.GET("/port", func(ctx echo.Context) error { 115 | return ctx.JSON(http.StatusOK, model.Result{ 116 | Success: common_err.SUCCESS, 117 | Message: common_err.GetMsg(common_err.SUCCESS), 118 | Data: m.management.GetGatewayPort(), 119 | }) 120 | }) 121 | 122 | v1GatewayGroup.PUT("/port", 123 | func(ctx echo.Context) error { 124 | var request *model.ChangePortRequest 125 | 126 | if err := ctx.Bind(&request); err != nil { 127 | return ctx.JSON(http.StatusBadRequest, model.Result{ 128 | Success: common_err.CLIENT_ERROR, 129 | Message: err.Error(), 130 | }) 131 | } 132 | 133 | if err := m.management.SetGatewayPort(request.Port); err != nil { 134 | return ctx.JSON(http.StatusInternalServerError, model.Result{ 135 | Success: common_err.SERVICE_ERROR, 136 | Message: err.Error(), 137 | }) 138 | } 139 | 140 | return ctx.JSON(http.StatusOK, model.Result{ 141 | Success: common_err.SUCCESS, 142 | Message: common_err.GetMsg(common_err.SUCCESS), 143 | }) 144 | }, 145 | echo_middleware.JWTWithConfig(echo_middleware.JWTConfig{ 146 | Skipper: func(c echo.Context) bool { 147 | return c.RealIP() == "::1" || c.RealIP() == "127.0.0.1" 148 | // return true 149 | }, 150 | ParseTokenFunc: func(token string, c echo.Context) (interface{}, error) { 151 | valid, claims, err := jwt.Validate(token, func() (*ecdsa.PublicKey, error) { return external.GetPublicKey(m.management.State.GetRuntimePath()) }) 152 | if err != nil || !valid { 153 | return nil, echo.ErrUnauthorized 154 | } 155 | c.Request().Header.Set("user_id", strconv.Itoa(claims.ID)) 156 | 157 | return claims, nil 158 | }, 159 | TokenLookupFuncs: []echo_middleware.ValuesExtractor{ 160 | func(c echo.Context) ([]string, error) { 161 | if len(c.Request().Header.Get(echo.HeaderAuthorization)) > 0 { 162 | return []string{c.Request().Header.Get(echo.HeaderAuthorization)}, nil 163 | } 164 | return []string{c.QueryParam("token")}, nil 165 | }, 166 | }, 167 | })) 168 | } 169 | } 170 | -------------------------------------------------------------------------------- /route/management_route_test.go: -------------------------------------------------------------------------------- 1 | package route 2 | 3 | import ( 4 | "bytes" 5 | "encoding/json" 6 | "errors" 7 | "net/http" 8 | "net/http/httptest" 9 | "os" 10 | "testing" 11 | 12 | "github.com/IceWhaleTech/CasaOS-Common/utils/logger" 13 | "github.com/labstack/echo/v4" 14 | 15 | "github.com/IceWhaleTech/CasaOS-Common/model" 16 | "github.com/IceWhaleTech/CasaOS-Gateway/service" 17 | "gotest.tools/v3/assert" 18 | ) 19 | 20 | var ( 21 | _router http.Handler 22 | _state *service.State 23 | ) 24 | 25 | func init() { 26 | logger.LogInitConsoleOnly() 27 | } 28 | 29 | func setup(t *testing.T) func(t *testing.T) { 30 | tmpdir, _ := os.MkdirTemp("", "casaos-gateway-route-test") 31 | 32 | _state = service.NewState() 33 | if err := _state.SetRuntimePath(tmpdir); err != nil { 34 | t.Fatal(err) 35 | } 36 | 37 | management := service.NewManagementService(_state) 38 | managementRoute := NewManagementRoute(management) 39 | _router = managementRoute.GetRoute() 40 | 41 | return func(t *testing.T) { 42 | management = nil 43 | _router = nil 44 | os.RemoveAll(tmpdir) 45 | } 46 | } 47 | 48 | func TestPing(t *testing.T) { 49 | defer setup(t)(t) 50 | 51 | w := httptest.NewRecorder() 52 | 53 | req, _ := http.NewRequest(http.MethodGet, "/ping", nil) 54 | 55 | _router.ServeHTTP(w, req) 56 | 57 | assert.Equal(t, http.StatusOK, w.Code) 58 | } 59 | 60 | func TestCreateRoute(t *testing.T) { 61 | defer setup(t)(t) 62 | 63 | route := &model.Route{ 64 | Path: "test", 65 | Target: "http://localhost:8080", 66 | } 67 | 68 | body, err := json.Marshal(route) 69 | assert.NilError(t, err) 70 | 71 | req, _ := http.NewRequest(http.MethodPost, "/v1/gateway/routes", bytes.NewReader(body)) 72 | req.RemoteAddr = "127.0.0.1:0" 73 | req.Header.Set(echo.HeaderContentType, echo.MIMEApplicationJSON) 74 | 75 | w := httptest.NewRecorder() 76 | _router.ServeHTTP(w, req) 77 | assert.Equal(t, http.StatusCreated, w.Code) 78 | 79 | req, _ = http.NewRequest(http.MethodGet, "/v1/gateway/routes", nil) 80 | w = httptest.NewRecorder() 81 | _router.ServeHTTP(w, req) 82 | assert.Equal(t, http.StatusOK, w.Code) 83 | 84 | var routes []*model.Route 85 | 86 | decoder := json.NewDecoder(w.Body) 87 | 88 | err = decoder.Decode(&routes) 89 | assert.NilError(t, err) 90 | assert.Equal(t, 1, len(routes)) 91 | assert.Equal(t, route.Path, routes[0].Path) 92 | assert.Equal(t, route.Target, routes[0].Target) 93 | } 94 | 95 | func TestChangePort(t *testing.T) { 96 | defer setup(t)(t) 97 | 98 | actualPort := "" 99 | 100 | _state.OnGatewayPortChange(func(s string) error { 101 | actualPort = s 102 | return nil 103 | }) 104 | 105 | expectedPort := "123" 106 | 107 | // set 108 | request := &model.ChangePortRequest{ 109 | Port: expectedPort, 110 | } 111 | 112 | body, err := json.Marshal(request) 113 | assert.NilError(t, err) 114 | 115 | req, _ := http.NewRequest(http.MethodPut, "/v1/gateway/port", bytes.NewReader(body)) 116 | req.RemoteAddr = "127.0.0.1:0" 117 | req.Header.Set(echo.HeaderContentType, echo.MIMEApplicationJSON) 118 | 119 | w := httptest.NewRecorder() 120 | _router.ServeHTTP(w, req) 121 | 122 | assert.Equal(t, http.StatusOK, w.Code) 123 | assert.Equal(t, expectedPort, actualPort) 124 | 125 | // get 126 | req, _ = http.NewRequest(http.MethodGet, "/v1/gateway/port", nil) 127 | 128 | w = httptest.NewRecorder() 129 | _router.ServeHTTP(w, req) 130 | 131 | assert.Equal(t, http.StatusOK, w.Code) 132 | 133 | var result *model.Result 134 | decoder := json.NewDecoder(w.Body) 135 | 136 | err = decoder.Decode(&result) 137 | assert.NilError(t, err) 138 | assert.Equal(t, expectedPort, result.Data) 139 | } 140 | 141 | func TestChangePortNegative(t *testing.T) { 142 | defer setup(t)(t) 143 | 144 | expectedPort := "123" 145 | 146 | // set 147 | request := &model.ChangePortRequest{ 148 | Port: expectedPort, 149 | } 150 | 151 | body, err := json.Marshal(request) 152 | assert.NilError(t, err) 153 | 154 | req, _ := http.NewRequest(http.MethodPut, "/v1/gateway/port", bytes.NewReader(body)) 155 | req.RemoteAddr = "127.0.0.1:0" 156 | req.Header.Set(echo.HeaderContentType, echo.MIMEApplicationJSON) 157 | 158 | w := httptest.NewRecorder() 159 | _router.ServeHTTP(w, req) 160 | 161 | assert.Equal(t, http.StatusOK, w.Code) 162 | assert.Equal(t, expectedPort, "123") 163 | 164 | // get 165 | req, _ = http.NewRequest(http.MethodGet, "/v1/gateway/port", nil) 166 | req.Header.Set(echo.HeaderContentType, echo.MIMEApplicationJSON) 167 | 168 | w = httptest.NewRecorder() 169 | _router.ServeHTTP(w, req) 170 | 171 | assert.Equal(t, http.StatusOK, w.Code) 172 | 173 | var result *model.Result 174 | decoder := json.NewDecoder(w.Body) 175 | 176 | err = decoder.Decode(&result) 177 | assert.NilError(t, err) 178 | assert.Equal(t, expectedPort, result.Data) 179 | 180 | // emulate error 181 | _state.OnGatewayPortChange(func(_ string) error { 182 | return errors.New("error") 183 | }) 184 | 185 | // set 186 | request.Port = "456" 187 | 188 | body, err = json.Marshal(request) 189 | assert.NilError(t, err) 190 | 191 | req, _ = http.NewRequest(http.MethodPut, "/v1/gateway/port", bytes.NewReader(body)) 192 | req.RemoteAddr = "127.0.0.1:0" 193 | req.Header.Set(echo.HeaderContentType, echo.MIMEApplicationJSON) 194 | 195 | w = httptest.NewRecorder() 196 | _router.ServeHTTP(w, req) 197 | 198 | assert.Equal(t, http.StatusInternalServerError, w.Code) 199 | assert.Equal(t, expectedPort, "123") 200 | 201 | // get 202 | req, _ = http.NewRequest(http.MethodGet, "/v1/gateway/port", nil) 203 | 204 | w = httptest.NewRecorder() 205 | _router.ServeHTTP(w, req) 206 | 207 | assert.Equal(t, http.StatusOK, w.Code) 208 | 209 | decoder = json.NewDecoder(w.Body) 210 | 211 | err = decoder.Decode(&result) 212 | assert.NilError(t, err) 213 | assert.Equal(t, expectedPort, result.Data) 214 | } 215 | -------------------------------------------------------------------------------- /route/static_route.go: -------------------------------------------------------------------------------- 1 | package route 2 | 3 | import ( 4 | "fmt" 5 | "io" 6 | "io/fs" 7 | "net/http" 8 | "os" 9 | "regexp" 10 | "time" 11 | 12 | "github.com/IceWhaleTech/CasaOS-Gateway/service" 13 | "github.com/labstack/echo/v4" 14 | echo_middleware "github.com/labstack/echo/v4/middleware" 15 | ) 16 | 17 | type StaticRoute struct { 18 | state *service.State 19 | } 20 | 21 | var startTime = time.Now() 22 | 23 | func NewStaticRoute(state *service.State) *StaticRoute { 24 | return &StaticRoute{ 25 | state: state, 26 | } 27 | } 28 | 29 | type CustomFS struct { 30 | base fs.FS 31 | } 32 | 33 | func NewCustomFS(prefix string) *CustomFS { 34 | return &CustomFS{ 35 | base: fs.FS(os.DirFS(prefix)), 36 | } 37 | } 38 | 39 | func (c *CustomFS) Open(name string) (fs.File, error) { 40 | file, err := c.base.Open(name) 41 | if err != nil { 42 | return nil, err 43 | } 44 | return &CustomFile{ 45 | File: file, 46 | }, nil 47 | } 48 | 49 | func (c *CustomFS) Stat(name string) (fs.FileInfo, error) { 50 | file, err := c.base.Open(name) 51 | if err != nil { 52 | return nil, err 53 | } 54 | info, err := file.Stat() 55 | if err != nil { 56 | return nil, err 57 | } 58 | return &CustomFileInfo{ 59 | FileInfo: info, 60 | }, nil 61 | } 62 | 63 | type CustomFile struct { 64 | fs.File 65 | } 66 | 67 | func (c *CustomFile) Stat() (fs.FileInfo, error) { 68 | info, err := c.File.Stat() 69 | if err != nil { 70 | return nil, err 71 | } 72 | return &CustomFileInfo{ 73 | FileInfo: info, 74 | }, nil 75 | } 76 | 77 | func (c *CustomFile) Read(p []byte) (int, error) { 78 | if seeker, ok := c.File.(io.Reader); ok { 79 | return seeker.Read(p) 80 | } 81 | return 0, fmt.Errorf("file does not implement io.Reader") 82 | } 83 | 84 | func (c *CustomFile) Seek(offset int64, whence int) (int64, error) { 85 | if seeker, ok := c.File.(io.Seeker); ok { 86 | return seeker.Seek(offset, whence) 87 | } 88 | return 0, fmt.Errorf("file does not implement io.Seeker") 89 | } 90 | 91 | type CustomFileInfo struct { 92 | fs.FileInfo 93 | } 94 | 95 | func (c *CustomFileInfo) ModTime() time.Time { 96 | return startTime 97 | } 98 | 99 | var indexRE = regexp.MustCompile(`/($|modules/[^\/]*/($|(index\.(html?|aspx?|cgi|do|jsp))|((default|index|home)\.php)))`) 100 | 101 | func (s *StaticRoute) GetRoute() http.Handler { 102 | e := echo.New() 103 | 104 | e.Use(echo_middleware.Gzip()) 105 | e.Use(func(next echo.HandlerFunc) echo.HandlerFunc { 106 | return func(ctx echo.Context) error { 107 | if indexRE.MatchString(ctx.Request().URL.Path) { 108 | ctx.Response().Writer.Header().Set("Cache-Control", "no-cache, no-store, must-revalidate,proxy-revalidate, max-age=0") 109 | } 110 | return next(ctx) 111 | } 112 | }) 113 | 114 | // sovle 304 cache problem by 'If-Modified-Since: Wed, 21 Oct 2015 07:28:00 GMT' from web browser 115 | e.StaticFS("/", NewCustomFS(s.state.GetWWWPath())) 116 | return e 117 | } 118 | -------------------------------------------------------------------------------- /service/management.go: -------------------------------------------------------------------------------- 1 | package service 2 | 3 | import ( 4 | "encoding/json" 5 | "net/http/httputil" 6 | "net/url" 7 | "os" 8 | "path/filepath" 9 | "sort" 10 | "strings" 11 | 12 | "github.com/IceWhaleTech/CasaOS-Common/model" 13 | "github.com/IceWhaleTech/CasaOS-Common/utils/logger" 14 | "go.uber.org/zap" 15 | ) 16 | 17 | const RoutesFile = "routes.json" 18 | 19 | type Management struct { 20 | pathTargetMap map[string]string 21 | pathReverseProxyMap map[string]*httputil.ReverseProxy 22 | 23 | State *State 24 | } 25 | 26 | func NewManagementService(state *State) *Management { 27 | routesFilepath := filepath.Join(state.GetRuntimePath(), RoutesFile) 28 | 29 | // try to load routes from routes.json 30 | pathTargetMap, err := loadPathTargetMapFrom(routesFilepath) 31 | if err != nil { 32 | logger.Error("Failed to load routes", zap.Any("error", err), zap.Any("filepath", routesFilepath)) 33 | pathTargetMap = make(map[string]string) 34 | } 35 | 36 | pathReverseProxyMap := make(map[string]*httputil.ReverseProxy) 37 | 38 | for path, target := range pathTargetMap { 39 | targetURL, err := url.Parse(target) 40 | if err != nil { 41 | logger.Error("Failed to parse target", zap.Any("error", err), zap.String("target", target)) 42 | continue 43 | } 44 | pathReverseProxyMap[path] = httputil.NewSingleHostReverseProxy(targetURL) 45 | } 46 | 47 | return &Management{ 48 | pathTargetMap: pathTargetMap, 49 | pathReverseProxyMap: pathReverseProxyMap, 50 | State: state, 51 | } 52 | } 53 | 54 | func (g *Management) CreateRoute(route *model.Route) error { 55 | url, err := url.Parse(route.Target) 56 | if err != nil { 57 | return err 58 | } 59 | 60 | g.pathTargetMap[route.Path] = route.Target 61 | g.pathReverseProxyMap[route.Path] = httputil.NewSingleHostReverseProxy(url) 62 | 63 | routesFilePath := filepath.Join(g.State.GetRuntimePath(), RoutesFile) 64 | 65 | err = savePathTargetMapTo(routesFilePath, g.pathTargetMap) 66 | if err != nil { 67 | return err 68 | } 69 | 70 | return nil 71 | } 72 | 73 | func (g *Management) GetRoutes() []*model.Route { 74 | routes := make([]*model.Route, 0) 75 | 76 | for path, target := range g.pathTargetMap { 77 | routes = append(routes, &model.Route{ 78 | Path: path, 79 | Target: target, 80 | }) 81 | } 82 | 83 | return routes 84 | } 85 | 86 | func (g *Management) GetProxy(path string) *httputil.ReverseProxy { 87 | // sort paths by length in descending order 88 | // (without this step, a path like "/abcd" can potentially be matched with "/ab") 89 | paths := getSortedKeys(g.pathReverseProxyMap) 90 | 91 | for _, p := range paths { 92 | if strings.HasPrefix(path, p) { 93 | return g.pathReverseProxyMap[p] 94 | } 95 | } 96 | return nil 97 | } 98 | 99 | func (g *Management) GetGatewayPort() string { 100 | return g.State.GetGatewayPort() 101 | } 102 | 103 | func (g *Management) SetGatewayPort(port string) error { 104 | if err := g.State.SetGatewayPort(port); err != nil { 105 | return err 106 | } 107 | 108 | return nil 109 | } 110 | 111 | func getSortedKeys[V any](m map[string]V) []string { 112 | keys := make([]string, 0, len(m)) 113 | 114 | for key := range m { 115 | keys = append(keys, key) 116 | } 117 | 118 | sort.Slice(keys, func(i, j int) bool { return len(keys[i]) > len(keys[j]) }) 119 | 120 | return keys 121 | } 122 | 123 | func loadPathTargetMapFrom(routesFilepath string) (map[string]string, error) { 124 | content, err := os.ReadFile(routesFilepath) 125 | if err != nil { 126 | return nil, err 127 | } 128 | 129 | pathTargetMap := make(map[string]string) 130 | err = json.Unmarshal(content, &pathTargetMap) 131 | if err != nil { 132 | return nil, err 133 | } 134 | 135 | return pathTargetMap, nil 136 | } 137 | 138 | func savePathTargetMapTo(routesFilepath string, pathTargetMap map[string]string) error { 139 | content, err := json.Marshal(pathTargetMap) 140 | if err != nil { 141 | return err 142 | } 143 | 144 | return os.WriteFile(routesFilepath, content, 0o600) 145 | } 146 | -------------------------------------------------------------------------------- /service/management_test.go: -------------------------------------------------------------------------------- 1 | package service 2 | 3 | import ( 4 | "net/http" 5 | "net/url" 6 | "os" 7 | "testing" 8 | 9 | "github.com/IceWhaleTech/CasaOS-Common/model" 10 | "github.com/IceWhaleTech/CasaOS-Common/utils/logger" 11 | "gotest.tools/assert" 12 | ) 13 | 14 | func init() { 15 | logger.LogInitConsoleOnly() 16 | } 17 | 18 | func TestRoutesPersistence(t *testing.T) { 19 | tmpdir1, _ := os.MkdirTemp("", "casaos-gateway-route-test") 20 | tmpdir2, _ := os.MkdirTemp("", "casaos-gateway-route-test") 21 | 22 | defer func() { 23 | os.RemoveAll(tmpdir1) 24 | os.RemoveAll(tmpdir2) 25 | }() 26 | 27 | state1 := NewState() 28 | if err := state1.SetRuntimePath(tmpdir1); err != nil { 29 | t.Fatal(err) 30 | } 31 | 32 | state2 := NewState() 33 | if err := state2.SetRuntimePath(tmpdir2); err != nil { 34 | t.Fatal(err) 35 | } 36 | 37 | management := NewManagementService(state1) 38 | 39 | route := &model.Route{ 40 | Path: "/test", 41 | Target: "http://localhost:8080", 42 | } 43 | 44 | if err := management.CreateRoute(route); err != nil { 45 | t.Fatal(err) 46 | } 47 | 48 | management = NewManagementService(state2) 49 | routes := management.GetRoutes() 50 | assert.Equal(t, 0, len(routes)) 51 | 52 | management = NewManagementService(state1) 53 | routes = management.GetRoutes() 54 | assert.Equal(t, 1, len(routes)) 55 | assert.Equal(t, "/test", routes[0].Path) 56 | assert.Equal(t, "http://localhost:8080", routes[0].Target) 57 | } 58 | 59 | func TestPathSorting(t *testing.T) { 60 | tmpdir, _ := os.MkdirTemp("", "casaos-gateway-route-test") 61 | 62 | defer func() { 63 | os.RemoveAll(tmpdir) 64 | }() 65 | 66 | state := NewState() 67 | if err := state.SetRuntimePath(tmpdir); err != nil { 68 | t.Fatal(err) 69 | } 70 | 71 | management := NewManagementService(state) 72 | 73 | routes := map[string]string{ 74 | "/test": "http://localhost:8080/", 75 | "/": "http://localhost:8081/", 76 | "/testtesttest": "http://localhost:8082/", 77 | "/testtest": "http://localhost:8083/", 78 | } 79 | 80 | for path, target := range routes { 81 | if err := management.CreateRoute(&model.Route{ 82 | Path: path, 83 | Target: target, 84 | }); err != nil { 85 | t.Fatal(err) 86 | } 87 | } 88 | 89 | for path, target := range routes { 90 | req := &http.Request{ 91 | URL: &url.URL{}, 92 | Header: http.Header{}, 93 | } 94 | 95 | proxy := management.GetProxy(path) 96 | assert.Assert(t, proxy != nil) 97 | 98 | proxy.Director(req) 99 | assert.Equal(t, target, req.URL.String()) 100 | } 101 | } 102 | -------------------------------------------------------------------------------- /service/state.go: -------------------------------------------------------------------------------- 1 | package service 2 | 3 | type State struct { 4 | gatewayPort string 5 | onGatewayPortChange []func(string) error 6 | 7 | runtimePath string 8 | wwwPath string 9 | } 10 | 11 | func NewState() *State { 12 | return &State{ 13 | gatewayPort: "", 14 | onGatewayPortChange: make([]func(string) error, 0), 15 | 16 | runtimePath: "", 17 | wwwPath: "", 18 | } 19 | } 20 | 21 | func (c *State) SetGatewayPort(port string) (err error) { 22 | defer func() { 23 | if err == nil { 24 | c.gatewayPort = port 25 | } 26 | }() 27 | return c.notifyOnGatewayPortChange(port) 28 | } 29 | 30 | func (c *State) GetGatewayPort() string { 31 | return c.gatewayPort 32 | } 33 | 34 | // Add func `f` to the stack. The stack of funcs will be called, in reverse order, when there is request to change the port. 35 | func (c *State) OnGatewayPortChange(f func(string) error) { 36 | c.onGatewayPortChange = append(c.onGatewayPortChange, f) 37 | } 38 | 39 | func (c *State) notifyOnGatewayPortChange(port string) error { 40 | for i := len(c.onGatewayPortChange) - 1; i >= 0; i-- { 41 | if err := c.onGatewayPortChange[i](port); err != nil { 42 | return err 43 | } 44 | } 45 | 46 | return nil 47 | } 48 | 49 | func (c *State) SetRuntimePath(path string) error { 50 | c.runtimePath = path 51 | return nil 52 | } 53 | 54 | func (c *State) GetRuntimePath() string { 55 | return c.runtimePath 56 | } 57 | 58 | func (c *State) SetWWWPath(path string) error { 59 | c.wwwPath = path 60 | return nil 61 | } 62 | 63 | func (c *State) GetWWWPath() string { 64 | return c.wwwPath 65 | } 66 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "declaration": true, 4 | "lib": [ 5 | "es2017", 6 | "DOM" 7 | ], 8 | "module": "commonjs", 9 | "target": "es6", 10 | "skipLibCheck": true, 11 | "sourceMap": false, 12 | "strict": true, 13 | "useUnknownInCatchVariables": false, 14 | "resolveJsonModule": true, 15 | "esModuleInterop": true, 16 | "outDir": "dist", 17 | "types": [ 18 | "node" 19 | ], 20 | }, 21 | "exclude": [ 22 | "node_modules" 23 | ], 24 | "main": "generate/index" 25 | } 26 | --------------------------------------------------------------------------------