├── .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 |
4 |
5 |
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 |
12 |
13 |
14 |
{{ content }}
15 |
16 |
17 |
23 |
Loading...
24 |
25 |
26 |
27 |
28 |
43 |
--------------------------------------------------------------------------------
/dashboard/src/components/FileListCard.vue:
--------------------------------------------------------------------------------
1 |
15 |
16 |
17 |
18 |
19 | {{ name }}
20 |
21 |
22 |
23 |
24 | Loading...
25 |
26 |
27 |
28 |
29 |
30 | {{ file.name }}
31 |
32 |
33 |
{{ formatBytes(file.size) }}
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
78 |
--------------------------------------------------------------------------------
/dashboard/src/components/HitsChart.vue:
--------------------------------------------------------------------------------
1 |
171 |
172 |
173 |
174 |
175 |
--------------------------------------------------------------------------------
/dashboard/src/components/HitsCharts.vue:
--------------------------------------------------------------------------------
1 |
81 |
82 |
83 |
84 |
{{ tr('title.day') }}
85 |
95 |
96 |
97 |
98 |
{{ tr('title.month') }}
99 |
109 |
110 |
111 |
112 |
{{ tr('title.year') }}
113 |
123 |
124 |
125 |
126 |
127 |
128 |
150 |
--------------------------------------------------------------------------------
/dashboard/src/components/LoginComp.vue:
--------------------------------------------------------------------------------
1 |
73 |
74 |
75 |
76 |
114 |
115 | {{ typeof errMsg === 'function' ? errMsg() : errMsg }}
116 |
117 |
118 |
119 |
120 |
140 |
--------------------------------------------------------------------------------
/dashboard/src/components/StatusButton.vue:
--------------------------------------------------------------------------------
1 |
59 |
60 |
61 |
68 |
69 |
143 |
--------------------------------------------------------------------------------
/dashboard/src/components/TransitionExpandGroup.vue:
--------------------------------------------------------------------------------
1 |
69 |
70 |
71 |
81 |
82 |
83 |
84 |
85 |
102 |
--------------------------------------------------------------------------------
/dashboard/src/components/UAChart.vue:
--------------------------------------------------------------------------------
1 |
126 |
127 |
128 |
129 |
130 |
--------------------------------------------------------------------------------
/dashboard/src/components/WebhookEditDialog.vue:
--------------------------------------------------------------------------------
1 |
4 |
5 |
6 |
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 |
2 | ]
3 |
This is an about page
4 |
5 |
6 |
7 |
16 |
--------------------------------------------------------------------------------
/dashboard/src/views/LogListView.vue:
--------------------------------------------------------------------------------
1 |
113 |
114 |
115 |
116 | {{ tr('title.logs') }}
117 |
118 |
125 |
126 |
127 |
146 |
147 |
148 |
158 |
--------------------------------------------------------------------------------
/dashboard/src/views/LoginView.vue:
--------------------------------------------------------------------------------
1 |
27 |
28 |
29 |
{{ tr('title.login') }}
30 |
31 |
32 |
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 |
--------------------------------------------------------------------------------