├── .dockerignore ├── .github └── workflows │ ├── build-tag.yml │ └── pull_request.yml ├── .gitignore ├── .goreleaser.yaml ├── .idea ├── .gitignore ├── go-openmcim.iml ├── inspectionProfiles │ └── Project_Default.xml ├── modules.xml └── vcs.xml ├── CONTRIBUTORS.md ├── Dockerfile ├── LICENSE ├── README.MD ├── api.go ├── api_token.go ├── bar.go ├── cache ├── cache.go ├── cache_inmem.go └── cache_redis.go ├── cluster.go ├── cmd_compress.go ├── cmd_webdav.go ├── config.go ├── config.yaml ├── dashboard.go ├── dashboard ├── .dockerignore ├── .eslintrc.cjs ├── .gitignore ├── .prettierrc.json ├── .vscode │ └── extensions.json ├── README.md ├── env.d.ts ├── index.html ├── package-lock.json ├── package.json ├── public │ ├── apple-touch-icon-180x180.png │ ├── favicon.ico │ ├── logo.png │ ├── maskable-icon-512x512.png │ ├── pwa-192x192.png │ ├── pwa-512x512.png │ └── pwa-64x64.png ├── src │ ├── App.vue │ ├── api │ │ ├── log.io.ts │ │ └── v0.ts │ ├── assets │ │ ├── base.css │ │ ├── lang │ │ │ ├── en-US.json │ │ │ └── zh-CN.json │ │ ├── main.css │ │ ├── theme.css │ │ └── utils.css │ ├── components │ │ ├── FileContentCard.vue │ │ ├── FileListCard.vue │ │ ├── HitsChart.vue │ │ ├── HitsCharts.vue │ │ ├── LogBlock.vue │ │ ├── LoginComp.vue │ │ ├── StatusButton.vue │ │ ├── TransitionExpandGroup.vue │ │ ├── UAChart.vue │ │ └── WebhookEditDialog.vue │ ├── config │ │ └── pwa.ts │ ├── cookies │ │ └── index.ts │ ├── lang │ │ ├── index.ts │ │ └── lang.ts │ ├── main.ts │ ├── router │ │ └── index.ts │ ├── sw.ts │ ├── utils │ │ ├── chart.ts │ │ ├── index.ts │ │ └── ring.ts │ └── views │ │ ├── AboutView.vue │ │ ├── HomeView.vue │ │ ├── LogListView.vue │ │ ├── LoginView.vue │ │ ├── SettingsView.vue │ │ └── settings │ │ └── NotificationsView.vue ├── tsconfig.json ├── tsconfig.node.json └── vite.config.ts ├── database ├── db.go ├── db_test.go ├── memory.go ├── sql.go └── sql_drives.go ├── docs ├── README.md └── deploy_with_windows.pdf ├── exitcodes.go ├── go-openbmclapi.exe ├── go.mod ├── go.sum ├── handler.go ├── help.go ├── hijacker.go ├── http_listener.go ├── images ├── LICENSE.rtf ├── MsiBanner.bmp ├── MsiBannerCat.bmp ├── MsiDialog.bmp └── app.ico ├── installer ├── service │ ├── go-openbmclapi.service │ └── installer.sh └── windows │ ├── CustomAction │ ├── .gitignore │ ├── CustomAction.cs │ └── CustomAction.csproj.gotmpl │ ├── Product.Var.wxi │ ├── Product.wxs │ ├── ProductLoc.en.wxl │ └── ProductLoc.zh.wxl ├── internal ├── build │ └── version.go └── gosrc │ ├── LICENSE │ └── httprange.go ├── lang ├── en │ ├── init.go │ └── us.go ├── lang.go └── zh │ ├── cn.go │ └── init.go ├── licsense.go ├── limited ├── api_rate.go ├── limited_conn.go ├── limited_conn_test.go └── util.go ├── log ├── logger.go ├── std.go └── std_test.go ├── main.go ├── notify ├── email │ ├── email.go │ └── templates │ │ ├── daily-report.gohtml │ │ ├── disabled.gohtml │ │ ├── enabled.gohtml │ │ ├── syncbegin.gohtml │ │ ├── syncdone.gohtml │ │ └── updates.gohtml ├── event.go ├── manager.go ├── stat.go └── webpush │ └── webpush.go ├── private └── .gitignore ├── robots.txt ├── scripts ├── build-windows.go ├── build.sh ├── decrypt-log.go ├── docker-run.sh └── dockerbuild.sh ├── service ├── go-openbmclapi.service └── installer.sh ├── storage ├── compressor.go ├── storage.go ├── storage_local.go ├── storage_mount.go └── storage_webdav.go ├── sync.go ├── token.go ├── update └── checker.go ├── util.go └── utils ├── buf.go ├── crypto.go ├── encoding.go ├── error.go ├── format.go ├── http.go ├── io.go ├── ishex_test.go └── util.go /.dockerignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | pnpm-debug.log* 8 | lerna-debug.log* 9 | 10 | node_modules 11 | .DS_Store 12 | dist 13 | dist-ssr 14 | coverage 15 | *.local 16 | 17 | /cypress/videos/ 18 | /cypress/screenshots/ 19 | 20 | # Editor directories and files 21 | .vscode/* 22 | !.vscode/extensions.json 23 | .idea 24 | *.suo 25 | *.ntvs* 26 | *.njsproj 27 | *.sln 28 | *.sw? 29 | 30 | # runtime 31 | /test 32 | /pems 33 | /_cache 34 | /data 35 | /logs 36 | /.tmp 37 | /__hijack 38 | /oss_mirror 39 | 40 | # binarys 41 | /output 42 | /build 43 | /build-all.sh 44 | /vendor 45 | 46 | # workspace 47 | go.work 48 | -------------------------------------------------------------------------------- /.github/workflows/build-tag.yml: -------------------------------------------------------------------------------- 1 | 2 | name: Create tagged release 3 | 4 | on: push 5 | 6 | jobs: 7 | test: 8 | runs-on: ubuntu-latest 9 | steps: 10 | - 11 | name: Action Checkout 12 | uses: actions/checkout@v4 13 | - 14 | name: Setup Golang 15 | uses: actions/setup-go@v5 16 | with: 17 | go-version-file: ./go.mod 18 | cache-dependency-path: ./go.sum 19 | - 20 | name: Generate 21 | run: go generate . 22 | - 23 | name: Test 24 | run: go test -v ./... 25 | 26 | create_release: 27 | runs-on: ubuntu-latest 28 | if: startsWith(github.ref, 'refs/tags/v') 29 | outputs: 30 | upload_url: ${{ steps.create_release.outputs.upload_url }} 31 | tag: ${{ steps.tag.outputs.tag }} 32 | steps: 33 | # - 34 | # name: Action Checkout 35 | # uses: actions/checkout@v4 36 | - 37 | name: Output TAG 38 | id: tag 39 | run: | 40 | export RELEASE_VERSION="${GITHUB_REF#refs/*/}" 41 | echo "tag=${RELEASE_VERSION}" 42 | echo "tag=${RELEASE_VERSION}" >> "$GITHUB_OUTPUT" 43 | - 44 | name: Create Release 45 | id: create_release 46 | uses: actions/create-release@v1 47 | env: 48 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 49 | with: 50 | tag_name: ${{ github.ref }} 51 | release_name: Release ${{ steps.tag.outputs.tag }} 52 | draft: true 53 | prerelease: false 54 | # - 55 | # name: Upload Certificates 56 | # uses: zyxkad/upload-release-asset-dir@v1 57 | # env: 58 | # GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 59 | # with: 60 | # upload_url: ${{ steps.create_release.outputs.upload_url }} 61 | # asset_dir: ./cert 62 | 63 | build: 64 | runs-on: ubuntu-latest 65 | needs: 66 | - create_release 67 | - test 68 | steps: 69 | - 70 | name: Action Checkout 71 | uses: actions/checkout@v4 72 | - 73 | name: Setup Golang 74 | uses: actions/setup-go@v5 75 | with: 76 | go-version-file: ./go.mod 77 | cache-dependency-path: ./go.sum 78 | - 79 | name: Generate 80 | run: go generate . 81 | - 82 | name: Build 83 | env: 84 | TAG: ${{ needs.create_release.outputs.tag }} 85 | run: bash ./scripts/build.sh 86 | - 87 | name: Upload Release Assets 88 | uses: zyxkad/upload-release-asset-dir@v2 89 | env: 90 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 91 | with: 92 | upload_url: ${{ needs.create_release.outputs.upload_url }} 93 | asset_dir: ./output 94 | 95 | build-windows: 96 | runs-on: windows-2022 97 | needs: 98 | - create_release 99 | steps: 100 | - 101 | name: Action Checkout 102 | uses: actions/checkout@v4 103 | - 104 | name: Setup Golang 105 | uses: actions/setup-go@v5 106 | with: 107 | go-version-file: ./go.mod 108 | cache-dependency-path: ./go.sum 109 | - 110 | name: Generate 111 | run: go generate . 112 | - 113 | name: Test 114 | run: go test -v ./... 115 | - 116 | name: Run build-windows.exe 117 | env: 118 | TAG: ${{ needs.create_release.outputs.tag }} 119 | CODE_SIGN_PFX: ${{ secrets.CODE_SIGN_PFX }} 120 | CODE_SIGN_PFX_PASSWORD: ${{ secrets.CODE_SIGN_PFX_PASSWORD }} 121 | run: go run ./scripts/build-windows.go 122 | - 123 | name: Upload Release Assets 124 | uses: zyxkad/upload-release-asset-dir@v2 125 | env: 126 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 127 | with: 128 | upload_url: ${{ needs.create_release.outputs.upload_url }} 129 | asset_dir: ./output 130 | 131 | build-docker: 132 | runs-on: ubuntu-latest 133 | if: startsWith(github.ref, 'refs/tags/') 134 | needs: 135 | - test 136 | steps: 137 | - 138 | name: Action Checkout 139 | uses: actions/checkout@v4 140 | - 141 | name: Get current tag 142 | run: echo "TAG=${GITHUB_REF#refs/*/}" >> $GITHUB_ENV 143 | - 144 | name: Set up QEMU 145 | uses: docker/setup-qemu-action@v3 146 | - 147 | name: Set up Docker Buildx 148 | uses: docker/setup-buildx-action@v3 149 | - 150 | name: Login to Docker Hub 151 | uses: docker/login-action@v3 152 | with: 153 | username: ${{ secrets.DOCKERHUB_USERNAME }} 154 | password: ${{ secrets.DOCKERHUB_TOKEN }} 155 | - 156 | name: Build and push 157 | uses: docker/build-push-action@v5 158 | with: 159 | context: . 160 | push: true 161 | tags: | 162 | craftmine/go-openbmclapi:latest 163 | craftmine/go-openbmclapi:${{env.TAG}} 164 | platforms: linux/amd64,linux/arm64 165 | cache-from: type=gha 166 | cache-to: type=gha,mode=max 167 | -------------------------------------------------------------------------------- /.github/workflows/pull_request.yml: -------------------------------------------------------------------------------- 1 | 2 | name: Check Pull Request 3 | on: 4 | pull_request: 5 | # 6 | 7 | jobs: 8 | request: 9 | permissions: 10 | issues: write 11 | pull-requests: write 12 | runs-on: ubuntu-latest 13 | steps: 14 | - 15 | name: Action Checkout 16 | uses: actions/checkout@v3 17 | - 18 | name: Setup Golang 19 | uses: actions/setup-go@v4 20 | with: 21 | go-version-file: ./go.mod 22 | cache-dependency-path: ./go.sum 23 | - 24 | name: Go Generate 25 | run: go generate . 26 | - 27 | name: Go Format 28 | uses: actions/github-script@v7 29 | with: 30 | script: | 31 | const child_process = require('child_process') 32 | const patch = await new Promise((resolve, reject) => child_process.exec('gofmt -d .', (error, stdout, stderr) => { 33 | if (stderr) { 34 | console.error(stderr) 35 | } 36 | if (error) { 37 | reject(error) 38 | return 39 | } 40 | resolve(stdout) 41 | })) 42 | if (patch) { 43 | await github.rest.issues.createComment({ 44 | issue_number: context.issue.number, 45 | owner: context.repo.owner, 46 | repo: context.repo.repo, 47 | body: "

Change for better format

\n\n````````diff\n" + patch + "\n````````\n\n
", 48 | }) 49 | } 50 | - 51 | name: Go Vet 52 | run: go vet ./... 53 | - 54 | name: Go Test 55 | run: go test -v ./... 56 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | 2 | # Mac OS 3 | .DS_Store 4 | 5 | # runtime 6 | /test 7 | /pems 8 | /_cache 9 | /data 10 | /logs 11 | /error.log 12 | /.tmp 13 | /__hijack 14 | /oss_mirror 15 | 16 | # binaries 17 | /output 18 | /build 19 | /vendor 20 | /*.wixobj 21 | 22 | # workspace 23 | go.work 24 | 25 | dist/ 26 | -------------------------------------------------------------------------------- /.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 | 4 | # The lines below are called `modelines`. See `:help modeline` 5 | # Feel free to remove those if you don't want/need to use them. 6 | # yaml-language-server: $schema=https://goreleaser.com/static/schema.json 7 | # vim: set ts=2 sw=2 tw=0 fo=cnqoj 8 | 9 | version: 2 10 | 11 | before: 12 | hooks: 13 | # You may remove this if you don't use go modules. 14 | - go mod tidy 15 | # you may remove this if you don't need go generate 16 | - go generate ./... 17 | 18 | builds: 19 | - env: 20 | - CGO_ENABLED=0 21 | goos: 22 | - linux 23 | - windows 24 | - darwin 25 | 26 | archives: 27 | - format: tar.gz 28 | # this name template makes the OS and Arch compatible with the results of `uname`. 29 | name_template: >- 30 | {{ .ProjectName }}_ 31 | {{- title .Os }}_ 32 | {{- if eq .Arch "amd64" }}x86_64 33 | {{- else if eq .Arch "386" }}i386 34 | {{- else }}{{ .Arch }}{{ end }} 35 | {{- if .Arm }}v{{ .Arm }}{{ end }} 36 | # use zip for windows archives 37 | format_overrides: 38 | - goos: windows 39 | format: zip 40 | 41 | changelog: 42 | sort: asc 43 | filters: 44 | exclude: 45 | - "^docs:" 46 | - "^test:" 47 | -------------------------------------------------------------------------------- /.idea/.gitignore: -------------------------------------------------------------------------------- 1 | # Default ignored files 2 | /shelf/ 3 | /workspace.xml 4 | # Editor-based HTTP Client requests 5 | /httpRequests/ 6 | # Datasource local storage ignored files 7 | /dataSources/ 8 | /dataSources.local.xml 9 | -------------------------------------------------------------------------------- /.idea/go-openmcim.iml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /.idea/inspectionProfiles/Project_Default.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 6 | -------------------------------------------------------------------------------- /.idea/modules.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /.idea/vcs.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /CONTRIBUTORS.md: -------------------------------------------------------------------------------- 1 | 2 | ## AUTHOR 3 | 4 | 5 | 6 | ## CONTRIBUTORS 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | # syntax=docker/dockerfile:1 2 | 3 | ARG GO_VERSION=1.21 4 | ARG REPO=github.com/LiterMC/go-openbmclapi 5 | ARG NPM_DIR=dashboard 6 | 7 | FROM node:21 AS WEB_BUILD 8 | 9 | ARG NPM_DIR 10 | 11 | WORKDIR /web/ 12 | COPY ["${NPM_DIR}/package.json", "${NPM_DIR}/package-lock.json", "/web/"] 13 | RUN --mount=type=cache,target=/root/.npm/_cacache \ 14 | npm ci --progress=false || { cat /root/.npm/_logs/*; exit 1; } 15 | COPY ["${NPM_DIR}", "/web/"] 16 | RUN npm run build || { cat /root/.npm/_logs/*; exit 1; } 17 | 18 | FROM golang:${GO_VERSION}-alpine AS BUILD 19 | 20 | ARG TAG 21 | ARG REPO 22 | ARG NPM_DIR 23 | 24 | WORKDIR "/go/src/${REPO}/" 25 | 26 | COPY ./go.mod ./go.sum "/go/src/${REPO}/" 27 | RUN go mod download 28 | COPY . "/go/src/${REPO}" 29 | COPY --from=WEB_BUILD "/web/dist" "/go/src/${REPO}/${NPM_DIR}/dist" 30 | 31 | ENV ldflags="-X 'github.com/LiterMC/go-openbmclapi/internal/build.BuildVersion=$TAG'" 32 | 33 | RUN --mount=type=cache,target=/root/.cache/go-build \ 34 | CGO_ENABLED=0 go build -v -o "/go/bin/go-openbmclapi" -ldflags="$ldflags" "." 35 | 36 | FROM alpine:latest 37 | 38 | WORKDIR /opt/openbmclapi 39 | COPY ./config.yaml /opt/openbmclapi/config.yaml 40 | 41 | COPY --from=BUILD "/go/bin/go-openbmclapi" "/go-openbmclapi" 42 | 43 | CMD ["/go-openbmclapi"] 44 | -------------------------------------------------------------------------------- /bar.go: -------------------------------------------------------------------------------- 1 | /** 2 | * OpenBmclAPI (Golang Edition) 3 | * Copyright (C) 2023 Kevin Z 4 | * All rights reserved 5 | * 6 | * This program is free software: you can redistribute it and/or modify 7 | * it under the terms of the GNU Affero General Public License as published 8 | * by the Free Software Foundation, either version 3 of the License, or 9 | * (at your option) any later version. 10 | * 11 | * This program is distributed in the hope that it will be useful, 12 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 13 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 14 | * GNU Affero General Public License for more details. 15 | * 16 | * You should have received a copy of the GNU Affero General Public License 17 | * along with this program. If not, see . 18 | */ 19 | 20 | package main 21 | 22 | import ( 23 | "io" 24 | "sync/atomic" 25 | "time" 26 | 27 | "github.com/vbauerster/mpb/v8" 28 | ) 29 | 30 | type ProxiedReader struct { 31 | io.Reader 32 | bar, total *mpb.Bar 33 | lastRead time.Time 34 | lastInc *atomic.Int64 35 | } 36 | 37 | func ProxyReader(r io.Reader, bar, total *mpb.Bar, lastInc *atomic.Int64) *ProxiedReader { 38 | return &ProxiedReader{ 39 | Reader: r, 40 | bar: bar, 41 | total: total, 42 | lastInc: lastInc, 43 | } 44 | } 45 | 46 | func (p *ProxiedReader) Read(buf []byte) (n int, err error) { 47 | start := p.lastRead 48 | if start.IsZero() { 49 | start = time.Now() 50 | } 51 | n, err = p.Reader.Read(buf) 52 | end := time.Now() 53 | p.lastRead = end 54 | used := end.Sub(start) 55 | 56 | p.bar.EwmaIncrBy(n, used) 57 | nowSt := end.UnixNano() 58 | last := p.lastInc.Swap(nowSt) 59 | p.total.EwmaIncrBy(n, (time.Duration)(nowSt-last)*time.Nanosecond) 60 | return 61 | } 62 | 63 | type ProxiedReadSeeker struct { 64 | io.ReadSeeker 65 | bar, total *mpb.Bar 66 | lastRead time.Time 67 | lastInc *atomic.Int64 68 | } 69 | 70 | func ProxyReadSeeker(r io.ReadSeeker, bar, total *mpb.Bar, lastInc *atomic.Int64) *ProxiedReadSeeker { 71 | return &ProxiedReadSeeker{ 72 | ReadSeeker: r, 73 | bar: bar, 74 | total: total, 75 | lastInc: lastInc, 76 | } 77 | } 78 | 79 | func (p *ProxiedReadSeeker) Read(buf []byte) (n int, err error) { 80 | start := p.lastRead 81 | if start.IsZero() { 82 | start = time.Now() 83 | } 84 | n, err = p.ReadSeeker.Read(buf) 85 | end := time.Now() 86 | p.lastRead = end 87 | used := end.Sub(start) 88 | 89 | p.bar.EwmaIncrBy(n, used) 90 | nowSt := end.UnixNano() 91 | last := p.lastInc.Swap(nowSt) 92 | p.total.EwmaIncrBy(n, (time.Duration)(nowSt-last)*time.Nanosecond) 93 | return 94 | } 95 | -------------------------------------------------------------------------------- /cache/cache.go: -------------------------------------------------------------------------------- 1 | /** 2 | * OpenBmclAPI (Golang Edition) 3 | * Copyright (C) 2024 Kevin Z 4 | * All rights reserved 5 | * 6 | * This program is free software: you can redistribute it and/or modify 7 | * it under the terms of the GNU Affero General Public License as published 8 | * by the Free Software Foundation, either version 3 of the License, or 9 | * (at your option) any later version. 10 | * 11 | * This program is distributed in the hope that it will be useful, 12 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 13 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 14 | * GNU Affero General Public License for more details. 15 | * 16 | * You should have received a copy of the GNU Affero General Public License 17 | * along with this program. If not, see . 18 | */ 19 | 20 | package cache 21 | 22 | import ( 23 | "time" 24 | 25 | "github.com/gregjones/httpcache" 26 | ) 27 | 28 | type CacheOpt struct { 29 | Expiration time.Duration 30 | } 31 | 32 | type Cache interface { 33 | Set(key string, value string, opt CacheOpt) 34 | Get(key string) (value string, ok bool) 35 | SetBytes(key string, value []byte, opt CacheOpt) 36 | GetBytes(key string) (value []byte, ok bool) 37 | Delete(key string) 38 | } 39 | 40 | type noCache struct{} 41 | 42 | func (noCache) Set(key string, value string, opt CacheOpt) {} 43 | func (noCache) Get(key string) (value string, ok bool) { return "", false } 44 | func (noCache) SetBytes(key string, value []byte, opt CacheOpt) {} 45 | func (noCache) GetBytes(key string) (value []byte, ok bool) { return nil, false } 46 | func (noCache) Delete(key string) {} 47 | 48 | var NoCache Cache = noCache{} 49 | 50 | type nsCache struct { 51 | ns string 52 | cache Cache 53 | } 54 | 55 | func NewCacheWithNamespace(c Cache, ns string) Cache { 56 | if c == NoCache { 57 | return NoCache 58 | } 59 | return &nsCache{ 60 | ns: ns, 61 | cache: c, 62 | } 63 | } 64 | 65 | func (c *nsCache) Set(key string, value string, opt CacheOpt) { 66 | c.cache.Set(c.ns+key, value, opt) 67 | } 68 | 69 | func (c *nsCache) Get(key string) (value string, ok bool) { 70 | return c.cache.Get(c.ns + key) 71 | } 72 | 73 | func (c *nsCache) SetBytes(key string, value []byte, opt CacheOpt) { 74 | c.cache.SetBytes(c.ns+key, value, opt) 75 | } 76 | 77 | func (c *nsCache) GetBytes(key string) (value []byte, ok bool) { 78 | return c.cache.GetBytes(c.ns + key) 79 | } 80 | 81 | func (c *nsCache) Delete(key string) { 82 | c.cache.Delete(c.ns + key) 83 | } 84 | 85 | type httpCacheWrapper struct { 86 | c Cache 87 | } 88 | 89 | func (c httpCacheWrapper) Set(key string, value []byte) { 90 | c.c.SetBytes(key, value, CacheOpt{ 91 | Expiration: time.Hour, 92 | }) 93 | } 94 | 95 | func (c httpCacheWrapper) Get(key string) (value []byte, ok bool) { 96 | return c.c.GetBytes(key) 97 | } 98 | 99 | func (c httpCacheWrapper) Delete(key string) { 100 | c.c.Delete(key) 101 | } 102 | 103 | func WrapToHTTPCache(c Cache) httpcache.Cache { 104 | return httpCacheWrapper{c} 105 | } 106 | -------------------------------------------------------------------------------- /cache/cache_inmem.go: -------------------------------------------------------------------------------- 1 | /** 2 | * OpenBmclAPI (Golang Edition) 3 | * Copyright (C) 2024 Kevin Z 4 | * All rights reserved 5 | * 6 | * This program is free software: you can redistribute it and/or modify 7 | * it under the terms of the GNU Affero General Public License as published 8 | * by the Free Software Foundation, either version 3 of the License, or 9 | * (at your option) any later version. 10 | * 11 | * This program is distributed in the hope that it will be useful, 12 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 13 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 14 | * GNU Affero General Public License for more details. 15 | * 16 | * You should have received a copy of the GNU Affero General Public License 17 | * along with this program. If not, see . 18 | */ 19 | 20 | package cache 21 | 22 | import ( 23 | "time" 24 | 25 | "github.com/patrickmn/go-cache" 26 | ) 27 | 28 | type InMemCache struct { 29 | cache *cache.Cache 30 | } 31 | 32 | var _ Cache = (*InMemCache)(nil) 33 | 34 | func NewInMemCache() *InMemCache { 35 | return &InMemCache{ 36 | cache: cache.New(cache.NoExpiration, time.Minute), 37 | } 38 | } 39 | 40 | func (c *InMemCache) Set(key string, value string, opt CacheOpt) { 41 | c.cache.Set(key, value, opt.Expiration) 42 | } 43 | 44 | func (c *InMemCache) Get(key string) (value string, ok bool) { 45 | v, ok := c.cache.Get(key) 46 | if !ok { 47 | return "", false 48 | } 49 | value, ok = v.(string) 50 | return 51 | } 52 | 53 | func (c *InMemCache) SetBytes(key string, value []byte, opt CacheOpt) { 54 | c.cache.Set(key, value, opt.Expiration) 55 | } 56 | 57 | func (c *InMemCache) GetBytes(key string) (value []byte, ok bool) { 58 | v, ok := c.cache.Get(key) 59 | if !ok { 60 | return nil, false 61 | } 62 | value, ok = v.([]byte) 63 | return 64 | } 65 | 66 | func (c *InMemCache) Delete(key string) { 67 | c.cache.Delete(key) 68 | } 69 | -------------------------------------------------------------------------------- /cache/cache_redis.go: -------------------------------------------------------------------------------- 1 | /** 2 | * OpenBmclAPI (Golang Edition) 3 | * Copyright (C) 2024 Kevin Z 4 | * All rights reserved 5 | * 6 | * This program is free software: you can redistribute it and/or modify 7 | * it under the terms of the GNU Affero General Public License as published 8 | * by the Free Software Foundation, either version 3 of the License, or 9 | * (at your option) any later version. 10 | * 11 | * This program is distributed in the hope that it will be useful, 12 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 13 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 14 | * GNU Affero General Public License for more details. 15 | * 16 | * You should have received a copy of the GNU Affero General Public License 17 | * along with this program. If not, see . 18 | */ 19 | 20 | package cache 21 | 22 | import ( 23 | "context" 24 | "encoding/base64" 25 | "time" 26 | 27 | "github.com/redis/go-redis/v9" 28 | ) 29 | 30 | type RedisCache struct { 31 | Client *redis.Client 32 | Context context.Context 33 | } 34 | 35 | var _ Cache = (*RedisCache)(nil) 36 | 37 | type RedisOptions struct { 38 | Network string `yaml:"network"` 39 | Addr string `yaml:"addr"` 40 | ClientName string `yaml:"client-name"` 41 | Username string `yaml:"username"` 42 | Password string `yaml:"password"` 43 | } 44 | 45 | func (o RedisOptions) ToRedis() *redis.Options { 46 | return &redis.Options{ 47 | Network: o.Network, 48 | Addr: o.Addr, 49 | ClientName: o.ClientName, 50 | Username: o.Username, 51 | Password: o.Password, 52 | } 53 | } 54 | 55 | func NewRedisCache(opt *redis.Options) *RedisCache { 56 | return NewRedisCacheByClient(redis.NewClient(opt)) 57 | } 58 | 59 | func NewRedisCacheByClient(cli *redis.Client) *RedisCache { 60 | return &RedisCache{ 61 | Client: cli, 62 | Context: context.Background(), 63 | } 64 | } 65 | 66 | func (c *RedisCache) Set(key string, value string, opt CacheOpt) { 67 | ctx, cancel := context.WithTimeout(c.Context, time.Second*3) 68 | defer cancel() 69 | c.Client.Set(ctx, key, value, opt.Expiration) 70 | } 71 | 72 | func (c *RedisCache) Get(key string) (value string, ok bool) { 73 | ctx, cancel := context.WithTimeout(c.Context, time.Second) 74 | defer cancel() 75 | cmd := c.Client.Get(ctx, key) 76 | if cmd.Err() != nil { 77 | return "", false 78 | } 79 | return cmd.Val(), true 80 | } 81 | 82 | func (c *RedisCache) SetBytes(key string, value []byte, opt CacheOpt) { 83 | v := base64.RawStdEncoding.EncodeToString(value) 84 | c.Set(key, v, opt) 85 | } 86 | 87 | func (c *RedisCache) GetBytes(key string) (value []byte, ok bool) { 88 | v, ok := c.Get(key) 89 | if !ok { 90 | return nil, false 91 | } 92 | value, err := base64.RawStdEncoding.DecodeString(v) 93 | if err != nil { 94 | return nil, false 95 | } 96 | return value, true 97 | } 98 | 99 | func (c *RedisCache) Delete(key string) { 100 | ctx, cancel := context.WithTimeout(c.Context, time.Second) 101 | defer cancel() 102 | c.Client.Del(ctx) 103 | } 104 | -------------------------------------------------------------------------------- /config.yaml: -------------------------------------------------------------------------------- 1 | log-slots: 7 2 | no-access-log: false 3 | access-log-slots: 16 4 | byoc: false 5 | use-cert: false 6 | trusted-x-forwarded-for: false 7 | public-host: "" 8 | public-port: 0 9 | port: 4000 10 | cluster-id: ${CLUSTER_ID} 11 | cluster-secret: ${CLUSTER_SECRET} 12 | sync-interval: 10 13 | only-gc-when-start: false 14 | download-max-conn: 16 15 | max-reconnect-count: 10 16 | certificates: 17 | - cert: /path/to/cert.pem 18 | key: /path/to/key.pem 19 | tunneler: 20 | enable: false 21 | tunnel-program: ./path/to/tunnel/program 22 | output-regex: \bNATedAddr\s+(?[0-9.]+|\[[0-9a-f:]+\]):(?\d+)$ 23 | tunnel-timeout: 0 24 | cache: 25 | type: inmem 26 | serve-limit: 27 | enable: false 28 | max-conn: 16384 29 | upload-rate: 10240 30 | api-rate-limit: 31 | anonymous: 32 | per-minute: 10 33 | per-hour: 120 34 | logged: 35 | per-minute: 120 36 | per-hour: 6000 37 | notification: 38 | enable-email: false 39 | email-smtp: smtp.example.com:25 40 | email-smtp-encryption: tls 41 | email-sender: noreply@example.com 42 | email-sender-password: example-password 43 | enable-webhook: true 44 | dashboard: 45 | enable: true 46 | username: "" 47 | password: "" 48 | pwa-name: GoOpenMCIM Dashboard 49 | pwa-short_name: GOMCIM Dash 50 | pwa-description: Go-OpenMCIM Internal Dashboard 51 | notification-subject: mailto:user@example.com 52 | github-api: 53 | update-check-interval: 1h0m0s 54 | authorization: "" 55 | database: 56 | driver: sqlite 57 | data-source-name: files.db 58 | hijack: 59 | enable: false 60 | enable-local-cache: false 61 | local-cache-path: hijack_cache 62 | require-auth: false 63 | auth-users: 64 | - username: example-username 65 | password: example-password 66 | storages: 67 | - type: local 68 | id: local-storage-0 69 | weight: 100 70 | data: 71 | cache-path: _cache 72 | compressor: "" 73 | webdav-users: 74 | example-user: 75 | endpoint: https://webdav.example.com/path/to/endpoint/ 76 | username: example-username 77 | password: example-password 78 | advanced: 79 | debug-log: false 80 | socket-io-log: false 81 | no-heavy-check: false 82 | no-gc: false 83 | heavy-check-interval: 120 84 | keepalive-timeout: 10 85 | skip-first-sync: false 86 | skip-signature-check: false 87 | no-fast-enable: false 88 | wait-before-enable: 0 89 | do-NOT-redirect-https-to-SECURE-hostname: false 90 | do-not-open-faq-on-windows: false 91 | -------------------------------------------------------------------------------- /dashboard.go: -------------------------------------------------------------------------------- 1 | /** 2 | * OpenBmclAPI (Golang Edition) 3 | * Copyright (C) 2023 Kevin Z 4 | * All rights reserved 5 | * 6 | * This program is free software: you can redistribute it and/or modify 7 | * it under the terms of the GNU Affero General Public License as published 8 | * by the Free Software Foundation, either version 3 of the License, or 9 | * (at your option) any later version. 10 | * 11 | * This program is distributed in the hope that it will be useful, 12 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 13 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 14 | * GNU Affero General Public License for more details. 15 | * 16 | * You should have received a copy of the GNU Affero General Public License 17 | * along with this program. If not, see . 18 | */ 19 | 20 | package main 21 | 22 | import ( 23 | "bytes" 24 | "compress/gzip" 25 | "embed" 26 | "encoding/json" 27 | "io" 28 | "io/fs" 29 | "mime" 30 | "net/http" 31 | "path" 32 | "strings" 33 | 34 | "github.com/LiterMC/go-openbmclapi/utils" 35 | ) 36 | 37 | //go:generate npm -C dashboard ci 38 | //go:generate npm -C dashboard run build 39 | 40 | //go:embed dashboard/dist 41 | var _dsbDist embed.FS 42 | var dsbDist = func() fs.FS { 43 | s, e := fs.Sub(_dsbDist, "dashboard/dist") 44 | if e != nil { 45 | panic(e) 46 | } 47 | return s 48 | }() 49 | 50 | //go:embed dashboard/dist/index.html 51 | var dsbIndexHtml string 52 | 53 | //go:embed dashboard/dist/manifest.webmanifest 54 | var _dsbManifest []byte 55 | var dsbManifest = func() (dsbManifest map[string]any) { 56 | err := json.Unmarshal(_dsbManifest, &dsbManifest) 57 | if err != nil { 58 | panic(err) 59 | } 60 | return 61 | }() 62 | 63 | func (cr *Cluster) serveDashboard(rw http.ResponseWriter, req *http.Request, pth string) { 64 | if req.Method != http.MethodGet && req.Method != http.MethodHead { 65 | rw.Header().Set("Allow", http.MethodGet+", "+http.MethodHead) 66 | http.Error(rw, "405 Method Not Allowed", http.StatusMethodNotAllowed) 67 | return 68 | } 69 | acceptEncoding := utils.SplitCSV(req.Header.Get("Accept-Encoding")) 70 | switch pth { 71 | case "": 72 | break 73 | case "manifest.webmanifest": 74 | buf, err := json.Marshal(dsbManifest) 75 | if err != nil { 76 | rw.WriteHeader(http.StatusInternalServerError) 77 | io.WriteString(rw, err.Error()) 78 | return 79 | } 80 | rw.Header().Set("Content-Type", "application/manifest+json") 81 | http.ServeContent(rw, req, "manifest.webmanifest", startTime, bytes.NewReader(buf)) 82 | return 83 | case "sw.js": 84 | // Must not cache service worker 85 | rw.Header().Set("Cache-Control", "no-store") 86 | fallthrough 87 | default: 88 | fd, err := dsbDist.Open(pth) 89 | if err == nil { 90 | defer fd.Close() 91 | stat, err := fd.Stat() 92 | if err != nil || stat.IsDir() { 93 | http.NotFound(rw, req) 94 | return 95 | } 96 | name := path.Base(pth) 97 | size := stat.Size() 98 | typ := mime.TypeByExtension(path.Ext(name)) 99 | if typ == "" { 100 | typ = "application/octet-stream" 101 | } 102 | rw.Header().Set("Content-Type", typ) 103 | if rw.Header().Get("Cache-Control") == "" { 104 | if strings.HasPrefix(pth, "assets/") { 105 | rw.Header().Set("Cache-Control", "public, max-age=2592000") 106 | } 107 | } 108 | if acceptEncoding["gzip"] != 0 && size > 1024 { 109 | buf := bytes.NewBuffer(nil) 110 | gw := gzip.NewWriter(buf) 111 | if _, err := io.Copy(gw, fd); err == nil { 112 | if err = gw.Close(); err == nil { 113 | rw.Header().Set("Content-Encoding", "gzip") 114 | http.ServeContent(rw, req, name, startTime, bytes.NewReader(buf.Bytes())) 115 | return 116 | } 117 | } 118 | } 119 | http.ServeContent(rw, req, name, startTime, fd.(io.ReadSeeker)) 120 | return 121 | } 122 | } 123 | rw.Header().Set("Content-Type", "text/html; charset=utf-8") 124 | http.ServeContent(rw, req, "index.html", startTime, strings.NewReader(dsbIndexHtml)) 125 | } 126 | -------------------------------------------------------------------------------- /dashboard/.dockerignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | pnpm-debug.log* 8 | lerna-debug.log* 9 | 10 | node_modules 11 | .DS_Store 12 | dev-dist 13 | dist 14 | dist-ssr 15 | coverage 16 | *.local 17 | 18 | /cypress/videos/ 19 | /cypress/screenshots/ 20 | 21 | # Editor directories and files 22 | .vscode/* 23 | !.vscode/extensions.json 24 | .idea 25 | *.suo 26 | *.ntvs* 27 | *.njsproj 28 | *.sln 29 | *.sw? 30 | -------------------------------------------------------------------------------- /dashboard/.eslintrc.cjs: -------------------------------------------------------------------------------- 1 | /* eslint-env node */ 2 | require('@rushstack/eslint-patch/modern-module-resolution') 3 | 4 | module.exports = { 5 | root: true, 6 | extends: [ 7 | 'plugin:vue/vue3-essential', 8 | 'eslint:recommended', 9 | '@vue/eslint-config-typescript', 10 | '@vue/eslint-config-prettier/skip-formatting', 11 | ], 12 | parserOptions: { 13 | ecmaVersion: 'latest', 14 | }, 15 | } 16 | -------------------------------------------------------------------------------- /dashboard/.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | pnpm-debug.log* 8 | lerna-debug.log* 9 | 10 | node_modules 11 | .DS_Store 12 | dev-dist 13 | dist 14 | dist-ssr 15 | coverage 16 | *.local 17 | 18 | /cypress/videos/ 19 | /cypress/screenshots/ 20 | 21 | # Editor directories and files 22 | .vscode/* 23 | !.vscode/extensions.json 24 | .idea 25 | *.suo 26 | *.ntvs* 27 | *.njsproj 28 | *.sln 29 | *.sw? 30 | -------------------------------------------------------------------------------- /dashboard/.prettierrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://json.schemastore.org/prettierrc", 3 | "semi": false, 4 | "useTabs": true, 5 | "singleQuote": true, 6 | "printWidth": 100, 7 | "trailingComma": "all" 8 | } -------------------------------------------------------------------------------- /dashboard/.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | "recommendations": ["Vue.volar", "Vue.vscode-typescript-vue-plugin"] 3 | } 4 | -------------------------------------------------------------------------------- /dashboard/README.md: -------------------------------------------------------------------------------- 1 | # dashboard 2 | 3 | This template should help get you started developing with Vue 3 in Vite. 4 | 5 | ## Recommended IDE Setup 6 | 7 | [VSCode](https://code.visualstudio.com/) + [Volar](https://marketplace.visualstudio.com/items?itemName=Vue.volar) (and disable Vetur) + [TypeScript Vue Plugin (Volar)](https://marketplace.visualstudio.com/items?itemName=Vue.vscode-typescript-vue-plugin). 8 | 9 | ## Type Support for `.vue` Imports in TS 10 | 11 | TypeScript cannot handle type information for `.vue` imports by default, so we replace the `tsc` CLI with `vue-tsc` for type checking. In editors, we need [TypeScript Vue Plugin (Volar)](https://marketplace.visualstudio.com/items?itemName=Vue.vscode-typescript-vue-plugin) to make the TypeScript language service aware of `.vue` types. 12 | 13 | If the standalone TypeScript plugin doesn't feel fast enough to you, Volar has also implemented a [Take Over Mode](https://github.com/johnsoncodehk/volar/discussions/471#discussioncomment-1361669) that is more performant. You can enable it by the following steps: 14 | 15 | 1. Disable the built-in TypeScript Extension 16 | 1) Run `Extensions: Show Built-in Extensions` from VSCode's command palette 17 | 2) Find `TypeScript and JavaScript Language Features`, right click and select `Disable (Workspace)` 18 | 2. Reload the VSCode window by running `Developer: Reload Window` from the command palette. 19 | 20 | ## Customize configuration 21 | 22 | See [Vite Configuration Reference](https://vitejs.dev/config/). 23 | 24 | ## Project Setup 25 | 26 | ```sh 27 | npm install 28 | ``` 29 | 30 | ### Compile and Hot-Reload for Development 31 | 32 | ```sh 33 | npm run dev 34 | ``` 35 | 36 | ### Type-Check, Compile and Minify for Production 37 | 38 | ```sh 39 | npm run build 40 | ``` 41 | 42 | ### Lint with [ESLint](https://eslint.org/) 43 | 44 | ```sh 45 | npm run lint 46 | ``` 47 | -------------------------------------------------------------------------------- /dashboard/env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | /// 3 | /// 4 | /// 5 | -------------------------------------------------------------------------------- /dashboard/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Go-OpenMCIM Dashboard 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 |
15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /dashboard/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "dashboard", 3 | "version": "0.0.0", 4 | "private": true, 5 | "scripts": { 6 | "build": "run-p type-check build-only", 7 | "lint": "eslint . --ext .vue,.js,.jsx,.cjs,.mjs,.ts,.tsx,.cts,.mts --fix --ignore-path .gitignore", 8 | "build-only": "vite build", 9 | "dev": "vite", 10 | "format": "prettier --write *.ts *.cjs src/", 11 | "preview": "vite preview", 12 | "build-preview": "npm run build && npm run preview", 13 | "type-check": "vue-tsc --noEmit" 14 | }, 15 | "dependencies": { 16 | "@types/pako": "^2.0.3", 17 | "axios": "^1.6.2", 18 | "chart.js": "^4.4.1", 19 | "js-sha256": "^0.11.0", 20 | "pako": "^2.1.0", 21 | "primeicons": "^6.0.1", 22 | "primevue": "^3.49.0", 23 | "register-service-worker": "^1.7.2", 24 | "vue": "^3.2.47", 25 | "vue-cookies": "^1.8.3", 26 | "vue-request": "^2.0.4", 27 | "vue-router": "^4.1.6", 28 | "workbox": "^0.0.0", 29 | "workbox-core": "^7.0.0", 30 | "workbox-precaching": "^7.0.0", 31 | "workbox-routing": "^7.0.0" 32 | }, 33 | "devDependencies": { 34 | "@rushstack/eslint-patch": "^1.2.0", 35 | "@types/node": "^18.14.2", 36 | "@vite-pwa/assets-generator": "^0.2.1", 37 | "@vitejs/plugin-vue": "^4.0.0", 38 | "@vue/eslint-config-prettier": "^7.1.0", 39 | "@vue/eslint-config-typescript": "^11.0.2", 40 | "@vue/tsconfig": "^0.1.3", 41 | "eslint": "^8.34.0", 42 | "eslint-plugin-vue": "^9.9.0", 43 | "npm-run-all": "^4.1.5", 44 | "prettier": "^2.8.4", 45 | "typescript": "~4.8.4", 46 | "vite": "^4.1.4", 47 | "vite-plugin-pwa": "^0.17.5", 48 | "vue-tsc": "^1.2.0", 49 | "workbox-background-sync": "^7.0.0", 50 | "workbox-window": "^7.0.0" 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /dashboard/public/apple-touch-icon-180x180.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mcmod-info-mirror/go-openmcim/b0d97f8536e989cce1b35e5c42da4081732fa7de/dashboard/public/apple-touch-icon-180x180.png -------------------------------------------------------------------------------- /dashboard/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mcmod-info-mirror/go-openmcim/b0d97f8536e989cce1b35e5c42da4081732fa7de/dashboard/public/favicon.ico -------------------------------------------------------------------------------- /dashboard/public/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mcmod-info-mirror/go-openmcim/b0d97f8536e989cce1b35e5c42da4081732fa7de/dashboard/public/logo.png -------------------------------------------------------------------------------- /dashboard/public/maskable-icon-512x512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mcmod-info-mirror/go-openmcim/b0d97f8536e989cce1b35e5c42da4081732fa7de/dashboard/public/maskable-icon-512x512.png -------------------------------------------------------------------------------- /dashboard/public/pwa-192x192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mcmod-info-mirror/go-openmcim/b0d97f8536e989cce1b35e5c42da4081732fa7de/dashboard/public/pwa-192x192.png -------------------------------------------------------------------------------- /dashboard/public/pwa-512x512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mcmod-info-mirror/go-openmcim/b0d97f8536e989cce1b35e5c42da4081732fa7de/dashboard/public/pwa-512x512.png -------------------------------------------------------------------------------- /dashboard/public/pwa-64x64.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mcmod-info-mirror/go-openmcim/b0d97f8536e989cce1b35e5c42da4081732fa7de/dashboard/public/pwa-64x64.png -------------------------------------------------------------------------------- /dashboard/src/api/log.io.ts: -------------------------------------------------------------------------------- 1 | interface BasicMsg { 2 | type: string 3 | } 4 | 5 | type BatchMsg = BasicMsg[] 6 | 7 | interface PingMsg { 8 | type: 'ping' 9 | data?: any 10 | } 11 | 12 | interface ErrorMsg { 13 | type: 'error' 14 | message: string 15 | } 16 | 17 | type LogLevel = 'DBUG' | 'INFO' | 'WARN' | 'ERRO' 18 | 19 | export interface LogMsg { 20 | type: 'log' 21 | time: number 22 | lvl: LogLevel 23 | log: string 24 | } 25 | 26 | export class LogIO { 27 | private ws: WebSocket | null = null 28 | private logListener: ((msg: LogMsg) => void)[] = [] 29 | private closeListener: ((err?: unknown) => void)[] = [] 30 | 31 | constructor(ws: WebSocket) { 32 | this.setWs(ws) 33 | } 34 | 35 | private setWs(ws: WebSocket): void { 36 | ws.addEventListener('close', () => this.onClose()) 37 | ws.addEventListener('message', (msg) => { 38 | const res = JSON.parse(msg.data) as BasicMsg | BatchMsg 39 | if (Array.isArray(res)) { 40 | for (const r of res) { 41 | this.onMessage(r) 42 | } 43 | } else { 44 | this.onMessage(res) 45 | } 46 | }) 47 | this.ws = ws 48 | } 49 | 50 | get isActive(): boolean { 51 | return !!this.ws && this.ws.readyState === WebSocket.OPEN 52 | } 53 | 54 | close(): void { 55 | if (this.ws) { 56 | this.onClose() 57 | this.ws.close() 58 | this.ws = null 59 | } 60 | } 61 | 62 | private onError(err: unknown): void { 63 | if (!this.ws) { 64 | return 65 | } 66 | for (const l of this.closeListener) { 67 | l(err) 68 | } 69 | } 70 | 71 | private onClose(): void { 72 | if (!this.ws) { 73 | return 74 | } 75 | for (const l of this.closeListener) { 76 | l() 77 | } 78 | } 79 | 80 | private onMessage(msg: BasicMsg): void { 81 | switch (msg.type) { 82 | case 'ping': 83 | this.ws?.send( 84 | JSON.stringify({ 85 | type: 'pong', 86 | data: (msg as PingMsg).data, 87 | }), 88 | ) 89 | break 90 | case 'error': 91 | if (this.ws) { 92 | this.onError(msg) 93 | this.ws.close() 94 | this.ws = null 95 | } 96 | break 97 | case 'log': 98 | this.onLog(msg as LogMsg) 99 | break 100 | } 101 | } 102 | 103 | private onLog(msg: LogMsg): void { 104 | for (const l of this.logListener) { 105 | l(msg) 106 | } 107 | } 108 | 109 | setLevel(lvl: LogLevel): void { 110 | this.ws?.send( 111 | JSON.stringify({ 112 | type: 'set-level', 113 | level: lvl, 114 | }), 115 | ) 116 | } 117 | 118 | addLogListener(l: (msg: LogMsg) => void): void { 119 | this.logListener.push(l) 120 | } 121 | 122 | addCloseListener(l: () => void): void { 123 | this.closeListener.push(l) 124 | console.debug('putted close listener', this.closeListener) 125 | } 126 | 127 | static async dial(token: string): Promise { 128 | const wsTarget = `${httpToWs(window.location.protocol)}//${window.location.host}/api/v0/log.io` 129 | const ws = new WebSocket(wsTarget) 130 | 131 | var connTimeout: ReturnType 132 | await new Promise((resolve, reject) => { 133 | connTimeout = setTimeout(() => { 134 | reject('WebSocket dial timeout') 135 | ws.close() 136 | }, 1000 * 15) 137 | ws.addEventListener('error', reject) 138 | ws.addEventListener('open', () => { 139 | ws.removeEventListener('error', reject) 140 | resolve() 141 | }) 142 | }).finally(() => clearTimeout(connTimeout)) 143 | 144 | var after: () => void 145 | await new Promise((resolve, reject) => { 146 | const listener = (msg: MessageEvent) => { 147 | console.debug('log.io auth result:', msg.data) 148 | try { 149 | const res = JSON.parse(msg.data) as BasicMsg 150 | if (res.type === 'error') { 151 | reject((res as ErrorMsg).message) 152 | } else if (res.type === 'ready') { 153 | resolve() 154 | } 155 | } catch (err) { 156 | reject(err) 157 | } 158 | } 159 | ws.addEventListener('message', listener) 160 | after = () => ws.removeEventListener('message', listener) 161 | ws.send( 162 | JSON.stringify({ 163 | token: token, 164 | }), 165 | ) 166 | }).finally(() => after()) 167 | 168 | return new LogIO(ws) 169 | } 170 | } 171 | 172 | function httpToWs(protocol: string): string { 173 | return protocol == 'http:' ? 'ws:' : 'wss:' 174 | } 175 | -------------------------------------------------------------------------------- /dashboard/src/assets/base.css: -------------------------------------------------------------------------------- 1 | *, 2 | *::before, 3 | *::after { 4 | box-sizing: border-box; 5 | } 6 | 7 | :root { 8 | font-size: 16px; 9 | --dialog-width: 60rem; 10 | } 11 | 12 | body { 13 | margin: 0; 14 | min-height: 100vh; 15 | transition: color 0.5s, background-color 0.5s; 16 | line-height: 1.6; 17 | font-family: Inter, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, 18 | Cantarell, 'Fira Sans', 'Droid Sans', 'Helvetica Neue', sans-serif; 19 | text-rendering: optimizeLegibility; 20 | -webkit-font-smoothing: antialiased; 21 | -moz-osx-font-smoothing: grayscale; 22 | } 23 | 24 | @media (max-width: 60rem) { 25 | :root { 26 | --dialog-width: 100vw; 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /dashboard/src/assets/lang/en-US.json: -------------------------------------------------------------------------------- 1 | { 2 | "lang": "English", 3 | "title": { 4 | "dashboard": "Dashboard", 5 | "day": "Day requests", 6 | "month": "Month requests", 7 | "year": "Year requests", 8 | "total": "Total requests", 9 | "hits": "Hits", 10 | "bytes": "Bytes", 11 | "user_agents": "Common User Agents", 12 | 13 | "login": "Login", 14 | "logout": "Logout", 15 | "username": "Username", 16 | "password": "Password", 17 | 18 | "pprof": { 19 | "heap": "Heap Dump", 20 | "goroutine": "Goroutine Dump", 21 | "allocs": "Allocs Dump", 22 | "block": "Block Trace", 23 | "mutex": "Mutex Trace" 24 | }, 25 | 26 | "settings": "Settings", 27 | "create-new": "Create new", 28 | "edit": "Edit", 29 | "operation": "Operation", 30 | 31 | "i18n": "Internationalization", 32 | "language": "Language", 33 | 34 | "debugs_and_logs": "Debugs & Logs", 35 | "logs": "Logs", 36 | 37 | "notification": "Notification", 38 | "notify": { 39 | "when.disabled": "Notify when disabled", 40 | "when.enabled": "Notify when enabled", 41 | "when.sync.done": "Notify when sync finished", 42 | "when.update.available": "Notify when updates available", 43 | "report": { 44 | "daily": "Report daily requests", 45 | "at": "Report at" 46 | }, 47 | "advanced": "Advanced options" 48 | }, 49 | 50 | "emails": "Emails", 51 | "email": { 52 | "recver": "Receiver" 53 | }, 54 | 55 | "webhooks": "Webhooks", 56 | "webhook": { 57 | "name": "Name", 58 | "endpoint": "EndPoint", 59 | "scopes": "Scopes", 60 | "auth-header": "Auth Header", 61 | "configure": "Configure webhook" 62 | } 63 | }, 64 | "button": { 65 | "enable": "Enable", 66 | "disable": "Disable" 67 | }, 68 | "message": { 69 | "server": { 70 | "run-for": "Server has been running for", 71 | "synchronizing": "Server is synchronizing ..." 72 | }, 73 | "login": { 74 | "input": { 75 | "username": "Please input the username", 76 | "password": "Please input the password" 77 | } 78 | }, 79 | "log": { 80 | "option": { 81 | "debug": "Debug Log" 82 | }, 83 | "login-to-view": " to view logs" 84 | }, 85 | "settings": { 86 | "login": { 87 | "auth": "Require authuation", 88 | "first": "You have to login before use this feature" 89 | }, 90 | "notify": { 91 | "cant.enable": "Cannot enable notification", 92 | "denied": "Notification is denied by the browser" 93 | }, 94 | "webpush": { 95 | "cant.enable": "Cannot enable Web Push", 96 | "denied": "Web Push is denied by the browser" 97 | }, 98 | "filelist": { 99 | "cant.fetch": "Cannot fetch filelist" 100 | } 101 | } 102 | }, 103 | "badge": { 104 | "server": { 105 | "status": { 106 | "error": "Disconnected", 107 | "enabled": "Running", 108 | "disabled": "Disabled" 109 | } 110 | } 111 | }, 112 | "unit": { 113 | "time": { 114 | "ms": "ms", 115 | "s": "s", 116 | "min": "min", 117 | "hour": "hour", 118 | "day": "day", 119 | "year": "year" 120 | } 121 | } 122 | } 123 | -------------------------------------------------------------------------------- /dashboard/src/assets/lang/zh-CN.json: -------------------------------------------------------------------------------- 1 | { 2 | "lang": "简体中文", 3 | "title": { 4 | "dashboard": "仪表盘", 5 | "day": "当日请求", 6 | "month": "当月请求", 7 | "year": "当年请求", 8 | "total": "全部请求", 9 | "hits": "请求数", 10 | "bytes": "流量", 11 | "user_agents": "常见用户代理", 12 | 13 | "login": "登录", 14 | "logout": "注销", 15 | "username": "用户名", 16 | "password": "密码", 17 | 18 | "pprof": { 19 | "heap": "内存转储", 20 | "goroutine": "Go协程转储", 21 | "allocs": "分配转储", 22 | "block": "阻塞跟踪", 23 | "mutex": "互斥锁跟踪" 24 | }, 25 | 26 | "settings": "设置", 27 | "create-new": "创建新的", 28 | "edit": "修改", 29 | "operation": "操作", 30 | 31 | "i18n": "国际化", 32 | "language": "语言", 33 | 34 | "debugs_and_logs": "调试 & 日志", 35 | "logs": "日志", 36 | 37 | "notification": "推送", 38 | "notify": { 39 | "when.disabled": "下线时发送通知", 40 | "when.enabled": "上线时发送通知", 41 | "when.sync.done": "同步完成时发送通知", 42 | "when.update.available": "有更新时发送通知", 43 | "report": { 44 | "daily": "报告每日请求", 45 | "at": "报告时间" 46 | }, 47 | "advanced": "高级选项" 48 | }, 49 | 50 | "emails": "邮件", 51 | "email": { 52 | "recver": "收件人" 53 | }, 54 | 55 | "webhooks": "网络钩子", 56 | "webhook": { 57 | "name": "名称", 58 | "endpoint": "端点", 59 | "scopes": "域", 60 | "auth-header": "授权标头", 61 | "configure": "配置网络钩子" 62 | } 63 | }, 64 | "button": { 65 | "enable": "启用", 66 | "disable": "禁用" 67 | }, 68 | "message": { 69 | "server": { 70 | "run-for": "服务器已运行", 71 | "synchronizing": "服务器同步中 ..." 72 | }, 73 | "login": { 74 | "input": { 75 | "username": "请输入用户名", 76 | "password": "请输入密码" 77 | } 78 | }, 79 | "log": { 80 | "option": { 81 | "debug": "调试日志" 82 | }, 83 | "login-to-view": "后查看日志" 84 | }, 85 | "settings": { 86 | "login": { 87 | "auth": "需要授权", 88 | "first": "使用此功能前请先登录" 89 | }, 90 | "notify": { 91 | "cant.enable": "无法启用通知", 92 | "denied": "通知已被浏览器拒绝" 93 | }, 94 | "webpush": { 95 | "cant.enable": "无法启用推送", 96 | "denied": "推送已被浏览器拒绝" 97 | }, 98 | "filelist": { 99 | "cant.fetch": "无法获取文件列表" 100 | } 101 | } 102 | }, 103 | "badge": { 104 | "server": { 105 | "status": { 106 | "error": "连接已断开", 107 | "enabled": "运行中", 108 | "disabled": "已停止" 109 | } 110 | } 111 | }, 112 | "unit": { 113 | "time": { 114 | "ms": "毫秒", 115 | "s": "秒", 116 | "min": "分钟", 117 | "hour": "小时", 118 | "day": "天", 119 | "year": "年" 120 | } 121 | } 122 | } 123 | -------------------------------------------------------------------------------- /dashboard/src/assets/main.css: -------------------------------------------------------------------------------- 1 | @import './base.css'; 2 | @import './utils.css'; 3 | 4 | #app { 5 | width: 100vw; 6 | margin: 0 auto; 7 | 8 | font-weight: normal; 9 | background-color: var(--surface-ground); 10 | } 11 | 12 | a.pi { 13 | color: inherit; 14 | text-decoration: none; 15 | } 16 | 17 | .p-toast { 18 | max-width: calc(100vw - 40px); 19 | } 20 | 21 | .p-disabled, 22 | .p-disabled * { 23 | cursor: not-allowed; 24 | pointer-events: auto; 25 | } 26 | 27 | @media (max-width: 60rem) { 28 | .p-dropdown .p-dropdown-label.p-inputtext { 29 | padding-left: 0.6rem; 30 | } 31 | 32 | .p-dropdown .p-dropdown-trigger { 33 | width: 2.1rem; 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /dashboard/src/assets/theme.css: -------------------------------------------------------------------------------- 1 | @import 'primevue/resources/themes/lara-light-green/theme.css' not (prefers-color-scheme: dark); 2 | @import 'primevue/resources/themes/lara-dark-green/theme.css' (prefers-color-scheme: dark); 3 | -------------------------------------------------------------------------------- /dashboard/src/assets/utils.css: -------------------------------------------------------------------------------- 1 | @keyframes flash { 2 | 0% { 3 | --flash-out: var(--flash-from); 4 | } 5 | 50% { 6 | --flash-out: var(--flash-to); 7 | } 8 | 100% { 9 | --flash-out: var(--flash-from); 10 | } 11 | } 12 | 13 | .flex-row-center { 14 | display: flex; 15 | flex-direction: row; 16 | align-items: center; 17 | } 18 | 19 | .flex-justify-end { 20 | justify-content: flex-end; 21 | } 22 | 23 | .flex-auto { 24 | overflow: hidden; 25 | flex-grow: 1; 26 | flex-shrink: 1; 27 | flex-basis: auto; 28 | } 29 | 30 | .width-full { 31 | width: 100%; 32 | } 33 | 34 | .margin-none { 35 | margin: 0 !important; 36 | } 37 | 38 | .margin-1 { 39 | margin-bottom: 1rem; 40 | } 41 | 42 | .font-bold { 43 | font-weight: bold; 44 | } 45 | 46 | .font-monospace-09 { 47 | font-family: monospace; 48 | font-size: 0.9rem; 49 | } 50 | 51 | .text-nowrap { 52 | white-space: nowrap; 53 | } 54 | 55 | .text-overflow-ellipsis { 56 | overflow: hidden; 57 | text-overflow: ellipsis; 58 | } 59 | 60 | .select-none { 61 | user-select: none; 62 | } 63 | 64 | .select-all { 65 | user-select: all; 66 | } 67 | 68 | .pointer { 69 | cursor: pointer; 70 | } 71 | -------------------------------------------------------------------------------- /dashboard/src/components/FileContentCard.vue: -------------------------------------------------------------------------------- 1 | 11 | 28 | 43 | -------------------------------------------------------------------------------- /dashboard/src/components/FileListCard.vue: -------------------------------------------------------------------------------- 1 | 15 | 42 | 78 | -------------------------------------------------------------------------------- /dashboard/src/components/HitsChart.vue: -------------------------------------------------------------------------------- 1 | 171 | 172 | 175 | -------------------------------------------------------------------------------- /dashboard/src/components/HitsCharts.vue: -------------------------------------------------------------------------------- 1 | 81 | 128 | 150 | -------------------------------------------------------------------------------- /dashboard/src/components/LoginComp.vue: -------------------------------------------------------------------------------- 1 | 73 | 74 | 119 | 120 | 140 | -------------------------------------------------------------------------------- /dashboard/src/components/StatusButton.vue: -------------------------------------------------------------------------------- 1 | 59 | 60 | 69 | 143 | -------------------------------------------------------------------------------- /dashboard/src/components/TransitionExpandGroup.vue: -------------------------------------------------------------------------------- 1 | 69 | 70 | 84 | 85 | 102 | -------------------------------------------------------------------------------- /dashboard/src/components/UAChart.vue: -------------------------------------------------------------------------------- 1 | 126 | 127 | 130 | -------------------------------------------------------------------------------- /dashboard/src/components/WebhookEditDialog.vue: -------------------------------------------------------------------------------- 1 | 4 | 7 | 11 | -------------------------------------------------------------------------------- /dashboard/src/config/pwa.ts: -------------------------------------------------------------------------------- 1 | import { VitePWA } from 'vite-plugin-pwa' 2 | 3 | export default VitePWA({ 4 | devOptions: { 5 | enabled: true, 6 | type: 'module', 7 | }, 8 | srcDir: 'src', 9 | filename: 'sw.ts', 10 | registerType: 'autoUpdate', 11 | strategies: 'injectManifest', // for custom SW 12 | includeAssets: ['favicon.ico'], 13 | manifest: { 14 | name: 'GoOpenBmclApi Dashboard', 15 | short_name: 'GOBA Dash', 16 | description: 'Go-Openbmclapi Internal Dashboard', 17 | theme_color: '#4c89fe', 18 | icons: [ 19 | { 20 | src: 'pwa-64x64.png', 21 | sizes: '64x64', 22 | type: 'image/png', 23 | }, 24 | { 25 | src: 'pwa-192x192.png', 26 | sizes: '192x192', 27 | type: 'image/png', 28 | }, 29 | { 30 | src: 'pwa-512x512.png', 31 | sizes: '512x512', 32 | type: 'image/png', 33 | purpose: 'any', 34 | }, 35 | { 36 | src: 'maskable-icon-512x512.png', 37 | sizes: '512x512', 38 | type: 'image/png', 39 | purpose: 'maskable', 40 | }, 41 | { 42 | src: 'logo.png', 43 | sizes: '512x512', 44 | type: 'image/png', 45 | }, 46 | ], 47 | }, 48 | workbox: { 49 | globPatterns: ['**/*.{js,css,html,ico,png,svg,woff2}'], 50 | }, 51 | }) 52 | -------------------------------------------------------------------------------- /dashboard/src/cookies/index.ts: -------------------------------------------------------------------------------- 1 | import { reactive, inject, watch, type Ref } from 'vue' 2 | import type { VueCookies } from 'vue-cookies' 3 | 4 | export function useCookies(): VueCookies { 5 | return inject('$cookies') as VueCookies 6 | } 7 | 8 | export function bindRefToCookie( 9 | ref: Ref, 10 | name: string, 11 | expires: number, 12 | cookies?: VueCookies, 13 | ): Ref { 14 | const c = cookies || useCookies() 15 | watch(ref, (value: string | null) => { 16 | if (value) { 17 | c.set(name, value, expires) 18 | } else { 19 | c.remove(name) 20 | } 21 | }) 22 | ref.value = c.get(name) 23 | return ref 24 | } 25 | 26 | export function bindRefToLocalStorage( 27 | ref: Ref, 28 | name: string, 29 | ): Ref { 30 | const s = localStorage.getItem(name) 31 | if (s) { 32 | try { 33 | ref.value = JSON.parse(s) 34 | } catch (_) {} 35 | } 36 | watch(ref, (value: T | undefined) => { 37 | if (value === undefined) { 38 | localStorage.removeItem(name) 39 | } else { 40 | localStorage.setItem(name, JSON.stringify(value)) 41 | } 42 | }) 43 | return ref 44 | } 45 | 46 | export function bindObjectToLocalStorage(obj: T, name: string): T { 47 | const active = reactive(obj) as T 48 | const s = localStorage.getItem(name) 49 | if (s) { 50 | try { 51 | const parsed = JSON.parse(s) 52 | for (const k of Object.keys(parsed)) { 53 | if (k in active) { 54 | active[k as keyof T] = parsed[k as keyof T] 55 | } 56 | } 57 | } catch (_) {} 58 | } 59 | watch(active, () => { 60 | localStorage.setItem(name, JSON.stringify(active)) 61 | }) 62 | return active 63 | } 64 | -------------------------------------------------------------------------------- /dashboard/src/lang/index.ts: -------------------------------------------------------------------------------- 1 | import { type Ref, ref, watch } from 'vue' 2 | import { Lang } from './lang' 3 | export * from './lang' 4 | import EN_US_JSON from '@/assets/lang/en-US.json' 5 | import ZH_CN_JSON from '@/assets/lang/zh-CN.json' 6 | 7 | // keys use with window.localStorage 8 | const HAS_LOCAL_STORAGE = 'localStorage' in globalThis 9 | const TR_LANG_CACHE_KEY = 'go-openbmclapi.dashboard.tr.lang' 10 | const TR_DATA_CACHE_KEY = 'go-openbmclapi.dashboard.tr.map' 11 | 12 | interface LangMap { 13 | [key: string]: string | LangMap 14 | } 15 | 16 | interface langItem { 17 | code: Lang 18 | tr: () => Promise 19 | } 20 | 21 | export const avaliableLangs = [ 22 | { code: new Lang('en-US'), tr: () => EN_US_JSON }, 23 | { code: new Lang('zh-CN'), tr: () => ZH_CN_JSON }, 24 | // { code: new Lang('zh-CN'), tr: () => import('@/assets/lang/zh-CN.json') }, 25 | ] 26 | 27 | export const defaultLang = avaliableLangs[0] 28 | const currentLang = ref(defaultLang) 29 | const currentTr: Ref = ref(null) 30 | 31 | ;(async function () { 32 | if (!HAS_LOCAL_STORAGE) { 33 | return 34 | } 35 | const langCache = localStorage.getItem(TR_LANG_CACHE_KEY) 36 | if (langCache) { 37 | for (let a of avaliableLangs) { 38 | if (a.code.match(langCache)) { 39 | currentLang.value = a 40 | localStorage.setItem(TR_LANG_CACHE_KEY, langCache) 41 | break 42 | } 43 | } 44 | } 45 | try { 46 | // use local cache before translate map loaded then refresh will not always flash words 47 | const data = JSON.parse(localStorage.getItem(TR_DATA_CACHE_KEY) as string) 48 | if (typeof data === 'object') { 49 | currentTr.value = data as LangMap 50 | } 51 | } catch {} 52 | currentTr.value = await currentLang.value.tr() 53 | localStorage.setItem(TR_DATA_CACHE_KEY, JSON.stringify(currentTr.value)) 54 | })() 55 | 56 | export function getLang(): Lang { 57 | return currentLang.value.code 58 | } 59 | 60 | export function setLang(lang: Lang | string): Lang | null { 61 | for (let a of avaliableLangs) { 62 | if (a.code.match(lang)) { 63 | if (HAS_LOCAL_STORAGE) { 64 | localStorage.setItem(TR_LANG_CACHE_KEY, a.code.toString()) 65 | } 66 | currentLang.value = a 67 | Promise.resolve(a.tr()).then((map) => { 68 | currentTr.value = map 69 | if (HAS_LOCAL_STORAGE) { 70 | localStorage.setItem(TR_DATA_CACHE_KEY, JSON.stringify(map)) 71 | } 72 | }) 73 | return a.code 74 | } 75 | } 76 | return null 77 | } 78 | 79 | export function tr(key: string, ...values: unknown[]): string { 80 | // console.debug('translating:', key) 81 | const item = currentLang.value 82 | let cur: string | LangMap | null = currentTr.value 83 | if (!cur || (key && typeof cur === 'string')) { 84 | return `{{${key}}}` 85 | } 86 | const keys = key.split('.') 87 | for (let i = 0; i < keys.length; i++) { 88 | const k = keys[i] 89 | if (typeof cur === 'string') { 90 | return `{{${key}}}` 91 | } 92 | if (!(k in cur) || typeof cur[k] === 'string') { 93 | cur = cur[keys.slice(i).join('.')] 94 | break 95 | } 96 | cur = cur[k] 97 | } 98 | if (typeof cur !== 'string') { 99 | return `{{${key}}}` 100 | } 101 | // TODO: apply values 102 | return cur 103 | } 104 | 105 | export const langNameMap: { [key: string]: string } = { 106 | 'en-US': 'English', 107 | 'zh-CN': '简体中文', 108 | } 109 | -------------------------------------------------------------------------------- /dashboard/src/lang/lang.ts: -------------------------------------------------------------------------------- 1 | const CODERE = /^([a-zA-Z]{2})(?:[_-]([a-zA-Z]{2}))?$/ 2 | 3 | export class Lang { 4 | readonly name: string 5 | readonly area?: string 6 | 7 | constructor(code: string) { 8 | const group = CODERE.exec(code.toLowerCase()) 9 | if (!group) { 10 | throw `"${code}" is not a valid language code` 11 | } 12 | ;[, this.name, this.area] = group 13 | } 14 | 15 | toString(): string { 16 | return this.area ? this.name + '-' + this.area.toUpperCase() : this.name 17 | } 18 | 19 | equals(code: unknown): boolean { 20 | if (!code) { 21 | return false 22 | } 23 | if (typeof code === 'string') { 24 | const group = CODERE.exec(code.toLowerCase()) 25 | if (!group) { 26 | return false 27 | } 28 | return this.name == group[1] && this.area == group[2] 29 | } 30 | if (code instanceof Lang) { 31 | return this.name == code.name && this.area == code.area 32 | } 33 | return false 34 | } 35 | 36 | match(code: unknown): boolean { 37 | if (!code) { 38 | return false 39 | } 40 | if (typeof code === 'string') { 41 | const group = CODERE.exec(code.toLowerCase()) 42 | if (!group) { 43 | return false 44 | } 45 | return this.name == group[1] && (!this.area || this.area == group[2]) 46 | } 47 | if (code instanceof Lang) { 48 | return this.name == code.name && (!this.area || this.area == code.area) 49 | } 50 | return false 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /dashboard/src/main.ts: -------------------------------------------------------------------------------- 1 | import { createApp, ref, watch, inject, type Ref } from 'vue' 2 | import vueCookies, { type VueCookies } from 'vue-cookies' 3 | import PrimeVue from 'primevue/config' 4 | import FocusTrap from 'primevue/focustrap' 5 | import ToastService from 'primevue/toastservice' 6 | import { registerSW } from 'virtual:pwa-register' 7 | import App from './App.vue' 8 | import router from './router' 9 | import { useCookies, bindRefToCookie } from './cookies' 10 | import './utils/chart' 11 | import { ping } from '@/api/v0' 12 | 13 | import './assets/theme.css' 14 | import 'primeicons/primeicons.css' 15 | import './assets/main.css' 16 | 17 | registerSW({ 18 | immediate: true, 19 | async onRegisteredSW( 20 | swScriptUrl: string, 21 | registration: ServiceWorkerRegistration | undefined, 22 | ): Promise { 23 | if (!registration) { 24 | return 25 | } 26 | // registration.sync.register('poll-state') 27 | }, 28 | }) 29 | 30 | const app = createApp(App) 31 | 32 | app.use(router) 33 | app.use(vueCookies, { expires: '30d', path: import.meta.env.BASE_URL }) 34 | 35 | app.use(PrimeVue, { ripple: true }) 36 | app.use(ToastService) 37 | app.directive('focustrap', FocusTrap) 38 | 39 | const cookies = (app as unknown as { $cookies: VueCookies }).$cookies 40 | 41 | const API_TOKEN_STORAGE_KEY = '_authToken' 42 | const token: Ref = bindRefToCookie( 43 | ref(null), 44 | API_TOKEN_STORAGE_KEY, 45 | 60 * 60 * 12, 46 | cookies, 47 | ) 48 | 49 | if (token.value) { 50 | ping(token.value).then((pong) => { 51 | if (!pong.authed) { 52 | console.warn('Token expired') 53 | token.value = null 54 | } 55 | }) 56 | } 57 | app.provide('token', token) 58 | 59 | app.mount('#app') 60 | -------------------------------------------------------------------------------- /dashboard/src/router/index.ts: -------------------------------------------------------------------------------- 1 | import { createRouter, createWebHistory } from 'vue-router' 2 | import HomeView from '@/views/HomeView.vue' 3 | 4 | const router = createRouter({ 5 | history: createWebHistory(import.meta.env.BASE_URL), 6 | routes: [ 7 | { 8 | path: '/', 9 | name: 'home', 10 | component: HomeView, 11 | }, 12 | { 13 | path: '/about', 14 | name: 'about', 15 | // route level code-splitting 16 | // this generates a separate chunk (About.[hash].js) for this route 17 | // which is lazy-loaded when the route is visited. 18 | component: () => import('@/views/AboutView.vue'), 19 | }, 20 | { 21 | path: '/login', 22 | name: 'login', 23 | component: () => import('@/views/LoginView.vue'), 24 | props: (route) => ({ next: route.query.next }), 25 | }, 26 | { 27 | path: '/settings', 28 | name: 'settings', 29 | component: () => import('@/views/SettingsView.vue'), 30 | }, 31 | { 32 | path: '/loglist', 33 | name: 'loglist', 34 | component: () => import('@/views/LogListView.vue'), 35 | }, 36 | { 37 | path: '/settings/notifications', 38 | name: 'settings/notifications', 39 | component: () => import('@/views/settings/NotificationsView.vue'), 40 | }, 41 | ], 42 | scrollBehavior(to, from, savedPosition) { 43 | return savedPosition || { top: 0 } 44 | }, 45 | }) 46 | 47 | export default router 48 | -------------------------------------------------------------------------------- /dashboard/src/utils/chart.ts: -------------------------------------------------------------------------------- 1 | import type { Ref } from 'vue' 2 | import { Chart } from 'chart.js/auto' 3 | 4 | interface Option { 5 | lineX: number | Readonly>> | null 6 | lineWidth: number 7 | lineColor: string 8 | } 9 | 10 | Chart.register({ 11 | id: 'custom-vertical-line', 12 | afterDatasetDraw: function (chart, args, { lineX, lineWidth, lineColor }: Option) { 13 | if (typeof lineX === 'object' && lineX) { 14 | lineX = lineX.value 15 | } 16 | if (typeof lineX === 'number' && lineX >= 0) { 17 | const ctx = chart.ctx 18 | const x = (lineX / chart.scales.x.max) * chart.chartArea.width + chart.chartArea.left 19 | const { top, bottom } = chart.scales.y 20 | 21 | ctx.save() 22 | ctx.beginPath() 23 | ctx.moveTo(x, top) 24 | ctx.lineTo(x, bottom) 25 | ctx.lineWidth = lineWidth 26 | ctx.strokeStyle = lineColor 27 | ctx.stroke() 28 | ctx.restore() 29 | } 30 | }, 31 | defaults: { 32 | lineX: null, 33 | lineWidth: 1, 34 | lineColor: '#ee5522', 35 | } as Option, 36 | }) 37 | -------------------------------------------------------------------------------- /dashboard/src/utils/index.ts: -------------------------------------------------------------------------------- 1 | import { tr, getLang, Lang } from '@/lang' 2 | 3 | const _EN = new Lang('en') 4 | const _ZH = new Lang('zh') 5 | 6 | export function formatNumber(num: number): string { 7 | const lang = getLang() 8 | var neg = '' 9 | if (num < 0) { 10 | neg = '-' 11 | num = -num 12 | } 13 | var res: string 14 | if (_ZH.match(lang)) { 15 | res = formatNumberZH(num) 16 | } else { 17 | res = formatNumberEN(num) 18 | } 19 | return neg + res 20 | } 21 | 22 | const nUnitsUS = ['k', 'm', 'B', 'T', 'Q'] 23 | 24 | function formatNumberEN(num: number): string { 25 | if (num < 1000) { 26 | return num.toString() 27 | } 28 | var unit 29 | for (const u of nUnitsUS) { 30 | unit = u 31 | num /= 1000 32 | if (num < 1000) { 33 | break 34 | } 35 | } 36 | return `${num.toFixed(2)} ${unit}` 37 | } 38 | 39 | const nUnitsZH = ['万', '亿', '兆', '京'] 40 | 41 | function formatNumberZH(num: number): string { 42 | if (num < 9000) { 43 | return num.toString() 44 | } 45 | var unit = '' 46 | for (const u of nUnitsZH) { 47 | unit = u 48 | num /= 10000 49 | if (num < 9000) { 50 | break 51 | } 52 | } 53 | return `${num.toFixed(2)} ${unit}` 54 | } 55 | 56 | const bUnits = ['KB', 'MB', 'GB', 'TB'] 57 | 58 | export function formatBytes(bytes: number): string { 59 | var neg = '' 60 | if (bytes < 0) { 61 | neg = '-' 62 | bytes = -bytes 63 | } 64 | if (bytes < 1000) { 65 | return `${neg}${bytes} B` 66 | } 67 | var unit = '' 68 | for (const u of bUnits) { 69 | unit = u 70 | bytes /= 1024 71 | if (bytes < 1000) { 72 | break 73 | } 74 | } 75 | return `${neg}${bytes.toFixed(2)} ${unit}` 76 | } 77 | 78 | export function formatTime(ms: number): string { 79 | var neg = '' 80 | if (ms < 0) { 81 | neg = '-' 82 | ms = -ms 83 | } 84 | var unit = tr('unit.time.ms') 85 | if (ms > 800) { 86 | ms /= 1000 87 | unit = tr('unit.time.s') 88 | if (ms > 50) { 89 | ms /= 60 90 | unit = tr('unit.time.min') 91 | if (ms > 50) { 92 | ms /= 60 93 | unit = tr('unit.time.hour') 94 | if (ms > 22) { 95 | ms /= 24 96 | unit = tr('unit.time.day') 97 | if (ms > 350) { 98 | ms /= 356 99 | unit = tr('unit.time.year') 100 | } 101 | } 102 | } 103 | } 104 | } 105 | return `${neg}${ms.toFixed(2)} ${unit}` 106 | } 107 | -------------------------------------------------------------------------------- /dashboard/src/utils/ring.ts: -------------------------------------------------------------------------------- 1 | export class RingBuffer extends Array { 2 | private start: number 3 | private end: number 4 | 5 | private constructor(size: number) { 6 | super(size + 1) 7 | this.start = 0 8 | this.end = 0 9 | } 10 | 11 | get _realLength(): number { 12 | return this.length 13 | } 14 | 15 | _realGet(i: number): T { 16 | return this[i] 17 | } 18 | 19 | _realSet(i: number, v: T): void { 20 | this[i] = v 21 | } 22 | 23 | _realReplace(start: number, items: T[]): void { 24 | for (let i = 0; i < items.length; i++) { 25 | this._realSet(start + i, items[i]) 26 | } 27 | // this.splice(start, items.length, ...items) 28 | } 29 | 30 | get size(): number { 31 | return this._realLength - 1 32 | } 33 | 34 | get avaliable(): number { 35 | if (this.start <= this.end) { 36 | return this.end - this.start 37 | } 38 | return this.length - this.start + this.end 39 | } 40 | 41 | at(i: number): T { 42 | return this._realGet((this.start + i) % this.length) 43 | } 44 | 45 | set(i: number, v: T): void { 46 | this._realSet((this.start + i) % this.length, v) 47 | } 48 | 49 | push(...items: T[]): number { 50 | if (items.length >= this.size) { 51 | this._realReplace(0, items.slice(-this.size)) 52 | this.start = 0 53 | this.end = this.size 54 | return this.avaliable 55 | } 56 | const adding = items.length 57 | const right = this._realLength - this.end 58 | if (adding > right) { 59 | this._realReplace(this.end, items.slice(0, right)) 60 | this._realReplace(0, items.slice(right)) 61 | } else { 62 | this._realReplace(this.end, items) 63 | } 64 | const oldend = this.end 65 | this.end = (oldend + adding) % this._realLength 66 | if ( 67 | (this.start <= this.end && this.end < oldend) || 68 | (oldend < this.start && this.start <= this.end) 69 | ) { 70 | this.start = (this.end + 1) % this._realLength 71 | } 72 | return this.avaliable 73 | } 74 | 75 | *keys(): IterableIterator { 76 | const start = this.start 77 | const end = this.end 78 | const length = this._realLength 79 | if (start <= end) { 80 | for (let i = start; i < end; i++) { 81 | yield i 82 | } 83 | } else { 84 | for (let i = start; i < length; i++) { 85 | yield i 86 | } 87 | for (let i = 0; i < end; i++) { 88 | yield i 89 | } 90 | } 91 | } 92 | 93 | *values(): IterableIterator { 94 | for (let i of this.keys()) { 95 | yield this._realGet(i) 96 | } 97 | } 98 | 99 | [Symbol.iterator](): IterableIterator { 100 | return this.values() 101 | } 102 | 103 | static create(size: number): T[] { 104 | const ring = new this(size) 105 | return new Proxy(ring, { 106 | get(target: RingBuffer, key: string | symbol): any { 107 | if (typeof key === 'string') { 108 | const index = parseInt(key, 10) 109 | if (!isNaN(index)) { 110 | return target.at(index) 111 | } 112 | } 113 | switch (key) { 114 | case 'length': 115 | return target.avaliable 116 | case '_realLength': 117 | return target.length 118 | case '_realGet': 119 | // case '_realSet': 120 | // case '_realReplace': 121 | return Reflect.get(target, key).bind(target) 122 | } 123 | return Reflect.get(target, key) 124 | }, 125 | set( 126 | target: RingBuffer, 127 | key: string | symbol, 128 | value: any, 129 | recvier: RingBuffer, 130 | ): boolean { 131 | // if (typeof key === 'string' && /^[1-9][0-9]*$/.test(key)) { 132 | // recvier.set(parseInt(key, 10), value) 133 | // return true 134 | // } 135 | return Reflect.set(target, key, value) 136 | }, 137 | ownKeys(target: RingBuffer): (string | symbol)[] { 138 | const keys: (string | symbol)[] = Array.of(...target.keys()).map((n) => n.toString()) 139 | for (let k of Reflect.ownKeys(target)) { 140 | if (typeof k !== 'string' || !/^[1-9][0-9]*$/.test(k)) { 141 | keys.push(k) 142 | } 143 | } 144 | return keys 145 | }, 146 | }) 147 | } 148 | } 149 | -------------------------------------------------------------------------------- /dashboard/src/views/AboutView.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 16 | -------------------------------------------------------------------------------- /dashboard/src/views/LogListView.vue: -------------------------------------------------------------------------------- 1 | 113 | 148 | 158 | -------------------------------------------------------------------------------- /dashboard/src/views/LoginView.vue: -------------------------------------------------------------------------------- 1 | 27 | 33 | 40 | -------------------------------------------------------------------------------- /dashboard/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "@vue/tsconfig/tsconfig.web.json", 3 | "include": ["env.d.ts", "src/**/*", "src/**/*.vue"], 4 | "compilerOptions": { 5 | "lib":[ 6 | "es2020", 7 | "dom", 8 | "webworker" 9 | ], 10 | "ignoreDeprecations": "5.0", 11 | "baseUrl": ".", 12 | "paths": { 13 | "@/*": ["./src/*"] 14 | } 15 | }, 16 | 17 | "references": [ 18 | { 19 | "path": "./tsconfig.node.json" 20 | } 21 | ] 22 | } 23 | -------------------------------------------------------------------------------- /dashboard/tsconfig.node.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "@vue/tsconfig/tsconfig.node.json", 3 | "include": ["vite.config.*", "vitest.config.*", "cypress.config.*", "playwright.config.*"], 4 | "compilerOptions": { 5 | "composite": true, 6 | "types": ["node"] 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /dashboard/vite.config.ts: -------------------------------------------------------------------------------- 1 | import { fileURLToPath, URL } from 'node:url' 2 | 3 | import { defineConfig } from 'vite' 4 | import vue from '@vitejs/plugin-vue' 5 | import pwa from './src/config/pwa' 6 | 7 | // https://vitejs.dev/config/ 8 | export default defineConfig(async ({ command, mode }) => { 9 | console.log(command, mode) 10 | const isdev = mode === 'development' 11 | const minify = isdev ? '' : 'esbuild' 12 | 13 | return { 14 | plugins: [vue(), pwa], 15 | resolve: { 16 | alias: { 17 | '@': fileURLToPath(new URL('./src', import.meta.url)), 18 | }, 19 | }, 20 | mode: mode, 21 | base: '/dashboard', 22 | build: { 23 | minify: minify, 24 | }, 25 | esbuild: { 26 | pure: isdev ? [] : ['console.debug'], 27 | }, 28 | } 29 | }) 30 | -------------------------------------------------------------------------------- /database/db_test.go: -------------------------------------------------------------------------------- 1 | /** 2 | * OpenBmclAPI (Golang Edition) 3 | * Copyright (C) 2024 Kevin Z 4 | * All rights reserved 5 | * 6 | * This program is free software: you can redistribute it and/or modify 7 | * it under the terms of the GNU Affero General Public License as published 8 | * by the Free Software Foundation, either version 3 of the License, or 9 | * (at your option) any later version. 10 | * 11 | * This program is distributed in the hope that it will be useful, 12 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 13 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 14 | * GNU Affero General Public License for more details. 15 | * 16 | * You should have received a copy of the GNU Affero General Public License 17 | * along with this program. If not, see . 18 | */ 19 | 20 | package database_test 21 | 22 | import ( 23 | "encoding/json" 24 | "time" 25 | 26 | . "github.com/LiterMC/go-openbmclapi/database" 27 | "testing" 28 | ) 29 | 30 | func TestUnmarshalNotificationScopes(t *testing.T) { 31 | data := []struct { 32 | S string 33 | V NotificationScopes 34 | }{ 35 | {`[]`, NotificationScopes{}}, 36 | {`["enabled"]`, NotificationScopes{Enabled: true}}, 37 | {`["enabled", "enabled"]`, NotificationScopes{Enabled: true}}, 38 | {`["enabled", "syncdone"]`, NotificationScopes{Enabled: true, SyncDone: true}}, 39 | {`{}`, NotificationScopes{}}, 40 | {`{"enabled": true}`, NotificationScopes{Enabled: true}}, 41 | {`{"enabled": false}`, NotificationScopes{Enabled: false}}, 42 | {`{"enabled": true, "syncdone": true}`, NotificationScopes{Enabled: true, SyncDone: true}}, 43 | } 44 | for i, d := range data { 45 | var v NotificationScopes 46 | if e := json.Unmarshal(([]byte)(d.S), &v); e != nil { 47 | t.Errorf("Cannot parse %q: %v", d.S, e) 48 | continue 49 | } 50 | if v != d.V { 51 | t.Errorf("Incorrect return value at %d (%s), expect %#v, got %#v", i, d.S, d.V, v) 52 | } 53 | } 54 | } 55 | 56 | var TwoFirst = time.Date(2000, 01, 01, 0, 0, 0, 0, time.UTC) 57 | var TwoSecond = time.Date(2000, 01, 02, 0, 0, 0, 0, time.UTC) 58 | 59 | func TestScheduleReadySince(t *testing.T) { 60 | data := []struct { 61 | S string 62 | L time.Time 63 | N time.Time 64 | V bool 65 | }{ 66 | {"00:00", TwoFirst, TwoFirst, false}, 67 | {"00:00", TwoFirst, TwoFirst.Add(time.Hour * 3), false}, 68 | {"00:00", TwoFirst, TwoSecond, true}, 69 | {"00:00", TwoFirst.Add(time.Minute * 59), TwoSecond, true}, 70 | {"00:00", TwoFirst.Add(time.Hour * 12), TwoSecond, true}, 71 | {"00:00", TwoFirst.Add(time.Hour*12 + 1), TwoSecond, false}, 72 | {"12:00", TwoFirst, TwoSecond.Add(time.Hour*12 - 1), true}, 73 | {"12:00", TwoFirst, TwoSecond.Add(time.Hour * 12), true}, 74 | {"12:00", TwoFirst, TwoSecond.Add(time.Hour * 23), true}, 75 | {"12:00", TwoFirst, TwoSecond.Add(time.Hour * 24), true}, 76 | {"12:00", TwoFirst, TwoSecond.Add(time.Hour * 25), true}, 77 | {"12:00", TwoFirst, TwoFirst.Add(time.Hour * 15), false}, 78 | {"12:00", TwoFirst, TwoFirst.Add(time.Hour*15 - 1), true}, 79 | {"23:59", TwoFirst, TwoSecond, true}, 80 | {"00:00", time.Time{}, TwoFirst, true}, 81 | {"00:00", time.Time{}, TwoFirst.Add(time.Hour), true}, 82 | {"00:00", time.Time{}, TwoFirst.Add(time.Hour * 3), false}, 83 | {"12:00", time.Time{}, TwoSecond.Add(-time.Hour + 1), false}, 84 | } 85 | for i, v := range data { 86 | var s Schedule 87 | if e := s.UnmarshalText(([]byte)(v.S)); e != nil { 88 | t.Fatalf("Cannot parse %q: %v", v.S, e) 89 | } 90 | r := s.ReadySince(v.L, v.N) 91 | if r != v.V { 92 | t.Errorf("Incorrect return value at %d (%s), expect %v, got %v", i, v.S, v.V, r) 93 | } 94 | } 95 | } 96 | -------------------------------------------------------------------------------- /database/sql_drives.go: -------------------------------------------------------------------------------- 1 | /** 2 | * OpenBmclAPI (Golang Edition) 3 | * Copyright (C) 2024 Kevin Z 4 | * All rights reserved 5 | * 6 | * This program is free software: you can redistribute it and/or modify 7 | * it under the terms of the GNU Affero General Public License as published 8 | * by the Free Software Foundation, either version 3 of the License, or 9 | * (at your option) any later version. 10 | * 11 | * This program is distributed in the hope that it will be useful, 12 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 13 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 14 | * GNU Affero General Public License for more details. 15 | * 16 | * You should have received a copy of the GNU Affero General Public License 17 | * along with this program. If not, see . 18 | */ 19 | 20 | package database 21 | 22 | import ( 23 | _ "github.com/glebarez/go-sqlite" // sqlite 24 | _ "github.com/go-sql-driver/mysql" // mysql 25 | _ "github.com/lib/pq" // postgres 26 | ) 27 | -------------------------------------------------------------------------------- /docs/README.md: -------------------------------------------------------------------------------- 1 | # 部署文档(保姆级) 2 | 3 | 由于 GitHub 访问速度较慢,你可以前往[这里](https://d.kstore.space/download/7507/deploy_with_windows.pdf)来查看镜像版本。 4 | -------------------------------------------------------------------------------- /docs/deploy_with_windows.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mcmod-info-mirror/go-openmcim/b0d97f8536e989cce1b35e5c42da4081732fa7de/docs/deploy_with_windows.pdf -------------------------------------------------------------------------------- /exitcodes.go: -------------------------------------------------------------------------------- 1 | /** 2 | * OpenBmclAPI (Golang Edition) 3 | * Copyright (C) 2024 Kevin Z 4 | * All rights reserved 5 | * 6 | * This program is free software: you can redistribute it and/or modify 7 | * it under the terms of the GNU Affero General Public License as published 8 | * by the Free Software Foundation, either version 3 of the License, or 9 | * (at your option) any later version. 10 | * 11 | * This program is distributed in the hope that it will be useful, 12 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 13 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 14 | * GNU Affero General Public License for more details. 15 | * 16 | * You should have received a copy of the GNU Affero General Public License 17 | * along with this program. If not, see . 18 | */ 19 | 20 | package main 21 | 22 | const ( 23 | CodeClientError = 0x01 24 | CodeServerError = 0x02 25 | CodeEnvironmentError = 0x04 26 | CodeUnexpectedError = 0x08 27 | ) 28 | 29 | const ( 30 | CodeClientOrServerError = CodeClientError | CodeServerError 31 | CodeClientOrEnvionmentError = CodeClientError | CodeEnvironmentError 32 | CodeClientUnexpectedError = CodeUnexpectedError | CodeClientError 33 | CodeServerOrEnvionmentError = CodeServerError | CodeEnvironmentError 34 | CodeServerUnexpectedError = CodeUnexpectedError | CodeServerError 35 | ) 36 | -------------------------------------------------------------------------------- /go-openbmclapi.exe: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mcmod-info-mirror/go-openmcim/b0d97f8536e989cce1b35e5c42da4081732fa7de/go-openbmclapi.exe -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/LiterMC/go-openbmclapi 2 | 3 | go 1.21.6 4 | 5 | require ( 6 | github.com/LiterMC/socket.io v0.2.4 7 | github.com/crow-misia/http-ece v0.0.1 8 | github.com/glebarez/go-sqlite v1.22.0 9 | github.com/go-sql-driver/mysql v1.8.0 10 | github.com/golang-jwt/jwt/v5 v5.2.0 11 | github.com/google/uuid v1.5.0 12 | github.com/gorilla/websocket v1.5.1 13 | github.com/gregjones/httpcache v0.0.0-20190611155906-901d90724c79 14 | github.com/hamba/avro/v2 v2.18.0 15 | github.com/klauspost/compress v1.17.4 16 | github.com/lib/pq v1.10.9 17 | github.com/libp2p/go-doh-resolver v0.4.0 18 | github.com/patrickmn/go-cache v2.1.0+incompatible 19 | github.com/redis/go-redis/v9 v9.4.0 20 | github.com/studio-b12/gowebdav v0.9.0 21 | github.com/vbauerster/mpb/v8 v8.7.2 22 | github.com/xhit/go-simple-mail/v2 v2.16.0 23 | gopkg.in/yaml.v3 v3.0.1 24 | ) 25 | 26 | require ( 27 | filippo.io/edwards25519 v1.1.0 // indirect 28 | github.com/VividCortex/ewma v1.2.0 // indirect 29 | github.com/acarl005/stripansi v0.0.0-20180116102854-5a71ef0e047d // indirect 30 | github.com/cespare/xxhash/v2 v2.2.0 // indirect 31 | github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect 32 | github.com/dustin/go-humanize v1.0.1 // indirect 33 | github.com/go-test/deep v1.1.1 // indirect 34 | github.com/ipfs/go-log/v2 v2.1.3 // indirect 35 | github.com/json-iterator/go v1.1.12 // indirect 36 | github.com/mattn/go-isatty v0.0.20 // indirect 37 | github.com/mattn/go-runewidth v0.0.15 // indirect 38 | github.com/miekg/dns v1.1.41 // indirect 39 | github.com/minio/blake2b-simd v0.0.0-20160723061019-3f5f724cb5b1 // indirect 40 | github.com/minio/sha256-simd v0.1.1-0.20190913151208-6de447530771 // indirect 41 | github.com/mitchellh/mapstructure v1.5.0 // indirect 42 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect 43 | github.com/modern-go/reflect2 v1.0.2 // indirect 44 | github.com/mr-tron/base58 v1.1.2 // indirect 45 | github.com/multiformats/go-multiaddr v0.2.0 // indirect 46 | github.com/multiformats/go-multiaddr-dns v0.3.0 // indirect 47 | github.com/multiformats/go-multihash v0.0.8 // indirect 48 | github.com/multiformats/go-varint v0.0.1 // indirect 49 | github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect 50 | github.com/rivo/uniseg v0.4.4 // indirect 51 | github.com/spaolacci/murmur3 v1.1.0 // indirect 52 | github.com/toorop/go-dkim v0.0.0-20201103131630-e1cd1a0a5208 // indirect 53 | go.uber.org/atomic v1.7.0 // indirect 54 | go.uber.org/multierr v1.6.0 // indirect 55 | go.uber.org/zap v1.16.0 // indirect 56 | golang.org/x/crypto v0.21.0 // indirect 57 | golang.org/x/net v0.21.0 // indirect 58 | golang.org/x/sys v0.18.0 // indirect 59 | modernc.org/libc v1.37.6 // indirect 60 | modernc.org/mathutil v1.6.0 // indirect 61 | modernc.org/memory v1.7.2 // indirect 62 | modernc.org/sqlite v1.28.0 // indirect 63 | ) 64 | -------------------------------------------------------------------------------- /help.go: -------------------------------------------------------------------------------- 1 | /** 2 | * OpenBmclAPI (Golang Edition) 3 | * Copyright (C) 2023 Kevin Z 4 | * All rights reserved 5 | * 6 | * This program is free software: you can redistribute it and/or modify 7 | * it under the terms of the GNU Affero General Public License as published 8 | * by the Free Software Foundation, either version 3 of the License, or 9 | * (at your option) any later version. 10 | * 11 | * This program is distributed in the hope that it will be useful, 12 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 13 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 14 | * GNU Affero General Public License for more details. 15 | * 16 | * You should have received a copy of the GNU Affero General Public License 17 | * along with this program. If not, see . 18 | */ 19 | 20 | package main 21 | 22 | import ( 23 | "fmt" 24 | "os" 25 | ) 26 | 27 | func printHelp() { 28 | fmt.Printf("Usage for %s\n\n", os.Args[0]) 29 | fmt.Println("Sub commands:") 30 | fmt.Println(" help") 31 | fmt.Println(" \t" + "Show this message") 32 | fmt.Println() 33 | fmt.Println(" main | serve | ") 34 | fmt.Println(" \t" + "Execute the main program") 35 | fmt.Println() 36 | fmt.Println(" license") 37 | fmt.Println(" \t" + "Print the full program license") 38 | fmt.Println() 39 | fmt.Println(" version") 40 | fmt.Println(" \t" + "Print the program's version") 41 | fmt.Println() 42 | fmt.Println(" zip-cache [options ...]") 43 | fmt.Println(" \t" + "Compress the cache directory") 44 | fmt.Println() 45 | fmt.Println(" Options:") 46 | fmt.Println(" " + "verbose | v : Show compressing files") 47 | fmt.Println(" " + "all | a : Compress all files") 48 | fmt.Println(" " + "overwrite | o : Overwrite compressed file even if it exists") 49 | fmt.Println(" " + "keep | k : Keep uncompressed file") 50 | fmt.Println() 51 | fmt.Println(" unzip-cache [options ...]") 52 | fmt.Println(" \t" + "Decompress the cache directory") 53 | fmt.Println() 54 | fmt.Println(" Options:") 55 | fmt.Println(" " + "verbose | v : Show decompressing files") 56 | fmt.Println(" " + "overwrite | o : Overwrite uncompressed file even if it exists") 57 | fmt.Println(" " + "keep | k : Keep compressed file") 58 | fmt.Println() 59 | fmt.Println(" upload-webdav") 60 | fmt.Println(" \t" + "Upload objects from local storage to webdav storage") 61 | } 62 | -------------------------------------------------------------------------------- /images/MsiBanner.bmp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mcmod-info-mirror/go-openmcim/b0d97f8536e989cce1b35e5c42da4081732fa7de/images/MsiBanner.bmp -------------------------------------------------------------------------------- /images/MsiBannerCat.bmp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mcmod-info-mirror/go-openmcim/b0d97f8536e989cce1b35e5c42da4081732fa7de/images/MsiBannerCat.bmp -------------------------------------------------------------------------------- /images/MsiDialog.bmp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mcmod-info-mirror/go-openmcim/b0d97f8536e989cce1b35e5c42da4081732fa7de/images/MsiDialog.bmp -------------------------------------------------------------------------------- /images/app.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mcmod-info-mirror/go-openmcim/b0d97f8536e989cce1b35e5c42da4081732fa7de/images/app.ico -------------------------------------------------------------------------------- /installer/service/go-openbmclapi.service: -------------------------------------------------------------------------------- 1 | [Unit] 2 | Description=Go-OpenBmclAPI Service 3 | Documentation=https://github.com/LiterMC/go-openbmclapi 4 | Wants=basic.target 5 | After=network.target 6 | 7 | [Service] 8 | Type=idle 9 | User=openbmclapi 10 | WorkingDirectory=/opt/openbmclapi 11 | ExecStart=/opt/openbmclapi/service-linux-go-openbmclapi 12 | ExecReload=/bin/kill -s HUP $MAINPID 13 | RestartSec=30 14 | Restart=on-failure 15 | TimeoutSec=30 16 | 17 | [Install] 18 | WantedBy=multi-user.target 19 | -------------------------------------------------------------------------------- /installer/service/installer.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # if [ $(id -u) -ne 0 ]; then 4 | # echo -e "\e[31mERROR: Not root user\e[0m" 5 | # exit 1 6 | # fi 7 | 8 | ARGS=() 9 | 10 | while [ $# -gt 0 ]; do 11 | case "$1" in 12 | -m|--mirror) 13 | shift 14 | MIRROR_PREFIX=$1 15 | ;; 16 | -t|--tag) 17 | shift 18 | TARGET_TAG=$1 19 | ;; 20 | -*) 21 | echo -e "\e[31mERROR: Unknown option $1\e[0m" 22 | exit 1 23 | ;; 24 | *) 25 | ARGS+=("$1") 26 | ;; 27 | esac 28 | shift 29 | done 30 | 31 | if [ ${#ARGS[@]} -ge 1 ]; then 32 | TARGET_TAG="${ARGS[0]}" 33 | fi 34 | 35 | if [ -n "$MIRROR_PREFIX" ] && [[ "$MIRROR_PREFIX" != */ ]]; then 36 | MIRROR_PREFIX="${MIRROR_PREFIX}/" 37 | fi 38 | 39 | echo "MIRROR_PREFIX=${MIRROR_PREFIX}" 40 | echo "TARGET_TAG=${TARGET_TAG}" 41 | 42 | REPO='LiterMC/go-openbmclapi' 43 | RAW_PREFIX="${MIRROR_PREFIX}https://raw.githubusercontent.com" 44 | RAW_REPO="$RAW_PREFIX/$REPO" 45 | BASE_PATH=/opt/openbmclapi 46 | USERNAME=openbmclapi 47 | 48 | if ! systemd --version >/dev/null 2>&1 ; then 49 | echo -e "\e[31mERROR: Failed to test systemd\e[0m" 50 | exit 1 51 | fi 52 | 53 | if [ ! -d /usr/lib/systemd/system/ ]; then 54 | echo -e "\e[31mERROR: /usr/lib/systemd/system/ is not exist\e[0m" 55 | exit 1 56 | fi 57 | 58 | if ! id $USERNAME >/dev/null 2>&1; then 59 | echo -e "\e[34m==> Creating user $USERNAME\e[0m" 60 | useradd $USERNAME || { 61 | echo -e "\e[31mERROR: Could not create user $USERNAME\e[0m" 62 | exit 1 63 | } 64 | fi 65 | 66 | 67 | function fetchGithubLatestTag(){ 68 | prefix="location: https://github.com/$REPO/releases/tag/" 69 | location=$(curl -fsSI "https://github.com/$REPO/releases/latest" | grep "$prefix" | tr -d "\r") 70 | [ $? = 0 ] || return 1 71 | export LATEST_TAG="${location#${prefix}}" 72 | } 73 | 74 | function fetchBlob(){ 75 | file=$1 76 | target=$2 77 | filemod=$3 78 | 79 | source="$RAW_REPO/$TARGET_TAG/$file" 80 | echo -e "\e[34m==> Downloading $source\e[0m" 81 | tmpf=$(mktemp -t go-openbmclapi.XXXXXXXXXXXX.downloading) 82 | curl -fsSL -o "$tmpf" "$source" || { rm "$tmpf"; return 1; } 83 | echo -e "\e[34m==> Downloaded $source\e[0m" 84 | mv "$tmpf" "$target" || return $? 85 | echo -e "\e[34m==> Installed to $target\e[0m" 86 | chown $USERNAME "$target" 87 | if [ -n "$filemod" ]; then 88 | chmod "$filemod" "$target" || return $? 89 | fi 90 | } 91 | 92 | echo 93 | 94 | if [ -f /usr/lib/systemd/system/go-openbmclapi.service ]; then 95 | echo -e "\e[33m==> WARN: go-openbmclapi.service is already installed, stopping\e[0m" 96 | systemctl disable --now go-openbmclapi.service 97 | fi 98 | 99 | if [ ! -n "$TARGET_TAG" ]; then 100 | echo -e "\e[34m==> Fetching latest tag for https://github.com/$REPO\e[0m" 101 | fetchGithubLatestTag 102 | TARGET_TAG=$LATEST_TAG 103 | echo 104 | echo -e "\e[32m*** go-openbmclapi LATEST TAG: $TARGET_TAG ***\e[0m" 105 | echo 106 | fi 107 | 108 | fetchBlob installer/service/go-openbmclapi.service /usr/lib/systemd/system/go-openbmclapi.service 0644 || exit $? 109 | 110 | [ -d "$BASE_PATH" ] || { mkdir -p "$BASE_PATH" && chmod 0755 "$BASE_PATH" && chown $USERNAME "$BASE_PATH"; } || exit $? 111 | 112 | # fetchBlob service/start-server.sh "$BASE_PATH/start-server.sh" 0755 || exit $? 113 | # fetchBlob service/stop-server.sh "$BASE_PATH/stop-server.sh" 0755 || exit $? 114 | # fetchBlob service/reload-server.sh "$BASE_PATH/reload-server.sh" 0755 || exit $? 115 | 116 | ARCH=$(uname -m) 117 | case "$ARCH" in 118 | amd64|x86_64) 119 | ARCH="amd64" 120 | ;; 121 | i386|i686) 122 | ARCH="386" 123 | ;; 124 | aarch64|armv8|arm64) 125 | ARCH="arm64" 126 | ;; 127 | armv7l|armv6|armv7) 128 | ARCH="arm" 129 | ;; 130 | *) 131 | echo -e "\e[31m 132 | Unknown CPU architecture: $ARCH 133 | Please report to https://github.com/LiterMC/go-openbmclapi/issues/new\e[0m" 134 | exit 1 135 | esac 136 | 137 | source="${MIRROR_PREFIX}https://github.com/$REPO/releases/download/$TARGET_TAG/go-openbmclapi-linux-$ARCH" 138 | echo -e "\e[34m==> Downloading $source\e[0m" 139 | 140 | curl -fL -o "$BASE_PATH/service-linux-go-openbmclapi" "$source" && \ 141 | chmod 0755 "$BASE_PATH/service-linux-go-openbmclapi" && \ 142 | chown $USERNAME "$BASE_PATH/service-linux-go-openbmclapi" || \ 143 | exit 1 144 | 145 | [ -f $BASE_PATH/config.yaml ] || fetchBlob config.yaml $BASE_PATH/config.yaml 0600 || exit $? 146 | 147 | echo -e "\e[34m==> Enabling go-openbmclapi.service\e[0m" 148 | systemctl enable go-openbmclapi.service || exit $? 149 | 150 | echo -e " 151 | ================================ Install successed ================================ 152 | 153 | Use 'systemctl start go-openbmclapi.service' to start openbmclapi server 154 | Use 'systemctl stop go-openbmclapi.service' to stop openbmclapi server 155 | Use 'systemctl reload go-openbmclapi.service' to reload openbmclapi server configs 156 | Use 'journalctl -f --output cat -u go-openbmclapi.service' to watch the openbmclapi logs 157 | " 158 | -------------------------------------------------------------------------------- /installer/windows/CustomAction/CustomAction.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Text; 4 | using Microsoft.Deployment.WindowsInstaller; 5 | 6 | namespace CustomAction { 7 | public class CustomAction { 8 | [CustomAction] 9 | public static ActionResult SetupCluster(Session session) { 10 | session.Log("Begin SetupCluster"); 11 | 12 | CustomActionData data = session.CustomActionData; 13 | 14 | session.Message(InstallMessage.Warning, 15 | new Record(new string[]{ 16 | string.Format("Data {0}", session.CustomActionData) 17 | })); 18 | 19 | return ActionResult.Success; 20 | } 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /installer/windows/CustomAction/CustomAction.csproj.gotmpl: -------------------------------------------------------------------------------- 1 |  2 | 3 | net8.0 4 | enable 5 | enable 6 | 7 | 8 | 9 | true 10 | CustomAction 11 | {{.WixDir}} 12 | $(WixToolPath)\sdk 13 | $(WixSdkPath)\wix.ca.targets 14 | wixtasks.dll 15 | 16 | 17 | 18 | 19 | $(WixSdkPath)\Microsoft.Deployment.WindowsInstaller.dll 20 | True 21 | 22 | 23 | 24 | -------------------------------------------------------------------------------- /installer/windows/Product.Var.wxi: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | -------------------------------------------------------------------------------- /installer/windows/ProductLoc.en.wxl: -------------------------------------------------------------------------------- 1 | 2 | 3 | 1033 4 | 5 | A newer version of [ProductName] is already installed. 6 | {\WixUI_Font_Title}Setup cluster 7 | 8 | Please input or copy-paste your cluster ID and secret. 9 | If you don't have one, please contact @bangbang93 10 | 11 | -------------------------------------------------------------------------------- /installer/windows/ProductLoc.zh.wxl: -------------------------------------------------------------------------------- 1 | 2 | 3 | 2052 4 | 5 | 更新版本的 [ProductName] 已经安装了 6 | {\WixUI_Font_Title}初始化节点 7 | 8 | 请输入或复制粘贴您的 Cluster ID 和 Cluster Secret 9 | 如果您没有节点 ID, 请联系 @bangbang93 获取 10 | 11 | -------------------------------------------------------------------------------- /internal/build/version.go: -------------------------------------------------------------------------------- 1 | /** 2 | * OpenBmclAPI (Golang Edition) 3 | * Copyright (C) 2023 Kevin Z 4 | * All rights reserved 5 | * 6 | * This program is free software: you can redistribute it and/or modify 7 | * it under the terms of the GNU Affero General Public License as published 8 | * by the Free Software Foundation, either version 3 of the License, or 9 | * (at your option) any later version. 10 | * 11 | * This program is distributed in the hope that it will be useful, 12 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 13 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 14 | * GNU Affero General Public License for more details. 15 | * 16 | * You should have received a copy of the GNU Affero General Public License 17 | * along with this program. If not, see . 18 | */ 19 | 20 | package build 21 | 22 | import ( 23 | "fmt" 24 | ) 25 | 26 | const ClusterVersion = "1.10.9" 27 | 28 | var BuildVersion string = "dev" 29 | 30 | var ClusterUserAgent string = fmt.Sprintf("openbmclapi-cluster/%s", ClusterVersion) 31 | var ClusterUserAgentFull string = fmt.Sprintf("%s go-openbmclapi-cluster/%s", ClusterUserAgent, BuildVersion) 32 | -------------------------------------------------------------------------------- /internal/gosrc/LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2009 The Go Authors. All rights reserved. 2 | 3 | Redistribution and use in source and binary forms, with or without 4 | modification, are permitted provided that the following conditions are 5 | met: 6 | 7 | * Redistributions of source code must retain the above copyright 8 | notice, this list of conditions and the following disclaimer. 9 | * Redistributions in binary form must reproduce the above 10 | copyright notice, this list of conditions and the following disclaimer 11 | in the documentation and/or other materials provided with the 12 | distribution. 13 | * Neither the name of Google Inc. nor the names of its 14 | contributors may be used to endorse or promote products derived from 15 | this software without specific prior written permission. 16 | 17 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 18 | "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 19 | LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR 20 | A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT 21 | OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, 22 | SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT 23 | LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, 24 | DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY 25 | THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 26 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 27 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. -------------------------------------------------------------------------------- /internal/gosrc/httprange.go: -------------------------------------------------------------------------------- 1 | // Copyright 2009 The Go Authors. All rights reserved. 2 | // Use of this source code is governed by a BSD-style 3 | // license that can be found in the LICENSE file. 4 | 5 | // gosrc exports some useful code from golang source 6 | // Original codes are at https://github.com/golang/go/ 7 | package gosrc 8 | 9 | import ( 10 | "errors" 11 | "net/textproto" 12 | "strconv" 13 | "strings" 14 | ) 15 | 16 | // HttpRange specifies the byte range to be sent to the client. 17 | type HttpRange struct { 18 | Start, Length int64 19 | } 20 | 21 | var ErrNoOverlap = errors.New("invalid range: failed to overlap") 22 | 23 | // ParseRange parses a Range header string as per RFC 7233. 24 | // ErrNoOverlap is returned if none of the ranges overlap. 25 | func ParseRange(s string, size int64) ([]HttpRange, error) { 26 | if s == "" { 27 | return nil, nil // header not present 28 | } 29 | const b = "bytes=" 30 | if !strings.HasPrefix(s, b) { 31 | return nil, errors.New("invalid range") 32 | } 33 | var ranges []HttpRange 34 | noOverlap := false 35 | for _, ra := range strings.Split(s[len(b):], ",") { 36 | ra = textproto.TrimString(ra) 37 | if ra == "" { 38 | continue 39 | } 40 | start, end, ok := strings.Cut(ra, "-") 41 | if !ok { 42 | return nil, errors.New("invalid range") 43 | } 44 | start, end = textproto.TrimString(start), textproto.TrimString(end) 45 | var r HttpRange 46 | if start == "" { 47 | // If no start is specified, end specifies the 48 | // range start relative to the end of the file, 49 | // and we are dealing with 50 | // which has to be a non-negative integer as per 51 | // RFC 7233 Section 2.1 "Byte-Ranges". 52 | if end == "" || end[0] == '-' { 53 | return nil, errors.New("invalid range") 54 | } 55 | i, err := strconv.ParseInt(end, 10, 64) 56 | if i < 0 || err != nil { 57 | return nil, errors.New("invalid range") 58 | } 59 | if i > size { 60 | i = size 61 | } 62 | r.Start = size - i 63 | r.Length = size - r.Start 64 | } else { 65 | i, err := strconv.ParseInt(start, 10, 64) 66 | if err != nil || i < 0 { 67 | return nil, errors.New("invalid range") 68 | } 69 | if i >= size { 70 | // If the range begins after the size of the content, 71 | // then it does not overlap. 72 | noOverlap = true 73 | continue 74 | } 75 | r.Start = i 76 | if end == "" { 77 | // If no end is specified, range extends to end of the file. 78 | r.Length = size - r.Start 79 | } else { 80 | i, err := strconv.ParseInt(end, 10, 64) 81 | if err != nil || r.Start > i { 82 | return nil, errors.New("invalid range") 83 | } 84 | if i >= size { 85 | i = size - 1 86 | } 87 | r.Length = i - r.Start + 1 88 | } 89 | } 90 | ranges = append(ranges, r) 91 | } 92 | if noOverlap && len(ranges) == 0 { 93 | // The specified ranges did not overlap with the content. 94 | return nil, ErrNoOverlap 95 | } 96 | return ranges, nil 97 | } 98 | -------------------------------------------------------------------------------- /lang/en/init.go: -------------------------------------------------------------------------------- 1 | /** 2 | * OpenBmclAPI (Golang Edition) 3 | * Copyright (C) 2024 Kevin Z 4 | * All rights reserved 5 | * 6 | * This program is free software: you can redistribute it and/or modify 7 | * it under the terms of the GNU Affero General Public License as published 8 | * by the Free Software Foundation, either version 3 of the License, or 9 | * (at your option) any later version. 10 | * 11 | * This program is distributed in the hope that it will be useful, 12 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 13 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 14 | * GNU Affero General Public License for more details. 15 | * 16 | * You should have received a copy of the GNU Affero General Public License 17 | * along with this program. If not, see . 18 | */ 19 | 20 | package en 21 | 22 | import ( 23 | "github.com/LiterMC/go-openbmclapi/lang" 24 | ) 25 | 26 | func init() { 27 | lang.RegisterLanguage("en-us", areaUS) 28 | } 29 | -------------------------------------------------------------------------------- /lang/lang.go: -------------------------------------------------------------------------------- 1 | /** 2 | * OpenBmclAPI (Golang Edition) 3 | * Copyright (C) 2024 Kevin Z 4 | * All rights reserved 5 | * 6 | * This program is free software: you can redistribute it and/or modify 7 | * it under the terms of the GNU Affero General Public License as published 8 | * by the Free Software Foundation, either version 3 of the License, or 9 | * (at your option) any later version. 10 | * 11 | * This program is distributed in the hope that it will be useful, 12 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 13 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 14 | * GNU Affero General Public License for more details. 15 | * 16 | * You should have received a copy of the GNU Affero General Public License 17 | * along with this program. If not, see . 18 | */ 19 | 20 | package lang 21 | 22 | import ( 23 | "os" 24 | "strings" 25 | ) 26 | 27 | type Language struct { 28 | Lang string 29 | Area string 30 | tr map[string]string 31 | } 32 | 33 | func (l *Language) Code() string { 34 | if l == nil { 35 | return "" 36 | } 37 | return l.Lang + "-" + strings.ToUpper(l.Area) 38 | } 39 | 40 | func (l *Language) Tr(name string) string { 41 | if s, ok := l.tr[name]; ok { 42 | return s 43 | } 44 | return "[[" + name + "]]" 45 | } 46 | 47 | var languages = make(map[string][]*Language) 48 | var currentLang *Language = nil 49 | 50 | func RegisterLanguage(code string, tr map[string]string) { 51 | code = strings.ToLower(code) 52 | if len(code) != 5 || (code[2] != '-' && code[2] != '_') { 53 | panic(code + " is not a language code") 54 | } 55 | lang, area := code[:2], code[3:] 56 | langs := languages[lang] 57 | for _, l := range langs { 58 | if l.Area == area { 59 | panic(code + " is already exists") 60 | } 61 | } 62 | l := &Language{ 63 | Lang: lang, 64 | Area: area, 65 | tr: tr, 66 | } 67 | languages[lang] = append(langs, l) 68 | } 69 | 70 | func GetLang() *Language { 71 | return currentLang 72 | } 73 | 74 | func Tr(name string) string { 75 | return currentLang.Tr(name) 76 | } 77 | 78 | func SetLang(code string) { 79 | code = strings.ToLower(code) 80 | var lang, area string 81 | if len(code) == 2 { 82 | lang = code 83 | } else if len(code) == 5 && (code[2] == '-' || code[2] == '_') { 84 | lang, area = code[:2], code[3:] 85 | } 86 | if lang == "" { 87 | return 88 | } 89 | langs := languages[lang] 90 | if len(langs) > 0 { 91 | currentLang = langs[0] 92 | } 93 | if area == "" { 94 | return 95 | } 96 | for _, l := range langs { 97 | if l.Area == area { 98 | currentLang = l 99 | } 100 | } 101 | } 102 | 103 | func ParseSystemLanguage() { 104 | lang, _, _ := strings.Cut(os.Getenv("LANG"), ".") 105 | if lang != "" { 106 | SetLang(lang) 107 | } 108 | // TODO: syscall.LoadDLL("kernel32") for windows 109 | } 110 | -------------------------------------------------------------------------------- /lang/zh/cn.go: -------------------------------------------------------------------------------- 1 | package zh 2 | 3 | var areaCN = map[string]string{ 4 | "program.starting": "正在启动 Go-OpenMCIM v%s (%s)", 5 | "error.set.cluster.id": "启动前请在 config.yaml 内设置 cluster-id 和 cluster-secret !", 6 | "error.init.failed": "无法初始化节点: %v", 7 | 8 | "program.exited": "节点正在退出, 代码 %d", 9 | "error.exit.please.read.faq": "请在提交问题前阅读 https://github.com/WetemCloud/go-openmcim?tab=readme-ov-file#faq", 10 | "warn.exit.detected.windows.open.browser": "检测到您是新手 Windows 用户. 我们正在帮助您打开浏览器 ...", 11 | 12 | "info.filelist.fetching": "获取文件列表中", 13 | "error.filelist.fetch.failed": "文件列表获取失败: %v", 14 | 15 | "error.address.listen.failed": "无法监听地址 %s: %v", 16 | 17 | "info.cert.requesting": "请求证书中, 请稍候 ...", 18 | "info.cert.requested": "证书请求完毕, 域名为 %s", 19 | "error.cert.not.set": "配置文件内没有提供证书", 20 | "error.cert.parse.failed": "无法解析证书密钥对[%d]: %v", 21 | "error.cert.request.failed": "证书请求失败: %v", 22 | "error.cert.requested.parse.failed": "无法解析已请求的证书: %v", 23 | 24 | "info.server.public.at": "服务器已在 https://%s (%s) 开放, 使用了 %d 个证书", 25 | "info.server.alternative.hosts": "备用域名:", 26 | "info.wait.first.sync": "正在等待第一次同步 ...", 27 | "info.cluster.enable.sending": "正在发送启用数据包", 28 | "info.cluster.enabled": "节点已启用", 29 | "error.cluster.enable.failed": "无法启用节点: %v", 30 | "error.cluster.disconnected": "节点从主控断开. exit.", 31 | "info.cluster.reconnect.keepalive": "保活失败, 重连中 ...", 32 | "info.cluster.reconnecting": "重连中 ...", 33 | "error.cluster.reconnect.failed": "无法连接到主控. exit.", 34 | "info.cluster.connect.prepare": "准备连接主控中 (%d/%d)", 35 | "error.cluster.connect.failed": "无法连接到主控 (%d/%d): %v", 36 | "error.cluster.connect.failed.toomuch": "节点重连次数过多. exit.", 37 | "error.cluster.auth.failed": "无法获取登录令牌: %v; exit.", 38 | 39 | "error.cluster.stat.save.failed": "Error when saving status: %v", 40 | "error.cluster.keepalive.send.failed": "无法发送保活数据包: %v", 41 | "error.cluster.keepalive.failed": "保活失败: %v", 42 | "info.cluster.keepalive.success": "保活成功: hits=%d bytes=%s; %v", 43 | 44 | "warn.server.closing": "关闭服务中 ...", 45 | "warn.server.closed": "服务器已关闭.", 46 | "info.cluster.disabling": "禁用节点中 ...", 47 | "error.cluster.disable.failed": "节点禁用失败: %v", 48 | "warn.cluster.disabled": "节点已禁用", 49 | "warn.httpserver.closing": "正在关闭 HTTP 服务器 ...", 50 | 51 | "info.check.start": "开始在 %s 检测文件. 强检查 = %v", 52 | "info.check.done": "文件在 %s 检查完毕, 缺失 %d 个文件", 53 | "error.check.failed": "无法检查 %s: %v", 54 | "hint.check.checking": "> 检查中 ", 55 | "warn.check.modified.size": "找到修改过的文件: %q 的大小为 %d, 预期 %d", 56 | "warn.check.modified.hash": "找到修改过的文件: %q 的哈希值为 %s, 预期 %s", 57 | "error.check.unknown.hash.method": "未知的哈希格式 %q", 58 | "error.check.open.failed": "无法打开 %q: %v", 59 | "error.check.hash.failed": "无法为 %s 计算哈希值: %v", 60 | 61 | "info.sync.prepare": "准备同步中, 文件列表长度为 %d ...", 62 | "hint.sync.start": "开始同步, 总计: %d, 字节: %s", 63 | "hint.sync.done": "文件同步完成, 用时: %v, %s/s", 64 | "error.sync.failed": "文件同步失败: %v", 65 | "info.sync.none": "所有文件已同步", 66 | "warn.sync.interrupted": "同步已中断", 67 | "info.sync.config": "同步配置: %#v", 68 | "error.sync.part.working": "仅有 %d / %d 个存储工作正常! 同步将在1分钟内开始", 69 | "hint.sync.total": "总计: ", 70 | "hint.sync.downloading": "> 下载中 ", 71 | "hint.sync.downloading.handler": "Downloading %s from handler", 72 | "info.sync.downloaded": "已下载 %s [%s] %.2f%%", 73 | "error.sync.download.failed": "下载失败 %s:\n\t%s", 74 | "error.sync.download.failed.retry": "下载失败 %s, %v 后重新尝试:\n\t%s", 75 | "error.sync.create.failed": "无法创建 %s/%s: %v", 76 | 77 | "info.gc.start": "正在清理 %s", 78 | "info.gc.done": "已清理 %s", 79 | "warn.gc.interrupted": "垃圾收集器在 %s 中断", 80 | "info.gc.found": "找到过期文件 %s", 81 | "error.gc.error": "垃圾收集错误: %v", 82 | 83 | "error.config.read.failed": "无法读取配置文件: %v", 84 | "error.config.encode.failed": "无法编码配置文件: %v", 85 | "error.config.write.failed": "无法写入配置文件: %v", 86 | "error.config.not.exists": "未找到配置文件, 正在创建", 87 | "error.config.created": "配置文件已创建, 请修改, 并重启程序", 88 | "error.config.parse.failed": "无法解析配置文件: %v", 89 | "error.config.alias.user.not.exists": "WebDav 别名用户 %q 不存在", 90 | 91 | "info.tunnel.running": "正在开始打洞, 执行 %q", 92 | "info.tunnel.detected": "检测到隧道已创建: host=%s, port=%d", 93 | "error.tunnel.failed": "打洞失败: %v", 94 | "error.tunnel.command.prepare.failed": "打洞指令准备失败: %v", 95 | 96 | "info.update.checking": "正在检测 Go-OpenMCIM 最新发布 ...", 97 | "error.update.check.failed": "更新检测失败: %v", 98 | "info.update.detected": "已检测到新版 Go-OpenMCIM: tag=%s, current=%s", 99 | "info.update.changelog": "更新日志 %s -> %s:\n%s", 100 | } 101 | -------------------------------------------------------------------------------- /lang/zh/init.go: -------------------------------------------------------------------------------- 1 | /** 2 | * OpenBmclAPI (Golang Edition) 3 | * Copyright (C) 2024 Kevin Z 4 | * All rights reserved 5 | * 6 | * This program is free software: you can redistribute it and/or modify 7 | * it under the terms of the GNU Affero General Public License as published 8 | * by the Free Software Foundation, either version 3 of the License, or 9 | * (at your option) any later version. 10 | * 11 | * This program is distributed in the hope that it will be useful, 12 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 13 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 14 | * GNU Affero General Public License for more details. 15 | * 16 | * You should have received a copy of the GNU Affero General Public License 17 | * along with this program. If not, see . 18 | */ 19 | 20 | package zh 21 | 22 | import ( 23 | "github.com/LiterMC/go-openbmclapi/lang" 24 | ) 25 | 26 | func init() { 27 | lang.RegisterLanguage("zh-cn", areaCN) 28 | } 29 | -------------------------------------------------------------------------------- /licsense.go: -------------------------------------------------------------------------------- 1 | /** 2 | * OpenBmclAPI (Golang Edition) 3 | * Copyright (C) 2023 Kevin Z 4 | * All rights reserved 5 | * 6 | * This program is free software: you can redistribute it and/or modify 7 | * it under the terms of the GNU Affero General Public License as published 8 | * by the Free Software Foundation, either version 3 of the License, or 9 | * (at your option) any later version. 10 | * 11 | * This program is distributed in the hope that it will be useful, 12 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 13 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 14 | * GNU Affero General Public License for more details. 15 | * 16 | * You should have received a copy of the GNU Affero General Public License 17 | * along with this program. If not, see . 18 | */ 19 | 20 | package main 21 | 22 | import ( 23 | _ "embed" 24 | "fmt" 25 | ) 26 | 27 | const cliHint = ` 28 | Go-OpenMCIM based on Go-OpenBMCLAPI, edited by WetemCloud 29 | Go-OpenBmclAPI Copyright (C) 2023 Kevin Z 30 | 31 | This program comes with ABSOLUTELY NO WARRANTY; 32 | This is free software, and you are welcome to redistribute it under certain conditions; 33 | Use subcommand 'license' for more information 34 | 35 | ` 36 | 37 | func printShortLicense() { 38 | fmt.Print(cliHint) 39 | } 40 | 41 | //go:embed LICENSE 42 | var fullLicense string 43 | 44 | func printLongLicense() { 45 | fmt.Println(fullLicense) 46 | } 47 | -------------------------------------------------------------------------------- /limited/limited_conn_test.go: -------------------------------------------------------------------------------- 1 | /** 2 | * OpenBmclAPI (Golang Edition) 3 | * Copyright (C) 2023 Kevin Z 4 | * All rights reserved 5 | * 6 | * This program is free software: you can redistribute it and/or modify 7 | * it under the terms of the GNU Affero General Public License as published 8 | * by the Free Software Foundation, either version 3 of the License, or 9 | * (at your option) any later version. 10 | * 11 | * This program is distributed in the hope that it will be useful, 12 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 13 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 14 | * GNU Affero General Public License for more details. 15 | * 16 | * You should have received a copy of the GNU Affero General Public License 17 | * along with this program. If not, see . 18 | */ 19 | 20 | package limited 21 | 22 | import ( 23 | "testing" 24 | 25 | "io" 26 | "net" 27 | "sync" 28 | ) 29 | 30 | type pipeListener struct { 31 | incoming chan net.Conn 32 | closeOnce sync.Once 33 | closed chan struct{} 34 | } 35 | 36 | var _ net.Listener = (*pipeListener)(nil) 37 | 38 | func newPipeListener() *pipeListener { 39 | return &pipeListener{ 40 | incoming: make(chan net.Conn, 0), 41 | closed: make(chan struct{}, 0), 42 | } 43 | } 44 | 45 | func (l *pipeListener) Dial() (net.Conn, error) { 46 | remote, conn := net.Pipe() 47 | select { 48 | case l.incoming <- remote: 49 | return conn, nil 50 | case <-l.closed: 51 | return nil, net.ErrClosed 52 | } 53 | } 54 | 55 | func (l *pipeListener) Close() error { 56 | l.closeOnce.Do(func() { close(l.closed) }) 57 | return nil 58 | } 59 | 60 | type pipeListenerAddr struct{} 61 | 62 | func (pipeListenerAddr) Network() string { return "imp" } 63 | func (pipeListenerAddr) String() string { return "" } 64 | 65 | func (l *pipeListener) Addr() net.Addr { 66 | return pipeListenerAddr{} 67 | } 68 | 69 | func (l *pipeListener) Accept() (net.Conn, error) { 70 | select { 71 | case conn := <-l.incoming: 72 | return conn, nil 73 | case <-l.closed: 74 | return nil, net.ErrClosed 75 | } 76 | } 77 | 78 | func TestLimitedListener(t *testing.T) { 79 | if true { 80 | return 81 | } 82 | 83 | imp := newPipeListener() 84 | l := NewLimitedListener(imp, 2, 0, 16) 85 | defer l.Close() 86 | 87 | l.SetMinWriteRate(6) 88 | 89 | const connCount = 5 90 | 91 | var wg sync.WaitGroup 92 | 93 | wg.Add(1) 94 | go func() { 95 | defer wg.Done() 96 | for i := 0; i < connCount; i++ { 97 | conn, _ := imp.Dial() 98 | wg.Add(1) 99 | go func(i int, conn net.Conn) { 100 | defer wg.Done() 101 | defer conn.Close() 102 | t.Logf("Dialed: %d", i) 103 | var buf [256]byte 104 | for { 105 | n, err := conn.Read(buf[:]) 106 | if err != nil { 107 | if err != io.EOF { 108 | t.Errorf("Read error: <%d> %v", i, err) 109 | } 110 | return 111 | } 112 | t.Logf("Read: <%d> %d %v", i, n, buf[:n]) 113 | } 114 | }(i, conn) 115 | } 116 | }() 117 | 118 | for i := 0; i < connCount; i++ { 119 | conn, err := l.Accept() 120 | if err != nil { 121 | t.Errorf("Error when accepting: %v", err) 122 | break 123 | } 124 | t.Logf("Accepted: %d", i) 125 | wg.Add(1) 126 | go func(i int, conn net.Conn) { 127 | defer wg.Done() 128 | defer conn.Close() 129 | buf := make([]byte, 35) 130 | buf[0] = 1 131 | buf[15] = 2 132 | buf[17] = 3 133 | buf[34] = 4 134 | if i == 1 { 135 | buf = buf[12:24] 136 | } else if i == 4 { 137 | a, b := buf[0:14], buf[14:] 138 | n, err := conn.Write(a) 139 | t.Logf("Wrote: <%d> %v %v", i, n, err) 140 | n, err = conn.Write(b) 141 | t.Logf("Wrote: <%d> %v %v", i, n, err) 142 | return 143 | } 144 | n, err := conn.Write(buf) 145 | t.Logf("Wrote: <%d> %v %v", i, n, err) 146 | }(i, conn) 147 | } 148 | 149 | wg.Wait() 150 | } 151 | -------------------------------------------------------------------------------- /limited/util.go: -------------------------------------------------------------------------------- 1 | /** 2 | * OpenBmclAPI (Golang Edition) 3 | * Copyright (C) 2024 Kevin Z 4 | * All rights reserved 5 | * 6 | * This program is free software: you can redistribute it and/or modify 7 | * it under the terms of the GNU Affero General Public License as published 8 | * by the Free Software Foundation, either version 3 of the License, or 9 | * (at your option) any later version. 10 | * 11 | * This program is distributed in the hope that it will be useful, 12 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 13 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 14 | * GNU Affero General Public License for more details. 15 | * 16 | * You should have received a copy of the GNU Affero General Public License 17 | * along with this program. If not, see . 18 | */ 19 | 20 | package limited 21 | 22 | import ( 23 | "context" 24 | "io" 25 | "sync" 26 | "sync/atomic" 27 | ) 28 | 29 | type slotInfo struct { 30 | id int 31 | buf []byte 32 | } 33 | 34 | type BufSlots struct { 35 | c chan slotInfo 36 | } 37 | 38 | func NewBufSlots(size int) *BufSlots { 39 | c := make(chan slotInfo, size) 40 | for i := 0; i < size; i++ { 41 | c <- slotInfo{ 42 | id: i, 43 | buf: make([]byte, 1024*512), 44 | } 45 | } 46 | return &BufSlots{ 47 | c: c, 48 | } 49 | } 50 | 51 | func (s *BufSlots) Len() int { 52 | return len(s.c) 53 | } 54 | 55 | func (s *BufSlots) Cap() int { 56 | return cap(s.c) 57 | } 58 | 59 | func (s *BufSlots) Alloc(ctx context.Context) (slotId int, buf []byte, free func()) { 60 | select { 61 | case slot := <-s.c: 62 | return slot.id, slot.buf, sync.OnceFunc(func() { 63 | s.c <- slot 64 | }) 65 | case <-ctx.Done(): 66 | return 0, nil, nil 67 | } 68 | } 69 | 70 | type Semaphore struct { 71 | c chan struct{} 72 | } 73 | 74 | // NewSemaphore create a semaphore 75 | // zero or negative size means infinity space 76 | func NewSemaphore(size int) *Semaphore { 77 | if size <= 0 { 78 | return nil 79 | } 80 | return &Semaphore{ 81 | c: make(chan struct{}, size), 82 | } 83 | } 84 | 85 | func (s *Semaphore) Len() int { 86 | if s == nil { 87 | return 0 88 | } 89 | return len(s.c) 90 | } 91 | 92 | func (s *Semaphore) Cap() int { 93 | if s == nil { 94 | return 0 95 | } 96 | return cap(s.c) 97 | } 98 | 99 | func (s *Semaphore) Acquire() { 100 | if s == nil { 101 | return 102 | } 103 | s.c <- struct{}{} 104 | } 105 | 106 | func (s *Semaphore) AcquireWithContext(ctx context.Context) bool { 107 | if s == nil { 108 | return true 109 | } 110 | return s.AcquireWithNotify(ctx.Done()) 111 | } 112 | 113 | func (s *Semaphore) AcquireWithNotify(notifier <-chan struct{}) bool { 114 | if s == nil { 115 | return true 116 | } 117 | select { 118 | case s.c <- struct{}{}: 119 | return true 120 | case <-notifier: 121 | return false 122 | } 123 | } 124 | 125 | func (s *Semaphore) Release() { 126 | if s == nil { 127 | return 128 | } 129 | <-s.c 130 | } 131 | 132 | func (s *Semaphore) Wait() { 133 | if s == nil { 134 | panic("Cannot wait on nil Semaphore") 135 | } 136 | for i := s.Cap(); i > 0; i-- { 137 | s.Acquire() 138 | } 139 | } 140 | 141 | func (s *Semaphore) WaitWithContext(ctx context.Context) bool { 142 | if s == nil { 143 | panic("Cannot wait on nil Semaphore") 144 | } 145 | for i := s.Cap(); i > 0; i-- { 146 | if !s.AcquireWithContext(ctx) { 147 | return false 148 | } 149 | } 150 | return true 151 | } 152 | 153 | type spProxyReader struct { 154 | io.Reader 155 | released atomic.Bool 156 | s *Semaphore 157 | } 158 | 159 | func (r *spProxyReader) Close() error { 160 | if !r.released.Swap(true) { 161 | r.s.Release() 162 | } 163 | if c, ok := r.Reader.(io.Closer); ok { 164 | return c.Close() 165 | } 166 | return nil 167 | } 168 | 169 | func (s *Semaphore) ProxyReader(r io.Reader) io.ReadCloser { 170 | return &spProxyReader{ 171 | Reader: r, 172 | s: s, 173 | } 174 | } 175 | -------------------------------------------------------------------------------- /log/std.go: -------------------------------------------------------------------------------- 1 | /** 2 | * OpenBmclAPI (Golang Edition) 3 | * Copyright (C) 2024 Kevin Z 4 | * All rights reserved 5 | * 6 | * This program is free software: you can redistribute it and/or modify 7 | * it under the terms of the GNU Affero General Public License as published 8 | * by the Free Software Foundation, either version 3 of the License, or 9 | * (at your option) any later version. 10 | * 11 | * This program is distributed in the hope that it will be useful, 12 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 13 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 14 | * GNU Affero General Public License for more details. 15 | * 16 | * You should have received a copy of the GNU Affero General Public License 17 | * along with this program. If not, see . 18 | */ 19 | 20 | package log 21 | 22 | import ( 23 | "bytes" 24 | "log" 25 | ) 26 | 27 | type stdLogProxyWriter struct { 28 | buf []byte 29 | filters []func(line []byte) bool 30 | } 31 | 32 | var defaultStdLogProxyWriter = new(stdLogProxyWriter) 33 | 34 | var ProxiedStdLog = log.New(defaultStdLogProxyWriter, "", 0) 35 | 36 | // AddStdLogFilter add a filter before print std log 37 | // If the filter returns true, the log will not be print 38 | // It's designed to ignore http tls error 39 | func AddStdLogFilter(filter func(line []byte) bool) { 40 | defaultStdLogProxyWriter.filters = append(defaultStdLogProxyWriter.filters, filter) 41 | } 42 | 43 | func (w *stdLogProxyWriter) log(buf []byte) { 44 | for _, f := range w.filters { 45 | if f(buf) { 46 | return 47 | } 48 | } 49 | Infof("[std]: %s", buf) 50 | } 51 | 52 | func (w *stdLogProxyWriter) Write(buf []byte) (n int, _ error) { 53 | n = len(buf) // always success 54 | if n == 0 { 55 | return 56 | } 57 | 58 | last := bytes.LastIndexByte(buf, '\n') 59 | if last < 0 { 60 | w.buf = append(w.buf, buf...) 61 | return 62 | } 63 | var splited [2][]byte 64 | lines := splitByteIgnoreLast(buf, '\n', splited[:0]) 65 | w.buf = append(w.buf, lines[0]...) 66 | w.log(w.buf) 67 | for i := 1; i < len(lines); i++ { 68 | w.log(lines[i]) 69 | } 70 | 71 | w.buf = append(w.buf[:0], buf[last+1:]...) 72 | return 73 | } 74 | 75 | func splitByteIgnoreLast(buf []byte, sep byte, splited [][]byte) [][]byte { 76 | for i := 0; i < len(buf); i++ { 77 | if buf[i] == sep { 78 | splited = append(splited, buf[:i]) 79 | buf = buf[i+1:] 80 | i = 0 81 | } 82 | } 83 | return splited 84 | } 85 | -------------------------------------------------------------------------------- /log/std_test.go: -------------------------------------------------------------------------------- 1 | /** 2 | * OpenBmclAPI (Golang Edition) 3 | * Copyright (C) 2024 Kevin Z 4 | * All rights reserved 5 | * 6 | * This program is free software: you can redistribute it and/or modify 7 | * it under the terms of the GNU Affero General Public License as published 8 | * by the Free Software Foundation, either version 3 of the License, or 9 | * (at your option) any later version. 10 | * 11 | * This program is distributed in the hope that it will be useful, 12 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 13 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 14 | * GNU Affero General Public License for more details. 15 | * 16 | * You should have received a copy of the GNU Affero General Public License 17 | * along with this program. If not, see . 18 | */ 19 | 20 | package log_test 21 | 22 | import ( 23 | "testing" 24 | 25 | "bytes" 26 | 27 | "github.com/LiterMC/go-openbmclapi/log" 28 | ) 29 | 30 | func TestProxiedStdLog(t *testing.T) { 31 | log.AddStdLogFilter(func(line []byte) bool { 32 | return bytes.HasPrefix(line, ([]byte)("http: TLS handshake error")) 33 | }) 34 | log.ProxiedStdLog.Printf("some messages log with printf") 35 | log.ProxiedStdLog.Println("some messages\n that cross multiple\n lines") 36 | log.ProxiedStdLog.Printf("http: TLS handshake error from %s", "127.0.0.1") 37 | log.ProxiedStdLog.Printf("the next message") 38 | log.ProxiedStdLog.Printf("http: crosslines log\nhttp: TLS handshake error from %s", "127.0.0.1") 39 | } 40 | -------------------------------------------------------------------------------- /notify/email/templates/daily-report.gohtml: -------------------------------------------------------------------------------- 1 | {{ define "daily-report" }} 2 |

Go-OpenBMCLAPI Cluster Daily Report

3 | 4 | *** TODO *** 5 | 6 |

7 | Stats: 8 |

 9 | 		
10 | 			{{ tojson .Stats }}
11 | 		
12 | 	
13 |

14 | 15 | {{ end }} 16 | -------------------------------------------------------------------------------- /notify/email/templates/disabled.gohtml: -------------------------------------------------------------------------------- 1 | {{ define "disabled" }} 2 |

Go-OpenBMCLAPI Cluster Disabled

3 |

4 | At: {{ .At }} 5 |

6 | {{ end }} 7 | -------------------------------------------------------------------------------- /notify/email/templates/enabled.gohtml: -------------------------------------------------------------------------------- 1 | {{ define "enabled" }} 2 |

Go-OpenBMCLAPI Cluster Enabled

3 |

4 | At: {{ .At }} 5 |

6 | {{ end }} 7 | -------------------------------------------------------------------------------- /notify/email/templates/syncbegin.gohtml: -------------------------------------------------------------------------------- 1 | {{ define "syncbegin" }} 2 |

Go-OpenBMCLAPI Cluster File Synchronize Begin

3 |

4 | At: {{ .At }} 5 |

6 |

7 | Count: {{ .Count }} 8 |

9 |

10 | Size: {{ .Size }} Bytes 11 |

12 | {{ end }} 13 | -------------------------------------------------------------------------------- /notify/email/templates/syncdone.gohtml: -------------------------------------------------------------------------------- 1 | {{ define "syncdone" }} 2 |

Go-OpenBMCLAPI Cluster Sync Finished

3 |

4 | At: {{ .At }} 5 |

6 | {{ end }} 7 | -------------------------------------------------------------------------------- /notify/email/templates/updates.gohtml: -------------------------------------------------------------------------------- 1 | {{ define "updates" }} 2 |

Go-OpenBMCLAPI Cluster {{ .Release.Tag }} Avaliable!

3 | 4 |

5 | View full release at {{ .Release.HtmlURL }} 6 |

7 | 8 |

Change Log

9 |
10 | {{ noescape .Release.Body }} 11 |
12 | 13 | {{ end }} 14 | -------------------------------------------------------------------------------- /notify/event.go: -------------------------------------------------------------------------------- 1 | /** 2 | * OpenBmclAPI (Golang Edition) 3 | * Copyright (C) 2024 Kevin Z 4 | * All rights reserved 5 | * 6 | * This program is free software: you can redistribute it and/or modify 7 | * it under the terms of the GNU Affero General Public License as published 8 | * by the Free Software Foundation, either version 3 of the License, or 9 | * (at your option) any later version. 10 | * 11 | * This program is distributed in the hope that it will be useful, 12 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 13 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 14 | * GNU Affero General Public License for more details. 15 | * 16 | * You should have received a copy of the GNU Affero General Public License 17 | * along with this program. If not, see . 18 | */ 19 | 20 | package notify 21 | 22 | import ( 23 | "time" 24 | 25 | "github.com/LiterMC/go-openbmclapi/update" 26 | ) 27 | 28 | type TimestampEvent struct { 29 | At time.Time 30 | } 31 | 32 | type ( 33 | EnabledEvent TimestampEvent 34 | 35 | DisabledEvent TimestampEvent 36 | 37 | SyncBeginEvent struct { 38 | TimestampEvent 39 | Count int 40 | Size int64 41 | } 42 | 43 | SyncDoneEvent TimestampEvent 44 | 45 | UpdateAvaliableEvent struct { 46 | Release *update.GithubRelease 47 | } 48 | 49 | ReportStatusEvent struct { 50 | TimestampEvent 51 | Stats *StatData 52 | } 53 | ) 54 | -------------------------------------------------------------------------------- /private/.gitignore: -------------------------------------------------------------------------------- 1 | 2 | * 3 | !/.gitignore 4 | -------------------------------------------------------------------------------- /robots.txt: -------------------------------------------------------------------------------- 1 | 2 | User-Agent: * 3 | 4 | Disallow: /api/ 5 | Disallow: /dashboard/login 6 | Disallow: /dashboard/settings/ 7 | Disallow: /download/ 8 | Disallow: /measure/ 9 | -------------------------------------------------------------------------------- /scripts/build.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | # OpenBmclAPI (Golang Edition) 3 | # Copyright (C) 2024 Kevin Z 4 | # All rights reserved 5 | # 6 | # This program is free software: you can redistribute it and/or modify 7 | # it under the terms of the GNU Affero General Public License as published 8 | # by the Free Software Foundation, either version 3 of the License, or 9 | # (at your option) any later version. 10 | # 11 | # This program is distributed in the hope that it will be useful, 12 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 13 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 14 | # GNU Affero General Public License for more details. 15 | # 16 | # You should have received a copy of the GNU Affero General Public License 17 | # along with this program. If not, see . 18 | 19 | available_platforms=( 20 | darwin/amd64 darwin/arm64 21 | linux/386 linux/amd64 linux/arm linux/arm64 22 | # windows/386 windows/arm 23 | # windows/amd64 windows/arm64 # windows builds are at build-windows.bat 24 | ) 25 | 26 | outputdir=output 27 | 28 | mkdir -p "$outputdir" 29 | 30 | [ -n "$TAG" ] || TAG=$(git describe --tags --match v[0-9]* --abbrev=0 --candidates=0 2>/dev/null || git log -1 --format="dev-%H") 31 | 32 | echo "Detected tag: $TAG" 33 | 34 | ldflags="-X 'github.com/LiterMC/go-openbmclapi/internal/build.BuildVersion=$TAG'" 35 | 36 | export CGO_ENABLED=0 37 | 38 | for p in "${available_platforms[@]}"; do 39 | os=${p%/*} 40 | arch=${p#*/} 41 | target="${outputdir}/go-openbmclapi-${os}-${arch}" 42 | if [[ "$os" == "windows" ]]; then 43 | target="${target}.exe" 44 | fi 45 | echo "Building $target ..." 46 | GOOS=$os GOARCH=$arch go build -o "$target" -ldflags "$ldflags" "$@" . || exit $? 47 | done 48 | -------------------------------------------------------------------------------- /scripts/decrypt-log.go: -------------------------------------------------------------------------------- 1 | // OpenBmclAPI (Golang Edition) 2 | // Copyright (C) 2024 Kevin Z 3 | // All rights reserved 4 | // 5 | // This program is free software: you can redistribute it and/or modify 6 | // it under the terms of the GNU Affero General Public License as published 7 | // by the Free Software Foundation, either version 3 of the License, or 8 | // (at your option) any later version. 9 | // 10 | // This program is distributed in the hope that it will be useful, 11 | // but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | // GNU Affero General Public License for more details. 14 | // 15 | // You should have received a copy of the GNU Affero General Public License 16 | // along with this program. If not, see . 17 | 18 | //go:build ignored 19 | 20 | package main 21 | 22 | import ( 23 | "compress/gzip" 24 | "crypto/aes" 25 | "crypto/cipher" 26 | "crypto/rsa" 27 | "crypto/x509" 28 | "encoding/pem" 29 | "errors" 30 | "flag" 31 | "fmt" 32 | "io" 33 | "os" 34 | "strings" 35 | "sync" 36 | ) 37 | 38 | func ParseRSAPrivateKey(content []byte) (key *rsa.PrivateKey, err error) { 39 | for { 40 | var blk *pem.Block 41 | blk, content = pem.Decode(content) 42 | if blk == nil { 43 | break 44 | } 45 | if blk.Type == "RSA PRIVATE KEY" { 46 | k, err := x509.ParsePKCS8PrivateKey(blk.Bytes) 47 | if err != nil { 48 | return nil, err 49 | } 50 | return k.(*rsa.PrivateKey), nil 51 | } 52 | } 53 | return nil, errors.New(`Cannot find "RSA PRIVATE KEY" in pem blocks`) 54 | } 55 | 56 | func DecryptStream(w io.Writer, r io.Reader, key *rsa.PrivateKey) (err error) { 57 | encryptedAESKey := make([]byte, key.Size()) 58 | if _, err = io.ReadFull(r, encryptedAESKey); err != nil { 59 | return 60 | } 61 | aesKey, err := rsa.DecryptPKCS1v15(nil, key, encryptedAESKey[:]) 62 | if err != nil { 63 | return 64 | } 65 | blk, err := aes.NewCipher(aesKey) 66 | if err != nil { 67 | return 68 | } 69 | iv := make([]byte, blk.BlockSize()) 70 | if _, err = io.ReadFull(r, iv); err != nil { 71 | return 72 | } 73 | sr := &cipher.StreamReader{ 74 | S: cipher.NewCTR(blk, iv), 75 | R: r, 76 | } 77 | buf := make([]byte, blk.BlockSize()*256) 78 | if _, err = io.CopyBuffer(w, sr, buf); err != nil { 79 | return 80 | } 81 | return 82 | } 83 | 84 | func main() { 85 | flag.Parse() 86 | keyFile, logFile := flag.Arg(0), flag.Arg(1) 87 | enableGzip := strings.Contains(logFile, ".gz.") 88 | keyContent, err := os.ReadFile(keyFile) 89 | if err != nil { 90 | fmt.Println("Cannot read", keyFile, ":", err) 91 | os.Exit(1) 92 | } 93 | key, err := ParseRSAPrivateKey(keyContent) 94 | if err != nil { 95 | fmt.Println("Cannot parse key from", keyFile, ":", err) 96 | os.Exit(1) 97 | } 98 | fd, err := os.Open(logFile) 99 | if err != nil { 100 | fmt.Println("Cannot open", logFile, ":", err) 101 | os.Exit(1) 102 | } 103 | defer fd.Close() 104 | dst, err := createNewFile(logFile) 105 | if err != nil { 106 | fmt.Println("Cannot create new file:", err) 107 | os.Exit(1) 108 | } 109 | dstName := dst.Name() 110 | defer dst.Close() 111 | var w io.WriteCloser = dst 112 | var wg sync.WaitGroup 113 | if enableGzip { 114 | pr, pw := io.Pipe() 115 | wg.Add(1) 116 | go func(w io.WriteCloser) { 117 | defer wg.Done() 118 | defer w.Close() 119 | gr, err := gzip.NewReader(pr) 120 | if err != nil { 121 | dst.Close() 122 | os.Remove(dstName) 123 | fmt.Println("Cannot decompress data:", err) 124 | os.Exit(2) 125 | } 126 | defer gr.Close() 127 | if _, err = io.Copy(w, gr); err != nil { 128 | dst.Close() 129 | os.Remove(dstName) 130 | fmt.Println("Cannot copy decompressed data:", err) 131 | os.Exit(2) 132 | } 133 | }(w) 134 | w = pw 135 | } 136 | if err := DecryptStream(w, fd, key); err != nil { 137 | dst.Close() 138 | os.Remove(dstName) 139 | fmt.Println("Decrypt error:", err) 140 | os.Exit(2) 141 | } 142 | w.Close() 143 | wg.Wait() 144 | } 145 | 146 | func createNewFile(name string) (fd *os.File, err error) { 147 | basename, _, _ := strings.Cut(name, ".") 148 | fd, err = os.OpenFile(basename+".log", os.O_WRONLY|os.O_CREATE|os.O_EXCL, 0600) 149 | if err == nil { 150 | return 151 | } 152 | if !errors.Is(err, os.ErrExist) { 153 | return 154 | } 155 | for i := 2; i < 100; i++ { 156 | fd, err = os.OpenFile(fmt.Sprintf("%s.%d.log", basename, i), os.O_WRONLY|os.O_CREATE|os.O_EXCL, 0600) 157 | if err == nil { 158 | return 159 | } 160 | if !errors.Is(err, os.ErrExist) { 161 | return 162 | } 163 | } 164 | return 165 | } 166 | -------------------------------------------------------------------------------- /scripts/docker-run.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | # OpenBmclAPI (Golang Edition) 3 | # Copyright (C) 2024 Kevin Z 4 | # All rights reserved 5 | # 6 | # This program is free software: you can redistribute it and/or modify 7 | # it under the terms of the GNU Affero General Public License as published 8 | # by the Free Software Foundation, either version 3 of the License, or 9 | # (at your option) any later version. 10 | # 11 | # This program is distributed in the hope that it will be useful, 12 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 13 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 14 | # GNU Affero General Public License for more details. 15 | # 16 | # You should have received a copy of the GNU Affero General Public License 17 | # along with this program. If not, see . 18 | 19 | docker pull craftmine/go-openbmclapi:latest || { 20 | echo "[ERROR] Failed to pull docker image 'craftmine/go-openbmclapi:latest'" 21 | if ! docker images craftmine/go-openbmclapi | grep latest; then 22 | echo "Cannot find docker image 'craftmine/go-openbmclapi:latest'" 23 | exit 1 24 | fi 25 | } 26 | 27 | docker run -d --name my-go-openbmclapi \ 28 | -e CLUSTER_ID=${CLUSTER_ID} \ 29 | -e CLUSTER_SECRET=${CLUSTER_SECRET} \ 30 | -e CLUSTER_PUBLIC_PORT=${CLUSTER_PUBLIC_PORT} \ 31 | -e CLUSTER_IP=${CLUSTER_IP} \ 32 | -v "${PWD}/cache":/opt/openbmclapi/cache \ 33 | -v "${PWD}/data":/opt/openbmclapi/data \ 34 | -v "${PWD}/logs":/opt/openbmclapi/logs \ 35 | -v "${PWD}/config.yaml":/opt/openbmclapi/config.yaml \ 36 | -p ${CLUSTER_PORT}:4000 \ 37 | craftmine/go-openbmclapi:latest 38 | -------------------------------------------------------------------------------- /scripts/dockerbuild.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # OpenBmclAPI (Golang Edition) 3 | # Copyright (C) 2024 Kevin Z 4 | # All rights reserved 5 | # 6 | # This program is free software: you can redistribute it and/or modify 7 | # it under the terms of the GNU Affero General Public License as published 8 | # by the Free Software Foundation, either version 3 of the License, or 9 | # (at your option) any later version. 10 | # 11 | # This program is distributed in the hope that it will be useful, 12 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 13 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 14 | # GNU Affero General Public License for more details. 15 | # 16 | # You should have received a copy of the GNU Affero General Public License 17 | # along with this program. If not, see . 18 | 19 | PUBLIC_PREFIX=craftmine/go-openbmclapi 20 | BUILD_PLATFORMS=(linux/arm64 linux/amd64) # 21 | 22 | [ -n "$TAG" ] || TAG=$(git describe --tags --match v[0-9]* --abbrev=0 2>/dev/null || git log -1 --format="dev-%H") 23 | 24 | function build(){ 25 | platform=$1 26 | fulltag="${PUBLIC_PREFIX}:${TAG}" 27 | echo 28 | echo "==> Building $fulltag" 29 | echo 30 | DOCKER_BUILDKIT=1 docker build --platform ${platform} \ 31 | --tag "$fulltag" \ 32 | --file "Dockerfile" \ 33 | --build-arg "TAG=$TAG" \ 34 | . || return $? 35 | echo 36 | docker tag "$fulltag" "${PUBLIC_PREFIX}:latest" 37 | echo "==> Pushing ${fulltag} ${PUBLIC_PREFIX}:latest" 38 | echo 39 | (docker push "$fulltag" && docker push "${PUBLIC_PREFIX}:latest") || return $? 40 | return 0 41 | } 42 | 43 | for platform in "${BUILD_PLATFORMS[@]}"; do 44 | build $platform || exit $? 45 | done 46 | -------------------------------------------------------------------------------- /service/go-openbmclapi.service: -------------------------------------------------------------------------------- 1 | # OpenBmclAPI (Golang Edition) 2 | # Copyright (C) 2024 Kevin Z 3 | # All rights reserved 4 | # 5 | # This program is free software: you can redistribute it and/or modify 6 | # it under the terms of the GNU Affero General Public License as published 7 | # by the Free Software Foundation, either version 3 of the License, or 8 | # (at your option) any later version. 9 | # 10 | # This program is distributed in the hope that it will be useful, 11 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | # GNU Affero General Public License for more details. 14 | # 15 | # You should have received a copy of the GNU Affero General Public License 16 | # along with this program. If not, see . 17 | 18 | [Unit] 19 | Description=Go-OpenBmclAPI Service 20 | Documentation=https://github.com/LiterMC/go-openbmclapi 21 | Wants=basic.target 22 | After=network.target 23 | 24 | [Service] 25 | Type=idle 26 | User=openbmclapi 27 | WorkingDirectory=/opt/openbmclapi 28 | ExecStart=/opt/openbmclapi/service-linux-go-openbmclapi 29 | ExecReload=/bin/kill -s HUP $MAINPID 30 | RestartSec=30 31 | Restart=on-failure 32 | TimeoutSec=30 33 | 34 | [Install] 35 | WantedBy=multi-user.target 36 | -------------------------------------------------------------------------------- /service/installer.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # OpenBmclAPI (Golang Edition) 3 | # Copyright (C) 2024 Kevin Z 4 | # All rights reserved 5 | # 6 | # This program is free software: you can redistribute it and/or modify 7 | # it under the terms of the GNU Affero General Public License as published 8 | # by the Free Software Foundation, either version 3 of the License, or 9 | # (at your option) any later version. 10 | # 11 | # This program is distributed in the hope that it will be useful, 12 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 13 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 14 | # GNU Affero General Public License for more details. 15 | # 16 | # You should have received a copy of the GNU Affero General Public License 17 | # along with this program. If not, see . 18 | 19 | # if [ $(id -u) -ne 0 ]; then 20 | # echo -e "\e[31mERROR: Not root user\e[0m" 21 | # exit 1 22 | # fi 23 | 24 | ARGS=() 25 | 26 | while [ $# -gt 0 ]; do 27 | case "$1" in 28 | -m|--mirror) 29 | shift 30 | MIRROR_PREFIX=$1 31 | ;; 32 | -t|--tag) 33 | shift 34 | TARGET_TAG=$1 35 | ;; 36 | -*) 37 | echo -e "\e[31mERROR: Unknown option $1\e[0m" 38 | exit 1 39 | ;; 40 | *) 41 | ARGS+=("$1") 42 | ;; 43 | esac 44 | shift 45 | done 46 | 47 | if [ ${#ARGS[@]} -ge 1 ]; then 48 | TARGET_TAG="${ARGS[0]}" 49 | fi 50 | 51 | if [ -n "$MIRROR_PREFIX" ] && [[ "$MIRROR_PREFIX" != */ ]]; then 52 | MIRROR_PREFIX="${MIRROR_PREFIX}/" 53 | fi 54 | 55 | echo "MIRROR_PREFIX=${MIRROR_PREFIX}" 56 | echo "TARGET_TAG=${TARGET_TAG}" 57 | 58 | REPO='LiterMC/go-openbmclapi' 59 | RAW_PREFIX="${MIRROR_PREFIX}https://raw.githubusercontent.com" 60 | RAW_REPO="$RAW_PREFIX/$REPO" 61 | BASE_PATH=/opt/openbmclapi 62 | USERNAME=openbmclapi 63 | 64 | if ! systemd --version >/dev/null 2>&1 ; then 65 | echo -e "\e[31mERROR: Failed to test systemd\e[0m" 66 | exit 1 67 | fi 68 | 69 | if [ ! -d /usr/lib/systemd/system/ ]; then 70 | echo -e "\e[31mERROR: /usr/lib/systemd/system/ is not exist\e[0m" 71 | exit 1 72 | fi 73 | 74 | if ! id $USERNAME >/dev/null 2>&1; then 75 | echo -e "\e[34m==> Creating user $USERNAME\e[0m" 76 | useradd $USERNAME || { 77 | echo -e "\e[31mERROR: Could not create user $USERNAME\e[0m" 78 | exit 1 79 | } 80 | fi 81 | 82 | 83 | function fetchGithubLatestTag(){ 84 | prefix="location: https://github.com/$REPO/releases/tag/" 85 | location=$(curl -fsSI "https://github.com/$REPO/releases/latest" | grep "$prefix" | tr -d "\r") 86 | [ $? = 0 ] || return 1 87 | export LATEST_TAG="${location#${prefix}}" 88 | } 89 | 90 | function fetchBlob(){ 91 | file=$1 92 | target=$2 93 | filemod=$3 94 | 95 | source="$RAW_REPO/$TARGET_TAG/$file" 96 | echo -e "\e[34m==> Downloading $source\e[0m" 97 | tmpf=$(mktemp -t go-openbmclapi.XXXXXXXXXXXX.downloading) 98 | curl -fsSL -o "$tmpf" "$source" || { rm "$tmpf"; return 1; } 99 | echo -e "\e[34m==> Downloaded $source\e[0m" 100 | mv "$tmpf" "$target" || return $? 101 | echo -e "\e[34m==> Installed to $target\e[0m" 102 | chown $USERNAME "$target" 103 | if [ -n "$filemod" ]; then 104 | chmod "$filemod" "$target" || return $? 105 | fi 106 | } 107 | 108 | echo 109 | 110 | if [ -f /usr/lib/systemd/system/go-openbmclapi.service ]; then 111 | echo -e "\e[33m==> WARN: go-openbmclapi.service is already installed, stopping\e[0m" 112 | systemctl disable --now go-openbmclapi.service 113 | fi 114 | 115 | if [ ! -n "$TARGET_TAG" ]; then 116 | echo -e "\e[34m==> Fetching latest tag for https://github.com/$REPO\e[0m" 117 | fetchGithubLatestTag 118 | TARGET_TAG=$LATEST_TAG 119 | echo 120 | echo -e "\e[32m*** go-openbmclapi LATEST TAG: $TARGET_TAG ***\e[0m" 121 | echo 122 | fi 123 | 124 | fetchBlob installer/service/go-openbmclapi.service /usr/lib/systemd/system/go-openbmclapi.service 0644 || exit $? 125 | 126 | [ -d "$BASE_PATH" ] || { mkdir -p "$BASE_PATH" && chmod 0755 "$BASE_PATH" && chown $USERNAME "$BASE_PATH"; } || exit $? 127 | 128 | # fetchBlob service/start-server.sh "$BASE_PATH/start-server.sh" 0755 || exit $? 129 | # fetchBlob service/stop-server.sh "$BASE_PATH/stop-server.sh" 0755 || exit $? 130 | # fetchBlob service/reload-server.sh "$BASE_PATH/reload-server.sh" 0755 || exit $? 131 | 132 | ARCH=$(uname -m) 133 | case "$ARCH" in 134 | amd64|x86_64) 135 | ARCH="amd64" 136 | ;; 137 | i386|i686) 138 | ARCH="386" 139 | ;; 140 | aarch64|armv8|arm64) 141 | ARCH="arm64" 142 | ;; 143 | armv7l|armv6|armv7) 144 | ARCH="arm" 145 | ;; 146 | *) 147 | echo -e "\e[31m 148 | Unknown CPU architecture: $ARCH 149 | Please report to https://github.com/LiterMC/go-openbmclapi/issues/new\e[0m" 150 | exit 1 151 | esac 152 | 153 | source="${MIRROR_PREFIX}https://github.com/$REPO/releases/download/$TARGET_TAG/go-openbmclapi-linux-$ARCH" 154 | echo -e "\e[34m==> Downloading $source\e[0m" 155 | 156 | curl -fL -o "$BASE_PATH/service-linux-go-openbmclapi" "$source" && \ 157 | chmod 0755 "$BASE_PATH/service-linux-go-openbmclapi" && \ 158 | chown $USERNAME "$BASE_PATH/service-linux-go-openbmclapi" || \ 159 | exit 1 160 | 161 | [ -f $BASE_PATH/config.yaml ] || fetchBlob config.yaml $BASE_PATH/config.yaml 0600 || exit $? 162 | 163 | echo -e "\e[34m==> Enabling go-openbmclapi.service\e[0m" 164 | systemctl enable go-openbmclapi.service || exit $? 165 | 166 | echo -e " 167 | ================================ Install successed ================================ 168 | 169 | Use 'systemctl start go-openbmclapi.service' to start openbmclapi server 170 | Use 'systemctl stop go-openbmclapi.service' to stop openbmclapi server 171 | Use 'systemctl reload go-openbmclapi.service' to reload openbmclapi server configs 172 | Use 'journalctl -f --output cat -u go-openbmclapi.service' to watch the openbmclapi logs 173 | " 174 | -------------------------------------------------------------------------------- /storage/compressor.go: -------------------------------------------------------------------------------- 1 | /** 2 | * OpenBmclAPI (Golang Edition) 3 | * Copyright (C) 2024 Kevin Z 4 | * All rights reserved 5 | * 6 | * This program is free software: you can redistribute it and/or modify 7 | * it under the terms of the GNU Affero General Public License as published 8 | * by the Free Software Foundation, either version 3 of the License, or 9 | * (at your option) any later version. 10 | * 11 | * This program is distributed in the hope that it will be useful, 12 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 13 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 14 | * GNU Affero General Public License for more details. 15 | * 16 | * You should have received a copy of the GNU Affero General Public License 17 | * along with this program. If not, see . 18 | */ 19 | 20 | package storage 21 | 22 | import ( 23 | "compress/gzip" 24 | "compress/zlib" 25 | "io" 26 | ) 27 | 28 | type Compressor string 29 | 30 | const ( 31 | NullCompressor Compressor = "" 32 | ZlibCompressor Compressor = "zlib" 33 | GzipCompressor Compressor = "gzip" 34 | ) 35 | 36 | func (c Compressor) Ext() string { 37 | switch c { 38 | case NullCompressor: 39 | return "" 40 | case ZlibCompressor: 41 | return ".zz" 42 | case GzipCompressor: 43 | return ".gz" 44 | default: 45 | panic("Unknown compressor: " + c) 46 | } 47 | } 48 | 49 | // Decompress the reader 50 | func (c Compressor) WrapReader(r io.Reader) (io.Reader, error) { 51 | switch c { 52 | case NullCompressor: 53 | return r, nil 54 | case ZlibCompressor: 55 | return zlib.NewReader(r) 56 | case GzipCompressor: 57 | return gzip.NewReader(r) 58 | default: 59 | panic("Unknown compressor: " + c) 60 | } 61 | } 62 | 63 | type nopWriteCloser struct { 64 | io.Writer 65 | } 66 | 67 | func (nopWriteCloser) Close() error { 68 | return nil 69 | } 70 | 71 | // Compress the writer 72 | func (c Compressor) WrapWriter(w io.Writer) io.WriteCloser { 73 | switch c { 74 | case NullCompressor: 75 | return nopWriteCloser{w} 76 | case ZlibCompressor: 77 | return zlib.NewWriter(w) 78 | case GzipCompressor: 79 | return gzip.NewWriter(w) 80 | default: 81 | panic("Unknown compressor: " + c) 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /storage/storage.go: -------------------------------------------------------------------------------- 1 | /** 2 | * OpenBmclAPI (Golang Edition) 3 | * Copyright (C) 2024 Kevin Z 4 | * All rights reserved 5 | * 6 | * This program is free software: you can redistribute it and/or modify 7 | * it under the terms of the GNU Affero General Public License as published 8 | * by the Free Software Foundation, either version 3 of the License, or 9 | * (at your option) any later version. 10 | * 11 | * This program is distributed in the hope that it will be useful, 12 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 13 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 14 | * GNU Affero General Public License for more details. 15 | * 16 | * You should have received a copy of the GNU Affero General Public License 17 | * along with this program. If not, see . 18 | */ 19 | 20 | package storage 21 | 22 | import ( 23 | "context" 24 | "fmt" 25 | "io" 26 | "net/http" 27 | "sort" 28 | "strings" 29 | 30 | "gopkg.in/yaml.v3" 31 | 32 | "github.com/LiterMC/go-openbmclapi/utils" 33 | ) 34 | 35 | type Storage interface { 36 | fmt.Stringer 37 | 38 | // Options should return the pointer of the storage options 39 | // which should be able to marshal/unmarshal with yaml format 40 | Options() any 41 | // SetOptions will be called with the same type of the Options() result 42 | SetOptions(any) 43 | // Init will be called before start to use a storage 44 | Init(context.Context) error 45 | CheckUpload(context.Context) error 46 | 47 | Size(hash string) (int64, error) 48 | Open(hash string) (io.ReadCloser, error) 49 | Create(hash string, r io.ReadSeeker) error 50 | Remove(hash string) error 51 | WalkDir(func(hash string, size int64) error) error 52 | 53 | ServeDownload(rw http.ResponseWriter, req *http.Request, hash string, size int64) (int64, error) 54 | ServeMeasure(rw http.ResponseWriter, req *http.Request, size int) error 55 | } 56 | 57 | const ( 58 | StorageLocal = "local" 59 | StorageMount = "mount" 60 | StorageWebdav = "webdav" 61 | ) 62 | 63 | type StorageFactory struct { 64 | New func() Storage 65 | NewConfig func() any 66 | } 67 | 68 | var storageFactories = make(map[string]StorageFactory, 3) 69 | 70 | func RegisterStorageFactory(typ string, inst StorageFactory) { 71 | if inst.New == nil || inst.NewConfig == nil { 72 | panic("nil function") 73 | } 74 | if _, ok := storageFactories[typ]; ok { 75 | panic(fmt.Errorf("Storage %q is already exists", typ)) 76 | } 77 | storageFactories[typ] = inst 78 | } 79 | 80 | func NewStorage(opt StorageOption) Storage { 81 | s := storageFactories[opt.Type].New() 82 | s.SetOptions(opt.Data) 83 | return s 84 | } 85 | 86 | type UnexpectedStorageTypeError struct { 87 | Type string 88 | } 89 | 90 | func (e *UnexpectedStorageTypeError) Error() string { 91 | types := make([]string, 0, len(storageFactories)) 92 | for t, _ := range storageFactories { 93 | types = append(types, t) 94 | } 95 | sort.Strings(types) 96 | return fmt.Sprintf("Unexpected storage type %q, must be one of %s", e.Type, strings.Join(types, ",")) 97 | } 98 | 99 | type BasicStorageOption struct { 100 | Type string `yaml:"type"` 101 | Id string `yaml:"id"` 102 | Weight uint `yaml:"weight"` 103 | } 104 | 105 | type StorageOption struct { 106 | BasicStorageOption `yaml:",inline"` 107 | Data any `yaml:"data"` 108 | } 109 | 110 | func (o *StorageOption) UnmarshalYAML(n *yaml.Node) (err error) { 111 | var opts struct { 112 | BasicStorageOption `yaml:",inline"` 113 | Data utils.RawYAML `yaml:"data"` 114 | } 115 | if err = n.Decode(&opts); err != nil { 116 | return 117 | } 118 | f, ok := storageFactories[opts.Type] 119 | if !ok { 120 | return &UnexpectedStorageTypeError{opts.Type} 121 | } 122 | o.BasicStorageOption = opts.BasicStorageOption 123 | o.Data = f.NewConfig() 124 | if opts.Data.Node == nil { 125 | return nil 126 | } 127 | return opts.Data.Decode(o.Data) 128 | } 129 | -------------------------------------------------------------------------------- /update/checker.go: -------------------------------------------------------------------------------- 1 | /** 2 | * OpenBmclAPI (Golang Edition) 3 | * Copyright (C) 2023 Kevin Z 4 | * All rights reserved 5 | * 6 | * This program is free software: you can redistribute it and/or modify 7 | * it under the terms of the GNU Affero General Public License as published 8 | * by the Free Software Foundation, either version 3 of the License, or 9 | * (at your option) any later version. 10 | * 11 | * This program is distributed in the hope that it will be useful, 12 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 13 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 14 | * GNU Affero General Public License for more details. 15 | * 16 | * You should have received a copy of the GNU Affero General Public License 17 | * along with this program. If not, see . 18 | */ 19 | 20 | package update 21 | 22 | import ( 23 | "bytes" 24 | "context" 25 | "encoding/json" 26 | "fmt" 27 | "net/http" 28 | "strconv" 29 | "time" 30 | 31 | "github.com/LiterMC/go-openbmclapi/internal/build" 32 | "github.com/LiterMC/go-openbmclapi/utils" 33 | ) 34 | 35 | const repoName = "LiterMC/go-openbmclapi" 36 | const lastetReleaseEndPoint = "https://api.github.com/repos/" + repoName + "/releases/latest" 37 | const cdnURL = "https://cdn.crashmc.com/" 38 | 39 | type GithubRelease struct { 40 | Tag ReleaseVersion `json:"tag_name"` 41 | HtmlURL string `json:"html_url"` 42 | Body string `json:"body"` 43 | } 44 | 45 | func Check(cli *http.Client, auth string) (_ *GithubRelease, err error) { 46 | if CurrentBuildTag == nil { 47 | return 48 | } 49 | 50 | ctx, cancel := context.WithTimeout(context.Background(), time.Second*30) 51 | defer cancel() 52 | 53 | req, err := http.NewRequest(http.MethodGet, lastetReleaseEndPoint, nil) 54 | if err != nil { 55 | return 56 | } 57 | if auth != "" { 58 | req.Header.Set("Authorization", auth) 59 | } 60 | var resp *http.Response 61 | { 62 | tctx, cancel := context.WithTimeout(ctx, time.Second*10) 63 | resp, err = cli.Do(req.WithContext(tctx)) 64 | cancel() 65 | } 66 | if err != nil { 67 | if req, err = http.NewRequest(http.MethodGet, cdnURL+lastetReleaseEndPoint, nil); err != nil { 68 | return 69 | } 70 | tctx, cancel := context.WithTimeout(ctx, time.Second*10) 71 | resp, err = cli.Do(req.WithContext(tctx)) 72 | cancel() 73 | if err != nil { 74 | return 75 | } 76 | } 77 | defer resp.Body.Close() 78 | if resp.StatusCode != http.StatusOK { 79 | return nil, utils.NewHTTPStatusErrorFromResponse(resp) 80 | } 81 | release := new(GithubRelease) 82 | if err = json.NewDecoder(resp.Body).Decode(release); err != nil { 83 | return 84 | } 85 | if !CurrentBuildTag.Less(&release.Tag) { 86 | return 87 | } 88 | return release, nil 89 | } 90 | 91 | type ReleaseVersion struct { 92 | Major, Minor, Patch int 93 | Build int 94 | } 95 | 96 | var CurrentBuildTag = func() (v *ReleaseVersion) { 97 | v = new(ReleaseVersion) 98 | if v.UnmarshalText(([]byte)(build.BuildVersion)) != nil { 99 | return nil 100 | } 101 | return 102 | }() 103 | 104 | func (v ReleaseVersion) String() string { 105 | return fmt.Sprintf("v%d.%d.%d-%d", v.Major, v.Minor, v.Patch, v.Build) 106 | } 107 | 108 | func (v *ReleaseVersion) UnmarshalJSON(data []byte) (err error) { 109 | var s string 110 | if err = json.Unmarshal(data, &s); err != nil { 111 | return 112 | } 113 | return v.UnmarshalText(([]byte)(s)) 114 | } 115 | 116 | func (v *ReleaseVersion) UnmarshalText(data []byte) (err error) { 117 | data, _ = bytes.CutPrefix(data, ([]byte)("v")) 118 | data, build, _ := bytes.Cut(data, ([]byte)("-")) 119 | if v.Build, err = strconv.Atoi((string)(build)); err != nil { 120 | return 121 | } 122 | vers := bytes.Split(data, ([]byte)(".")) 123 | if len(vers) != 3 { 124 | return fmt.Errorf("Unexpected release tag format %q", vers) 125 | } 126 | if v.Major, err = strconv.Atoi((string)(vers[0])); err != nil { 127 | return 128 | } 129 | if v.Minor, err = strconv.Atoi((string)(vers[1])); err != nil { 130 | return 131 | } 132 | if v.Patch, err = strconv.Atoi((string)(vers[2])); err != nil { 133 | return 134 | } 135 | return 136 | } 137 | 138 | func (v *ReleaseVersion) Less(w *ReleaseVersion) bool { 139 | return v.Major < w.Major || v.Major == w.Major && (v.Minor < w.Minor || v.Minor == w.Minor && (v.Patch < w.Patch || v.Patch == w.Patch && (v.Build < w.Build))) 140 | } 141 | -------------------------------------------------------------------------------- /util.go: -------------------------------------------------------------------------------- 1 | /** 2 | * OpenBmclAPI (Golang Edition) 3 | * Copyright (C) 2023 Kevin Z 4 | * All rights reserved 5 | * 6 | * This program is free software: you can redistribute it and/or modify 7 | * it under the terms of the GNU Affero General Public License as published 8 | * by the Free Software Foundation, either version 3 of the License, or 9 | * (at your option) any later version. 10 | * 11 | * This program is distributed in the hope that it will be useful, 12 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 13 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 14 | * GNU Affero General Public License for more details. 15 | * 16 | * You should have received a copy of the GNU Affero General Public License 17 | * along with this program. If not, see . 18 | */ 19 | 20 | package main 21 | 22 | import ( 23 | "context" 24 | "crypto" 25 | "crypto/x509" 26 | "fmt" 27 | "io" 28 | "math/rand" 29 | "net/http" 30 | "net/url" 31 | "os" 32 | "slices" 33 | "strings" 34 | "time" 35 | 36 | "github.com/LiterMC/go-openbmclapi/log" 37 | ) 38 | 39 | var closedCh = func() <-chan struct{} { 40 | ch := make(chan struct{}, 0) 41 | close(ch) 42 | return ch 43 | }() 44 | 45 | func createInterval(ctx context.Context, do func(), delay time.Duration) { 46 | ticker := time.NewTicker(delay) 47 | context.AfterFunc(ctx, func() { 48 | ticker.Stop() 49 | }) 50 | go func() { 51 | defer log.RecordPanic() 52 | defer ticker.Stop() 53 | 54 | for range ticker.C { 55 | do() 56 | // If another a tick passed during the job, ignore it 57 | select { 58 | case <-ticker.C: 59 | default: 60 | } 61 | } 62 | }() 63 | return 64 | } 65 | 66 | func getHashMethod(l int) (hashMethod crypto.Hash, err error) { 67 | switch l { 68 | case 32: 69 | hashMethod = crypto.MD5 70 | case 40: 71 | hashMethod = crypto.SHA1 72 | default: 73 | err = fmt.Errorf("Unknown hash length %d", l) 74 | } 75 | return 76 | } 77 | 78 | func parseCertCommonName(body []byte) (string, error) { 79 | cert, err := x509.ParseCertificate(body) 80 | if err != nil { 81 | return "", err 82 | } 83 | return cert.Subject.CommonName, nil 84 | } 85 | 86 | var rd = func() chan int32 { 87 | ch := make(chan int32, 64) 88 | r := rand.New(rand.NewSource(time.Now().Unix())) 89 | go func() { 90 | for { 91 | ch <- r.Int31() 92 | } 93 | }() 94 | return ch 95 | }() 96 | 97 | func randIntn(n int) int { 98 | rn := <-rd 99 | return (int)(rn) % n 100 | } 101 | 102 | func forEachFromRandomIndex(leng int, cb func(i int) (done bool)) (done bool) { 103 | if leng <= 0 { 104 | return false 105 | } 106 | start := randIntn(leng) 107 | for i := start; i < leng; i++ { 108 | if cb(i) { 109 | return true 110 | } 111 | } 112 | for i := 0; i < start; i++ { 113 | if cb(i) { 114 | return true 115 | } 116 | } 117 | return false 118 | } 119 | 120 | func forEachFromRandomIndexWithPossibility(poss []uint, total uint, cb func(i int) (done bool)) (done bool) { 121 | leng := len(poss) 122 | if leng == 0 { 123 | return false 124 | } 125 | if total == 0 { 126 | return forEachFromRandomIndex(leng, cb) 127 | } 128 | n := (uint)(randIntn((int)(total))) 129 | start := 0 130 | for i, p := range poss { 131 | if n < p { 132 | start = i 133 | break 134 | } 135 | n -= p 136 | } 137 | for i := start; i < leng; i++ { 138 | if cb(i) { 139 | return true 140 | } 141 | } 142 | for i := 0; i < start; i++ { 143 | if cb(i) { 144 | return true 145 | } 146 | } 147 | return false 148 | } 149 | 150 | func copyFile(src, dst string, mode os.FileMode) (err error) { 151 | var srcFd, dstFd *os.File 152 | if srcFd, err = os.Open(src); err != nil { 153 | return 154 | } 155 | defer srcFd.Close() 156 | if dstFd, err = os.OpenFile(dst, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, mode); err != nil { 157 | return 158 | } 159 | defer dstFd.Close() 160 | _, err = io.Copy(dstFd, srcFd) 161 | return 162 | } 163 | 164 | type RedirectError struct { 165 | Redirects []*url.URL 166 | Err error 167 | } 168 | 169 | func ErrorFromRedirect(err error, resp *http.Response) *RedirectError { 170 | redirects := make([]*url.URL, 0, 4) 171 | for resp != nil && resp.Request != nil { 172 | redirects = append(redirects, resp.Request.URL) 173 | resp = resp.Request.Response 174 | } 175 | if len(redirects) > 1 { 176 | slices.Reverse(redirects) 177 | } else { 178 | redirects = nil 179 | } 180 | return &RedirectError{ 181 | Redirects: redirects, 182 | Err: err, 183 | } 184 | } 185 | 186 | func (e *RedirectError) Error() string { 187 | if len(e.Redirects) == 0 { 188 | return e.Err.Error() 189 | } 190 | 191 | var b strings.Builder 192 | b.WriteString("Redirect from:\n\t") 193 | for _, r := range e.Redirects { 194 | b.WriteString("- ") 195 | b.WriteString(r.String()) 196 | b.WriteString("\n\t") 197 | } 198 | b.WriteString(e.Err.Error()) 199 | return b.String() 200 | } 201 | 202 | func (e *RedirectError) Unwrap() error { 203 | return e.Err 204 | } 205 | -------------------------------------------------------------------------------- /utils/buf.go: -------------------------------------------------------------------------------- 1 | /** 2 | * OpenBmclAPI (Golang Edition) 3 | * Copyright (C) 2024 Kevin Z 4 | * All rights reserved 5 | * 6 | * This program is free software: you can redistribute it and/or modify 7 | * it under the terms of the GNU Affero General Public License as published 8 | * by the Free Software Foundation, either version 3 of the License, or 9 | * (at your option) any later version. 10 | * 11 | * This program is distributed in the hope that it will be useful, 12 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 13 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 14 | * GNU Affero General Public License for more details. 15 | * 16 | * You should have received a copy of the GNU Affero General Public License 17 | * along with this program. If not, see . 18 | */ 19 | 20 | package utils 21 | 22 | import ( 23 | "sync" 24 | ) 25 | 26 | const MbChunkSize = 1024 * 1024 27 | 28 | var MbChunk [MbChunkSize]byte 29 | 30 | const BUF_SIZE = 1024 * 512 // 512KB 31 | var bufPool = sync.Pool{ 32 | New: func() any { 33 | return new([]byte) 34 | }, 35 | } 36 | 37 | func AllocBuf() ([]byte, func()) { 38 | suggestSize := BUF_SIZE 39 | ptr := bufPool.Get().(*[]byte) 40 | if *ptr == nil { 41 | *ptr = make([]byte, suggestSize) 42 | } 43 | return *ptr, func() { 44 | bufPool.Put(ptr) 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /utils/crypto.go: -------------------------------------------------------------------------------- 1 | /** 2 | * OpenBmclAPI (Golang Edition) 3 | * Copyright (C) 2024 Kevin Z 4 | * All rights reserved 5 | * 6 | * This program is free software: you can redistribute it and/or modify 7 | * it under the terms of the GNU Affero General Public License as published 8 | * by the Free Software Foundation, either version 3 of the License, or 9 | * (at your option) any later version. 10 | * 11 | * This program is distributed in the hope that it will be useful, 12 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 13 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 14 | * GNU Affero General Public License for more details. 15 | * 16 | * You should have received a copy of the GNU Affero General Public License 17 | * along with this program. If not, see . 18 | */ 19 | 20 | package utils 21 | 22 | import ( 23 | "crypto/aes" 24 | "crypto/cipher" 25 | "crypto/hmac" 26 | "crypto/rand" 27 | "crypto/rsa" 28 | "crypto/sha256" 29 | "crypto/subtle" 30 | "crypto/x509" 31 | "encoding/base64" 32 | "encoding/hex" 33 | "encoding/pem" 34 | "errors" 35 | "io" 36 | "os" 37 | "path/filepath" 38 | ) 39 | 40 | func ComparePasswd(p1, p2 string) bool { 41 | a := sha256.Sum256(([]byte)(p1)) 42 | b := sha256.Sum256(([]byte)(p2)) 43 | return subtle.ConstantTimeCompare(a[:], b[:]) == 0 44 | } 45 | 46 | func BytesAsSha256(b []byte) string { 47 | buf := sha256.Sum256(b) 48 | return base64.RawURLEncoding.EncodeToString(buf[:]) 49 | } 50 | 51 | // return a URL encoded base64 string 52 | func AsSha256(s string) string { 53 | buf := sha256.Sum256(([]byte)(s)) 54 | return base64.RawURLEncoding.EncodeToString(buf[:]) 55 | } 56 | 57 | func AsSha256Hex(s string) string { 58 | buf := sha256.Sum256(([]byte)(s)) 59 | return hex.EncodeToString(buf[:]) 60 | } 61 | 62 | func HMACSha256Hex(key, data string) string { 63 | m := hmac.New(sha256.New, ([]byte)(key)) 64 | m.Write(([]byte)(data)) 65 | buf := m.Sum(nil) 66 | return hex.EncodeToString(buf[:]) 67 | } 68 | 69 | func GenRandB64(n int) (s string, err error) { 70 | buf := make([]byte, n) 71 | if _, err = io.ReadFull(rand.Reader, buf); err != nil { 72 | return 73 | } 74 | s = base64.RawURLEncoding.EncodeToString(buf) 75 | return 76 | } 77 | 78 | func LoadOrCreateHmacKey(dataDir string) (key []byte, err error) { 79 | path := filepath.Join(dataDir, "server.hmac.private_key") 80 | buf, err := os.ReadFile(path) 81 | if err != nil { 82 | if !errors.Is(err, os.ErrNotExist) { 83 | return 84 | } 85 | var sbuf string 86 | if sbuf, err = GenRandB64(256); err != nil { 87 | return 88 | } 89 | buf = ([]byte)(sbuf) 90 | if err = os.WriteFile(path, buf, 0600); err != nil { 91 | return 92 | } 93 | } 94 | key = make([]byte, base64.RawURLEncoding.DecodedLen(len(buf))) 95 | if _, err = base64.RawURLEncoding.Decode(key, buf); err != nil { 96 | return 97 | } 98 | return 99 | } 100 | 101 | const developorRSAKey = ` 102 | -----BEGIN RSA PUBLIC KEY----- 103 | MIIBCgKCAQEAqIvK9cVuDtD/V4w7/xIPI2mnv2VV0CTQfelDaEB4vonsblwIp3VV 104 | 1S3oYXY8thyCscBKG/AkryKHS0U1TXoIMPDai3vkLDL5sY4mmh4aFCoGKdRmNNyr 105 | kAjaLo51gnadCMbaoxVNzQ1naJvZjU02ClJvBjtTETUzrqFnx8th04P0bSZHZEwV 106 | vmCniuzzuAcNI92hpoSEqp4WGqINp2hoAFnoHENgnAsb94jJX4VfWbMySR1O+ykz 107 | RkGvslKPhvv/YkCxy0Xi2FgXnb+xw/CXevkTvu7WSVodvZ0OtAHPTp6kCTWt3tun 108 | PN0d0McYg73htD0ItifOcJQWPPnFZKezUQIDAQAB 109 | -----END RSA PUBLIC KEY-----` 110 | 111 | var DeveloporPublicKey *rsa.PublicKey 112 | 113 | func init() { 114 | var err error 115 | DeveloporPublicKey, err = ParseRSAPublicKey(([]byte)(developorRSAKey)) 116 | if err != nil { 117 | panic(err) 118 | } 119 | } 120 | 121 | func ParseRSAPublicKey(content []byte) (key *rsa.PublicKey, err error) { 122 | for { 123 | var blk *pem.Block 124 | blk, content = pem.Decode(content) 125 | if blk == nil { 126 | break 127 | } 128 | if blk.Type == "RSA PUBLIC KEY" { 129 | return x509.ParsePKCS1PublicKey(blk.Bytes) 130 | } 131 | } 132 | return nil, errors.New(`Cannot find "RSA PUBLIC KEY" in pem blocks`) 133 | } 134 | 135 | func EncryptStream(w io.Writer, r io.Reader, publicKey *rsa.PublicKey) (err error) { 136 | var aesKey [16]byte 137 | if _, err = io.ReadFull(rand.Reader, aesKey[:]); err != nil { 138 | return 139 | } 140 | // rsa.EncryptOAEP(sha256.New, rand.Reader, publicKey, aesKey[:], ([]byte)("aes-stream-key")) 141 | encryptedAESKey, err := rsa.EncryptPKCS1v15(rand.Reader, publicKey, aesKey[:]) 142 | if err != nil { 143 | return 144 | } 145 | blk, err := aes.NewCipher(aesKey[:]) 146 | if err != nil { 147 | return 148 | } 149 | iv := make([]byte, blk.BlockSize()) 150 | if _, err = io.ReadFull(rand.Reader, iv); err != nil { 151 | return 152 | } 153 | sw := &cipher.StreamWriter{ 154 | S: cipher.NewCTR(blk, iv), 155 | W: w, 156 | } 157 | if _, err = w.Write(encryptedAESKey); err != nil { 158 | return 159 | } 160 | if _, err = w.Write(iv); err != nil { 161 | return 162 | } 163 | buf := make([]byte, blk.BlockSize()*256) 164 | if _, err = io.CopyBuffer(sw, r, buf); err != nil { 165 | return 166 | } 167 | return 168 | } 169 | -------------------------------------------------------------------------------- /utils/encoding.go: -------------------------------------------------------------------------------- 1 | /** 2 | * OpenBmclAPI (Golang Edition) 3 | * Copyright (C) 2024 Kevin Z 4 | * All rights reserved 5 | * 6 | * This program is free software: you can redistribute it and/or modify 7 | * it under the terms of the GNU Affero General Public License as published 8 | * by the Free Software Foundation, either version 3 of the License, or 9 | * (at your option) any later version. 10 | * 11 | * This program is distributed in the hope that it will be useful, 12 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 13 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 14 | * GNU Affero General Public License for more details. 15 | * 16 | * You should have received a copy of the GNU Affero General Public License 17 | * along with this program. If not, see . 18 | */ 19 | 20 | package utils 21 | 22 | import ( 23 | "time" 24 | 25 | "gopkg.in/yaml.v3" 26 | ) 27 | 28 | var Hex256 = [256]string{ 29 | "00", "01", "02", "03", "04", "05", "06", "07", "08", "09", "0a", "0b", "0c", "0d", "0e", "0f", 30 | "10", "11", "12", "13", "14", "15", "16", "17", "18", "19", "1a", "1b", "1c", "1d", "1e", "1f", 31 | "20", "21", "22", "23", "24", "25", "26", "27", "28", "29", "2a", "2b", "2c", "2d", "2e", "2f", 32 | "30", "31", "32", "33", "34", "35", "36", "37", "38", "39", "3a", "3b", "3c", "3d", "3e", "3f", 33 | "40", "41", "42", "43", "44", "45", "46", "47", "48", "49", "4a", "4b", "4c", "4d", "4e", "4f", 34 | "50", "51", "52", "53", "54", "55", "56", "57", "58", "59", "5a", "5b", "5c", "5d", "5e", "5f", 35 | "60", "61", "62", "63", "64", "65", "66", "67", "68", "69", "6a", "6b", "6c", "6d", "6e", "6f", 36 | "70", "71", "72", "73", "74", "75", "76", "77", "78", "79", "7a", "7b", "7c", "7d", "7e", "7f", 37 | "80", "81", "82", "83", "84", "85", "86", "87", "88", "89", "8a", "8b", "8c", "8d", "8e", "8f", 38 | "90", "91", "92", "93", "94", "95", "96", "97", "98", "99", "9a", "9b", "9c", "9d", "9e", "9f", 39 | "a0", "a1", "a2", "a3", "a4", "a5", "a6", "a7", "a8", "a9", "aa", "ab", "ac", "ad", "ae", "af", 40 | "b0", "b1", "b2", "b3", "b4", "b5", "b6", "b7", "b8", "b9", "ba", "bb", "bc", "bd", "be", "bf", 41 | "c0", "c1", "c2", "c3", "c4", "c5", "c6", "c7", "c8", "c9", "ca", "cb", "cc", "cd", "ce", "cf", 42 | "d0", "d1", "d2", "d3", "d4", "d5", "d6", "d7", "d8", "d9", "da", "db", "dc", "dd", "de", "df", 43 | "e0", "e1", "e2", "e3", "e4", "e5", "e6", "e7", "e8", "e9", "ea", "eb", "ec", "ed", "ee", "ef", 44 | "f0", "f1", "f2", "f3", "f4", "f5", "f6", "f7", "f8", "f9", "fa", "fb", "fc", "fd", "fe", "ff", 45 | } 46 | 47 | const NumToHexMap = "0123456789abcdef" 48 | 49 | var HexToNumMap = [256]int{ 50 | '0': 0x0, '1': 0x1, '2': 0x2, '3': 0x3, '4': 0x4, '5': 0x5, '6': 0x6, '7': 0x7, '8': 0x8, '9': 0x9, 51 | 'a': 0xa, 'b': 0xb, 'c': 0xc, 'd': 0xd, 'e': 0xe, 'f': 0xf, 52 | } 53 | 54 | func IsHex(s string) bool { 55 | if len(s) < 2 || len(s)%2 != 0 { 56 | return false 57 | } 58 | for i := 0; i < len(s); i++ { 59 | if s[i] != '0' && HexToNumMap[s[i]] == 0 { 60 | return false 61 | } 62 | } 63 | return true 64 | } 65 | 66 | func HexTo256(s string) (n int) { 67 | return HexToNumMap[s[0]]*0x10 + HexToNumMap[s[1]] 68 | } 69 | 70 | type RawYAML struct { 71 | *yaml.Node 72 | } 73 | 74 | var ( 75 | _ yaml.Marshaler = RawYAML{} 76 | _ yaml.Unmarshaler = (*RawYAML)(nil) 77 | ) 78 | 79 | func (r RawYAML) MarshalYAML() (any, error) { 80 | return r.Node, nil 81 | } 82 | 83 | func (r *RawYAML) UnmarshalYAML(n *yaml.Node) (err error) { 84 | r.Node = n 85 | return nil 86 | } 87 | 88 | type YAMLDuration time.Duration 89 | 90 | func (d YAMLDuration) Dur() time.Duration { 91 | return (time.Duration)(d) 92 | } 93 | 94 | func (d YAMLDuration) MarshalYAML() (any, error) { 95 | return (time.Duration)(d).String(), nil 96 | } 97 | 98 | func (d *YAMLDuration) UnmarshalYAML(n *yaml.Node) (err error) { 99 | var v string 100 | if err = n.Decode(&v); err != nil { 101 | return 102 | } 103 | var td time.Duration 104 | if td, err = time.ParseDuration(v); err != nil { 105 | return 106 | } 107 | *d = (YAMLDuration)(td) 108 | return nil 109 | } 110 | -------------------------------------------------------------------------------- /utils/error.go: -------------------------------------------------------------------------------- 1 | /** 2 | * OpenBmclAPI (Golang Edition) 3 | * Copyright (C) 2024 Kevin Z 4 | * All rights reserved 5 | * 6 | * This program is free software: you can redistribute it and/or modify 7 | * it under the terms of the GNU Affero General Public License as published 8 | * by the Free Software Foundation, either version 3 of the License, or 9 | * (at your option) any later version. 10 | * 11 | * This program is distributed in the hope that it will be useful, 12 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 13 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 14 | * GNU Affero General Public License for more details. 15 | * 16 | * You should have received a copy of the GNU Affero General Public License 17 | * along with this program. If not, see . 18 | */ 19 | 20 | package utils 21 | 22 | import ( 23 | "fmt" 24 | "net/http" 25 | ) 26 | 27 | type HTTPStatusError struct { 28 | Code int 29 | URL string 30 | Message string 31 | } 32 | 33 | func NewHTTPStatusErrorFromResponse(res *http.Response) (e *HTTPStatusError) { 34 | e = &HTTPStatusError{ 35 | Code: res.StatusCode, 36 | } 37 | if res.Request != nil { 38 | e.URL = res.Request.URL.String() 39 | } 40 | if res.Body != nil { 41 | var buf [512]byte 42 | n, _ := res.Body.Read(buf[:]) 43 | msg := (string)(buf[:n]) 44 | for _, b := range msg { 45 | if b < 0x20 && b != '\r' && b != '\n' && b != '\t' || b == 0x7f { 46 | msg = fmt.Sprintf("", buf[:n]) 47 | break 48 | } 49 | } 50 | e.Message = msg 51 | } 52 | return 53 | } 54 | 55 | func (e *HTTPStatusError) Error() string { 56 | s := fmt.Sprintf("Unexpected http status %d %s", e.Code, http.StatusText(e.Code)) 57 | if e.URL != "" { 58 | s += " for " + e.URL 59 | } 60 | if e.Message != "" { 61 | s += ":\n\t" + e.Message 62 | } 63 | return s 64 | } 65 | -------------------------------------------------------------------------------- /utils/format.go: -------------------------------------------------------------------------------- 1 | /** 2 | * OpenBmclAPI (Golang Edition) 3 | * Copyright (C) 2024 Kevin Z 4 | * All rights reserved 5 | * 6 | * This program is free software: you can redistribute it and/or modify 7 | * it under the terms of the GNU Affero General Public License as published 8 | * by the Free Software Foundation, either version 3 of the License, or 9 | * (at your option) any later version. 10 | * 11 | * This program is distributed in the hope that it will be useful, 12 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 13 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 14 | * GNU Affero General Public License for more details. 15 | * 16 | * You should have received a copy of the GNU Affero General Public License 17 | * along with this program. If not, see . 18 | */ 19 | 20 | package utils 21 | 22 | import ( 23 | "fmt" 24 | "strconv" 25 | "strings" 26 | ) 27 | 28 | func SplitCSV(line string) (values map[string]float32) { 29 | list := strings.Split(line, ",") 30 | values = make(map[string]float32, len(list)) 31 | for _, v := range list { 32 | name, opt, _ := strings.Cut(strings.ToLower(strings.TrimSpace(v)), ";") 33 | var q float64 = 1 34 | if v, ok := strings.CutPrefix(opt, "q="); ok { 35 | q, _ = strconv.ParseFloat(v, 32) 36 | } 37 | values[name] = (float32)(q) 38 | } 39 | return 40 | } 41 | 42 | const byteUnits = "KMGTPE" 43 | 44 | func BytesToUnit(size float64) string { 45 | if size < 1000 { 46 | return fmt.Sprintf("%dB", (int)(size)) 47 | } 48 | var unit rune 49 | for _, u := range byteUnits { 50 | unit = u 51 | size /= 1024 52 | if size < 1000 { 53 | break 54 | } 55 | } 56 | return fmt.Sprintf("%.1f%sB", size, string(unit)) 57 | } 58 | 59 | func ParseCacheControl(str string) (exp int64, ok bool) { 60 | for _, v := range strings.Split(strings.ToLower(str), ",") { 61 | v = strings.TrimSpace(v) 62 | switch v { 63 | case "private": 64 | fallthrough 65 | case "no-store": 66 | return 0, false 67 | case "no-cache": 68 | exp = 0 69 | default: 70 | if maxAge, is := strings.CutPrefix(v, "max-age="); is { 71 | n, err := strconv.ParseInt(maxAge, 10, 64) 72 | if err != nil { 73 | return 74 | } 75 | exp = n 76 | } 77 | } 78 | } 79 | ok = true 80 | return 81 | } 82 | -------------------------------------------------------------------------------- /utils/http.go: -------------------------------------------------------------------------------- 1 | /** 2 | * OpenBmclAPI (Golang Edition) 3 | * Copyright (C) 2024 Kevin Z 4 | * All rights reserved 5 | * 6 | * This program is free software: you can redistribute it and/or modify 7 | * it under the terms of the GNU Affero General Public License as published 8 | * by the Free Software Foundation, either version 3 of the License, or 9 | * (at your option) any later version. 10 | * 11 | * This program is distributed in the hope that it will be useful, 12 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 13 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 14 | * GNU Affero General Public License for more details. 15 | * 16 | * You should have received a copy of the GNU Affero General Public License 17 | * along with this program. If not, see . 18 | */ 19 | 20 | package utils 21 | 22 | import ( 23 | "bufio" 24 | "errors" 25 | "io" 26 | "net" 27 | "net/http" 28 | "path" 29 | "runtime" 30 | 31 | "github.com/LiterMC/go-openbmclapi/log" 32 | ) 33 | 34 | type StatusResponseWriter struct { 35 | http.ResponseWriter 36 | Status int 37 | Wrote int64 38 | beforeWriteHeader []func(status int) 39 | } 40 | 41 | var _ http.Hijacker = (*StatusResponseWriter)(nil) 42 | 43 | func WrapAsStatusResponseWriter(rw http.ResponseWriter) *StatusResponseWriter { 44 | if srw, ok := rw.(*StatusResponseWriter); ok { 45 | return srw 46 | } 47 | return &StatusResponseWriter{ResponseWriter: rw} 48 | } 49 | 50 | func getCaller() (caller runtime.Frame) { 51 | pc := make([]uintptr, 16) 52 | n := runtime.Callers(3, pc) 53 | frames := runtime.CallersFrames(pc[:n]) 54 | frame, more := frames.Next() 55 | _ = more 56 | return frame 57 | } 58 | 59 | func (w *StatusResponseWriter) BeforeWriteHeader(cb func(status int)) { 60 | w.beforeWriteHeader = append(w.beforeWriteHeader, cb) 61 | } 62 | 63 | func (w *StatusResponseWriter) WriteHeader(status int) { 64 | if w.Status == 0 { 65 | for _, cb := range w.beforeWriteHeader { 66 | cb(status) 67 | } 68 | w.Status = status 69 | w.ResponseWriter.WriteHeader(status) 70 | } else { 71 | caller := getCaller() 72 | log.Warnf("http: superfluous response.WriteHeader call with status %d from %s (%s:%d)", 73 | status, caller.Function, path.Base(caller.File), caller.Line) 74 | } 75 | } 76 | 77 | func (w *StatusResponseWriter) Write(buf []byte) (n int, err error) { 78 | n, err = w.ResponseWriter.Write(buf) 79 | w.Wrote += (int64)(n) 80 | return 81 | } 82 | 83 | func (w *StatusResponseWriter) ReadFrom(r io.Reader) (n int64, err error) { 84 | if rf, ok := w.ResponseWriter.(io.ReaderFrom); ok { 85 | n, err = rf.ReadFrom(r) 86 | w.Wrote += n 87 | return 88 | } 89 | n, err = io.Copy(w.ResponseWriter, r) 90 | w.Wrote += n 91 | return 92 | } 93 | 94 | func (w *StatusResponseWriter) Hijack() (net.Conn, *bufio.ReadWriter, error) { 95 | h, ok := w.ResponseWriter.(http.Hijacker) 96 | if ok { 97 | return h.Hijack() 98 | } 99 | return nil, nil, errors.New("ResponseWriter is not http.Hijacker") 100 | } 101 | 102 | type MiddleWare interface { 103 | ServeMiddle(rw http.ResponseWriter, req *http.Request, next http.Handler) 104 | } 105 | 106 | type MiddleWareFunc func(rw http.ResponseWriter, req *http.Request, next http.Handler) 107 | 108 | var _ MiddleWare = (MiddleWareFunc)(nil) 109 | 110 | func (m MiddleWareFunc) ServeMiddle(rw http.ResponseWriter, req *http.Request, next http.Handler) { 111 | m(rw, req, next) 112 | } 113 | 114 | type HttpMiddleWareHandler struct { 115 | final http.Handler 116 | middles []MiddleWare 117 | } 118 | 119 | func NewHttpMiddleWareHandler(final http.Handler) *HttpMiddleWareHandler { 120 | return &HttpMiddleWareHandler{ 121 | final: final, 122 | } 123 | } 124 | 125 | func (m *HttpMiddleWareHandler) ServeHTTP(rw http.ResponseWriter, req *http.Request) { 126 | i := 0 127 | var getNext func() http.Handler 128 | getNext = func() http.Handler { 129 | j := i 130 | if j > len(m.middles) { 131 | // unreachable 132 | panic("HttpMiddleWareHandler: called getNext too much times") 133 | } 134 | i++ 135 | if j == len(m.middles) { 136 | return m.final 137 | } 138 | mid := m.middles[j] 139 | 140 | called := false 141 | return (http.HandlerFunc)(func(rw http.ResponseWriter, req *http.Request) { 142 | if called { 143 | panic("HttpMiddleWareHandler: Called next function twice") 144 | } 145 | called = true 146 | mid.ServeMiddle(rw, req, getNext()) 147 | }) 148 | } 149 | getNext().ServeHTTP(rw, req) 150 | } 151 | 152 | func (m *HttpMiddleWareHandler) Use(mids ...MiddleWare) { 153 | m.middles = append(m.middles, mids...) 154 | } 155 | 156 | func (m *HttpMiddleWareHandler) UseFunc(fns ...MiddleWareFunc) { 157 | for _, fn := range fns { 158 | m.middles = append(m.middles, fn) 159 | } 160 | } 161 | -------------------------------------------------------------------------------- /utils/io.go: -------------------------------------------------------------------------------- 1 | /** 2 | * OpenBmclAPI (Golang Edition) 3 | * Copyright (C) 2024 Kevin Z 4 | * All rights reserved 5 | * 6 | * This program is free software: you can redistribute it and/or modify 7 | * it under the terms of the GNU Affero General Public License as published 8 | * by the Free Software Foundation, either version 3 of the License, or 9 | * (at your option) any later version. 10 | * 11 | * This program is distributed in the hope that it will be useful, 12 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 13 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 14 | * GNU Affero General Public License for more details. 15 | * 16 | * You should have received a copy of the GNU Affero General Public License 17 | * along with this program. If not, see . 18 | */ 19 | 20 | package utils 21 | 22 | import ( 23 | "errors" 24 | "io" 25 | ) 26 | 27 | type CountReader struct { 28 | io.ReadSeeker 29 | N int64 30 | } 31 | 32 | func (r *CountReader) Read(buf []byte) (n int, err error) { 33 | n, err = r.ReadSeeker.Read(buf) 34 | r.N += (int64)(n) 35 | return 36 | } 37 | 38 | type emptyReader struct{} 39 | 40 | var ( 41 | EmptyReader = emptyReader{} 42 | 43 | _ io.ReaderAt = EmptyReader 44 | ) 45 | 46 | func (emptyReader) ReadAt(buf []byte, _ int64) (int, error) { return len(buf), nil } 47 | 48 | type devNull struct{} 49 | 50 | var ( 51 | DevNull = devNull{} 52 | 53 | _ io.ReaderAt = DevNull 54 | _ io.ReadSeeker = DevNull 55 | _ io.Writer = DevNull 56 | ) 57 | 58 | func (devNull) Read([]byte) (int, error) { return 0, io.EOF } 59 | func (devNull) ReadAt([]byte, int64) (int, error) { return 0, io.EOF } 60 | func (devNull) Seek(int64, int) (int64, error) { return 0, nil } 61 | func (devNull) Write(buf []byte) (int, error) { return len(buf), nil } 62 | 63 | type NoLastNewLineWriter struct { 64 | io.Writer 65 | } 66 | 67 | var _ io.Writer = (*NoLastNewLineWriter)(nil) 68 | 69 | func (w *NoLastNewLineWriter) Write(buf []byte) (int, error) { 70 | if l := len(buf) - 1; l >= 0 && buf[l] == '\n' { 71 | buf = buf[:l] 72 | } 73 | return w.Writer.Write(buf) 74 | } 75 | 76 | var ErrNotSeeker = errors.New("r is not an io.Seeker") 77 | 78 | func GetReaderRemainSize(r io.Reader) (n int64, err error) { 79 | // for strings.Reader and bytes.Reader 80 | if l, ok := r.(interface{ Len() int }); ok { 81 | return (int64)(l.Len()), nil 82 | } 83 | if s, ok := r.(io.Seeker); ok { 84 | var cur, end int64 85 | if cur, err = s.Seek(0, io.SeekCurrent); err != nil { 86 | return 87 | } 88 | if end, err = s.Seek(0, io.SeekEnd); err != nil { 89 | return 90 | } 91 | if n = end - cur; n < 0 { 92 | n = 0 93 | } 94 | if _, err = s.Seek(cur, io.SeekStart); err != nil { 95 | return 96 | } 97 | } else { 98 | err = ErrNotSeeker 99 | } 100 | return 101 | } 102 | -------------------------------------------------------------------------------- /utils/ishex_test.go: -------------------------------------------------------------------------------- 1 | /** 2 | * OpenBmclAPI (Golang Edition) 3 | * Copyright (C) 2024 Kevin Z 4 | * All rights reserved 5 | * 6 | * This program is free software: you can redistribute it and/or modify 7 | * it under the terms of the GNU Affero General Public License as published 8 | * by the Free Software Foundation, either version 3 of the License, or 9 | * (at your option) any later version. 10 | * 11 | * This program is distributed in the hope that it will be useful, 12 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 13 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 14 | * GNU Affero General Public License for more details. 15 | * 16 | * You should have received a copy of the GNU Affero General Public License 17 | * along with this program. If not, see . 18 | */ 19 | 20 | package utils_test 21 | 22 | import ( 23 | "testing" 24 | 25 | . "github.com/LiterMC/go-openbmclapi/utils" 26 | ) 27 | 28 | func TestIsHex(t *testing.T) { 29 | var data = []struct { 30 | S string 31 | B bool 32 | }{ 33 | {"", false}, 34 | {"0", false}, 35 | {"00", true}, 36 | {"000", false}, 37 | {"0f", true}, 38 | {"f0", true}, 39 | {"af", true}, 40 | {"fa", true}, 41 | {"ff", true}, 42 | {"aa", true}, 43 | {"11", true}, 44 | {"fg", false}, 45 | {"58fe4669c65dbde8e0d58afb83e21620", true}, 46 | {"1cad5d7f9ed285a04784429ab2a4615d5c59ea88", true}, 47 | } 48 | for _, d := range data { 49 | ok := IsHex(d.S) 50 | if ok != d.B { 51 | t.Errorf("IsHex(%q) returned %v, but expected %v", d.S, ok, d.B) 52 | } 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /utils/util.go: -------------------------------------------------------------------------------- 1 | /** 2 | * OpenBmclAPI (Golang Edition) 3 | * Copyright (C) 2024 Kevin Z 4 | * All rights reserved 5 | * 6 | * This program is free software: you can redistribute it and/or modify 7 | * it under the terms of the GNU Affero General Public License as published 8 | * by the Free Software Foundation, either version 3 of the License, or 9 | * (at your option) any later version. 10 | * 11 | * This program is distributed in the hope that it will be useful, 12 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 13 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 14 | * GNU Affero General Public License for more details. 15 | * 16 | * You should have received a copy of the GNU Affero General Public License 17 | * along with this program. If not, see . 18 | */ 19 | 20 | package utils 21 | 22 | import ( 23 | "errors" 24 | "os" 25 | "path/filepath" 26 | "sync" 27 | ) 28 | 29 | type SyncMap[K comparable, V any] struct { 30 | l sync.RWMutex 31 | m map[K]V 32 | } 33 | 34 | func NewSyncMap[K comparable, V any]() *SyncMap[K, V] { 35 | return &SyncMap[K, V]{ 36 | m: make(map[K]V), 37 | } 38 | } 39 | 40 | func (m *SyncMap[K, V]) Len() int { 41 | m.l.RLock() 42 | defer m.l.RUnlock() 43 | return len(m.m) 44 | } 45 | 46 | func (m *SyncMap[K, V]) RawMap() map[K]V { 47 | return m.m 48 | } 49 | 50 | func (m *SyncMap[K, V]) Set(k K, v V) { 51 | m.l.Lock() 52 | defer m.l.Unlock() 53 | m.m[k] = v 54 | } 55 | 56 | func (m *SyncMap[K, V]) Get(k K) V { 57 | m.l.RLock() 58 | defer m.l.RUnlock() 59 | return m.m[k] 60 | } 61 | 62 | func (m *SyncMap[K, V]) Has(k K) bool { 63 | m.l.RLock() 64 | defer m.l.RUnlock() 65 | _, ok := m.m[k] 66 | return ok 67 | } 68 | 69 | func (m *SyncMap[K, V]) GetOrSet(k K, setter func() V) (v V, has bool) { 70 | m.l.RLock() 71 | v, has = m.m[k] 72 | m.l.RUnlock() 73 | if has { 74 | return 75 | } 76 | m.l.Lock() 77 | defer m.l.Unlock() 78 | v, has = m.m[k] 79 | if !has { 80 | v = setter() 81 | m.m[k] = v 82 | } 83 | return 84 | } 85 | 86 | func WalkCacheDir(cacheDir string, walker func(hash string, size int64) (err error)) (err error) { 87 | for _, dir := range Hex256 { 88 | files, err := os.ReadDir(filepath.Join(cacheDir, dir)) 89 | if err != nil { 90 | if errors.Is(err, os.ErrNotExist) { 91 | continue 92 | } 93 | return err 94 | } 95 | for _, f := range files { 96 | if !f.IsDir() { 97 | if hash := f.Name(); len(hash) >= 2 && hash[:2] == dir { 98 | if info, err := f.Info(); err == nil { 99 | if err := walker(hash, info.Size()); err != nil { 100 | return err 101 | } 102 | } 103 | } 104 | } 105 | } 106 | } 107 | return nil 108 | } 109 | --------------------------------------------------------------------------------