feat: Phase 1 — extract the client (web SPA + desktop) to dtoro/oikos-web
Problem: the hexagonal refactor churns the backend tree for nine more phases; the UI delivery stack (web/ SPA, cmd/desktop Wails wrapper, compose/web image) must move to its own repo first so doc/layout rewrites land once on a backend-only tree. Change: - New repo git.hubris.network/dtoro/oikos-web (v0.33.0): web/, desktop/ (updateURL repointed to oikos-web releases), compose/, own CI (web + desktop jobs), own deploy script (CI-green gate, TOCTOU guard, version-tagged images, prune-to-3), own webhook receiver on :9798 + launchd unit, own compose project publishing the same 8091:80. - Cutover executed on mac-mini in order: oikos stack's web service stopped+removed, oikos-web project brought up on 8091; outer Caddy untouched (targets the published port) — serving + Authentik flow + /wails 404 quirk verified post-cutover. - Stripped from oikos: web/, cmd/desktop/, compose/web/, desktop CI workflow, ci.yml web job, Makefile ui/desktop/desktop-package/install targets, the compose web service, oikos-web from deploy.sh's fallback prune list; wails + go-keyring dropped from go.mod, vendor synced. - README / CONTRIBUTING / AGENTS.md / .agents dev+operations docs now point at the new repo; mbse + mascot design docs carry a path note. Risk: production SPA serving depends on the new pipeline now; rollback is versioned-image re-up of the old web service from a pre-split checkout (port 8091). Desktop builds installed before the split still check dtoro/oikos releases — one manual reinstall, noted in the oikos-web release notes. Verification: go vet, make test (race), make generate-check, golangci (no new findings; baseline down 400→365); post-cutover curls — localhost:8091 200, /wails/runtime.js 404, outer Caddy 302 Authentik.
This commit is contained in:
@@ -27,8 +27,6 @@ internal/domain/ Core types: entities, approvals, executions, signals
|
|||||||
internal/ontology/ Type hierarchy validation, relationship checks
|
internal/ontology/ Type hierarchy validation, relationship checks
|
||||||
internal/knowledge/ Knowledge YAML seed ingestion
|
internal/knowledge/ Knowledge YAML seed ingestion
|
||||||
internal/config/ Config loading from env vars
|
internal/config/ Config loading from env vars
|
||||||
web/ Control-room SPA (Svelte 5) — standalone static build, not
|
|
||||||
embedded in the oikos binary (plans/2026-07-12-wails-desktop-app.md)
|
|
||||||
api/openapi.yaml REST API contract. Source of truth for endpoints.
|
api/openapi.yaml REST API contract. Source of truth for endpoints.
|
||||||
api/codegen.yaml oapi-codegen config → generates internal/httpapi/gen/
|
api/codegen.yaml oapi-codegen config → generates internal/httpapi/gen/
|
||||||
migrations/ Forward-only SQL. Format: NNN_name.up.sql. No down migrations.
|
migrations/ Forward-only SQL. Format: NNN_name.up.sql. No down migrations.
|
||||||
|
|||||||
@@ -81,8 +81,10 @@ the REST API. Closest current equivalents for what used to live here:
|
|||||||
| `homelab decide <action> <entity>` | No direct equivalent — classification now happens inline inside `run`, not as a separate dry-run call |
|
| `homelab decide <action> <entity>` | No direct equivalent — classification now happens inline inside `run`, not as a separate dry-run call |
|
||||||
|
|
||||||
There is no separately-deployed "Oikos Console" anymore — the control-room
|
There is no separately-deployed "Oikos Console" anymore — the control-room
|
||||||
SPA (`web/`) is the operator dashboard, served standalone (see
|
SPA is the operator dashboard. It lives in its own repo
|
||||||
[plans/done/2026-07-12-wails-desktop-app.md](../../plans/done/2026-07-12-wails-desktop-app.md)).
|
(`dtoro/oikos-web`, local checkout `~/Projects/oikos-web`) with its own
|
||||||
|
deploy pipeline, publishing the same host port 8091 as before (Phase 1 of
|
||||||
|
[plans/2026-08-15-hexagonal-architecture.md](../../plans/2026-08-15-hexagonal-architecture.md)).
|
||||||
|
|
||||||
## Related
|
## Related
|
||||||
- [Hubris host](../../archive/knowledge/hosts/hubris.md)
|
- [Hubris host](../../archive/knowledge/hosts/hubris.md)
|
||||||
|
|||||||
@@ -77,6 +77,8 @@ Session: {id[:8]} — "{title[:60]}"
|
|||||||
- `cmd/nomos/store.go` — session + message persistence
|
- `cmd/nomos/store.go` — session + message persistence
|
||||||
- `internal/mcp/server.go` — all tool implementations (`run`, `list_lxcs`, …)
|
- `internal/mcp/server.go` — all tool implementations (`run`, `list_lxcs`, …)
|
||||||
- `web/src/lib/components/ToolCallGroup.svelte` — tool result display
|
- `web/src/lib/components/ToolCallGroup.svelte` — tool result display
|
||||||
|
(in the `dtoro/oikos-web` repo, `~/Projects/oikos-web`, since the
|
||||||
|
Phase 1 client extraction)
|
||||||
- `nomos/SOUL.md` — agent persona and tool selection rules
|
- `nomos/SOUL.md` — agent persona and tool selection rules
|
||||||
- `plans/done/2026-07-09-chat-sessions-improvements.md` — prior session findings
|
- `plans/done/2026-07-09-chat-sessions-improvements.md` — prior session findings
|
||||||
- `plans/done/2026-07-09-session-execution-and-ux-fixes.md` — latest plan
|
- `plans/done/2026-07-09-session-execution-and-ux-fixes.md` — latest plan
|
||||||
|
|||||||
@@ -70,30 +70,3 @@ jobs:
|
|||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v4
|
||||||
- name: docker build (verify image builds; no push)
|
- name: docker build (verify image builds; no push)
|
||||||
run: docker build -f compose/oikos/Dockerfile -t oikos:ci .
|
run: docker build -f compose/oikos/Dockerfile -t oikos:ci .
|
||||||
|
|
||||||
web:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
defaults:
|
|
||||||
run:
|
|
||||||
working-directory: web
|
|
||||||
steps:
|
|
||||||
- uses: actions/checkout@v4
|
|
||||||
- uses: actions/setup-node@v4
|
|
||||||
with:
|
|
||||||
node-version: '22'
|
|
||||||
cache: npm
|
|
||||||
cache-dependency-path: web/package-lock.json
|
|
||||||
- run: npm ci
|
|
||||||
- name: svelte-check (advisory — baseline not yet clean)
|
|
||||||
run: npm run check
|
|
||||||
continue-on-error: true
|
|
||||||
- name: eslint (advisory — baseline not yet clean)
|
|
||||||
run: npm run lint
|
|
||||||
continue-on-error: true
|
|
||||||
- name: prettier format check (advisory — baseline not yet clean)
|
|
||||||
run: npm run format:check
|
|
||||||
continue-on-error: true
|
|
||||||
- name: test
|
|
||||||
run: npm run test
|
|
||||||
- name: build
|
|
||||||
run: npm run build
|
|
||||||
|
|||||||
@@ -1,70 +0,0 @@
|
|||||||
name: Desktop App
|
|
||||||
on:
|
|
||||||
push:
|
|
||||||
branches:
|
|
||||||
- main
|
|
||||||
tags:
|
|
||||||
- 'desktop-*'
|
|
||||||
- 'v[0-9]+.[0-9]+.[0-9]*'
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
build:
|
|
||||||
name: Build Linux (amd64)
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
steps:
|
|
||||||
- uses: actions/checkout@v4
|
|
||||||
|
|
||||||
- uses: actions/setup-node@v4
|
|
||||||
with:
|
|
||||||
node-version: '22'
|
|
||||||
|
|
||||||
- run: npm ci
|
|
||||||
working-directory: web
|
|
||||||
|
|
||||||
- run: npm run build
|
|
||||||
working-directory: web
|
|
||||||
|
|
||||||
- run: |
|
|
||||||
rm -rf cmd/desktop/frontend/dist
|
|
||||||
mkdir -p cmd/desktop/frontend/dist
|
|
||||||
cp -r web/dist/* cmd/desktop/frontend/dist/
|
|
||||||
|
|
||||||
- uses: actions/setup-go@v5
|
|
||||||
with:
|
|
||||||
go-version: '1.26'
|
|
||||||
|
|
||||||
- run: |
|
|
||||||
sudo apt-get update
|
|
||||||
sudo apt-get install -y libgtk-3-dev libwebkit2gtk-4.1-dev
|
|
||||||
|
|
||||||
- run: CGO_ENABLED=1 go build -o build/bin/Oikos .
|
|
||||||
working-directory: cmd/desktop
|
|
||||||
|
|
||||||
- run: |
|
|
||||||
cd cmd/desktop/build/bin
|
|
||||||
tar czf oikos-desktop-linux-amd64.tar.gz Oikos
|
|
||||||
sha256sum oikos-desktop-linux-amd64.tar.gz > oikos-desktop-linux-amd64.tar.gz.sha256
|
|
||||||
|
|
||||||
- uses: actions/upload-artifact@v4
|
|
||||||
with:
|
|
||||||
name: oikos-desktop-linux-amd64
|
|
||||||
path: |
|
|
||||||
cmd/desktop/build/bin/oikos-desktop-linux-amd64.tar.gz
|
|
||||||
cmd/desktop/build/bin/oikos-desktop-linux-amd64.tar.gz.sha256
|
|
||||||
|
|
||||||
release:
|
|
||||||
name: Attach to Release
|
|
||||||
needs: build
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
if: startsWith(github.ref, 'refs/tags/')
|
|
||||||
steps:
|
|
||||||
- uses: actions/download-artifact@v4
|
|
||||||
with:
|
|
||||||
name: oikos-desktop-linux-amd64
|
|
||||||
|
|
||||||
- uses: https://gitea.com/actions/release-action@v1
|
|
||||||
with:
|
|
||||||
files: |
|
|
||||||
oikos-desktop-linux-amd64.tar.gz
|
|
||||||
oikos-desktop-linux-amd64.tar.gz.sha256
|
|
||||||
api_key: ${{ secrets.GITEA_TOKEN }}
|
|
||||||
16
.gitignore
vendored
16
.gitignore
vendored
@@ -12,19 +12,3 @@ backups/
|
|||||||
.env
|
.env
|
||||||
.infisical-credentials
|
.infisical-credentials
|
||||||
|
|
||||||
# Web UI (Svelte 5) — build artifacts. The SPA is a standalone static build,
|
|
||||||
# deployed separately from the oikos binary (plans/2026-07-12-wails-desktop-app.md
|
|
||||||
# 0.1), so the output dir is just a build artifact.
|
|
||||||
web/dist/
|
|
||||||
web/node_modules/
|
|
||||||
|
|
||||||
# Wails desktop app — frontend copy for embedding
|
|
||||||
cmd/desktop/frontend/dist/
|
|
||||||
cmd/desktop/build/
|
|
||||||
cmd/desktop/Oikos
|
|
||||||
/desktop
|
|
||||||
/eval
|
|
||||||
|
|
||||||
# Local tooling artifacts (Playwright MCP session logs, stray screenshots)
|
|
||||||
.playwright-mcp/
|
|
||||||
config-screen.png
|
|
||||||
|
|||||||
3
.playwright-mcp/console-2026-08-03T16-15-12-151Z.log
Normal file
3
.playwright-mcp/console-2026-08-03T16-15-12-151Z.log
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
[ 65ms] [ERROR] Failed to load resource: the server responded with a status of 404 (Not Found) @ http://localhost:5173/wails/runtime.js:0
|
||||||
|
[ 217ms] [VERBOSE] [DOM] Input elements should have autocomplete attributes (suggested: "new-password"): (More info: https://goo.gl/9p2vKq) %o @ http://localhost:5173/:0
|
||||||
|
[ 62110ms] [VERBOSE] [DOM] Input elements should have autocomplete attributes (suggested: "new-password"): (More info: https://goo.gl/9p2vKq) %o @ http://localhost:5173/:0
|
||||||
4
.playwright-mcp/console-2026-08-08T18-52-28-596Z.log
Normal file
4
.playwright-mcp/console-2026-08-08T18-52-28-596Z.log
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
[ 5732ms] [ERROR] Access to fetch at 'https://oikos.hubris.network/api/v1/audit?limit=5' from origin 'null' has been blocked by CORS policy: The request client is not a secure context and the resource is in more-private address space `local`. @ about:blank:0
|
||||||
|
[ 5732ms] [ERROR] Failed to load resource: net::ERR_FAILED @ https://oikos.hubris.network/api/v1/audit?limit=5:0
|
||||||
|
[ 21353ms] [ERROR] Access to fetch at 'https://oikos.hubris.network/api/v1/audit?limit=3' from origin 'null' has been blocked by CORS policy: The request client is not a secure context and the resource is in more-private address space `local`. @ about:blank:0
|
||||||
|
[ 21353ms] [ERROR] Failed to load resource: net::ERR_FAILED @ https://oikos.hubris.network/api/v1/audit?limit=3:0
|
||||||
14
.playwright-mcp/page-2026-08-03T16-15-12-623Z.yml
Normal file
14
.playwright-mcp/page-2026-08-03T16-15-12-623Z.yml
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
- generic [ref=e7]:
|
||||||
|
- generic [ref=e13]:
|
||||||
|
- heading "Connect to Oikos" [level=1] [ref=e14]
|
||||||
|
- paragraph [ref=e15]: Configure your control room connection
|
||||||
|
- generic [ref=e16]:
|
||||||
|
- generic [ref=e17]: Server URL
|
||||||
|
- textbox "Server URL" [ref=e19]:
|
||||||
|
- /placeholder: https://oikos.hubris.network
|
||||||
|
- generic [ref=e20]:
|
||||||
|
- button "Login with Authentik" [ref=e21] [cursor=pointer]
|
||||||
|
- generic [ref=e22]: or use
|
||||||
|
- generic [ref=e26]:
|
||||||
|
- textbox "Bearer token" [ref=e28]
|
||||||
|
- button "Connect" [ref=e29] [cursor=pointer]
|
||||||
@@ -154,7 +154,9 @@ API/MCP bearer token — there is one shared
|
|||||||
secret (`OIKOS_MCP_BEARER_TOKEN`, validated in `internal/httpapi/server.go`'s
|
secret (`OIKOS_MCP_BEARER_TOKEN`, validated in `internal/httpapi/server.go`'s
|
||||||
`combinedAuth`); get it from the operator until per-client token issuance
|
`combinedAuth`); get it from the operator until per-client token issuance
|
||||||
exists. The SPA has its own flow instead: a first-launch Config screen that
|
exists. The SPA has its own flow instead: a first-launch Config screen that
|
||||||
stores a token in `localStorage` (see `web/src/pages/Config.svelte`).
|
stores a token in `localStorage` (the SPA lives in the `dtoro/oikos-web`
|
||||||
|
repo — `web/src/pages/Config.svelte` there — since the Phase 1 client
|
||||||
|
extraction).
|
||||||
|
|
||||||
## 5. Knowledge conventions
|
## 5. Knowledge conventions
|
||||||
|
|
||||||
|
|||||||
@@ -9,8 +9,9 @@ repo, see [.agents/dev/CONTRIBUTING.md](.agents/dev/CONTRIBUTING.md).
|
|||||||
- **Go 1.26+** (see `go.mod` for pinned version)
|
- **Go 1.26+** (see `go.mod` for pinned version)
|
||||||
- **PostgreSQL with TimescaleDB** — the compose stack includes `timescale/timescaledb:2.17.2-pg16`
|
- **PostgreSQL with TimescaleDB** — the compose stack includes `timescale/timescaledb:2.17.2-pg16`
|
||||||
- **Docker** for the full dev stack
|
- **Docker** for the full dev stack
|
||||||
- **Node 22+** for `web/` (the control-room SPA — standalone, not part of the
|
- The control-room SPA and desktop app live in their own repo —
|
||||||
compose stack or the `oikos` binary)
|
[dtoro/oikos-web](https://git.hubris.network/dtoro/oikos-web) (Node 22+
|
||||||
|
there); this repo is backend-only since the hexagonal refactor Phase 1
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# Start dependencies (Postgres + Redis). api/nomos require a shared bearer
|
# Start dependencies (Postgres + Redis). api/nomos require a shared bearer
|
||||||
@@ -26,44 +27,24 @@ make test-db
|
|||||||
# Build the binary
|
# Build the binary
|
||||||
make build
|
make build
|
||||||
|
|
||||||
# SPA dev server (proxies to api/nomos, injecting the same token)
|
# SPA dev server (own repo — dtoro/oikos-web)
|
||||||
cd web && OIKOS_API_TOKEN=dev-token npm run dev
|
cd ~/Projects/oikos-web/web && OIKOS_API_TOKEN=dev-token npm run dev
|
||||||
|
|
||||||
# Desktop app (macOS)
|
# Desktop app (macOS — also in the oikos-web repo)
|
||||||
make desktop # build .app bundle
|
cd ~/Projects/oikos-web && make install
|
||||||
make install # build + install to /Applications
|
|
||||||
./cmd/desktop/build/bin/oikos-desktop.app/Contents/MacOS/oikos-desktop # run from terminal to see logs
|
|
||||||
```
|
```
|
||||||
|
|
||||||
### Desktop app auth
|
### Desktop app
|
||||||
|
|
||||||
The desktop app uses the same API as the browser SPA. First launch:
|
The desktop app's auth (Authentik login → keychain-persisted token) and
|
||||||
|
auto-update (Gitea releases every 6 hours) moved with it to
|
||||||
1. Enter `https://oikos.hubris.network` as Server URL
|
[dtoro/oikos-web](https://git.hubris.network/dtoro/oikos-web) — see that
|
||||||
2. **Login with Authentik** tab → opens system browser → authenticate
|
repo's README. Auto-update now tracks oikos-web releases; builds installed
|
||||||
3. Callback page shows token → copy → paste into Token tab → Connect
|
before the split need one manual reinstall.
|
||||||
4. Token is persisted to the macOS keychain — subsequent launches skip setup
|
|
||||||
|
|
||||||
The app stores credentials via `github.com/zalando/go-keyring` (service: `com.hubris.oikos-desktop`).
|
|
||||||
|
|
||||||
### Desktop app auto-update
|
|
||||||
|
|
||||||
- Checks Gitea releases every 6 hours
|
|
||||||
- System tray → **Check for Updates** triggers an immediate check
|
|
||||||
- Download, extract, replace the app in `/Applications`, and relaunch
|
|
||||||
- Versions are compared against the `version` var in `main.go`, injected from the repo `VERSION` file at link time (`make desktop` passes `-ldflags "-X main.version=$(cat VERSION)"`)
|
|
||||||
|
|
||||||
## Project structure
|
## Project structure
|
||||||
|
|
||||||
```
|
```
|
||||||
cmd/desktop/ Wails v3 desktop app (macOS + Linux)
|
|
||||||
main.go Thin shell: webview, system tray, notifications, auto-update
|
|
||||||
wails.json Wails project config
|
|
||||||
entitlements.plist macOS code-signing entitlements
|
|
||||||
icon.png System tray icon (embedded)
|
|
||||||
icon.icns App bundle icon (white logo on black rounded rect)
|
|
||||||
Taskfile.yml Wails v3 build tasks
|
|
||||||
Info.plist.template macOS bundle metadata
|
|
||||||
cmd/oikos/ Single-binary entry point
|
cmd/oikos/ Single-binary entry point
|
||||||
cmd/nomos/ Nomos MCP client gateway
|
cmd/nomos/ Nomos MCP client gateway
|
||||||
cmd/webhook/ Gitea deploy-webhook receiver (push-to-deploy on mac-mini)
|
cmd/webhook/ Gitea deploy-webhook receiver (push-to-deploy on mac-mini)
|
||||||
@@ -87,8 +68,6 @@ internal/ All Go packages
|
|||||||
secrets/ Infisical + SOPS backend
|
secrets/ Infisical + SOPS backend
|
||||||
ontology/ Type hierarchy, relationship validation
|
ontology/ Type hierarchy, relationship validation
|
||||||
knowledge/ Knowledge YAML seed ingestion
|
knowledge/ Knowledge YAML seed ingestion
|
||||||
web/ Control-room SPA (Svelte 5) — standalone, not embedded
|
|
||||||
in the oikos binary; see plans/2026-07-12-wails-desktop-app.md
|
|
||||||
api/openapi.yaml API contract — the source of truth for endpoints
|
api/openapi.yaml API contract — the source of truth for endpoints
|
||||||
migrations/ Forward-only SQL migrations (TimescaleDB)
|
migrations/ Forward-only SQL migrations (TimescaleDB)
|
||||||
seeds/ Bootstrap YAML: ontology, inventory, policy, knowledge
|
seeds/ Bootstrap YAML: ontology, inventory, policy, knowledge
|
||||||
@@ -118,11 +97,6 @@ docs/operations/ Runbooks (rollback, etc.)
|
|||||||
| `make export` | Export DB state to YAML seeds |
|
| `make export` | Export DB state to YAML seeds |
|
||||||
| `make dev` | Start compose dev stack |
|
| `make dev` | Start compose dev stack |
|
||||||
| `make clean` | Remove binary + test cache |
|
| `make clean` | Remove binary + test cache |
|
||||||
| `make ui` | Build the SPA (`web/dist/`) |
|
|
||||||
| `make deploy-ui` | Build + deploy the SPA to the Caddy host |
|
|
||||||
| `make desktop` | Build the Wails desktop app for the current platform |
|
|
||||||
| `make desktop-package` | Build + package (zip on macOS, tar.gz on Linux) |
|
|
||||||
| `make install` | Build + install to `/Applications` (macOS) |
|
|
||||||
| `make webhook` | Build `cmd/webhook` (deploy-webhook receiver) |
|
| `make webhook` | Build `cmd/webhook` (deploy-webhook receiver) |
|
||||||
| `make tidy` | `go mod tidy` |
|
| `make tidy` | `go mod tidy` |
|
||||||
|
|
||||||
|
|||||||
38
Makefile
38
Makefile
@@ -1,4 +1,4 @@
|
|||||||
.PHONY: build webhook test test-db lint generate generate-check dev migrate seed export clean tidy ui desktop desktop-package install
|
.PHONY: build webhook test test-db lint generate generate-check dev migrate seed export clean tidy
|
||||||
|
|
||||||
BINARY := bin/oikos
|
BINARY := bin/oikos
|
||||||
GO ?= go
|
GO ?= go
|
||||||
@@ -55,45 +55,9 @@ export:
|
|||||||
dev:
|
dev:
|
||||||
docker compose --profile dev up -d
|
docker compose --profile dev up -d
|
||||||
|
|
||||||
# Local sanity-check build of the SPA. Not embedded in the oikos binary
|
|
||||||
# (plans/2026-07-12-wails-desktop-app.md 0.1) — deploys as its own
|
|
||||||
# container (compose/web/Dockerfile) via `docker compose --profile full
|
|
||||||
# up -d web`, same push-to-main pipeline as everything else.
|
|
||||||
ui:
|
|
||||||
cd web && npm run build
|
|
||||||
|
|
||||||
desktop: ui ## Build the Wails desktop app for the current platform
|
|
||||||
rm -rf cmd/desktop/frontend/dist
|
|
||||||
mkdir -p cmd/desktop/frontend/dist
|
|
||||||
cp -r web/dist/* cmd/desktop/frontend/dist/
|
|
||||||
cd cmd/desktop && CGO_ENABLED=1 go build -tags desktop -ldflags "-X main.version=$$(cat ../VERSION)" -o build/bin/Oikos .
|
|
||||||
|
|
||||||
desktop-package: desktop ## Build + package the desktop app (zip on macOS, tar.gz on Linux)
|
|
||||||
@case $$(uname -s) in \
|
|
||||||
Darwin) \
|
|
||||||
APP="cmd/desktop/build/bin/Oikos.app"; \
|
|
||||||
rm -rf "$$APP"; \
|
|
||||||
mkdir -p "$$APP/Contents/MacOS"; \
|
|
||||||
mkdir -p "$$APP/Contents/Resources"; \
|
|
||||||
cp cmd/desktop/build/bin/Oikos "$$APP/Contents/MacOS/Oikos"; \
|
|
||||||
cp cmd/desktop/icon.icns "$$APP/Contents/Resources/icon.icns"; \
|
|
||||||
sed "s/\$$(VERSION)/$$(cat VERSION)/" cmd/desktop/Info.plist.template > "$$APP/Contents/Info.plist"; \
|
|
||||||
cd cmd/desktop/build/bin && zip -r oikos-desktop-darwin-$$(uname -m).zip Oikos.app ;; \
|
|
||||||
Linux) \
|
|
||||||
cd cmd/desktop/build/bin && tar czf oikos-desktop-linux-$$(uname -m).tar.gz Oikos ;; \
|
|
||||||
esac
|
|
||||||
@echo "Package: cmd/desktop/build/bin/"
|
|
||||||
|
|
||||||
install: desktop-package ## Install to /Applications
|
|
||||||
rm -rf /Applications/Oikos.app
|
|
||||||
cp -r cmd/desktop/build/bin/Oikos.app /Applications/
|
|
||||||
@echo "Installed to /Applications/Oikos.app"
|
|
||||||
|
|
||||||
clean:
|
clean:
|
||||||
rm -f $(BINARY)
|
rm -f $(BINARY)
|
||||||
rm -rf bin
|
rm -rf bin
|
||||||
rm -rf cmd/desktop/build
|
|
||||||
rm -rf cmd/desktop/frontend/dist
|
|
||||||
$(GO) clean -testcache
|
$(GO) clean -testcache
|
||||||
|
|
||||||
tidy:
|
tidy:
|
||||||
|
|||||||
24
README.md
24
README.md
@@ -29,8 +29,8 @@ OIKOS_DATABASE_URL="postgres://oikos:oikos_dev@localhost:5432/oikos?sslmode=disa
|
|||||||
OIKOS_API_TOKEN=dev-token \
|
OIKOS_API_TOKEN=dev-token \
|
||||||
go run ./cmd/oikos all
|
go run ./cmd/oikos all
|
||||||
|
|
||||||
# Control-room SPA (separate from the Go binary — see web/)
|
# Control-room SPA + desktop app: own repo — dtoro/oikos-web
|
||||||
cd web && OIKOS_API_TOKEN=dev-token npm run dev # http://localhost:5173
|
# (~/Projects/oikos-web; cd web && OIKOS_API_TOKEN=dev-token npm run dev)
|
||||||
```
|
```
|
||||||
|
|
||||||
## Architecture
|
## Architecture
|
||||||
@@ -106,12 +106,14 @@ oikos secret migrate # SOPS → Infisical
|
|||||||
|
|
||||||
### Web UI
|
### Web UI
|
||||||
|
|
||||||
`web/` is a standalone Svelte 5 SPA — not embedded in the `oikos` binary, not
|
The control-room SPA and the Wails desktop wrapper live in their own repo,
|
||||||
part of `docker-compose.yml`. It talks to `api`/`nomos` over HTTP with a
|
[dtoro/oikos-web](https://git.hubris.network/dtoro/oikos-web) (local
|
||||||
bearer token entered on first launch (see `web/src/pages/Config.svelte`).
|
checkout `~/Projects/oikos-web`) — extracted in Phase 1 of
|
||||||
Build with `make ui`, deploy with `make deploy-ui` (Caddy serves the static
|
[plans/2026-08-15-hexagonal-architecture.md](plans/2026-08-15-hexagonal-architecture.md).
|
||||||
output). A native desktop wrapper exists at `cmd/desktop/` — see
|
The SPA talks to `api`/`nomos` over HTTP with a bearer token entered on
|
||||||
[plans/done/2026-07-12-wails-desktop-app.md](plans/done/2026-07-12-wails-desktop-app.md).
|
first launch. It deploys as its own compose project publishing `8091:80`;
|
||||||
|
the outer Caddy (LXC 121) targets that published port, so serving and auth
|
||||||
|
are unchanged from the pre-split stack.
|
||||||
|
|
||||||
## Repo layout
|
## Repo layout
|
||||||
|
|
||||||
@@ -119,11 +121,9 @@ output). A native desktop wrapper exists at `cmd/desktop/` — see
|
|||||||
cmd/oikos/ Go entry point — single binary
|
cmd/oikos/ Go entry point — single binary
|
||||||
cmd/nomos/ Nomos MCP client gateway
|
cmd/nomos/ Nomos MCP client gateway
|
||||||
cmd/webhook/ Gitea deploy-webhook receiver (push-to-deploy on mac-mini)
|
cmd/webhook/ Gitea deploy-webhook receiver (push-to-deploy on mac-mini)
|
||||||
cmd/desktop/ Wails desktop wrapper around the SPA
|
internal/ Go packages (actuator, checkdefaults, config, core, db,
|
||||||
internal/ Go packages (actuator, checkdefaults, config, db, domain,
|
domain, httpapi, knowledge, learning, mcp, observability,
|
||||||
httpapi, knowledge, learning, mcp, observability,
|
|
||||||
ontology, policy, safego, scheduler, secrets)
|
ontology, policy, safego, scheduler, secrets)
|
||||||
web/ Control-room SPA (Svelte 5) — standalone, not embedded
|
|
||||||
api/openapi.yaml API contract (OpenAPI 3.1)
|
api/openapi.yaml API contract (OpenAPI 3.1)
|
||||||
migrations/ Forward-only SQL migrations (TimescaleDB)
|
migrations/ Forward-only SQL migrations (TimescaleDB)
|
||||||
seeds/ Bootstrap YAML (ontology, inventory, policy, knowledge)
|
seeds/ Bootstrap YAML (ontology, inventory, policy, knowledge)
|
||||||
|
|||||||
@@ -1,30 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8"?>
|
|
||||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
|
||||||
<plist version="1.0">
|
|
||||||
<dict>
|
|
||||||
<key>CFBundleDevelopmentRegion</key>
|
|
||||||
<string>en</string>
|
|
||||||
<key>CFBundleExecutable</key>
|
|
||||||
<string>Oikos</string>
|
|
||||||
<key>CFBundleIdentifier</key>
|
|
||||||
<string>com.hubris.oikos-desktop</string>
|
|
||||||
<key>CFBundleIconFile</key>
|
|
||||||
<string>icon</string>
|
|
||||||
<key>CFBundleInfoDictionaryVersion</key>
|
|
||||||
<string>6.0</string>
|
|
||||||
<key>CFBundleName</key>
|
|
||||||
<string>Oikos</string>
|
|
||||||
<key>CFBundlePackageType</key>
|
|
||||||
<string>APPL</string>
|
|
||||||
<key>CFBundleShortVersionString</key>
|
|
||||||
<string>$(VERSION)</string>
|
|
||||||
<key>CFBundleVersion</key>
|
|
||||||
<string>$(VERSION)</string>
|
|
||||||
<key>LSMinimumSystemVersion</key>
|
|
||||||
<string>13.0</string>
|
|
||||||
<key>NSHighResolutionCapable</key>
|
|
||||||
<true/>
|
|
||||||
<key>NSHumanReadableCopyright</key>
|
|
||||||
<string>Copyright © 2026 Hubris. All rights reserved.</string>
|
|
||||||
</dict>
|
|
||||||
</plist>
|
|
||||||
@@ -1,14 +0,0 @@
|
|||||||
version: '3'
|
|
||||||
|
|
||||||
tasks:
|
|
||||||
build:
|
|
||||||
summary: Build the Oikos desktop app
|
|
||||||
cmds:
|
|
||||||
- go build -o build/bin/Oikos .
|
|
||||||
env:
|
|
||||||
CGO_ENABLED: 1
|
|
||||||
|
|
||||||
dev:
|
|
||||||
summary: Run in development mode
|
|
||||||
cmds:
|
|
||||||
- go run .
|
|
||||||
@@ -1,12 +0,0 @@
|
|||||||
//go:build desktop
|
|
||||||
|
|
||||||
package main
|
|
||||||
|
|
||||||
import "embed"
|
|
||||||
|
|
||||||
// assets is the embedded web SPA. Built only with the `desktop` tag, which is
|
|
||||||
// set by `make desktop` after it copies web/dist/* into cmd/desktop/frontend/dist
|
|
||||||
// (a gitignored build artifact). See assets_stub.go for the default build.
|
|
||||||
//
|
|
||||||
//go:embed frontend/dist
|
|
||||||
var assets embed.FS
|
|
||||||
@@ -1,12 +0,0 @@
|
|||||||
//go:build !desktop
|
|
||||||
|
|
||||||
package main
|
|
||||||
|
|
||||||
import "embed"
|
|
||||||
|
|
||||||
// assets is an empty FS for the default (non-desktop) build. The real embedded
|
|
||||||
// SPA lives in assets_embed.go behind the `desktop` build tag, because
|
|
||||||
// frontend/dist is a gitignored artifact that only exists after `make desktop`
|
|
||||||
// copies web/dist/* into it. This stub lets `go build ./...` compile cleanly on
|
|
||||||
// a fresh checkout without the frontend built.
|
|
||||||
var assets embed.FS
|
|
||||||
@@ -1,26 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8"?>
|
|
||||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
|
||||||
<plist version="1.0">
|
|
||||||
<dict>
|
|
||||||
<key>com.apple.security.app-sandbox</key>
|
|
||||||
<false/>
|
|
||||||
<key>com.apple.security.cs.allow-unsigned-executable-memory</key>
|
|
||||||
<true/>
|
|
||||||
<key>com.apple.security.cs.disable-library-validation</key>
|
|
||||||
<true/>
|
|
||||||
<key>com.apple.security.device.audio-input</key>
|
|
||||||
<false/>
|
|
||||||
<key>com.apple.security.device.camera</key>
|
|
||||||
<false/>
|
|
||||||
<key>com.apple.security.files.user-selected.read-write</key>
|
|
||||||
<true/>
|
|
||||||
<key>com.apple.security.network.client</key>
|
|
||||||
<true/>
|
|
||||||
<key>com.apple.security.network.server</key>
|
|
||||||
<false/>
|
|
||||||
<key>keychain-access-groups</key>
|
|
||||||
<array>
|
|
||||||
<string>$(AppIdentifierPrefix)com.hubris.oikos-desktop</string>
|
|
||||||
</array>
|
|
||||||
</dict>
|
|
||||||
</plist>
|
|
||||||
1
cmd/desktop/frontend/dist/.gitkeep
vendored
1
cmd/desktop/frontend/dist/.gitkeep
vendored
@@ -1 +0,0 @@
|
|||||||
placeholder
|
|
||||||
Binary file not shown.
Binary file not shown.
|
Before Width: | Height: | Size: 1.5 KiB |
@@ -1,790 +0,0 @@
|
|||||||
package main
|
|
||||||
|
|
||||||
import (
|
|
||||||
"crypto/rand"
|
|
||||||
"crypto/sha256"
|
|
||||||
_ "embed" // required by the //go:embed icon.png directive below
|
|
||||||
"encoding/base64"
|
|
||||||
"encoding/json"
|
|
||||||
"fmt"
|
|
||||||
"io"
|
|
||||||
"io/fs"
|
|
||||||
"log"
|
|
||||||
"net"
|
|
||||||
"net/http"
|
|
||||||
"net/url"
|
|
||||||
"os"
|
|
||||||
"os/exec"
|
|
||||||
"os/user"
|
|
||||||
"path/filepath"
|
|
||||||
"runtime"
|
|
||||||
"strings"
|
|
||||||
"sync"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/wailsapp/wails/v3/pkg/application"
|
|
||||||
"github.com/wailsapp/wails/v3/pkg/events"
|
|
||||||
"github.com/zalando/go-keyring"
|
|
||||||
)
|
|
||||||
|
|
||||||
// assets is the embedded web SPA, defined in assets_embed.go (`desktop` build
|
|
||||||
// tag, real //go:embed frontend/dist) and assets_stub.go (default build, empty
|
|
||||||
// FS). frontend/dist is a gitignored artifact populated by `make desktop`; the
|
|
||||||
// stub keeps `go build ./...` working on a clean checkout.
|
|
||||||
|
|
||||||
//go:embed icon.png
|
|
||||||
var iconPNG []byte
|
|
||||||
|
|
||||||
const (
|
|
||||||
keyringService = "com.hubris.oikos-desktop"
|
|
||||||
keyringUser = "oikos"
|
|
||||||
updateURL = "https://git.hubris.network/api/v1/repos/dtoro/oikos/releases"
|
|
||||||
pollInterval = 30 * time.Second
|
|
||||||
updateInterval = 6 * time.Hour
|
|
||||||
oidcCallbackPort = 18901
|
|
||||||
)
|
|
||||||
|
|
||||||
// version is injected at link time via -ldflags "-X main.version=$(cat VERSION)"
|
|
||||||
// (Makefile desktop target). The default keeps a non-empty fallback for
|
|
||||||
// `go build ./cmd/desktop` without ldflags.
|
|
||||||
var version = "0.1.0-dev"
|
|
||||||
|
|
||||||
type OikosConfig struct {
|
|
||||||
ApiUrl string `json:"apiUrl"`
|
|
||||||
Token string `json:"token,omitempty"`
|
|
||||||
IsDesktop bool `json:"isDesktop"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// ---- ConfigService ----
|
|
||||||
|
|
||||||
type ConfigService struct{}
|
|
||||||
|
|
||||||
func (c *ConfigService) Name() string { return "config" }
|
|
||||||
|
|
||||||
func (c *ConfigService) SaveConfig(apiUrl, token string) error {
|
|
||||||
cfg := OikosConfig{ApiUrl: apiUrl, Token: token, IsDesktop: true}
|
|
||||||
data, _ := json.Marshal(cfg)
|
|
||||||
return keyring.Set(keyringService, keyringUser, string(data))
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *ConfigService) ClearConfig() error {
|
|
||||||
return keyring.Delete(keyringService, keyringUser)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *ConfigService) GetStoredConfig() *OikosConfig {
|
|
||||||
return loadConfig()
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *ConfigService) EnableAutoStart() error {
|
|
||||||
if runtime.GOOS != "darwin" {
|
|
||||||
return fmt.Errorf("autostart not supported on %s", runtime.GOOS)
|
|
||||||
}
|
|
||||||
usr, _ := user.Current()
|
|
||||||
dir := filepath.Join(usr.HomeDir, "Library", "LaunchAgents")
|
|
||||||
os.MkdirAll(dir, 0755)
|
|
||||||
|
|
||||||
exe, _ := os.Executable()
|
|
||||||
plist := fmt.Sprintf(`<?xml version="1.0" encoding="UTF-8"?>
|
|
||||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
|
||||||
<plist version="1.0">
|
|
||||||
<dict>
|
|
||||||
<key>Label</key>
|
|
||||||
<string>com.hubris.oikos-desktop</string>
|
|
||||||
<key>ProgramArguments</key>
|
|
||||||
<array>
|
|
||||||
<string>%s</string>
|
|
||||||
</array>
|
|
||||||
<key>RunAtLoad</key>
|
|
||||||
<true/>
|
|
||||||
<key>KeepAlive</key>
|
|
||||||
<false/>
|
|
||||||
</dict>
|
|
||||||
</plist>`, exe)
|
|
||||||
|
|
||||||
return os.WriteFile(filepath.Join(dir, "com.hubris.oikos-desktop.plist"), []byte(plist), 0644)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *ConfigService) DisableAutoStart() error {
|
|
||||||
if runtime.GOOS != "darwin" {
|
|
||||||
return fmt.Errorf("autostart not supported on %s", runtime.GOOS)
|
|
||||||
}
|
|
||||||
usr, _ := user.Current()
|
|
||||||
path := filepath.Join(usr.HomeDir, "Library", "LaunchAgents", "com.hubris.oikos-desktop.plist")
|
|
||||||
return os.Remove(path)
|
|
||||||
}
|
|
||||||
|
|
||||||
// ---- Local OIDC server (runs alongside the webview) ----
|
|
||||||
|
|
||||||
type oidcSession struct {
|
|
||||||
apiUrl string
|
|
||||||
verifier string
|
|
||||||
state string
|
|
||||||
ch chan string
|
|
||||||
}
|
|
||||||
|
|
||||||
var (
|
|
||||||
oidcSessionsMu sync.Mutex
|
|
||||||
oidcSessions = make(map[string]*oidcSession)
|
|
||||||
)
|
|
||||||
|
|
||||||
func startOIDCServer() *http.Server {
|
|
||||||
mux := http.NewServeMux()
|
|
||||||
|
|
||||||
cors := func(w http.ResponseWriter, r *http.Request) {
|
|
||||||
w.Header().Set("Access-Control-Allow-Origin", "*")
|
|
||||||
w.Header().Set("Access-Control-Allow-Methods", "GET, OPTIONS")
|
|
||||||
w.Header().Set("Access-Control-Allow-Headers", "Content-Type")
|
|
||||||
if r.Method == "OPTIONS" {
|
|
||||||
w.WriteHeader(http.StatusOK)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
h := func(path string, handler func(http.ResponseWriter, *http.Request)) {
|
|
||||||
mux.HandleFunc(path, func(w http.ResponseWriter, r *http.Request) {
|
|
||||||
cors(w, r)
|
|
||||||
if r.Method == "OPTIONS" {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
handler(w, r)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
h("/oidc/start", func(w http.ResponseWriter, r *http.Request) {
|
|
||||||
apiUrl := strings.TrimRight(r.URL.Query().Get("apiUrl"), "/")
|
|
||||||
returnURL := r.URL.Query().Get("ret")
|
|
||||||
if apiUrl == "" {
|
|
||||||
http.Error(w, "apiUrl required", http.StatusBadRequest)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if returnURL == "" {
|
|
||||||
returnURL = "/?desktop=1"
|
|
||||||
}
|
|
||||||
|
|
||||||
oidcCfg, err := fetchOIDCConfig(apiUrl)
|
|
||||||
if err != nil {
|
|
||||||
http.Error(w, err.Error(), http.StatusServiceUnavailable)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
verifier, challenge, _ := pkceParams()
|
|
||||||
state := randomString(32)
|
|
||||||
redirectURI := fmt.Sprintf("http://127.0.0.1:%d/oidc/callback", oidcCallbackPort)
|
|
||||||
|
|
||||||
ch := make(chan string, 1)
|
|
||||||
oidcSessionsMu.Lock()
|
|
||||||
sessionID := randomString(16)
|
|
||||||
oidcSessions[sessionID] = &oidcSession{apiUrl: apiUrl, verifier: verifier, state: state, ch: ch}
|
|
||||||
oidcSessionsMu.Unlock()
|
|
||||||
|
|
||||||
authURL := fmt.Sprintf("%s?%s",
|
|
||||||
oidcCfg.AuthorizationEndpoint,
|
|
||||||
url.Values{
|
|
||||||
"response_type": {"code"},
|
|
||||||
"client_id": {oidcCfg.ClientID},
|
|
||||||
"redirect_uri": {redirectURI},
|
|
||||||
"code_challenge": {challenge},
|
|
||||||
"code_challenge_method": {"S256"},
|
|
||||||
"state": {state},
|
|
||||||
"scope": {"openid profile email"},
|
|
||||||
}.Encode(),
|
|
||||||
)
|
|
||||||
|
|
||||||
exec.Command("open", authURL).Start()
|
|
||||||
|
|
||||||
select {
|
|
||||||
case token := <-ch:
|
|
||||||
if token != "" {
|
|
||||||
c := &ConfigService{}
|
|
||||||
c.SaveConfig(apiUrl, token)
|
|
||||||
returnURL += "&token=" + url.QueryEscape(token)
|
|
||||||
}
|
|
||||||
case <-time.After(5 * time.Minute):
|
|
||||||
}
|
|
||||||
|
|
||||||
w.Header().Set("Content-Type", "text/html; charset=utf-8")
|
|
||||||
fmt.Fprintf(w, `<!DOCTYPE html><html><head><meta charset="UTF-8"><title>Oikos</title>
|
|
||||||
<meta http-equiv="refresh" content="0;url=%s">
|
|
||||||
<style>body{font-family:-apple-system,BlinkMacSystemFont,sans-serif;background:#0a0a0a;color:#e0e0e0;display:flex;align-items:center;justify-content:center;min-height:100vh;margin:0}
|
|
||||||
.card{background:#1a1a1a;border:1px solid #2a2a2a;border-radius:12px;padding:32px;max-width:400px;text-align:center}
|
|
||||||
h1{font-size:18px;margin-bottom:8px}.ok{color:#22c55e;font-size:14px}</style>
|
|
||||||
</head><body><div class="card"><h1>Connected</h1><p class="ok">Redirecting back to Oikos…</p></div></body></html>`, returnURL)
|
|
||||||
})
|
|
||||||
|
|
||||||
h("/oidc/callback", func(w http.ResponseWriter, r *http.Request) {
|
|
||||||
code := r.URL.Query().Get("code")
|
|
||||||
gotState := r.URL.Query().Get("state")
|
|
||||||
|
|
||||||
w.Header().Set("Content-Type", "text/html; charset=utf-8")
|
|
||||||
|
|
||||||
oidcSessionsMu.Lock()
|
|
||||||
var session *oidcSession
|
|
||||||
var sessionID string
|
|
||||||
for id, s := range oidcSessions {
|
|
||||||
if s.state == gotState {
|
|
||||||
session = s
|
|
||||||
sessionID = id
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
oidcSessionsMu.Unlock()
|
|
||||||
|
|
||||||
if session == nil {
|
|
||||||
w.WriteHeader(http.StatusBadRequest)
|
|
||||||
w.Write([]byte("Invalid state."))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
token, err := exchangeCode(
|
|
||||||
session.apiUrl,
|
|
||||||
code, session.verifier,
|
|
||||||
fmt.Sprintf("http://127.0.0.1:%d/oidc/callback", oidcCallbackPort),
|
|
||||||
)
|
|
||||||
|
|
||||||
oidcSessionsMu.Lock()
|
|
||||||
delete(oidcSessions, sessionID)
|
|
||||||
oidcSessionsMu.Unlock()
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
w.WriteHeader(http.StatusInternalServerError)
|
|
||||||
fmt.Fprintf(w, "Token exchange failed: %v", err)
|
|
||||||
session.ch <- ""
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
w.Write([]byte(`<!DOCTYPE html><html><head><meta charset="UTF-8"><title>Oikos</title>
|
|
||||||
<style>body{font-family:-apple-system,BlinkMacSystemFont,sans-serif;background:#0a0a0a;color:#e0e0e0;display:flex;align-items:center;justify-content:center;min-height:100vh;margin:0}
|
|
||||||
.card{background:#1a1a1a;border:1px solid #2a2a2a;border-radius:12px;padding:32px;max-width:400px;text-align:center}
|
|
||||||
h1{font-size:18px;margin-bottom:8px}.ok{color:#22c55e;font-size:14px}</style>
|
|
||||||
</head><body><div class="card"><h1>Connected</h1><p class="ok">You can close this window and return to Oikos.</p></div></body></html>`))
|
|
||||||
session.ch <- token
|
|
||||||
})
|
|
||||||
|
|
||||||
mux.HandleFunc("/oidc/config", func(w http.ResponseWriter, r *http.Request) {
|
|
||||||
apiUrl := strings.TrimRight(r.URL.Query().Get("apiUrl"), "/")
|
|
||||||
if apiUrl == "" {
|
|
||||||
http.Error(w, "apiUrl required", http.StatusBadRequest)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
cfg, err := fetchOIDCConfig(apiUrl)
|
|
||||||
if err != nil {
|
|
||||||
http.Error(w, err.Error(), http.StatusServiceUnavailable)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
w.Header().Set("Content-Type", "application/json")
|
|
||||||
json.NewEncoder(w).Encode(cfg)
|
|
||||||
})
|
|
||||||
|
|
||||||
mux.HandleFunc("/update/check", func(w http.ResponseWriter, r *http.Request) {
|
|
||||||
latest := fetchLatestRelease()
|
|
||||||
w.Header().Set("Content-Type", "application/json")
|
|
||||||
w.Header().Set("Access-Control-Allow-Origin", "*")
|
|
||||||
if latest == nil {
|
|
||||||
json.NewEncoder(w).Encode(map[string]string{"current": version})
|
|
||||||
return
|
|
||||||
}
|
|
||||||
hasAsset := false
|
|
||||||
for _, a := range latest.Assets {
|
|
||||||
if strings.Contains(a.Name, "darwin") {
|
|
||||||
hasAsset = true
|
|
||||||
updater.mu.Lock()
|
|
||||||
updater.latestURL = a.BrowserDownloadURL
|
|
||||||
updater.mu.Unlock()
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
json.NewEncoder(w).Encode(map[string]string{
|
|
||||||
"current": version,
|
|
||||||
"latest": latest.Version,
|
|
||||||
"has_asset": fmt.Sprintf("%t", hasAsset),
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
listener, err := net.Listen("tcp", fmt.Sprintf("127.0.0.1:%d", oidcCallbackPort))
|
|
||||||
if err != nil {
|
|
||||||
log.Printf("OIDC server: %v", err)
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
log.Printf("OIDC server listening on %s", listener.Addr())
|
|
||||||
srv := &http.Server{Handler: mux}
|
|
||||||
go srv.Serve(listener)
|
|
||||||
return srv
|
|
||||||
}
|
|
||||||
|
|
||||||
// ---- Window persistence ----
|
|
||||||
|
|
||||||
type windowState struct {
|
|
||||||
X int `json:"x"`
|
|
||||||
Y int `json:"y"`
|
|
||||||
Width int `json:"width"`
|
|
||||||
Height int `json:"height"`
|
|
||||||
}
|
|
||||||
|
|
||||||
func windowStatePath() string {
|
|
||||||
usr, _ := user.Current()
|
|
||||||
return filepath.Join(usr.HomeDir, ".config", "oikos", "window.json")
|
|
||||||
}
|
|
||||||
|
|
||||||
func loadWindowState() *windowState {
|
|
||||||
data, err := os.ReadFile(windowStatePath())
|
|
||||||
if err != nil {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
var ws windowState
|
|
||||||
if err := json.Unmarshal(data, &ws); err != nil {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
if ws.Width < 200 || ws.Height < 200 {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
return &ws
|
|
||||||
}
|
|
||||||
|
|
||||||
func saveWindowState(w application.Window) {
|
|
||||||
x, y := w.Position()
|
|
||||||
width, height := w.Size()
|
|
||||||
ws := windowState{X: x, Y: y, Width: width, Height: height}
|
|
||||||
data, _ := json.Marshal(ws)
|
|
||||||
|
|
||||||
usr, _ := user.Current()
|
|
||||||
dir := filepath.Join(usr.HomeDir, ".config", "oikos")
|
|
||||||
os.MkdirAll(dir, 0755)
|
|
||||||
os.WriteFile(filepath.Join(dir, "window.json"), data, 0644)
|
|
||||||
}
|
|
||||||
|
|
||||||
func loadConfig() *OikosConfig {
|
|
||||||
data, err := keyring.Get(keyringService, keyringUser)
|
|
||||||
if err != nil {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
var cfg OikosConfig
|
|
||||||
if err := json.Unmarshal([]byte(data), &cfg); err != nil {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
cfg.IsDesktop = true
|
|
||||||
return &cfg
|
|
||||||
}
|
|
||||||
|
|
||||||
type oidcConfig struct {
|
|
||||||
Issuer string `json:"issuer"`
|
|
||||||
ClientID string `json:"client_id"`
|
|
||||||
AuthorizationEndpoint string `json:"authorization_endpoint"`
|
|
||||||
}
|
|
||||||
|
|
||||||
func fetchOIDCConfig(apiUrl string) (*oidcConfig, error) {
|
|
||||||
resp, err := http.Get(apiUrl + "/api/v1/auth/oidc-config")
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
defer resp.Body.Close()
|
|
||||||
if resp.StatusCode != 200 {
|
|
||||||
return nil, fmt.Errorf("server returned %d", resp.StatusCode)
|
|
||||||
}
|
|
||||||
var cfg oidcConfig
|
|
||||||
if err := json.NewDecoder(resp.Body).Decode(&cfg); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
return &cfg, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func pkceParams() (verifier, challenge string, _ error) {
|
|
||||||
v := randomString(64)
|
|
||||||
h := sha256.Sum256([]byte(v))
|
|
||||||
return v, base64.RawURLEncoding.EncodeToString(h[:]), nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func randomString(n int) string {
|
|
||||||
b := make([]byte, n)
|
|
||||||
rand.Read(b)
|
|
||||||
return base64.RawURLEncoding.EncodeToString(b)
|
|
||||||
}
|
|
||||||
|
|
||||||
func exchangeCode(apiUrl, code, verifier, redirectURI string) (string, error) {
|
|
||||||
body, _ := json.Marshal(map[string]string{
|
|
||||||
"grant_type": "authorization_code",
|
|
||||||
"code": code,
|
|
||||||
"code_verifier": verifier,
|
|
||||||
"redirect_uri": redirectURI,
|
|
||||||
})
|
|
||||||
|
|
||||||
resp, err := http.Post(apiUrl+"/api/v1/auth/oidc-token", "application/json", strings.NewReader(string(body)))
|
|
||||||
if err != nil {
|
|
||||||
return "", err
|
|
||||||
}
|
|
||||||
defer resp.Body.Close()
|
|
||||||
|
|
||||||
if resp.StatusCode != 200 {
|
|
||||||
b, _ := io.ReadAll(resp.Body)
|
|
||||||
return "", fmt.Errorf("token endpoint: %d — %s", resp.StatusCode, string(b))
|
|
||||||
}
|
|
||||||
|
|
||||||
var tokens struct {
|
|
||||||
AccessToken string `json:"access_token"`
|
|
||||||
}
|
|
||||||
if err := json.NewDecoder(resp.Body).Decode(&tokens); err != nil {
|
|
||||||
return "", err
|
|
||||||
}
|
|
||||||
if tokens.AccessToken == "" {
|
|
||||||
return "", fmt.Errorf("no access_token in response")
|
|
||||||
}
|
|
||||||
return tokens.AccessToken, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// ---- Notifications ----
|
|
||||||
|
|
||||||
type dashboardSummary struct {
|
|
||||||
ApprovalsPending int `json:"approvals_pending"`
|
|
||||||
Signals struct {
|
|
||||||
Critical int `json:"critical"`
|
|
||||||
} `json:"signals_by_severity"`
|
|
||||||
}
|
|
||||||
|
|
||||||
func (d *dashboardSummary) alertCount() int {
|
|
||||||
return d.ApprovalsPending + d.Signals.Critical
|
|
||||||
}
|
|
||||||
|
|
||||||
func notify(title, subtitle string) {
|
|
||||||
if runtime.GOOS != "darwin" {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
script := fmt.Sprintf(
|
|
||||||
`display notification "%s" with title "%s" sound name "default"`,
|
|
||||||
strings.ReplaceAll(subtitle, `"`, `\"`),
|
|
||||||
strings.ReplaceAll(title, `"`, `\"`),
|
|
||||||
)
|
|
||||||
exec.Command("osascript", "-e", script).Run()
|
|
||||||
}
|
|
||||||
|
|
||||||
func pollDashboard(cfg *OikosConfig) {
|
|
||||||
if cfg == nil || cfg.ApiUrl == "" || cfg.Token == "" {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
var lastCount int
|
|
||||||
first := true
|
|
||||||
|
|
||||||
for {
|
|
||||||
req, err := http.NewRequest("GET", cfg.ApiUrl+"/api/v1/dashboard/summary", nil)
|
|
||||||
if err != nil {
|
|
||||||
time.Sleep(pollInterval)
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
req.Header.Set("Authorization", "Bearer "+cfg.Token)
|
|
||||||
|
|
||||||
resp, err := http.DefaultClient.Do(req)
|
|
||||||
if err != nil {
|
|
||||||
time.Sleep(pollInterval)
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
body, _ := io.ReadAll(resp.Body)
|
|
||||||
resp.Body.Close()
|
|
||||||
|
|
||||||
var summary dashboardSummary
|
|
||||||
if err := json.Unmarshal(body, &summary); err != nil {
|
|
||||||
time.Sleep(pollInterval)
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
if first {
|
|
||||||
lastCount = summary.alertCount()
|
|
||||||
first = false
|
|
||||||
} else {
|
|
||||||
current := summary.alertCount()
|
|
||||||
if current > lastCount {
|
|
||||||
notify("Oikos", fmt.Sprintf("%d pending approval(s), %d critical signal(s)", summary.ApprovalsPending, summary.Signals.Critical))
|
|
||||||
}
|
|
||||||
lastCount = current
|
|
||||||
}
|
|
||||||
|
|
||||||
time.Sleep(pollInterval)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// ---- Auto-update ----
|
|
||||||
|
|
||||||
type giteaRelease struct {
|
|
||||||
TagName string `json:"tag_name"`
|
|
||||||
Assets []struct {
|
|
||||||
Name string `json:"name"`
|
|
||||||
BrowserDownloadURL string `json:"browser_download_url"`
|
|
||||||
} `json:"assets"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type updateState struct {
|
|
||||||
mu sync.Mutex
|
|
||||||
latestURL string
|
|
||||||
}
|
|
||||||
|
|
||||||
var updater = &updateState{}
|
|
||||||
|
|
||||||
// CheckForUpdates checks Gitea releases for a newer version. If found, stores
|
|
||||||
// the download URL and returns the latest version string (empty if current).
|
|
||||||
func (c *ConfigService) CheckForUpdates() string {
|
|
||||||
latest := fetchLatestRelease()
|
|
||||||
if latest == nil || latest.Version == version {
|
|
||||||
return ""
|
|
||||||
}
|
|
||||||
for _, a := range latest.Assets {
|
|
||||||
if strings.Contains(a.Name, "darwin") {
|
|
||||||
updater.mu.Lock()
|
|
||||||
updater.latestURL = a.BrowserDownloadURL
|
|
||||||
updater.mu.Unlock()
|
|
||||||
return latest.Version
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return ""
|
|
||||||
}
|
|
||||||
|
|
||||||
// InstallUpdate downloads the stored update, replaces the app, and restarts.
|
|
||||||
func (c *ConfigService) InstallUpdate() error {
|
|
||||||
updater.mu.Lock()
|
|
||||||
url := updater.latestURL
|
|
||||||
updater.mu.Unlock()
|
|
||||||
if url == "" {
|
|
||||||
return fmt.Errorf("no update available")
|
|
||||||
}
|
|
||||||
return doUpdate(url)
|
|
||||||
}
|
|
||||||
|
|
||||||
type latestRelease struct {
|
|
||||||
Version string
|
|
||||||
Assets []struct {
|
|
||||||
Name string
|
|
||||||
BrowserDownloadURL string
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func fetchLatestRelease() *latestRelease {
|
|
||||||
resp, err := http.Get(updateURL + "?draft=false&pre-release=false&limit=1")
|
|
||||||
if err != nil {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
defer resp.Body.Close()
|
|
||||||
|
|
||||||
var releases []giteaRelease
|
|
||||||
if err := json.NewDecoder(resp.Body).Decode(&releases); err != nil || len(releases) == 0 {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
r := releases[0]
|
|
||||||
v := strings.TrimPrefix(r.TagName, "v")
|
|
||||||
if v == version {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
lr := &latestRelease{Version: v}
|
|
||||||
for _, a := range r.Assets {
|
|
||||||
lr.Assets = append(lr.Assets, struct {
|
|
||||||
Name string
|
|
||||||
BrowserDownloadURL string
|
|
||||||
}{a.Name, a.BrowserDownloadURL})
|
|
||||||
}
|
|
||||||
return lr
|
|
||||||
}
|
|
||||||
|
|
||||||
func doUpdate(downloadURL string) error {
|
|
||||||
tmp, err := os.CreateTemp("", "oikos-update-*.zip")
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
defer os.Remove(tmp.Name())
|
|
||||||
|
|
||||||
resp, err := http.Get(downloadURL)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
defer resp.Body.Close()
|
|
||||||
|
|
||||||
if _, err := io.Copy(tmp, resp.Body); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
tmp.Close()
|
|
||||||
|
|
||||||
extractDir, err := os.MkdirTemp("", "oikos-extract")
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
defer os.RemoveAll(extractDir)
|
|
||||||
|
|
||||||
cmd := exec.Command("unzip", "-o", tmp.Name(), "-d", extractDir)
|
|
||||||
if out, err := cmd.CombinedOutput(); err != nil {
|
|
||||||
return fmt.Errorf("unzip: %w: %s", err, out)
|
|
||||||
}
|
|
||||||
|
|
||||||
newApp := filepath.Join(extractDir, "Oikos.app")
|
|
||||||
if _, err := os.Stat(newApp); err != nil {
|
|
||||||
return fmt.Errorf("extracted app not found: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
currentApp := "/Applications/Oikos.app"
|
|
||||||
if _, err := os.Stat(currentApp); os.IsNotExist(err) {
|
|
||||||
if exe, err := os.Executable(); err == nil {
|
|
||||||
currentApp = filepath.Dir(filepath.Dir(filepath.Dir(exe)))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
script := fmt.Sprintf(`#!/bin/bash
|
|
||||||
sleep 2
|
|
||||||
rm -rf "%s"
|
|
||||||
mv "%s" "%s"
|
|
||||||
open "%s"
|
|
||||||
rm "$0"
|
|
||||||
`, currentApp, newApp, currentApp, currentApp)
|
|
||||||
|
|
||||||
scriptPath := filepath.Join(os.TempDir(), "oikos-update.sh")
|
|
||||||
if err := os.WriteFile(scriptPath, []byte(script), 0755); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
app := application.Get()
|
|
||||||
exec.Command("open", scriptPath).Start()
|
|
||||||
if app != nil {
|
|
||||||
app.Quit()
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func checkUpdates() {
|
|
||||||
for {
|
|
||||||
time.Sleep(updateInterval)
|
|
||||||
|
|
||||||
latest := fetchLatestRelease()
|
|
||||||
if latest == nil {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, a := range latest.Assets {
|
|
||||||
if strings.Contains(a.Name, "darwin") {
|
|
||||||
updater.mu.Lock()
|
|
||||||
updater.latestURL = a.BrowserDownloadURL
|
|
||||||
updater.mu.Unlock()
|
|
||||||
|
|
||||||
app := application.Get()
|
|
||||||
if app == nil {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
msg := fmt.Sprintf("Version %s is available (you have %s).", latest.Version, version)
|
|
||||||
app.Dialog.Info().
|
|
||||||
SetTitle("Update Available").
|
|
||||||
SetMessage(msg).
|
|
||||||
Show()
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// ---- Main ----
|
|
||||||
|
|
||||||
func main() {
|
|
||||||
oidcSrv := startOIDCServer()
|
|
||||||
defer oidcSrv.Close()
|
|
||||||
|
|
||||||
distFS, err := fs.Sub(assets, "frontend/dist")
|
|
||||||
if err != nil {
|
|
||||||
log.Fatalf("embedded assets: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
app := application.New(application.Options{
|
|
||||||
Name: "Oikos",
|
|
||||||
Description: "Homelab Control Room",
|
|
||||||
Services: []application.Service{
|
|
||||||
application.NewService(&ConfigService{}),
|
|
||||||
},
|
|
||||||
Assets: application.AssetOptions{
|
|
||||||
Handler: application.AssetFileServerFS(distFS),
|
|
||||||
},
|
|
||||||
Mac: application.MacOptions{
|
|
||||||
ApplicationShouldTerminateAfterLastWindowClosed: false,
|
|
||||||
},
|
|
||||||
})
|
|
||||||
|
|
||||||
systemTray := app.SystemTray.New()
|
|
||||||
systemTray.SetTooltip("Oikos")
|
|
||||||
systemTray.SetIcon(iconPNG)
|
|
||||||
|
|
||||||
trayMenu := application.NewMenu()
|
|
||||||
trayMenu.Add("Open Oikos").OnClick(func(ctx *application.Context) {
|
|
||||||
for _, w := range app.Window.GetAll() {
|
|
||||||
w.Show()
|
|
||||||
w.Focus()
|
|
||||||
}
|
|
||||||
})
|
|
||||||
trayMenu.AddSeparator()
|
|
||||||
trayMenu.Add("Check for Updates").OnClick(func(ctx *application.Context) {
|
|
||||||
go func() {
|
|
||||||
latest := fetchLatestRelease()
|
|
||||||
if latest == nil {
|
|
||||||
app.Dialog.Info().SetTitle("Up to Date").SetMessage("You are running the latest version (" + version + ").").Show()
|
|
||||||
return
|
|
||||||
}
|
|
||||||
for _, a := range latest.Assets {
|
|
||||||
if strings.Contains(a.Name, "darwin") {
|
|
||||||
updater.mu.Lock()
|
|
||||||
updater.latestURL = a.BrowserDownloadURL
|
|
||||||
updater.mu.Unlock()
|
|
||||||
msg := fmt.Sprintf("Version %s is available (you have %s). Install now?", latest.Version, version)
|
|
||||||
d := app.Dialog.Question().SetTitle("Update Available").SetMessage(msg)
|
|
||||||
yes := d.AddButton("Install")
|
|
||||||
yes.OnClick(func() { doUpdate(updater.latestURL) })
|
|
||||||
no := d.AddButton("Later")
|
|
||||||
d.SetDefaultButton(yes)
|
|
||||||
d.SetCancelButton(no)
|
|
||||||
d.Show()
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
app.Dialog.Info().SetTitle("Up to Date").SetMessage("You are running the latest version (" + version + ").").Show()
|
|
||||||
}()
|
|
||||||
})
|
|
||||||
trayMenu.AddSeparator()
|
|
||||||
trayMenu.Add("Quit").OnClick(func(ctx *application.Context) {
|
|
||||||
app.Quit()
|
|
||||||
})
|
|
||||||
systemTray.SetMenu(trayMenu)
|
|
||||||
|
|
||||||
ws := loadWindowState()
|
|
||||||
width, height := 1400, 900
|
|
||||||
if ws != nil {
|
|
||||||
width = ws.Width
|
|
||||||
height = ws.Height
|
|
||||||
}
|
|
||||||
|
|
||||||
window := app.Window.NewWithOptions(application.WebviewWindowOptions{
|
|
||||||
Title: "Oikos",
|
|
||||||
Width: width,
|
|
||||||
Height: height,
|
|
||||||
MinWidth: 1024,
|
|
||||||
MinHeight: 700,
|
|
||||||
URL: "/?desktop=1",
|
|
||||||
})
|
|
||||||
|
|
||||||
if ws != nil {
|
|
||||||
window.SetPosition(ws.X, ws.Y)
|
|
||||||
} else {
|
|
||||||
window.Center()
|
|
||||||
}
|
|
||||||
|
|
||||||
window.RegisterHook(events.Common.WindowClosing, func(e *application.WindowEvent) {
|
|
||||||
window.Hide()
|
|
||||||
e.Cancel()
|
|
||||||
})
|
|
||||||
|
|
||||||
window.Show()
|
|
||||||
|
|
||||||
systemTray.AttachWindow(window)
|
|
||||||
systemTray.Run()
|
|
||||||
|
|
||||||
app.OnShutdown(func() {
|
|
||||||
saveWindowState(window)
|
|
||||||
})
|
|
||||||
|
|
||||||
go pollDashboard(loadConfig())
|
|
||||||
go checkUpdates()
|
|
||||||
|
|
||||||
err = app.Run()
|
|
||||||
if err != nil {
|
|
||||||
log.Fatal(err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,5 +0,0 @@
|
|||||||
<svg width="88" height="88" viewBox="0 0 110 120" xmlns="http://www.w3.org/2000/svg">
|
|
||||||
<g transform="translate(9, 10) scale(0.9)">
|
|
||||||
<path fill="#ffffff" d="m45.601 1q20.993 0 33.71 15.946 10.799 13.625 10.799 31.287 0 12.414-5.9548 25.131-5.9548 12.717-16.451 19.176-10.395 6.4592-23.213 6.4592-20.892 0-33.205-16.653-10.395-14.029-10.395-31.489 0-12.717 6.2577-25.232 6.3584-12.616 16.653-18.57 10.295-6.0556 21.801-6.0556zm-3.128 6.5605q-5.3492 0-10.799 3.2296-5.3492 3.1287-8.68 11.102-3.3305 7.9735-3.3305 20.488 0 20.185 7.973 34.82 8.0743 14.634 21.195 14.634 9.7896 0 16.149-8.0743 6.3584-8.0743 6.3584-27.755 0-24.627-10.597-38.756-7.1657-9.6888-18.268-9.6888z"/>
|
|
||||||
</g>
|
|
||||||
</svg>
|
|
||||||
|
Before Width: | Height: | Size: 690 B |
@@ -1,9 +0,0 @@
|
|||||||
{
|
|
||||||
"name": "oikos",
|
|
||||||
"outputfilename": "oikos-desktop",
|
|
||||||
"frontend:dir": "frontend",
|
|
||||||
"author": {
|
|
||||||
"name": "Hubris",
|
|
||||||
"email": "d.toro.v@pm.me"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,27 +0,0 @@
|
|||||||
:80 {
|
|
||||||
root * /srv
|
|
||||||
|
|
||||||
# /wails/runtime.js is injected by the Wails desktop wrapper, which serves
|
|
||||||
# the same dist/ from its own asset handler. In a browser it does not
|
|
||||||
# exist, and the SPA fallback below answered it with index.html — so the
|
|
||||||
# browser parsed "<!doctype html>" as JavaScript and threw
|
|
||||||
# "SyntaxError: expected expression, got '<'" on every page load.
|
|
||||||
# Return a real 404 instead: the tag fails quietly, and the desktop app is
|
|
||||||
# unaffected because it never reaches this server.
|
|
||||||
handle /wails/* {
|
|
||||||
error 404
|
|
||||||
}
|
|
||||||
|
|
||||||
# Same reasoning for any other asset: a missing .js/.css/.map answered with
|
|
||||||
# HTML is always a confusing parse error rather than an honest 404. Only
|
|
||||||
# real routes should fall through to the SPA.
|
|
||||||
@asset path_regexp \.(js|mjs|css|map|json|png|jpg|svg|ico|woff2?)$
|
|
||||||
handle @asset {
|
|
||||||
file_server
|
|
||||||
}
|
|
||||||
|
|
||||||
handle {
|
|
||||||
file_server
|
|
||||||
try_files {path} /index.html
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,20 +0,0 @@
|
|||||||
# Dockerfile for the oikos control-room SPA. Built separately from the
|
|
||||||
# oikos binary (compose/oikos/Dockerfile) — see docker-compose.yml's `web`
|
|
||||||
# service. The outer production Caddy (caddy-conf repo, LXC 121) handles
|
|
||||||
# Authentik + splits /api/*, /mcp, /agent/* off to the api service; this
|
|
||||||
# container only serves static files with SPA-fallback routing.
|
|
||||||
|
|
||||||
FROM node:22-alpine AS builder
|
|
||||||
|
|
||||||
WORKDIR /build/web
|
|
||||||
COPY web/package.json web/package-lock.json ./
|
|
||||||
COPY web/vendor /build/vendor
|
|
||||||
RUN npm install --no-audit --no-fund
|
|
||||||
COPY VERSION ./
|
|
||||||
COPY web/ ./
|
|
||||||
RUN npm run build
|
|
||||||
|
|
||||||
FROM caddy:2-alpine
|
|
||||||
|
|
||||||
COPY --from=builder /build/web/dist /srv
|
|
||||||
COPY compose/web/Caddyfile /etc/caddy/Caddyfile
|
|
||||||
BIN
config-screen.png
Normal file
BIN
config-screen.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 539 KiB |
@@ -235,22 +235,10 @@ services:
|
|||||||
retries: 3
|
retries: 3
|
||||||
start_period: 15s
|
start_period: 15s
|
||||||
|
|
||||||
# Control-room SPA — static build served behind Caddy. The outer
|
# The control-room SPA moved to its own repo (dtoro/oikos-web, Phase 1 of
|
||||||
# production Caddy (caddy-conf repo, LXC 121) splits /api/*, /mcp,
|
# plans/2026-08-15-hexagonal-architecture.md) with its own compose project
|
||||||
# /agent/* off to api:8090 and sends everything else here; this
|
# publishing the same host port 8091 — the outer Caddy targets the published
|
||||||
# container only serves static files with SPA-fallback routing.
|
# port, so nothing here changes for routing.
|
||||||
web:
|
|
||||||
image: oikos-web:${OIKOS_VERSION:-latest}
|
|
||||||
build:
|
|
||||||
context: .
|
|
||||||
dockerfile: compose/web/Dockerfile
|
|
||||||
restart: unless-stopped
|
|
||||||
profiles: ["dev", "full"]
|
|
||||||
ports:
|
|
||||||
- "8091:80"
|
|
||||||
stop_signal: SIGTERM
|
|
||||||
mem_limit: 64m
|
|
||||||
cpus: 0.25
|
|
||||||
|
|
||||||
# Redis (required by Infisical — Phase 5)
|
# Redis (required by Infisical — Phase 5)
|
||||||
redis:
|
redis:
|
||||||
|
|||||||
@@ -1,5 +1,9 @@
|
|||||||
# Oikos — Desktop Mascot Subsystem Model
|
# Oikos — Desktop Mascot Subsystem Model
|
||||||
|
|
||||||
|
> **Path note (2026-08-15):** the `web/src/...` paths and relative links
|
||||||
|
> below predate the Phase 1 client extraction — that tree now lives in the
|
||||||
|
> `dtoro/oikos-web` repo. Read them as `web/src/...` under that checkout.
|
||||||
|
|
||||||
> Companion to [the platform Model](../mbse/README.md) and
|
> Companion to [the platform Model](../mbse/README.md) and
|
||||||
> [the Framework](../mbse/framework.md). This document is a **subsystem
|
> [the Framework](../mbse/framework.md). This document is a **subsystem
|
||||||
> Model** in Holt's sense — it conforms to the same Framework (Ontology +
|
> Model** in Holt's sense — it conforms to the same Framework (Ontology +
|
||||||
|
|||||||
@@ -1,5 +1,10 @@
|
|||||||
# Oikos — System Model
|
# Oikos — System Model
|
||||||
|
|
||||||
|
> **Path note (2026-08-15):** `web/`, `cmd/desktop`, and
|
||||||
|
> `compose/web/Dockerfile` references below moved to the `dtoro/oikos-web`
|
||||||
|
> repo in the Phase 1 client extraction
|
||||||
|
> (plans/2026-08-15-hexagonal-architecture.md).
|
||||||
|
|
||||||
A single Model-Based Systems Engineering (MBSE) view of Oikos, structured
|
A single Model-Based Systems Engineering (MBSE) view of Oikos, structured
|
||||||
after *Systems Engineering Demystified* (2nd ed., Jon Holt): one underlying
|
after *Systems Engineering Demystified* (2nd ed., Jon Holt): one underlying
|
||||||
system — the Oikos entity graph, its OODA control loop, and the services
|
system — the Oikos entity graph, its OODA control loop, and the services
|
||||||
|
|||||||
@@ -1,5 +1,9 @@
|
|||||||
# Oikos — Component Views
|
# Oikos — Component Views
|
||||||
|
|
||||||
|
> **Path note (2026-08-15):** `web/src/...` and `cmd/desktop` references
|
||||||
|
> below moved to the `dtoro/oikos-web` repo in the Phase 1 client
|
||||||
|
> extraction (plans/2026-08-15-hexagonal-architecture.md).
|
||||||
|
|
||||||
> Companion to [the Model](README.md) and [the Framework](framework.md).
|
> Companion to [the Model](README.md) and [the Framework](framework.md).
|
||||||
> Where README.md's nine Views cut across the whole system by *concern*
|
> Where README.md's nine Views cut across the whole system by *concern*
|
||||||
> (requirements, behavior, risk...), this document cuts across it by
|
> (requirements, behavior, risk...), this document cuts across it by
|
||||||
|
|||||||
11
go.mod
11
go.mod
@@ -14,8 +14,6 @@ require (
|
|||||||
github.com/modelcontextprotocol/go-sdk v1.6.1
|
github.com/modelcontextprotocol/go-sdk v1.6.1
|
||||||
github.com/oapi-codegen/runtime v1.4.2
|
github.com/oapi-codegen/runtime v1.4.2
|
||||||
github.com/openai/openai-go v1.12.0
|
github.com/openai/openai-go v1.12.0
|
||||||
github.com/wailsapp/wails/v3 v3.0.0-alpha2.117
|
|
||||||
github.com/zalando/go-keyring v0.2.8
|
|
||||||
golang.org/x/crypto v0.53.0
|
golang.org/x/crypto v0.53.0
|
||||||
golang.org/x/sync v0.21.0
|
golang.org/x/sync v0.21.0
|
||||||
golang.org/x/sys v0.46.0
|
golang.org/x/sys v0.46.0
|
||||||
@@ -28,7 +26,6 @@ require (
|
|||||||
cloud.google.com/go/auth/oauth2adapt v0.2.8 // indirect
|
cloud.google.com/go/auth/oauth2adapt v0.2.8 // indirect
|
||||||
cloud.google.com/go/compute/metadata v0.9.0 // indirect
|
cloud.google.com/go/compute/metadata v0.9.0 // indirect
|
||||||
cloud.google.com/go/iam v1.1.11 // indirect
|
cloud.google.com/go/iam v1.1.11 // indirect
|
||||||
github.com/adrg/xdg v0.5.3 // indirect
|
|
||||||
github.com/apapsch/go-jsonmerge/v2 v2.0.0 // indirect
|
github.com/apapsch/go-jsonmerge/v2 v2.0.0 // indirect
|
||||||
github.com/aws/aws-sdk-go-v2 v1.27.2 // indirect
|
github.com/aws/aws-sdk-go-v2 v1.27.2 // indirect
|
||||||
github.com/aws/aws-sdk-go-v2/config v1.27.18 // indirect
|
github.com/aws/aws-sdk-go-v2/config v1.27.18 // indirect
|
||||||
@@ -44,16 +41,13 @@ require (
|
|||||||
github.com/aws/aws-sdk-go-v2/service/sts v1.28.12 // indirect
|
github.com/aws/aws-sdk-go-v2/service/sts v1.28.12 // indirect
|
||||||
github.com/aws/smithy-go v1.20.2 // indirect
|
github.com/aws/smithy-go v1.20.2 // indirect
|
||||||
github.com/cespare/xxhash/v2 v2.3.0 // indirect
|
github.com/cespare/xxhash/v2 v2.3.0 // indirect
|
||||||
github.com/coder/websocket v1.8.14 // indirect
|
github.com/dlclark/regexp2 v1.11.5 // indirect
|
||||||
github.com/danieljoos/wincred v1.2.3 // indirect
|
|
||||||
github.com/felixge/httpsnoop v1.0.4 // indirect
|
github.com/felixge/httpsnoop v1.0.4 // indirect
|
||||||
github.com/go-logr/logr v1.4.3 // indirect
|
github.com/go-logr/logr v1.4.3 // indirect
|
||||||
github.com/go-logr/stdr v1.2.2 // indirect
|
github.com/go-logr/stdr v1.2.2 // indirect
|
||||||
github.com/go-ole/go-ole v1.3.0 // indirect
|
|
||||||
github.com/go-openapi/jsonpointer v0.22.5 // indirect
|
github.com/go-openapi/jsonpointer v0.22.5 // indirect
|
||||||
github.com/go-openapi/swag/jsonname v0.25.5 // indirect
|
github.com/go-openapi/swag/jsonname v0.25.5 // indirect
|
||||||
github.com/go-resty/resty/v2 v2.13.1 // indirect
|
github.com/go-resty/resty/v2 v2.13.1 // indirect
|
||||||
github.com/godbus/dbus/v5 v5.2.2 // indirect
|
|
||||||
github.com/gofrs/flock v0.8.1 // indirect
|
github.com/gofrs/flock v0.8.1 // indirect
|
||||||
github.com/google/s2a-go v0.1.9 // indirect
|
github.com/google/s2a-go v0.1.9 // indirect
|
||||||
github.com/googleapis/enterprise-certificate-proxy v0.3.11 // indirect
|
github.com/googleapis/enterprise-certificate-proxy v0.3.11 // indirect
|
||||||
@@ -62,9 +56,6 @@ require (
|
|||||||
github.com/jackc/pgpassfile v1.0.0 // indirect
|
github.com/jackc/pgpassfile v1.0.0 // indirect
|
||||||
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect
|
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect
|
||||||
github.com/jackc/puddle/v2 v2.2.2 // indirect
|
github.com/jackc/puddle/v2 v2.2.2 // indirect
|
||||||
github.com/jchv/go-winloader v0.0.0-20250406163304-c1995be93bd1 // indirect
|
|
||||||
github.com/mattn/go-colorable v0.1.14 // indirect
|
|
||||||
github.com/mattn/go-isatty v0.0.20 // indirect
|
|
||||||
github.com/oasdiff/yaml v0.1.0 // indirect
|
github.com/oasdiff/yaml v0.1.0 // indirect
|
||||||
github.com/oasdiff/yaml3 v0.0.13 // indirect
|
github.com/oasdiff/yaml3 v0.0.13 // indirect
|
||||||
github.com/oracle/oci-go-sdk/v65 v65.95.2 // indirect
|
github.com/oracle/oci-go-sdk/v65 v65.95.2 // indirect
|
||||||
|
|||||||
29
go.sum
29
go.sum
@@ -7,8 +7,6 @@ cloud.google.com/go/compute/metadata v0.9.0/go.mod h1:E0bWwX5wTnLPedCKqk3pJmVgCB
|
|||||||
cloud.google.com/go/iam v1.1.11 h1:0mQ8UKSfdHLut6pH9FM3bI55KWR46ketn0PuXleDyxw=
|
cloud.google.com/go/iam v1.1.11 h1:0mQ8UKSfdHLut6pH9FM3bI55KWR46ketn0PuXleDyxw=
|
||||||
cloud.google.com/go/iam v1.1.11/go.mod h1:biXoiLWYIKntto2joP+62sd9uW5EpkZmKIvfNcTWlnQ=
|
cloud.google.com/go/iam v1.1.11/go.mod h1:biXoiLWYIKntto2joP+62sd9uW5EpkZmKIvfNcTWlnQ=
|
||||||
github.com/RaveNoX/go-jsoncommentstrip v1.0.0/go.mod h1:78ihd09MekBnJnxpICcwzCMzGrKSKYe4AqU6PDYYpjk=
|
github.com/RaveNoX/go-jsoncommentstrip v1.0.0/go.mod h1:78ihd09MekBnJnxpICcwzCMzGrKSKYe4AqU6PDYYpjk=
|
||||||
github.com/adrg/xdg v0.5.3 h1:xRnxJXne7+oWDatRhR1JLnvuccuIeCoBu2rtuLqQB78=
|
|
||||||
github.com/adrg/xdg v0.5.3/go.mod h1:nlTsY+NNiCBGCK2tpm09vRqfVzrc2fLmXGpBLF0zlTQ=
|
|
||||||
github.com/apapsch/go-jsonmerge/v2 v2.0.0 h1:axGnT1gRIfimI7gJifB699GoE/oq+F2MU7Dml6nw9rQ=
|
github.com/apapsch/go-jsonmerge/v2 v2.0.0 h1:axGnT1gRIfimI7gJifB699GoE/oq+F2MU7Dml6nw9rQ=
|
||||||
github.com/apapsch/go-jsonmerge/v2 v2.0.0/go.mod h1:lvDnEdqiQrp0O42VQGgmlKpxL1AP2+08jFMw88y4klk=
|
github.com/apapsch/go-jsonmerge/v2 v2.0.0/go.mod h1:lvDnEdqiQrp0O42VQGgmlKpxL1AP2+08jFMw88y4klk=
|
||||||
github.com/aws/aws-sdk-go-v2 v1.27.2 h1:pLsTXqX93rimAOZG2FIYraDQstZaaGVVN4tNw65v0h8=
|
github.com/aws/aws-sdk-go-v2 v1.27.2 h1:pLsTXqX93rimAOZG2FIYraDQstZaaGVVN4tNw65v0h8=
|
||||||
@@ -42,11 +40,7 @@ github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UF
|
|||||||
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
|
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
|
||||||
github.com/cncf/xds/go v0.0.0-20251210132809-ee656c7534f5 h1:6xNmx7iTtyBRev0+D/Tv1FZd4SCg8axKApyNyRsAt/w=
|
github.com/cncf/xds/go v0.0.0-20251210132809-ee656c7534f5 h1:6xNmx7iTtyBRev0+D/Tv1FZd4SCg8axKApyNyRsAt/w=
|
||||||
github.com/cncf/xds/go v0.0.0-20251210132809-ee656c7534f5/go.mod h1:KdCmV+x/BuvyMxRnYBlmVaq4OLiKW6iRQfvC62cvdkI=
|
github.com/cncf/xds/go v0.0.0-20251210132809-ee656c7534f5/go.mod h1:KdCmV+x/BuvyMxRnYBlmVaq4OLiKW6iRQfvC62cvdkI=
|
||||||
github.com/coder/websocket v1.8.14 h1:9L0p0iKiNOibykf283eHkKUHHrpG7f65OE3BhhO7v9g=
|
|
||||||
github.com/coder/websocket v1.8.14/go.mod h1:NX3SzP+inril6yawo5CQXx8+fk145lPDC6pumgx0mVg=
|
|
||||||
github.com/coreos/go-systemd/v22 v22.3.2/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc=
|
github.com/coreos/go-systemd/v22 v22.3.2/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc=
|
||||||
github.com/danieljoos/wincred v1.2.3 h1:v7dZC2x32Ut3nEfRH+vhoZGvN72+dQ/snVXo/vMFLdQ=
|
|
||||||
github.com/danieljoos/wincred v1.2.3/go.mod h1:6qqX0WNrS4RzPZ1tnroDzq9kY3fu1KwE7MRLQK4X0bs=
|
|
||||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
@@ -65,15 +59,11 @@ github.com/go-chi/chi/v5 v5.3.1 h1:3j4HZLGZQ3JpMCrPJF/Jl3mYJfWLKBfNJ6quurUGCf8=
|
|||||||
github.com/go-chi/chi/v5 v5.3.1/go.mod h1:R+tYY2hNuVUUjxoPtqUdgBqevM9s9njzkTLutVsOCto=
|
github.com/go-chi/chi/v5 v5.3.1/go.mod h1:R+tYY2hNuVUUjxoPtqUdgBqevM9s9njzkTLutVsOCto=
|
||||||
github.com/go-chi/cors v1.2.2 h1:Jmey33TE+b+rB7fT8MUy1u0I4L+NARQlK6LhzKPSyQE=
|
github.com/go-chi/cors v1.2.2 h1:Jmey33TE+b+rB7fT8MUy1u0I4L+NARQlK6LhzKPSyQE=
|
||||||
github.com/go-chi/cors v1.2.2/go.mod h1:sSbTewc+6wYHBBCW7ytsFSn836hqM7JxpglAy2Vzc58=
|
github.com/go-chi/cors v1.2.2/go.mod h1:sSbTewc+6wYHBBCW7ytsFSn836hqM7JxpglAy2Vzc58=
|
||||||
github.com/go-json-experiment/json v0.0.0-20251027170946-4849db3c2f7e h1:Lf/gRkoycfOBPa42vU2bbgPurFong6zXeFtPoxholzU=
|
|
||||||
github.com/go-json-experiment/json v0.0.0-20251027170946-4849db3c2f7e/go.mod h1:uNVvRXArCGbZ508SxYYTC5v1JWoz2voff5pm25jU1Ok=
|
|
||||||
github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
|
github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
|
||||||
github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI=
|
github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI=
|
||||||
github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
|
github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
|
||||||
github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
|
github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
|
||||||
github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
|
github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
|
||||||
github.com/go-ole/go-ole v1.3.0 h1:Dt6ye7+vXGIKZ7Xtk4s6/xVdGDQynvom7xCFEdWr6uE=
|
|
||||||
github.com/go-ole/go-ole v1.3.0/go.mod h1:5LS6F96DhAwUc7C+1HLexzMXY1xGRSryjyPPKW6zv78=
|
|
||||||
github.com/go-openapi/jsonpointer v0.22.5 h1:8on/0Yp4uTb9f4XvTrM2+1CPrV05QPZXu+rvu2o9jcA=
|
github.com/go-openapi/jsonpointer v0.22.5 h1:8on/0Yp4uTb9f4XvTrM2+1CPrV05QPZXu+rvu2o9jcA=
|
||||||
github.com/go-openapi/jsonpointer v0.22.5/go.mod h1:gyUR3sCvGSWchA2sUBJGluYMbe1zazrYWIkWPjjMUY0=
|
github.com/go-openapi/jsonpointer v0.22.5/go.mod h1:gyUR3sCvGSWchA2sUBJGluYMbe1zazrYWIkWPjjMUY0=
|
||||||
github.com/go-openapi/swag/jsonname v0.25.5 h1:8p150i44rv/Drip4vWI3kGi9+4W9TdI3US3uUYSFhSo=
|
github.com/go-openapi/swag/jsonname v0.25.5 h1:8p150i44rv/Drip4vWI3kGi9+4W9TdI3US3uUYSFhSo=
|
||||||
@@ -83,8 +73,6 @@ github.com/go-openapi/testify/v2 v2.4.0/go.mod h1:HCPmvFFnheKK2BuwSA0TbbdxJ3I16p
|
|||||||
github.com/go-resty/resty/v2 v2.13.1 h1:x+LHXBI2nMB1vqndymf26quycC4aggYJ7DECYbiz03g=
|
github.com/go-resty/resty/v2 v2.13.1 h1:x+LHXBI2nMB1vqndymf26quycC4aggYJ7DECYbiz03g=
|
||||||
github.com/go-resty/resty/v2 v2.13.1/go.mod h1:GznXlLxkq6Nh4sU59rPmUw3VtgpO3aS96ORAI6Q7d+0=
|
github.com/go-resty/resty/v2 v2.13.1/go.mod h1:GznXlLxkq6Nh4sU59rPmUw3VtgpO3aS96ORAI6Q7d+0=
|
||||||
github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
|
github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
|
||||||
github.com/godbus/dbus/v5 v5.2.2 h1:TUR3TgtSVDmjiXOgAAyaZbYmIeP3DPkld3jgKGV8mXQ=
|
|
||||||
github.com/godbus/dbus/v5 v5.2.2/go.mod h1:3AAv2+hPq5rdnr5txxxRwiGjPXamgoIHgz9FPBfOp3c=
|
|
||||||
github.com/gofrs/flock v0.8.1 h1:+gYjHKf32LDeiEEFhQaotPbLuUXjY5ZqxKgXy7n59aw=
|
github.com/gofrs/flock v0.8.1 h1:+gYjHKf32LDeiEEFhQaotPbLuUXjY5ZqxKgXy7n59aw=
|
||||||
github.com/gofrs/flock v0.8.1/go.mod h1:F1TvTiK9OcQqauNUHlbJvyl9Qa1QvF/gOUDKA14jxHU=
|
github.com/gofrs/flock v0.8.1/go.mod h1:F1TvTiK9OcQqauNUHlbJvyl9Qa1QvF/gOUDKA14jxHU=
|
||||||
github.com/golang-jwt/jwt/v5 v5.3.1 h1:kYf81DTWFe7t+1VvL7eS+jKFVWaUnK9cB1qbwn63YCY=
|
github.com/golang-jwt/jwt/v5 v5.3.1 h1:kYf81DTWFe7t+1VvL7eS+jKFVWaUnK9cB1qbwn63YCY=
|
||||||
@@ -115,19 +103,11 @@ github.com/jackc/pgx/v5 v5.10.0 h1:VhSvgU2jSli8o3AqIEOTJr7rZwAEUVo4E4XhR94Zfr0=
|
|||||||
github.com/jackc/pgx/v5 v5.10.0/go.mod h1:mal1tBGAFfLHvZzaYh77YS/eC6IX9OWbRV1QIIM0Jn4=
|
github.com/jackc/pgx/v5 v5.10.0/go.mod h1:mal1tBGAFfLHvZzaYh77YS/eC6IX9OWbRV1QIIM0Jn4=
|
||||||
github.com/jackc/puddle/v2 v2.2.2 h1:PR8nw+E/1w0GLuRFSmiioY6UooMp6KJv0/61nB7icHo=
|
github.com/jackc/puddle/v2 v2.2.2 h1:PR8nw+E/1w0GLuRFSmiioY6UooMp6KJv0/61nB7icHo=
|
||||||
github.com/jackc/puddle/v2 v2.2.2/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4=
|
github.com/jackc/puddle/v2 v2.2.2/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4=
|
||||||
github.com/jchv/go-winloader v0.0.0-20250406163304-c1995be93bd1 h1:njuLRcjAuMKr7kI3D85AXWkw6/+v9PwtV6M6o11sWHQ=
|
|
||||||
github.com/jchv/go-winloader v0.0.0-20250406163304-c1995be93bd1/go.mod h1:alcuEEnZsY1WQsagKhZDsoPCRoOijYqhZvPwLG0kzVs=
|
|
||||||
github.com/juju/gnuflag v0.0.0-20171113085948-2ce1bb71843d/go.mod h1:2PavIy+JPciBPrBUjwbNvtwB6RQlve+hkpll6QSNmOE=
|
github.com/juju/gnuflag v0.0.0-20171113085948-2ce1bb71843d/go.mod h1:2PavIy+JPciBPrBUjwbNvtwB6RQlve+hkpll6QSNmOE=
|
||||||
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
|
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
|
||||||
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
|
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
|
||||||
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
||||||
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
|
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
|
||||||
github.com/matryer/is v1.4.1 h1:55ehd8zaGABKLXQUe2awZ99BD/PTc2ls+KV/dXphgEQ=
|
|
||||||
github.com/matryer/is v1.4.1/go.mod h1:8I/i5uYgLzgsgEloJE1U6xx5HkBQpAZvepWuujKwMRU=
|
|
||||||
github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE=
|
|
||||||
github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8=
|
|
||||||
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
|
|
||||||
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
|
||||||
github.com/modelcontextprotocol/go-sdk v1.6.1 h1:0zOSupjKUxPKSocPT1Wtago+mUHU2/uZ4xSOY0FGReU=
|
github.com/modelcontextprotocol/go-sdk v1.6.1 h1:0zOSupjKUxPKSocPT1Wtago+mUHU2/uZ4xSOY0FGReU=
|
||||||
github.com/modelcontextprotocol/go-sdk v1.6.1/go.mod h1:kzm3kzFL1/+AziGOE0nUs3gvPoNxMCvkxokMkuFapXQ=
|
github.com/modelcontextprotocol/go-sdk v1.6.1/go.mod h1:kzm3kzFL1/+AziGOE0nUs3gvPoNxMCvkxokMkuFapXQ=
|
||||||
github.com/oapi-codegen/nullable v1.1.0 h1:eAh8JVc5430VtYVnq00Hrbpag9PFRGWLjxR1/3KntMs=
|
github.com/oapi-codegen/nullable v1.1.0 h1:eAh8JVc5430VtYVnq00Hrbpag9PFRGWLjxR1/3KntMs=
|
||||||
@@ -164,8 +144,6 @@ github.com/spkg/bom v0.0.0-20160624110644-59b7046e48ad/go.mod h1:qLr4V1qq6nMqFKk
|
|||||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||||
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
|
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
|
||||||
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
|
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
|
||||||
github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY=
|
|
||||||
github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=
|
|
||||||
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
||||||
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||||
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||||
@@ -183,16 +161,12 @@ github.com/tidwall/pretty v1.2.1 h1:qjsOFOWWQl+N3RsoF5/ssm1pHmJJwhjlSbZ51I6wMl4=
|
|||||||
github.com/tidwall/pretty v1.2.1/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU=
|
github.com/tidwall/pretty v1.2.1/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU=
|
||||||
github.com/tidwall/sjson v1.2.5 h1:kLy8mja+1c9jlljvWTlSazM7cKDRfJuR/bOJhcY5NcY=
|
github.com/tidwall/sjson v1.2.5 h1:kLy8mja+1c9jlljvWTlSazM7cKDRfJuR/bOJhcY5NcY=
|
||||||
github.com/tidwall/sjson v1.2.5/go.mod h1:Fvgq9kS/6ociJEDnK0Fk1cpYF4FIW6ZF7LAe+6jwd28=
|
github.com/tidwall/sjson v1.2.5/go.mod h1:Fvgq9kS/6ociJEDnK0Fk1cpYF4FIW6ZF7LAe+6jwd28=
|
||||||
github.com/wailsapp/wails/v3 v3.0.0-alpha2.117 h1:udyjqPG3AIgkod5QDR/WblCkpV8R86BFPSrsWxSyt5Y=
|
|
||||||
github.com/wailsapp/wails/v3 v3.0.0-alpha2.117/go.mod h1:74WH2FScMsgucZvHHvv7eOefDXCm/CjuIxqhhZgPhKg=
|
|
||||||
github.com/yosida95/uritemplate/v3 v3.0.2 h1:Ed3Oyj9yrmi9087+NczuL5BwkIc4wvTb5zIM+UJPGz4=
|
github.com/yosida95/uritemplate/v3 v3.0.2 h1:Ed3Oyj9yrmi9087+NczuL5BwkIc4wvTb5zIM+UJPGz4=
|
||||||
github.com/yosida95/uritemplate/v3 v3.0.2/go.mod h1:ILOh0sOhIJR3+L/8afwt/kE++YT040gmv5BQTMR2HP4=
|
github.com/yosida95/uritemplate/v3 v3.0.2/go.mod h1:ILOh0sOhIJR3+L/8afwt/kE++YT040gmv5BQTMR2HP4=
|
||||||
github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78 h1:ilQV1hzziu+LLM3zUTJ0trRztfwgjqKnBWNtSRkbmwM=
|
github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78 h1:ilQV1hzziu+LLM3zUTJ0trRztfwgjqKnBWNtSRkbmwM=
|
||||||
github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78/go.mod h1:aL8wCCfTfSfmXjznFBSZNN13rSJjlIOI1fUNAtF7rmI=
|
github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78/go.mod h1:aL8wCCfTfSfmXjznFBSZNN13rSJjlIOI1fUNAtF7rmI=
|
||||||
github.com/yuin/goldmark v1.4.0/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k=
|
github.com/yuin/goldmark v1.4.0/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k=
|
||||||
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
|
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
|
||||||
github.com/zalando/go-keyring v0.2.8 h1:6sD/Ucpl7jNq10rM2pgqTs0sZ9V3qMrqfIIy5YPccHs=
|
|
||||||
github.com/zalando/go-keyring v0.2.8/go.mod h1:tsMo+VpRq5NGyKfxoBVjCuMrG47yj8cmakZDO5QGii0=
|
|
||||||
go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64=
|
go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64=
|
||||||
go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y=
|
go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y=
|
||||||
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.61.0 h1:q4XOmH/0opmeuJtPsbFNivyl7bCt7yRBbeEm2sC/XtQ=
|
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.61.0 h1:q4XOmH/0opmeuJtPsbFNivyl7bCt7yRBbeEm2sC/XtQ=
|
||||||
@@ -242,16 +216,13 @@ golang.org/x/sync v0.21.0 h1:HLII4xRRTtCRkxYp4HNFF0Js/Og6q2i++KXbg0gHCwM=
|
|||||||
golang.org/x/sync v0.21.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0=
|
golang.org/x/sync v0.21.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0=
|
||||||
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||||
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
golang.org/x/sys v0.0.0-20200810151505-1b9f1253b3ed/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
|
||||||
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.0.0-20210809222454-d867a43fc93e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.0.0-20210809222454-d867a43fc93e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
|
||||||
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
|
||||||
golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||||
golang.org/x/sys v0.19.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
golang.org/x/sys v0.19.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||||
|
|||||||
3
internal/remote/.kilo/kilo.jsonc
Normal file
3
internal/remote/.kilo/kilo.jsonc
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
{
|
||||||
|
"snapshot": false
|
||||||
|
}
|
||||||
@@ -238,7 +238,7 @@ echo "[6/8] prune old image tags (keep 3)"
|
|||||||
if [ -n "$OIKOS_VERSION" ]; then
|
if [ -n "$OIKOS_VERSION" ]; then
|
||||||
images=$(docker compose --profile "$PROFILE" config --images 2>/dev/null || true)
|
images=$(docker compose --profile "$PROFILE" config --images 2>/dev/null || true)
|
||||||
if [ -z "$images" ]; then
|
if [ -z "$images" ]; then
|
||||||
images="oikos-api oikos-scheduler oikos-migrate oikos-seed oikos-nomos oikos-web"
|
images="oikos-api oikos-scheduler oikos-migrate oikos-seed oikos-nomos"
|
||||||
fi
|
fi
|
||||||
printf '%s\n' $images | sed 's/:.*//' | grep '^oikos-' | sort -u | while read -r repo; do
|
printf '%s\n' $images | sed 's/:.*//' | grep '^oikos-' | sort -u | while read -r repo; do
|
||||||
docker image ls "$repo" --format '{{.Tag}}' 2>/dev/null | grep '^v' | sort -rV | tail -n +4 | while read -r tag; do
|
docker image ls "$repo" --format '{{.Tag}}' 2>/dev/null | grep '^v' | sort -rV | tail -n +4 | while read -r tag; do
|
||||||
|
|||||||
77
vendor/github.com/adrg/xdg/CODE_OF_CONDUCT.md
generated
vendored
77
vendor/github.com/adrg/xdg/CODE_OF_CONDUCT.md
generated
vendored
@@ -1,77 +0,0 @@
|
|||||||
# Contributor Covenant Code of Conduct
|
|
||||||
|
|
||||||
## Our Pledge
|
|
||||||
|
|
||||||
In the interest of fostering an open and welcoming environment, we as
|
|
||||||
contributors and maintainers pledge to making participation in our project and
|
|
||||||
our community a harassment-free experience for everyone, regardless of age,
|
|
||||||
body size, disability, ethnicity, sex characteristics, gender identity and
|
|
||||||
expression, level of experience, education, socio-economic status, nationality,
|
|
||||||
personal appearance, race, religion, or sexual identity and orientation.
|
|
||||||
|
|
||||||
## Our Standards
|
|
||||||
|
|
||||||
Examples of behaviour that contributes to creating a positive environment
|
|
||||||
include:
|
|
||||||
|
|
||||||
* Using welcoming and inclusive language
|
|
||||||
* Being respectful of differing viewpoints and experiences
|
|
||||||
* Gracefully accepting constructive criticism
|
|
||||||
* Focusing on what is best for the community
|
|
||||||
* Showing empathy towards other community members
|
|
||||||
|
|
||||||
Examples of unacceptable behaviour by participants include:
|
|
||||||
|
|
||||||
* The use of sexualized language or imagery and unwelcome sexual attention or
|
|
||||||
advances
|
|
||||||
* Trolling, insulting/derogatory comments, and personal or political attacks
|
|
||||||
* Public or private harassment
|
|
||||||
* Publishing others' private information, such as a physical or electronic
|
|
||||||
address, without explicit permission
|
|
||||||
* Other conduct which could reasonably be considered inappropriate in a
|
|
||||||
professional setting
|
|
||||||
|
|
||||||
## Our Responsibilities
|
|
||||||
|
|
||||||
Project maintainers are responsible for clarifying the standards of acceptable
|
|
||||||
behaviour and are expected to take appropriate and fair corrective action in
|
|
||||||
response to any instances of unacceptable behaviour.
|
|
||||||
|
|
||||||
Project maintainers have the right and responsibility to remove, edit, or
|
|
||||||
reject comments, commits, code, wiki edits, issues, and other contributions
|
|
||||||
that are not aligned to this Code of Conduct, or to ban temporarily or
|
|
||||||
permanently any contributor for other behaviour that they deem inappropriate,
|
|
||||||
threatening, offensive, or harmful.
|
|
||||||
|
|
||||||
## Scope
|
|
||||||
|
|
||||||
This Code of Conduct applies both within project spaces and in public spaces
|
|
||||||
when an individual is representing the project or its community. Examples of
|
|
||||||
representing a project or community include using an official project e-mail
|
|
||||||
address, posting via an official social media account, or acting as an appointed
|
|
||||||
representative at an online or offline event. Representation of a project may be
|
|
||||||
further defined and clarified by project maintainers.
|
|
||||||
|
|
||||||
## Enforcement
|
|
||||||
|
|
||||||
Instances of abusive, harassing, or otherwise unacceptable behaviour may be
|
|
||||||
reported by contacting the project team at adrg@epistack.com. All complaints
|
|
||||||
will be reviewed and investigated and will result in a response that is deemed
|
|
||||||
necessary and appropriate to the circumstances. The project team is obligated to
|
|
||||||
maintain confidentiality with regard to the reporter of an incident.
|
|
||||||
Further details of specific enforcement policies may be posted separately.
|
|
||||||
|
|
||||||
Project maintainers who do not follow or enforce the Code of Conduct in good
|
|
||||||
faith may face temporary or permanent repercussions as determined by other
|
|
||||||
members of the project's leadership.
|
|
||||||
|
|
||||||
## Attribution
|
|
||||||
|
|
||||||
This Code of Conduct is adapted from the [Contributor Covenant][homepage],
|
|
||||||
version 1.4, available at
|
|
||||||
https://www.contributor-covenant.org/version/1/4/code-of-conduct.html
|
|
||||||
|
|
||||||
[homepage]: https://www.contributor-covenant.org
|
|
||||||
|
|
||||||
For answers to common questions about this code of conduct, see
|
|
||||||
https://www.contributor-covenant.org/faq
|
|
||||||
135
vendor/github.com/adrg/xdg/CONTRIBUTING.md
generated
vendored
135
vendor/github.com/adrg/xdg/CONTRIBUTING.md
generated
vendored
@@ -1,135 +0,0 @@
|
|||||||
# Contributing to this project
|
|
||||||
|
|
||||||
Contributions in the form of pull requests, issues or just general feedback,
|
|
||||||
are always welcome. Please take a moment to review this document in order to
|
|
||||||
make the contribution process easy and effective for everyone involved.
|
|
||||||
|
|
||||||
Following these guidelines helps to communicate that you respect the time of
|
|
||||||
the developers managing and developing this open source project. In return,
|
|
||||||
they should reciprocate that respect in addressing your issue or assessing
|
|
||||||
patches and features.
|
|
||||||
|
|
||||||
## Using the issue tracker
|
|
||||||
|
|
||||||
The issue tracker is the preferred channel for [bug reports](#bugs),
|
|
||||||
[features requests](#features) and [submitting pull
|
|
||||||
requests](#pull-requests), but please respect the following restrictions:
|
|
||||||
|
|
||||||
* Please **do not** use the issue tracker for personal support requests (use
|
|
||||||
[Stack Overflow](http://stackoverflow.com) or IRC).
|
|
||||||
* Please **do not** derail or troll issues. Keep the discussion on topic and
|
|
||||||
respect the opinions of others.
|
|
||||||
|
|
||||||
<a name="bugs"></a>
|
|
||||||
## Bug reports
|
|
||||||
|
|
||||||
A bug is a _demonstrable problem_ that is caused by the code in the repository.
|
|
||||||
Good bug reports are extremely helpful - thank you!
|
|
||||||
|
|
||||||
Guidelines for bug reports:
|
|
||||||
|
|
||||||
1. **Use the GitHub issue search** — check if the issue has already been
|
|
||||||
reported.
|
|
||||||
2. **Check if the issue has been fixed** — try to reproduce it using the
|
|
||||||
latest `master` or development branch in the repository.
|
|
||||||
3. **Isolate the problem** — create a reduced test case.
|
|
||||||
|
|
||||||
A good bug report shouldn't leave others needing to chase you up for more
|
|
||||||
information. Please try to be as detailed as possible in your report. What is
|
|
||||||
your environment? What steps will reproduce the issue? What browser(s) and OS
|
|
||||||
experience the problem? What would you expect to be the outcome? All these
|
|
||||||
details will help people to fix any potential bugs.
|
|
||||||
|
|
||||||
Example:
|
|
||||||
|
|
||||||
> Short and descriptive example bug report title
|
|
||||||
>
|
|
||||||
> A summary of the issue and the browser/OS environment in which it occurs. If
|
|
||||||
> suitable, include the steps required to reproduce the bug.
|
|
||||||
>
|
|
||||||
> 1. This is the first step
|
|
||||||
> 2. This is the second step
|
|
||||||
> 3. Further steps, etc.
|
|
||||||
>
|
|
||||||
> `<url>` - a link to the reduced test case
|
|
||||||
>
|
|
||||||
> Any other information you want to share that is relevant to the issue being
|
|
||||||
> reported. This might include the lines of code that you have identified as
|
|
||||||
> causing the bug, and potential solutions (and your opinions on their
|
|
||||||
> merits).
|
|
||||||
|
|
||||||
|
|
||||||
<a name="features"></a>
|
|
||||||
## Feature requests
|
|
||||||
|
|
||||||
Feature requests are welcome. But take a moment to find out whether your idea
|
|
||||||
fits with the scope and aims of the project. It's up to *you* to make a strong
|
|
||||||
case to convince the project's developers of the merits of this feature. Please
|
|
||||||
provide as much detail and context as possible.
|
|
||||||
|
|
||||||
|
|
||||||
<a name="pull-requests"></a>
|
|
||||||
## Pull requests
|
|
||||||
|
|
||||||
Good pull requests - patches, improvements, new features - are a fantastic
|
|
||||||
help. They should remain focused in scope and avoid containing unrelated
|
|
||||||
commits.
|
|
||||||
|
|
||||||
**Please ask first** before embarking on any significant pull request (e.g.
|
|
||||||
implementing features, refactoring code, porting to a different language),
|
|
||||||
otherwise you risk spending a lot of time working on something that the
|
|
||||||
project's developers might not want to merge into the project.
|
|
||||||
|
|
||||||
Please adhere to the coding conventions used throughout a project (indentation,
|
|
||||||
accurate comments, etc.) and any other requirements (such as test coverage).
|
|
||||||
|
|
||||||
Follow this process if you'd like your work considered for inclusion in the
|
|
||||||
project:
|
|
||||||
|
|
||||||
1. [Fork](http://help.github.com/fork-a-repo/) the project, clone your fork,
|
|
||||||
and configure the remotes:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Clone your fork of the repo into the current directory
|
|
||||||
git clone https://github.com/<your-username>/<repo-name>
|
|
||||||
# Navigate to the newly cloned directory
|
|
||||||
cd <repo-name>
|
|
||||||
# Assign the original repo to a remote called "upstream"
|
|
||||||
git remote add upstream https://github.com/<upstream-owner>/<repo-name>
|
|
||||||
```
|
|
||||||
|
|
||||||
2. If you cloned a while ago, get the latest changes from upstream:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
git checkout <dev-branch>
|
|
||||||
git pull upstream <dev-branch>
|
|
||||||
```
|
|
||||||
|
|
||||||
3. Create a new topic branch (off the main project development branch) to
|
|
||||||
contain your feature, change, or fix:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
git checkout -b <topic-branch-name>
|
|
||||||
```
|
|
||||||
|
|
||||||
4. Commit your changes in logical chunks and use descriptive commit messages.
|
|
||||||
Use [interactive rebase](https://help.github.com/articles/interactive-rebase)
|
|
||||||
to tidy up your commits before making them public.
|
|
||||||
|
|
||||||
5. Locally merge (or rebase) the upstream development branch into your topic branch:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
git pull [--rebase] upstream <dev-branch>
|
|
||||||
```
|
|
||||||
|
|
||||||
6. Push your topic branch up to your fork:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
git push origin <topic-branch-name>
|
|
||||||
```
|
|
||||||
|
|
||||||
7. [Open a Pull Request](https://help.github.com/articles/using-pull-requests/)
|
|
||||||
with a clear title and description.
|
|
||||||
|
|
||||||
**IMPORTANT**: By submitting a patch, you agree to allow the project owner to
|
|
||||||
license your work under the same license as that used by the project.
|
|
||||||
21
vendor/github.com/adrg/xdg/LICENSE
generated
vendored
21
vendor/github.com/adrg/xdg/LICENSE
generated
vendored
@@ -1,21 +0,0 @@
|
|||||||
The MIT License (MIT)
|
|
||||||
|
|
||||||
Copyright (c) 2014 Adrian-George Bostan <adrg@epistack.com>
|
|
||||||
|
|
||||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
||||||
of this software and associated documentation files (the "Software"), to deal
|
|
||||||
in the Software without restriction, including without limitation the rights
|
|
||||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
||||||
copies of the Software, and to permit persons to whom the Software is
|
|
||||||
furnished to do so, subject to the following conditions:
|
|
||||||
|
|
||||||
The above copyright notice and this permission notice shall be included in
|
|
||||||
all copies or substantial portions of the Software.
|
|
||||||
|
|
||||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
||||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
||||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
||||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
||||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
||||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
|
||||||
THE SOFTWARE.
|
|
||||||
302
vendor/github.com/adrg/xdg/README.md
generated
vendored
302
vendor/github.com/adrg/xdg/README.md
generated
vendored
@@ -1,302 +0,0 @@
|
|||||||
<h1 align="center">
|
|
||||||
<div>
|
|
||||||
<img src="https://raw.githubusercontent.com/adrg/adrg.github.io/master/assets/projects/xdg/logo.svg" alt="xdg logo"/>
|
|
||||||
</div>
|
|
||||||
</h1>
|
|
||||||
|
|
||||||
<h4 align="center">Go implementation of the XDG Base Directory Specification and XDG user directories.</h4>
|
|
||||||
|
|
||||||
<p align="center">
|
|
||||||
<a href="https://github.com/adrg/xdg/actions/workflows/tests.yml">
|
|
||||||
<img alt="Tests status" src="https://github.com/adrg/xdg/actions/workflows/tests.yml/badge.svg">
|
|
||||||
</a>
|
|
||||||
<a href="https://app.codecov.io/gh/adrg/xdg">
|
|
||||||
<img alt="Code coverage" src="https://codecov.io/gh/adrg/xdg/branch/master/graphs/badge.svg?branch=master">
|
|
||||||
</a>
|
|
||||||
<a href="https://pkg.go.dev/github.com/adrg/xdg">
|
|
||||||
<img alt="pkg.go.dev documentation" src="https://img.shields.io/badge/go.dev-reference-007d9c?logo=go&logoColor=white">
|
|
||||||
</a>
|
|
||||||
<a href="https://opensource.org/licenses/MIT" rel="nofollow">
|
|
||||||
<img alt="MIT license" src="https://img.shields.io/github/license/adrg/xdg">
|
|
||||||
</a>
|
|
||||||
<br />
|
|
||||||
<a href="https://goreportcard.com/report/github.com/adrg/xdg">
|
|
||||||
<img alt="Go report card" src="https://goreportcard.com/badge/github.com/adrg/xdg">
|
|
||||||
</a>
|
|
||||||
<a href="https://github.com/avelino/awesome-go#configuration">
|
|
||||||
<img alt="Awesome Go" src="https://awesome.re/mentioned-badge.svg">
|
|
||||||
</a>
|
|
||||||
<a href="https://github.com/adrg/xdg/graphs/contributors">
|
|
||||||
<img alt="GitHub contributors" src="https://img.shields.io/github/contributors/adrg/xdg" />
|
|
||||||
</a>
|
|
||||||
<a href="https://github.com/adrg/xdg/issues">
|
|
||||||
<img alt="GitHub open issues" src="https://img.shields.io/github/issues-raw/adrg/xdg">
|
|
||||||
</a>
|
|
||||||
<a href="https://ko-fi.com/T6T72WATK">
|
|
||||||
<img alt="Buy me a coffee" src="https://img.shields.io/static/v1.svg?label=%20&message=Buy%20me%20a%20coffee&color=579fbf&logo=buy%20me%20a%20coffee&logoColor=white">
|
|
||||||
</a>
|
|
||||||
</p>
|
|
||||||
|
|
||||||
Provides an implementation of the [XDG Base Directory Specification](https://specifications.freedesktop.org/basedir-spec/basedir-spec-latest.html).
|
|
||||||
The specification defines a set of standard paths for storing application files,
|
|
||||||
including data and configuration files. For portability and flexibility reasons,
|
|
||||||
applications should use the XDG defined locations instead of hardcoding paths.
|
|
||||||
|
|
||||||
The package also includes the locations of well known [user directories](https://wiki.archlinux.org/index.php/XDG_user_directories),
|
|
||||||
support for the non-standard `XDG_BIN_HOME` directory, as well as other common directories such as fonts and applications.
|
|
||||||
|
|
||||||
The current implementation supports **most flavors of Unix**, **Windows**, **macOS** and **Plan 9**.
|
|
||||||
On Windows, where XDG environment variables are not usually set, the package uses [Known Folders](https://docs.microsoft.com/en-us/windows/win32/shell/known-folders)
|
|
||||||
as defaults. Therefore, appropriate locations are used for common [folders](https://docs.microsoft.com/en-us/windows/win32/shell/knownfolderid) which may have been redirected.
|
|
||||||
|
|
||||||
See usage [examples](#usage) below. Full documentation can be found at https://pkg.go.dev/github.com/adrg/xdg.
|
|
||||||
|
|
||||||
## Installation
|
|
||||||
go get github.com/adrg/xdg
|
|
||||||
|
|
||||||
## Default locations
|
|
||||||
|
|
||||||
The package defines sensible defaults for XDG variables which are empty or not
|
|
||||||
present in the environment.
|
|
||||||
|
|
||||||
- On Unix-like operating systems, XDG environment variables are typically defined.
|
|
||||||
Appropriate default locations are used for the environment variables which are not set.
|
|
||||||
- On Windows, XDG environment variables are usually not set. If that is the case,
|
|
||||||
the package relies on the appropriate [Known Folders](https://docs.microsoft.com/en-us/windows/win32/shell/knownfolderid).
|
|
||||||
Sensible fallback locations are used for the folders which are not set.
|
|
||||||
|
|
||||||
### XDG Base Directory
|
|
||||||
|
|
||||||
<details open>
|
|
||||||
<summary><strong>Unix-like operating systems</strong></summary>
|
|
||||||
<br/>
|
|
||||||
|
|
||||||
| <a href="#xdg-base-directory"><img width="400" height="0"></a> | <a href="#xdg-base-directory"><img width="500" height="0"></a><p>Unix</p> | <a href="#xdg-base-directory"><img width="600" height="0"></a><p>macOS</p> | <a href="#xdg-base-directory"><img width="500" height="0"></a><p>Plan 9</p> |
|
|
||||||
| :------------------------------------------------------------: | :-----------------------------------------------------------------------: | :-------------------------------------------------------------------------------------------------------------------------------------------------: | :-------------------------------------------------------------------------: |
|
|
||||||
| <kbd><b>XDG_DATA_HOME</b></kbd> | <kbd>~/.local/share</kbd> | <kbd>~/Library/Application Support</kbd> | <kbd>$home/lib</kbd> |
|
|
||||||
| <kbd><b>XDG_DATA_DIRS</b></kbd> | <kbd>/usr/local/share</kbd><br/><kbd>/usr/share</kbd> | <kbd>/Library/Application Support</kbd><kbd>~/.local/share</kbd> | <kbd>/lib</kbd> |
|
|
||||||
| <kbd><b>XDG_CONFIG_HOME</b></kbd> | <kbd>~/.config</kbd> | <kbd>~/Library/Application Support</kbd> | <kbd>$home/lib</kbd> |
|
|
||||||
| <kbd><b>XDG_CONFIG_DIRS</b></kbd> | <kbd>/etc/xdg</kbd> | <kbd>~/Library/Preferences</kbd><br/><kbd>/Library/Application Support</kbd><br/><kbd>/Library/Preferences</kbd><br/><kbd>~/.config</kbd> | <kbd>/lib</kbd> |
|
|
||||||
| <kbd><b>XDG_STATE_HOME</b></kbd> | <kbd>~/.local/state</kbd> | <kbd>~/Library/Application Support</kbd> | <kbd>$home/lib/state</kbd> |
|
|
||||||
| <kbd><b>XDG_CACHE_HOME</b></kbd> | <kbd>~/.cache</kbd> | <kbd>~/Library/Caches</kbd> | <kbd>$home/lib/cache</kbd> |
|
|
||||||
| <kbd><b>XDG_RUNTIME_DIR</b></kbd> | <kbd>/run/user/$UID</kbd> | <kbd>~/Library/Application Support</kbd> | <kbd>/tmp</kbd> |
|
|
||||||
| <kbd><b>XDG_BIN_HOME</b></kbd> | <kbd>~/.local/bin</kbd> | <kbd>~/.local/bin</kbd> | <kbd>$home/bin</kbd> |
|
|
||||||
|
|
||||||
</details>
|
|
||||||
|
|
||||||
<details open>
|
|
||||||
<summary><strong>Microsoft Windows</strong></summary>
|
|
||||||
<br/>
|
|
||||||
|
|
||||||
| <a href="#xdg-base-directory"><img width="400" height="0"></a> | <a href="#xdg-base-directory"><img width="700" height="0"></a><p>Known Folder(s)</p> | <a href="#xdg-base-directory"><img width="900" height="0"></a><p>Fallback(s)</p> |
|
|
||||||
| :------------------------------------------------------------: | :---------------------------------------------------------------------------------------: | :------------------------------------------------------------------------------: |
|
|
||||||
| <kbd><b>XDG_DATA_HOME</b></kbd> | <kbd>LocalAppData</kbd> | <kbd>%LOCALAPPDATA%</kbd> |
|
|
||||||
| <kbd><b>XDG_DATA_DIRS</b></kbd> | <kbd>RoamingAppData</kbd><br/><kbd>ProgramData</kbd> | <kbd>%APPADATA%</kbd><br/><kbd>%ProgramData%</kbd> |
|
|
||||||
| <kbd><b>XDG_CONFIG_HOME</b></kbd> | <kbd>LocalAppData</kbd> | <kbd>%LOCALAPPDATA%</kbd> |
|
|
||||||
| <kbd><b>XDG_CONFIG_DIRS</b></kbd> | <kbd>ProgramData</kbd><br/><kbd>RoamingAppData</kbd> | <kbd>%ProgramData%</kbd><br/><kbd>%APPDATA%</kbd> |
|
|
||||||
| <kbd><b>XDG_STATE_HOME</b></kbd> | <kbd>LocalAppData</kbd> | <kbd>%LOCALAPPDATA%</kbd> |
|
|
||||||
| <kbd><b>XDG_CACHE_HOME</b></kbd> | <kbd>LocalAppData\cache</kbd> | <kbd>%LOCALAPPDATA%\cache</kbd> |
|
|
||||||
| <kbd><b>XDG_RUNTIME_DIR</b></kbd> | <kbd>LocalAppData</kbd> | <kbd>%LOCALAPPDATA%</kbd> |
|
|
||||||
| <kbd><b>XDG_BIN_HOME</b></kbd> | <kbd>UserProgramFiles</kbd> | <kbd>%LOCALAPPDATA%\Programs</kbd> |
|
|
||||||
|
|
||||||
</details>
|
|
||||||
|
|
||||||
### XDG user directories
|
|
||||||
|
|
||||||
XDG user directories environment variables are usually **not** set on most
|
|
||||||
operating systems. However, if they are present in the environment, they take
|
|
||||||
precedence. Appropriate fallback locations are used for the environment
|
|
||||||
variables which are not set.
|
|
||||||
|
|
||||||
- On Unix-like operating systems (except macOS and Plan 9), the package reads the [user-dirs.dirs](https://man.archlinux.org/man/user-dirs.dirs.5.en) config file.
|
|
||||||
- On Windows, the package uses the appropriate [Known Folders](https://docs.microsoft.com/en-us/windows/win32/shell/knownfolderid).
|
|
||||||
|
|
||||||
Lastly, default locations are used for any user directories which are not set,
|
|
||||||
as shown in the following tables.
|
|
||||||
|
|
||||||
<details open>
|
|
||||||
<summary><strong>Unix-like operating systems</strong></summary>
|
|
||||||
<br/>
|
|
||||||
|
|
||||||
| <a href="#xdg-user-directories"><img width="500" height="0"></a> | <a href="#xdg-user-directories"><img width="500" height="0"></a><p>Unix</p> | <a href="#xdg-user-directories"><img width="500" height="0"></a><p>macOS</p> | <a href="#xdg-user-directories"><img width="500" height="0"></a><p>Plan 9</p> |
|
|
||||||
| :--------------------------------------------------------------: | :-------------------------------------------------------------------------: | :---------------------------------------------------------------------------: | :---------------------------------------------------------------------------: |
|
|
||||||
| <kbd><b>XDG_DESKTOP_DIR</b></kbd> | <kbd>~/Desktop</kbd> | <kbd>~/Desktop</kbd> | <kbd>$home/desktop</kbd> |
|
|
||||||
| <kbd><b>XDG_DOWNLOAD_DIR</b></kbd> | <kbd>~/Downloads</kbd> | <kbd>~/Downloads</kbd> | <kbd>$home/downloads</kbd> |
|
|
||||||
| <kbd><b>XDG_DOCUMENTS_DIR</b></kbd> | <kbd>~/Documents</kbd> | <kbd>~/Documents</kbd> | <kbd>$home/documents</kbd> |
|
|
||||||
| <kbd><b>XDG_MUSIC_DIR</b></kbd> | <kbd>~/Music</kbd> | <kbd>~/Music</kbd> | <kbd>$home/music</kbd> |
|
|
||||||
| <kbd><b>XDG_PICTURES_DIR</b></kbd> | <kbd>~/Pictures</kbd> | <kbd>~/Pictures</kbd> | <kbd>$home/pictures</kbd> |
|
|
||||||
| <kbd><b>XDG_VIDEOS_DIR</b></kbd> | <kbd>~/Videos</kbd> | <kbd>~/Movies</kbd> | <kbd>$home/videos</kbd> |
|
|
||||||
| <kbd><b>XDG_TEMPLATES_DIR</b></kbd> | <kbd>~/Templates</kbd> | <kbd>~/Templates</kbd> | <kbd>$home/templates</kbd> |
|
|
||||||
| <kbd><b>XDG_PUBLICSHARE_DIR</b></kbd> | <kbd>~/Public</kbd> | <kbd>~/Public</kbd> | <kbd>$home/public</kbd> |
|
|
||||||
|
|
||||||
</details>
|
|
||||||
|
|
||||||
<details open>
|
|
||||||
<summary><strong>Microsoft Windows</strong></summary>
|
|
||||||
<br/>
|
|
||||||
|
|
||||||
| <a href="#xdg-user-directories"><img width="500" height="0"></a> | <a href="#xdg-user-directories"><img width="600" height="0"></a><p>Known Folder(s)</p> | <a href="#xdg-user-directories"><img width="900" height="0"></a><p>Fallback(s)</p> |
|
|
||||||
| :--------------------------------------------------------------: | :-----------------------------------------------------------------------------------------: | :--------------------------------------------------------------------------------: |
|
|
||||||
| <kbd><b>XDG_DESKTOP_DIR</b></kbd> | <kbd>Desktop</kbd> | <kbd>%USERPROFILE%\Desktop</kbd> |
|
|
||||||
| <kbd><b>XDG_DOWNLOAD_DIR</b></kbd> | <kbd>Downloads</kbd> | <kbd>%USERPROFILE%\Downloads</kbd> |
|
|
||||||
| <kbd><b>XDG_DOCUMENTS_DIR</b></kbd> | <kbd>Documents</kbd> | <kbd>%USERPROFILE%\Documents</kbd> |
|
|
||||||
| <kbd><b>XDG_MUSIC_DIR</b></kbd> | <kbd>Music</kbd> | <kbd>%USERPROFILE%\Music</kbd> |
|
|
||||||
| <kbd><b>XDG_PICTURES_DIR</b></kbd> | <kbd>Pictures</kbd> | <kbd>%USERPROFILE%\Pictures</kbd> |
|
|
||||||
| <kbd><b>XDG_VIDEOS_DIR</b></kbd> | <kbd>Videos</kbd> | <kbd>%USERPROFILE%\Videos</kbd> |
|
|
||||||
| <kbd><b>XDG_TEMPLATES_DIR</b></kbd> | <kbd>Templates</kbd> | <kbd>%APPDATA%\Microsoft\Windows\Templates</kbd> |
|
|
||||||
| <kbd><b>XDG_PUBLICSHARE_DIR</b></kbd> | <kbd>Public</kbd> | <kbd>%PUBLIC%</kbd> |
|
|
||||||
|
|
||||||
</details>
|
|
||||||
|
|
||||||
### Other directories
|
|
||||||
|
|
||||||
<details open>
|
|
||||||
<summary><strong>Unix-like operating systems</strong></summary>
|
|
||||||
<br/>
|
|
||||||
|
|
||||||
| <a href="#other-directories"><img width="400" height="0"></a> | <a href="#other-directories"><img width="600" height="0"></a><p>Unix</p> | <a href="#other-directories"><img width="600" height="0"></a><p>macOS</p> | <a href="#other-directories"><img width="400" height="0"></a><p>Plan 9</p> |
|
|
||||||
| :-----------------------------------------------------------: | :--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------: | :---------------------------------------------------------------------------------------------------------------------------------: | :------------------------------------------------------------------------: |
|
|
||||||
| <kbd><b>Home</b></kbd> | <kbd>$HOME</kbd> | <kbd>$HOME</kbd> | <kbd>$home</kbd> |
|
|
||||||
| <kbd><b>Applications</b></kbd> | <kbd>$XDG_DATA_HOME/applications</kbd><br/><kbd>~/.local/share/applications</kbd><br/><kbd>/usr/local/share/applications</kbd><br/><kbd>/usr/share/applications</kbd><br/><kbd>$XDG_DATA_DIRS/applications</kbd> | <kbd>/Applications</kbd> | <kbd>$home/bin</kbd><br/><kbd>/bin</kbd> |
|
|
||||||
| <kbd><b>Fonts</b></kbd> | <kbd>$XDG_DATA_HOME/fonts</kbd><br/><kbd>~/.fonts</kbd><br/><kbd>~/.local/share/fonts</kbd><br/><kbd>/usr/local/share/fonts</kbd><br/><kbd>/usr/share/fonts</kbd><br/><kbd>$XDG_DATA_DIRS/fonts</kbd> | <kbd>~/Library/Fonts</kbd><br/><kbd>/Library/Fonts</kbd><br/><kbd>/System/Library/Fonts</kbd><br/><kbd>/Network/Library/Fonts</kbd> | <kbd>$home/lib/font</kbd><br/><kbd>/lib/font</kbd> |
|
|
||||||
|
|
||||||
</details>
|
|
||||||
|
|
||||||
<details open>
|
|
||||||
<summary><strong>Microsoft Windows</strong></summary>
|
|
||||||
<br/>
|
|
||||||
|
|
||||||
| <a href="#other-directories"><img width="400" height="0"></a> | <a href="#other-directories"><img width="300" height="0"></a><p>Known Folder(s)</p> | <a href="#other-directories"><img width="1300" height="0"></a><p>Fallback(s)</p> |
|
|
||||||
| :-----------------------------------------------------------: | :------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------: | :--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------: |
|
|
||||||
| <kbd><b>Home</b></kbd> | <kbd>Profile</kbd> | <kbd>%USERPROFILE%</kbd> |
|
|
||||||
| <kbd><b>Applications</b></kbd> | <kbd>Programs</kbd><br/><kbd>CommonPrograms</kbd> <br/><kbd>ProgramFiles</kbd><br/><kbd>ProgramFilesCommon</kbd><br/><kbd>UserProgramFiles</kbd><br/><kbd>UserProgramFilesCommon</kbd> | <kbd>%APPDATA%\Microsoft\Windows\Start Menu\Programs</kbd><br/><kbd>%ProgramData%\Microsoft\Windows\Start Menu\Programs</kbd><br/><kbd>%ProgramFiles%</kbd><br/><kbd>%ProgramFiles%\Common Files</kbd><br/><kbd>%LOCALAPPDATA%\Programs</kbd><br/><kbd>%LOCALAPPDATA%\Programs\Common</kbd>|
|
|
||||||
| <kbd><b>Fonts</b></kbd> | <kbd>Fonts</kbd> | <kbd>%SystemRoot%\Fonts</kbd><br/><kbd>%LOCALAPPDATA%\Microsoft\Windows\Fonts</kbd> |
|
|
||||||
|
|
||||||
</details>
|
|
||||||
|
|
||||||
## Usage
|
|
||||||
|
|
||||||
#### XDG Base Directory
|
|
||||||
|
|
||||||
```go
|
|
||||||
package main
|
|
||||||
|
|
||||||
import (
|
|
||||||
"log"
|
|
||||||
|
|
||||||
"github.com/adrg/xdg"
|
|
||||||
)
|
|
||||||
|
|
||||||
func main() {
|
|
||||||
// XDG Base Directory paths.
|
|
||||||
log.Println("Home data directory:", xdg.DataHome)
|
|
||||||
log.Println("Data directories:", xdg.DataDirs)
|
|
||||||
log.Println("Home config directory:", xdg.ConfigHome)
|
|
||||||
log.Println("Config directories:", xdg.ConfigDirs)
|
|
||||||
log.Println("Home state directory:", xdg.StateHome)
|
|
||||||
log.Println("Cache directory:", xdg.CacheHome)
|
|
||||||
log.Println("Runtime directory:", xdg.RuntimeDir)
|
|
||||||
log.Println("Home binaries directory:", xdg.BinHome)
|
|
||||||
|
|
||||||
// Other common directories.
|
|
||||||
log.Println("Home directory:", xdg.Home)
|
|
||||||
log.Println("Application directories:", xdg.ApplicationDirs)
|
|
||||||
log.Println("Font directories:", xdg.FontDirs)
|
|
||||||
|
|
||||||
// Obtain a suitable location for application config files.
|
|
||||||
// ConfigFile takes one parameter which must contain the name of the file,
|
|
||||||
// but it can also contain a set of parent directories. If the directories
|
|
||||||
// don't exist, they will be created relative to the base config directory.
|
|
||||||
// It is recommended for files to be saved inside an application directory
|
|
||||||
// relative to the base directory rather than directly inside the base
|
|
||||||
// directory (e.g. `appname/config.yaml` instead of `appname-config.yaml`).
|
|
||||||
configFilePath, err := xdg.ConfigFile("appname/config.yaml")
|
|
||||||
if err != nil {
|
|
||||||
log.Fatal(err)
|
|
||||||
}
|
|
||||||
log.Println("Save the config file at:", configFilePath)
|
|
||||||
|
|
||||||
// For other types of application files use:
|
|
||||||
// xdg.DataFile()
|
|
||||||
// xdg.StateFile()
|
|
||||||
// xdg.CacheFile()
|
|
||||||
// xdg.RuntimeFile()
|
|
||||||
|
|
||||||
// Finding application config files.
|
|
||||||
// SearchConfigFile takes one parameter which must contain the name of
|
|
||||||
// the file, but it can also contain a set of parent directories relative
|
|
||||||
// to the config search paths (xdg.ConfigHome and xdg.ConfigDirs).
|
|
||||||
configFilePath, err = xdg.SearchConfigFile("appname/config.yaml")
|
|
||||||
if err != nil {
|
|
||||||
log.Fatal(err)
|
|
||||||
}
|
|
||||||
log.Println("Config file was found at:", configFilePath)
|
|
||||||
|
|
||||||
// For other types of application files use:
|
|
||||||
// xdg.SearchDataFile()
|
|
||||||
// xdg.SearchStateFile()
|
|
||||||
// xdg.SearchCacheFile()
|
|
||||||
// xdg.SearchRuntimeFile()
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
#### XDG user directories
|
|
||||||
|
|
||||||
```go
|
|
||||||
package main
|
|
||||||
|
|
||||||
import (
|
|
||||||
"log"
|
|
||||||
|
|
||||||
"github.com/adrg/xdg"
|
|
||||||
)
|
|
||||||
|
|
||||||
func main() {
|
|
||||||
// XDG user directories.
|
|
||||||
log.Println("Desktop directory:", xdg.UserDirs.Desktop)
|
|
||||||
log.Println("Download directory:", xdg.UserDirs.Download)
|
|
||||||
log.Println("Documents directory:", xdg.UserDirs.Documents)
|
|
||||||
log.Println("Music directory:", xdg.UserDirs.Music)
|
|
||||||
log.Println("Pictures directory:", xdg.UserDirs.Pictures)
|
|
||||||
log.Println("Videos directory:", xdg.UserDirs.Videos)
|
|
||||||
log.Println("Templates directory:", xdg.UserDirs.Templates)
|
|
||||||
log.Println("Public directory:", xdg.UserDirs.PublicShare)
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
## Stargazers over time
|
|
||||||
|
|
||||||
[](https://starchart.cc/adrg/xdg)
|
|
||||||
|
|
||||||
## Contributing
|
|
||||||
|
|
||||||
Contributions in the form of pull requests, issues or just general feedback,
|
|
||||||
are always welcome.
|
|
||||||
See [CONTRIBUTING.MD](CONTRIBUTING.md).
|
|
||||||
|
|
||||||
**Contributors**:
|
|
||||||
[adrg](https://github.com/adrg),
|
|
||||||
[wichert](https://github.com/wichert),
|
|
||||||
[bouncepaw](https://github.com/bouncepaw),
|
|
||||||
[gabriel-vasile](https://github.com/gabriel-vasile),
|
|
||||||
[KalleDK](https://github.com/KalleDK),
|
|
||||||
[nvkv](https://github.com/nvkv),
|
|
||||||
[djdv](https://github.com/djdv),
|
|
||||||
[rrjjvv](https://github.com/rrjjvv),
|
|
||||||
[GreyXor](https://github.com/GreyXor),
|
|
||||||
[Rican7](https://github.com/Rican7),
|
|
||||||
[nothub](https://github.com/nothub).
|
|
||||||
|
|
||||||
## References
|
|
||||||
|
|
||||||
For more information see:
|
|
||||||
* [XDG Base Directory Specification](https://specifications.freedesktop.org/basedir-spec/basedir-spec-latest.html)
|
|
||||||
* [XDG user directories](https://wiki.archlinux.org/index.php/XDG_user_directories)
|
|
||||||
* [Windows Known Folders](https://docs.microsoft.com/en-us/windows/win32/shell/knownfolderid)
|
|
||||||
|
|
||||||
## License
|
|
||||||
|
|
||||||
Copyright (c) 2014 Adrian-George Bostan.
|
|
||||||
|
|
||||||
This project is licensed under the [MIT license](https://opensource.org/licenses/MIT).
|
|
||||||
See [LICENSE](LICENSE) for more details.
|
|
||||||
82
vendor/github.com/adrg/xdg/base_dirs.go
generated
vendored
82
vendor/github.com/adrg/xdg/base_dirs.go
generated
vendored
@@ -1,82 +0,0 @@
|
|||||||
package xdg
|
|
||||||
|
|
||||||
import (
|
|
||||||
"os"
|
|
||||||
|
|
||||||
"github.com/adrg/xdg/internal/pathutil"
|
|
||||||
)
|
|
||||||
|
|
||||||
// XDG Base Directory environment variables.
|
|
||||||
const (
|
|
||||||
envDataHome = "XDG_DATA_HOME"
|
|
||||||
envDataDirs = "XDG_DATA_DIRS"
|
|
||||||
envConfigHome = "XDG_CONFIG_HOME"
|
|
||||||
envConfigDirs = "XDG_CONFIG_DIRS"
|
|
||||||
envStateHome = "XDG_STATE_HOME"
|
|
||||||
envCacheHome = "XDG_CACHE_HOME"
|
|
||||||
envRuntimeDir = "XDG_RUNTIME_DIR"
|
|
||||||
|
|
||||||
// Non-standard.
|
|
||||||
envBinHome = "XDG_BIN_HOME"
|
|
||||||
)
|
|
||||||
|
|
||||||
type baseDirectories struct {
|
|
||||||
dataHome string
|
|
||||||
data []string
|
|
||||||
configHome string
|
|
||||||
config []string
|
|
||||||
stateHome string
|
|
||||||
cacheHome string
|
|
||||||
runtime string
|
|
||||||
|
|
||||||
// Non-standard.
|
|
||||||
binHome string
|
|
||||||
fonts []string
|
|
||||||
applications []string
|
|
||||||
}
|
|
||||||
|
|
||||||
func (bd baseDirectories) dataFile(relPath string) (string, error) {
|
|
||||||
return pathutil.Create(relPath, append([]string{bd.dataHome}, bd.data...))
|
|
||||||
}
|
|
||||||
|
|
||||||
func (bd baseDirectories) configFile(relPath string) (string, error) {
|
|
||||||
return pathutil.Create(relPath, append([]string{bd.configHome}, bd.config...))
|
|
||||||
}
|
|
||||||
|
|
||||||
func (bd baseDirectories) stateFile(relPath string) (string, error) {
|
|
||||||
return pathutil.Create(relPath, []string{bd.stateHome})
|
|
||||||
}
|
|
||||||
|
|
||||||
func (bd baseDirectories) cacheFile(relPath string) (string, error) {
|
|
||||||
return pathutil.Create(relPath, []string{bd.cacheHome})
|
|
||||||
}
|
|
||||||
|
|
||||||
func (bd baseDirectories) runtimeFile(relPath string) (string, error) {
|
|
||||||
var paths []string
|
|
||||||
for _, p := range pathutil.Unique([]string{bd.runtime, os.TempDir()}) {
|
|
||||||
if pathutil.Exists(p) {
|
|
||||||
paths = append(paths, p)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return pathutil.Create(relPath, paths)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (bd baseDirectories) searchDataFile(relPath string) (string, error) {
|
|
||||||
return pathutil.Search(relPath, append([]string{bd.dataHome}, bd.data...))
|
|
||||||
}
|
|
||||||
|
|
||||||
func (bd baseDirectories) searchConfigFile(relPath string) (string, error) {
|
|
||||||
return pathutil.Search(relPath, append([]string{bd.configHome}, bd.config...))
|
|
||||||
}
|
|
||||||
|
|
||||||
func (bd baseDirectories) searchStateFile(relPath string) (string, error) {
|
|
||||||
return pathutil.Search(relPath, []string{bd.stateHome})
|
|
||||||
}
|
|
||||||
|
|
||||||
func (bd baseDirectories) searchCacheFile(relPath string) (string, error) {
|
|
||||||
return pathutil.Search(relPath, []string{bd.cacheHome})
|
|
||||||
}
|
|
||||||
|
|
||||||
func (bd baseDirectories) searchRuntimeFile(relPath string) (string, error) {
|
|
||||||
return pathutil.Search(relPath, pathutil.Unique([]string{bd.runtime, os.TempDir()}))
|
|
||||||
}
|
|
||||||
11
vendor/github.com/adrg/xdg/codecov.yml
generated
vendored
11
vendor/github.com/adrg/xdg/codecov.yml
generated
vendored
@@ -1,11 +0,0 @@
|
|||||||
coverage:
|
|
||||||
status:
|
|
||||||
project:
|
|
||||||
default:
|
|
||||||
target: 90%
|
|
||||||
threshold: 1%
|
|
||||||
patch:
|
|
||||||
default:
|
|
||||||
target: 100%
|
|
||||||
ignore:
|
|
||||||
- "paths_plan9.go"
|
|
||||||
105
vendor/github.com/adrg/xdg/doc.go
generated
vendored
105
vendor/github.com/adrg/xdg/doc.go
generated
vendored
@@ -1,105 +0,0 @@
|
|||||||
/*
|
|
||||||
Package xdg provides an implementation of the XDG Base Directory Specification.
|
|
||||||
The specification defines a set of standard paths for storing application files
|
|
||||||
including data and configuration files. For portability and flexibility reasons,
|
|
||||||
applications should use the XDG defined locations instead of hardcoding paths.
|
|
||||||
The package also includes the locations of well known user directories.
|
|
||||||
|
|
||||||
The current implementation supports most flavors of Unix, Windows, Mac OS and Plan 9.
|
|
||||||
|
|
||||||
For more information regarding the XDG Base Directory Specification see:
|
|
||||||
https://specifications.freedesktop.org/basedir-spec/basedir-spec-latest.html
|
|
||||||
|
|
||||||
For more information regarding the XDG user directories see:
|
|
||||||
https://wiki.archlinux.org/index.php/XDG_user_directories
|
|
||||||
|
|
||||||
For more information regarding the Windows Known Folders see:
|
|
||||||
https://docs.microsoft.com/en-us/windows/win32/shell/known-folders
|
|
||||||
|
|
||||||
# Usage
|
|
||||||
|
|
||||||
XDG Base Directory
|
|
||||||
|
|
||||||
package main
|
|
||||||
|
|
||||||
import (
|
|
||||||
"log"
|
|
||||||
|
|
||||||
"github.com/adrg/xdg"
|
|
||||||
)
|
|
||||||
|
|
||||||
func main() {
|
|
||||||
// XDG Base Directory paths.
|
|
||||||
log.Println("Home data directory:", xdg.DataHome)
|
|
||||||
log.Println("Data directories:", xdg.DataDirs)
|
|
||||||
log.Println("Home config directory:", xdg.ConfigHome)
|
|
||||||
log.Println("Config directories:", xdg.ConfigDirs)
|
|
||||||
log.Println("Home state directory:", xdg.StateHome)
|
|
||||||
log.Println("Cache directory:", xdg.CacheHome)
|
|
||||||
log.Println("Runtime directory:", xdg.RuntimeDir)
|
|
||||||
log.Println("Home binaries directory:", xdg.BinHome)
|
|
||||||
|
|
||||||
// Other common directories.
|
|
||||||
log.Println("Home directory:", xdg.Home)
|
|
||||||
log.Println("Application directories:", xdg.ApplicationDirs)
|
|
||||||
log.Println("Font directories:", xdg.FontDirs)
|
|
||||||
|
|
||||||
// Obtain a suitable location for application config files.
|
|
||||||
// ConfigFile takes one parameter which must contain the name of the file,
|
|
||||||
// but it can also contain a set of parent directories. If the directories
|
|
||||||
// don't exist, they will be created relative to the base config directory.
|
|
||||||
// It is recommended for files to be saved inside an application directory
|
|
||||||
// relative to the base directory rather than directly inside the base
|
|
||||||
// directory (e.g. `appname/config.yaml` instead of `appname-config.yaml`).
|
|
||||||
configFilePath, err := xdg.ConfigFile("appname/config.yaml")
|
|
||||||
if err != nil {
|
|
||||||
log.Fatal(err)
|
|
||||||
}
|
|
||||||
log.Println("Save the config file at:", configFilePath)
|
|
||||||
|
|
||||||
// For other types of application files use:
|
|
||||||
// xdg.DataFile()
|
|
||||||
// xdg.StateFile()
|
|
||||||
// xdg.CacheFile()
|
|
||||||
// xdg.RuntimeFile()
|
|
||||||
|
|
||||||
// Finding application config files.
|
|
||||||
// SearchConfigFile takes one parameter which must contain the name of
|
|
||||||
// the file, but it can also contain a set of parent directories relative
|
|
||||||
// to the config search paths (xdg.ConfigHome and xdg.ConfigDirs).
|
|
||||||
configFilePath, err = xdg.SearchConfigFile("appname/config.yaml")
|
|
||||||
if err != nil {
|
|
||||||
log.Fatal(err)
|
|
||||||
}
|
|
||||||
log.Println("Config file was found at:", configFilePath)
|
|
||||||
|
|
||||||
// For other types of application files use:
|
|
||||||
// xdg.SearchDataFile()
|
|
||||||
// xdg.SearchStateFile()
|
|
||||||
// xdg.SearchCacheFile()
|
|
||||||
// xdg.SearchRuntimeFile()
|
|
||||||
}
|
|
||||||
|
|
||||||
XDG user directories
|
|
||||||
|
|
||||||
package main
|
|
||||||
|
|
||||||
import (
|
|
||||||
"log"
|
|
||||||
|
|
||||||
"github.com/adrg/xdg"
|
|
||||||
)
|
|
||||||
|
|
||||||
func main() {
|
|
||||||
// XDG user directories.
|
|
||||||
log.Println("Desktop directory:", xdg.UserDirs.Desktop)
|
|
||||||
log.Println("Download directory:", xdg.UserDirs.Download)
|
|
||||||
log.Println("Documents directory:", xdg.UserDirs.Documents)
|
|
||||||
log.Println("Music directory:", xdg.UserDirs.Music)
|
|
||||||
log.Println("Pictures directory:", xdg.UserDirs.Pictures)
|
|
||||||
log.Println("Videos directory:", xdg.UserDirs.Videos)
|
|
||||||
log.Println("Templates directory:", xdg.UserDirs.Templates)
|
|
||||||
log.Println("Public directory:", xdg.UserDirs.PublicShare)
|
|
||||||
}
|
|
||||||
*/
|
|
||||||
package xdg
|
|
||||||
114
vendor/github.com/adrg/xdg/internal/pathutil/pathutil.go
generated
vendored
114
vendor/github.com/adrg/xdg/internal/pathutil/pathutil.go
generated
vendored
@@ -1,114 +0,0 @@
|
|||||||
package pathutil
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"os"
|
|
||||||
"path/filepath"
|
|
||||||
)
|
|
||||||
|
|
||||||
// Unique eliminates the duplicate paths from the provided slice and returns
|
|
||||||
// the result. The paths are expanded using the `ExpandHome` function and only
|
|
||||||
// absolute paths are kept. The items in the output slice are in the order in
|
|
||||||
// which they occur in the input slice.
|
|
||||||
func Unique(paths []string) []string {
|
|
||||||
var (
|
|
||||||
uniq []string
|
|
||||||
registry = map[string]struct{}{}
|
|
||||||
)
|
|
||||||
|
|
||||||
for _, p := range paths {
|
|
||||||
if p = ExpandHome(p); p != "" && filepath.IsAbs(p) {
|
|
||||||
if _, ok := registry[p]; ok {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
registry[p] = struct{}{}
|
|
||||||
uniq = append(uniq, p)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return uniq
|
|
||||||
}
|
|
||||||
|
|
||||||
// First returns the first absolute path from the provided slice.
|
|
||||||
// The paths in the input slice are expanded using the `ExpandHome` function.
|
|
||||||
func First(paths []string) string {
|
|
||||||
for _, p := range paths {
|
|
||||||
if p = ExpandHome(p); p != "" && filepath.IsAbs(p) {
|
|
||||||
return p
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return ""
|
|
||||||
}
|
|
||||||
|
|
||||||
// Create returns a suitable location relative to which the file with the
|
|
||||||
// specified `name` can be written. The first path from the provided `paths`
|
|
||||||
// slice which is successfully created (or already exists) is used as a base
|
|
||||||
// path for the file. The `name` parameter should contain the name of the file
|
|
||||||
// which is going to be written in the location returned by this function, but
|
|
||||||
// it can also contain a set of parent directories, which will be created
|
|
||||||
// relative to the selected parent path.
|
|
||||||
func Create(name string, paths []string) (string, error) {
|
|
||||||
searchedPaths := make([]string, 0, len(paths))
|
|
||||||
for _, p := range paths {
|
|
||||||
p = filepath.Join(p, name)
|
|
||||||
|
|
||||||
dir := filepath.Dir(p)
|
|
||||||
if Exists(dir) {
|
|
||||||
return p, nil
|
|
||||||
}
|
|
||||||
if err := os.MkdirAll(dir, os.ModeDir|0o700); err == nil {
|
|
||||||
return p, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
searchedPaths = append(searchedPaths, dir)
|
|
||||||
}
|
|
||||||
|
|
||||||
return "", fmt.Errorf("could not create any of the following paths: %v",
|
|
||||||
searchedPaths)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Search searches for the file with the specified `name` in the provided
|
|
||||||
// slice of `paths`. The `name` parameter must contain the name of the file,
|
|
||||||
// but it can also contain a set of parent directories.
|
|
||||||
func Search(name string, paths []string) (string, error) {
|
|
||||||
searchedPaths := make([]string, 0, len(paths))
|
|
||||||
for _, p := range paths {
|
|
||||||
p = filepath.Join(p, name)
|
|
||||||
if Exists(p) {
|
|
||||||
return p, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
searchedPaths = append(searchedPaths, filepath.Dir(p))
|
|
||||||
}
|
|
||||||
|
|
||||||
return "", fmt.Errorf("could not locate `%s` in any of the following paths: %v",
|
|
||||||
filepath.Base(name), searchedPaths)
|
|
||||||
}
|
|
||||||
|
|
||||||
// EnvPath returns the value of the environment variable with the specified
|
|
||||||
// `name` if it is an absolute path, or the first absolute fallback path.
|
|
||||||
// All paths are expanded using the `ExpandHome` function.
|
|
||||||
func EnvPath(name string, fallbackPaths ...string) string {
|
|
||||||
dir := ExpandHome(os.Getenv(name))
|
|
||||||
if dir != "" && filepath.IsAbs(dir) {
|
|
||||||
return dir
|
|
||||||
}
|
|
||||||
|
|
||||||
return First(fallbackPaths)
|
|
||||||
}
|
|
||||||
|
|
||||||
// EnvPathList reads the value of the environment variable with the specified
|
|
||||||
// `name` and attempts to extract a list of absolute paths from it. If there
|
|
||||||
// are none, a list of absolute fallback paths is returned instead. Duplicate
|
|
||||||
// paths are removed from the returned slice. All paths are expanded using the
|
|
||||||
// `ExpandHome` function.
|
|
||||||
func EnvPathList(name string, fallbackPaths ...string) []string {
|
|
||||||
dirs := Unique(filepath.SplitList(os.Getenv(name)))
|
|
||||||
if len(dirs) != 0 {
|
|
||||||
return dirs
|
|
||||||
}
|
|
||||||
|
|
||||||
return Unique(fallbackPaths)
|
|
||||||
}
|
|
||||||
40
vendor/github.com/adrg/xdg/internal/pathutil/pathutil_plan9.go
generated
vendored
40
vendor/github.com/adrg/xdg/internal/pathutil/pathutil_plan9.go
generated
vendored
@@ -1,40 +0,0 @@
|
|||||||
package pathutil
|
|
||||||
|
|
||||||
import (
|
|
||||||
"errors"
|
|
||||||
"io/fs"
|
|
||||||
"os"
|
|
||||||
"path/filepath"
|
|
||||||
"strings"
|
|
||||||
)
|
|
||||||
|
|
||||||
// UserHomeDir returns the home directory of the current user.
|
|
||||||
func UserHomeDir() string {
|
|
||||||
if home := os.Getenv("home"); home != "" {
|
|
||||||
return home
|
|
||||||
}
|
|
||||||
|
|
||||||
return "/"
|
|
||||||
}
|
|
||||||
|
|
||||||
// Exists returns true if the specified path exists.
|
|
||||||
func Exists(path string) bool {
|
|
||||||
_, err := os.Stat(path)
|
|
||||||
return err == nil || errors.Is(err, fs.ErrExist)
|
|
||||||
}
|
|
||||||
|
|
||||||
// ExpandHome substitutes `~` and `$home` at the start of the specified `path`.
|
|
||||||
func ExpandHome(path string) string {
|
|
||||||
home := UserHomeDir()
|
|
||||||
if path == "" || home == "" {
|
|
||||||
return path
|
|
||||||
}
|
|
||||||
if path[0] == '~' {
|
|
||||||
return filepath.Join(home, path[1:])
|
|
||||||
}
|
|
||||||
if strings.HasPrefix(path, "$home") {
|
|
||||||
return filepath.Join(home, path[5:])
|
|
||||||
}
|
|
||||||
|
|
||||||
return path
|
|
||||||
}
|
|
||||||
42
vendor/github.com/adrg/xdg/internal/pathutil/pathutil_unix.go
generated
vendored
42
vendor/github.com/adrg/xdg/internal/pathutil/pathutil_unix.go
generated
vendored
@@ -1,42 +0,0 @@
|
|||||||
//go:build aix || darwin || dragonfly || freebsd || (js && wasm) || nacl || linux || netbsd || openbsd || solaris
|
|
||||||
|
|
||||||
package pathutil
|
|
||||||
|
|
||||||
import (
|
|
||||||
"errors"
|
|
||||||
"io/fs"
|
|
||||||
"os"
|
|
||||||
"path/filepath"
|
|
||||||
"strings"
|
|
||||||
)
|
|
||||||
|
|
||||||
// UserHomeDir returns the home directory of the current user.
|
|
||||||
func UserHomeDir() string {
|
|
||||||
if home := os.Getenv("HOME"); home != "" {
|
|
||||||
return home
|
|
||||||
}
|
|
||||||
|
|
||||||
return "/"
|
|
||||||
}
|
|
||||||
|
|
||||||
// Exists returns true if the specified path exists.
|
|
||||||
func Exists(path string) bool {
|
|
||||||
_, err := os.Stat(path)
|
|
||||||
return err == nil || errors.Is(err, fs.ErrExist)
|
|
||||||
}
|
|
||||||
|
|
||||||
// ExpandHome substitutes `~` and `$HOME` at the start of the specified `path`.
|
|
||||||
func ExpandHome(path string) string {
|
|
||||||
home := UserHomeDir()
|
|
||||||
if path == "" || home == "" {
|
|
||||||
return path
|
|
||||||
}
|
|
||||||
if path[0] == '~' {
|
|
||||||
return filepath.Join(home, path[1:])
|
|
||||||
}
|
|
||||||
if strings.HasPrefix(path, "$HOME") {
|
|
||||||
return filepath.Join(home, path[5:])
|
|
||||||
}
|
|
||||||
|
|
||||||
return path
|
|
||||||
}
|
|
||||||
71
vendor/github.com/adrg/xdg/internal/pathutil/pathutil_windows.go
generated
vendored
71
vendor/github.com/adrg/xdg/internal/pathutil/pathutil_windows.go
generated
vendored
@@ -1,71 +0,0 @@
|
|||||||
package pathutil
|
|
||||||
|
|
||||||
import (
|
|
||||||
"errors"
|
|
||||||
"io/fs"
|
|
||||||
"os"
|
|
||||||
"path/filepath"
|
|
||||||
"strings"
|
|
||||||
|
|
||||||
"golang.org/x/sys/windows"
|
|
||||||
)
|
|
||||||
|
|
||||||
// UserHomeDir returns the home directory of the current user.
|
|
||||||
func UserHomeDir() string {
|
|
||||||
return KnownFolder(windows.FOLDERID_Profile, []string{"USERPROFILE"}, nil)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Exists returns true if the specified path exists.
|
|
||||||
func Exists(path string) bool {
|
|
||||||
fi, err := os.Lstat(path)
|
|
||||||
if fi != nil && fi.Mode()&os.ModeSymlink != 0 {
|
|
||||||
_, err = filepath.EvalSymlinks(path)
|
|
||||||
}
|
|
||||||
|
|
||||||
return err == nil || errors.Is(err, fs.ErrExist)
|
|
||||||
}
|
|
||||||
|
|
||||||
// ExpandHome substitutes `%USERPROFILE%` at the start of the specified `path`.
|
|
||||||
func ExpandHome(path string) string {
|
|
||||||
home := UserHomeDir()
|
|
||||||
if path == "" || home == "" {
|
|
||||||
return path
|
|
||||||
}
|
|
||||||
if strings.HasPrefix(path, `%USERPROFILE%`) {
|
|
||||||
return filepath.Join(home, path[13:])
|
|
||||||
}
|
|
||||||
|
|
||||||
return path
|
|
||||||
}
|
|
||||||
|
|
||||||
// KnownFolder returns the location of the folder with the specified ID.
|
|
||||||
// If that fails, the folder location is determined by reading the provided
|
|
||||||
// environment variables (the first non-empty read value is returned).
|
|
||||||
// If that fails as well, the first non-empty fallback is returned.
|
|
||||||
// If all of the above fails, the function returns an empty string.
|
|
||||||
func KnownFolder(id *windows.KNOWNFOLDERID, envVars []string, fallbacks []string) string {
|
|
||||||
if id != nil {
|
|
||||||
flags := []uint32{windows.KF_FLAG_DEFAULT, windows.KF_FLAG_DEFAULT_PATH}
|
|
||||||
for _, flag := range flags {
|
|
||||||
p, _ := windows.KnownFolderPath(id, flag|windows.KF_FLAG_DONT_VERIFY)
|
|
||||||
if p != "" {
|
|
||||||
return p
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, envVar := range envVars {
|
|
||||||
p := os.Getenv(envVar)
|
|
||||||
if p != "" {
|
|
||||||
return p
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, fallback := range fallbacks {
|
|
||||||
if fallback != "" {
|
|
||||||
return fallback
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return ""
|
|
||||||
}
|
|
||||||
82
vendor/github.com/adrg/xdg/internal/userdirs/config_unix.go
generated
vendored
82
vendor/github.com/adrg/xdg/internal/userdirs/config_unix.go
generated
vendored
@@ -1,82 +0,0 @@
|
|||||||
//go:build aix || dragonfly || freebsd || (js && wasm) || nacl || linux || netbsd || openbsd || solaris
|
|
||||||
|
|
||||||
package userdirs
|
|
||||||
|
|
||||||
import (
|
|
||||||
"bufio"
|
|
||||||
"io"
|
|
||||||
"os"
|
|
||||||
"strings"
|
|
||||||
|
|
||||||
"github.com/adrg/xdg/internal/pathutil"
|
|
||||||
)
|
|
||||||
|
|
||||||
// ParseConfigFile parses the user directories config file at the
|
|
||||||
// specified location.
|
|
||||||
func ParseConfigFile(name string) (*Directories, error) {
|
|
||||||
f, err := os.Open(name)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
defer f.Close()
|
|
||||||
|
|
||||||
return ParseConfig(f)
|
|
||||||
}
|
|
||||||
|
|
||||||
// ParseConfig parses the user directories config file contained in
|
|
||||||
// the provided reader.
|
|
||||||
func ParseConfig(r io.Reader) (*Directories, error) {
|
|
||||||
dirs := &Directories{}
|
|
||||||
fieldsMap := map[string]*string{
|
|
||||||
EnvDesktopDir: &dirs.Desktop,
|
|
||||||
EnvDownloadDir: &dirs.Download,
|
|
||||||
EnvDocumentsDir: &dirs.Documents,
|
|
||||||
EnvMusicDir: &dirs.Music,
|
|
||||||
EnvPicturesDir: &dirs.Pictures,
|
|
||||||
EnvVideosDir: &dirs.Videos,
|
|
||||||
EnvTemplatesDir: &dirs.Templates,
|
|
||||||
EnvPublicShareDir: &dirs.PublicShare,
|
|
||||||
}
|
|
||||||
|
|
||||||
scanner := bufio.NewScanner(r)
|
|
||||||
for scanner.Scan() {
|
|
||||||
line := strings.TrimSpace(scanner.Text())
|
|
||||||
if len(line) == 0 || line[0] == '#' {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
if !strings.HasPrefix(line, "XDG_") {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
parts := strings.Split(line, "=")
|
|
||||||
if len(parts) < 2 {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
// Parse key.
|
|
||||||
field, ok := fieldsMap[strings.TrimSpace(parts[0])]
|
|
||||||
if !ok {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
// Parse value.
|
|
||||||
runes := []rune(strings.TrimSpace(parts[1]))
|
|
||||||
|
|
||||||
lenRunes := len(runes)
|
|
||||||
if lenRunes <= 2 || runes[0] != '"' {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
for i := 1; i < lenRunes; i++ {
|
|
||||||
if runes[i] == '"' {
|
|
||||||
*field = pathutil.ExpandHome(string(runes[1:i]))
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if err := scanner.Err(); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
return dirs, nil
|
|
||||||
}
|
|
||||||
40
vendor/github.com/adrg/xdg/internal/userdirs/userdirs.go
generated
vendored
40
vendor/github.com/adrg/xdg/internal/userdirs/userdirs.go
generated
vendored
@@ -1,40 +0,0 @@
|
|||||||
package userdirs
|
|
||||||
|
|
||||||
// XDG user directories environment variables.
|
|
||||||
const (
|
|
||||||
EnvDesktopDir = "XDG_DESKTOP_DIR"
|
|
||||||
EnvDownloadDir = "XDG_DOWNLOAD_DIR"
|
|
||||||
EnvDocumentsDir = "XDG_DOCUMENTS_DIR"
|
|
||||||
EnvMusicDir = "XDG_MUSIC_DIR"
|
|
||||||
EnvPicturesDir = "XDG_PICTURES_DIR"
|
|
||||||
EnvVideosDir = "XDG_VIDEOS_DIR"
|
|
||||||
EnvTemplatesDir = "XDG_TEMPLATES_DIR"
|
|
||||||
EnvPublicShareDir = "XDG_PUBLICSHARE_DIR"
|
|
||||||
)
|
|
||||||
|
|
||||||
// Directories defines the locations of well known user directories.
|
|
||||||
type Directories struct {
|
|
||||||
// Desktop defines the location of the user's desktop directory.
|
|
||||||
Desktop string
|
|
||||||
|
|
||||||
// Download defines a suitable location for user downloaded files.
|
|
||||||
Download string
|
|
||||||
|
|
||||||
// Documents defines a suitable location for user document files.
|
|
||||||
Documents string
|
|
||||||
|
|
||||||
// Music defines a suitable location for user audio files.
|
|
||||||
Music string
|
|
||||||
|
|
||||||
// Pictures defines a suitable location for user image files.
|
|
||||||
Pictures string
|
|
||||||
|
|
||||||
// VideosDir defines a suitable location for user video files.
|
|
||||||
Videos string
|
|
||||||
|
|
||||||
// Templates defines a suitable location for user template files.
|
|
||||||
Templates string
|
|
||||||
|
|
||||||
// PublicShare defines a suitable location for user shared files.
|
|
||||||
PublicShare string
|
|
||||||
}
|
|
||||||
60
vendor/github.com/adrg/xdg/paths_darwin.go
generated
vendored
60
vendor/github.com/adrg/xdg/paths_darwin.go
generated
vendored
@@ -1,60 +0,0 @@
|
|||||||
package xdg
|
|
||||||
|
|
||||||
import (
|
|
||||||
"path/filepath"
|
|
||||||
|
|
||||||
"github.com/adrg/xdg/internal/pathutil"
|
|
||||||
"github.com/adrg/xdg/internal/userdirs"
|
|
||||||
)
|
|
||||||
|
|
||||||
func initDirs(home string) {
|
|
||||||
initBaseDirs(home)
|
|
||||||
initUserDirs(home)
|
|
||||||
}
|
|
||||||
|
|
||||||
func initBaseDirs(home string) {
|
|
||||||
homeAppSupport := filepath.Join(home, "Library", "Application Support")
|
|
||||||
rootAppSupport := "/Library/Application Support"
|
|
||||||
|
|
||||||
// Initialize standard directories.
|
|
||||||
baseDirs.dataHome = pathutil.EnvPath(envDataHome, homeAppSupport)
|
|
||||||
baseDirs.data = pathutil.EnvPathList(envDataDirs,
|
|
||||||
rootAppSupport,
|
|
||||||
filepath.Join(home, ".local", "share"),
|
|
||||||
)
|
|
||||||
baseDirs.configHome = pathutil.EnvPath(envConfigHome, homeAppSupport)
|
|
||||||
baseDirs.config = pathutil.EnvPathList(envConfigDirs,
|
|
||||||
filepath.Join(home, "Library", "Preferences"),
|
|
||||||
rootAppSupport,
|
|
||||||
"/Library/Preferences",
|
|
||||||
filepath.Join(home, ".config"),
|
|
||||||
)
|
|
||||||
baseDirs.stateHome = pathutil.EnvPath(envStateHome, homeAppSupport)
|
|
||||||
baseDirs.cacheHome = pathutil.EnvPath(envCacheHome, filepath.Join(home, "Library", "Caches"))
|
|
||||||
baseDirs.runtime = pathutil.EnvPath(envRuntimeDir, homeAppSupport)
|
|
||||||
|
|
||||||
// Initialize non-standard directories.
|
|
||||||
baseDirs.binHome = pathutil.EnvPath(envBinHome, filepath.Join(home, ".local", "bin"))
|
|
||||||
|
|
||||||
baseDirs.applications = []string{
|
|
||||||
"/Applications",
|
|
||||||
}
|
|
||||||
|
|
||||||
baseDirs.fonts = []string{
|
|
||||||
filepath.Join(home, "Library/Fonts"),
|
|
||||||
"/Library/Fonts",
|
|
||||||
"/System/Library/Fonts",
|
|
||||||
"/Network/Library/Fonts",
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func initUserDirs(home string) {
|
|
||||||
UserDirs.Desktop = pathutil.EnvPath(userdirs.EnvDesktopDir, filepath.Join(home, "Desktop"))
|
|
||||||
UserDirs.Download = pathutil.EnvPath(userdirs.EnvDownloadDir, filepath.Join(home, "Downloads"))
|
|
||||||
UserDirs.Documents = pathutil.EnvPath(userdirs.EnvDocumentsDir, filepath.Join(home, "Documents"))
|
|
||||||
UserDirs.Music = pathutil.EnvPath(userdirs.EnvMusicDir, filepath.Join(home, "Music"))
|
|
||||||
UserDirs.Pictures = pathutil.EnvPath(userdirs.EnvPicturesDir, filepath.Join(home, "Pictures"))
|
|
||||||
UserDirs.Videos = pathutil.EnvPath(userdirs.EnvVideosDir, filepath.Join(home, "Movies"))
|
|
||||||
UserDirs.Templates = pathutil.EnvPath(userdirs.EnvTemplatesDir, filepath.Join(home, "Templates"))
|
|
||||||
UserDirs.PublicShare = pathutil.EnvPath(userdirs.EnvPublicShareDir, filepath.Join(home, "Public"))
|
|
||||||
}
|
|
||||||
51
vendor/github.com/adrg/xdg/paths_plan9.go
generated
vendored
51
vendor/github.com/adrg/xdg/paths_plan9.go
generated
vendored
@@ -1,51 +0,0 @@
|
|||||||
package xdg
|
|
||||||
|
|
||||||
import (
|
|
||||||
"path/filepath"
|
|
||||||
|
|
||||||
"github.com/adrg/xdg/internal/pathutil"
|
|
||||||
"github.com/adrg/xdg/internal/userdirs"
|
|
||||||
)
|
|
||||||
|
|
||||||
func initDirs(home string) {
|
|
||||||
initBaseDirs(home)
|
|
||||||
initUserDirs(home)
|
|
||||||
}
|
|
||||||
|
|
||||||
func initBaseDirs(home string) {
|
|
||||||
homeLibDir := filepath.Join(home, "lib")
|
|
||||||
rootLibDir := "/lib"
|
|
||||||
|
|
||||||
// Initialize standard directories.
|
|
||||||
baseDirs.dataHome = pathutil.EnvPath(envDataHome, homeLibDir)
|
|
||||||
baseDirs.data = pathutil.EnvPathList(envDataDirs, rootLibDir)
|
|
||||||
baseDirs.configHome = pathutil.EnvPath(envConfigHome, homeLibDir)
|
|
||||||
baseDirs.config = pathutil.EnvPathList(envConfigDirs, rootLibDir)
|
|
||||||
baseDirs.stateHome = pathutil.EnvPath(envStateHome, filepath.Join(homeLibDir, "state"))
|
|
||||||
baseDirs.cacheHome = pathutil.EnvPath(envCacheHome, filepath.Join(homeLibDir, "cache"))
|
|
||||||
baseDirs.runtime = pathutil.EnvPath(envRuntimeDir, "/tmp")
|
|
||||||
|
|
||||||
// Initialize non-standard directories.
|
|
||||||
baseDirs.binHome = pathutil.EnvPath(envBinHome, filepath.Join(home, "bin"))
|
|
||||||
|
|
||||||
baseDirs.applications = []string{
|
|
||||||
filepath.Join(home, "bin"),
|
|
||||||
"/bin",
|
|
||||||
}
|
|
||||||
|
|
||||||
baseDirs.fonts = []string{
|
|
||||||
filepath.Join(homeLibDir, "font"),
|
|
||||||
"/lib/font",
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func initUserDirs(home string) {
|
|
||||||
UserDirs.Desktop = pathutil.EnvPath(userdirs.EnvDesktopDir, filepath.Join(home, "desktop"))
|
|
||||||
UserDirs.Download = pathutil.EnvPath(userdirs.EnvDownloadDir, filepath.Join(home, "downloads"))
|
|
||||||
UserDirs.Documents = pathutil.EnvPath(userdirs.EnvDocumentsDir, filepath.Join(home, "documents"))
|
|
||||||
UserDirs.Music = pathutil.EnvPath(userdirs.EnvMusicDir, filepath.Join(home, "music"))
|
|
||||||
UserDirs.Pictures = pathutil.EnvPath(userdirs.EnvPicturesDir, filepath.Join(home, "pictures"))
|
|
||||||
UserDirs.Videos = pathutil.EnvPath(userdirs.EnvVideosDir, filepath.Join(home, "videos"))
|
|
||||||
UserDirs.Templates = pathutil.EnvPath(userdirs.EnvTemplatesDir, filepath.Join(home, "templates"))
|
|
||||||
UserDirs.PublicShare = pathutil.EnvPath(userdirs.EnvPublicShareDir, filepath.Join(home, "public"))
|
|
||||||
}
|
|
||||||
70
vendor/github.com/adrg/xdg/paths_unix.go
generated
vendored
70
vendor/github.com/adrg/xdg/paths_unix.go
generated
vendored
@@ -1,70 +0,0 @@
|
|||||||
//go:build aix || dragonfly || freebsd || (js && wasm) || nacl || linux || netbsd || openbsd || solaris
|
|
||||||
|
|
||||||
package xdg
|
|
||||||
|
|
||||||
import (
|
|
||||||
"os"
|
|
||||||
"path/filepath"
|
|
||||||
"strconv"
|
|
||||||
|
|
||||||
"github.com/adrg/xdg/internal/pathutil"
|
|
||||||
"github.com/adrg/xdg/internal/userdirs"
|
|
||||||
)
|
|
||||||
|
|
||||||
func initDirs(home string) {
|
|
||||||
initBaseDirs(home)
|
|
||||||
initUserDirs(home, baseDirs.configHome)
|
|
||||||
}
|
|
||||||
|
|
||||||
func initBaseDirs(home string) {
|
|
||||||
// Initialize standard directories.
|
|
||||||
baseDirs.dataHome = pathutil.EnvPath(envDataHome, filepath.Join(home, ".local", "share"))
|
|
||||||
baseDirs.data = pathutil.EnvPathList(envDataDirs, "/usr/local/share", "/usr/share")
|
|
||||||
baseDirs.configHome = pathutil.EnvPath(envConfigHome, filepath.Join(home, ".config"))
|
|
||||||
baseDirs.config = pathutil.EnvPathList(envConfigDirs, "/etc/xdg")
|
|
||||||
baseDirs.stateHome = pathutil.EnvPath(envStateHome, filepath.Join(home, ".local", "state"))
|
|
||||||
baseDirs.cacheHome = pathutil.EnvPath(envCacheHome, filepath.Join(home, ".cache"))
|
|
||||||
baseDirs.runtime = pathutil.EnvPath(envRuntimeDir, filepath.Join("/run/user", strconv.Itoa(os.Getuid())))
|
|
||||||
|
|
||||||
// Initialize non-standard directories.
|
|
||||||
baseDirs.binHome = pathutil.EnvPath(envBinHome, filepath.Join(home, ".local", "bin"))
|
|
||||||
|
|
||||||
appDirs := []string{
|
|
||||||
filepath.Join(baseDirs.dataHome, "applications"),
|
|
||||||
filepath.Join(home, ".local/share/applications"),
|
|
||||||
"/usr/local/share/applications",
|
|
||||||
"/usr/share/applications",
|
|
||||||
}
|
|
||||||
|
|
||||||
fontDirs := []string{
|
|
||||||
filepath.Join(baseDirs.dataHome, "fonts"),
|
|
||||||
filepath.Join(home, ".fonts"),
|
|
||||||
filepath.Join(home, ".local/share/fonts"),
|
|
||||||
"/usr/local/share/fonts",
|
|
||||||
"/usr/share/fonts",
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, dir := range baseDirs.data {
|
|
||||||
appDirs = append(appDirs, filepath.Join(dir, "applications"))
|
|
||||||
fontDirs = append(fontDirs, filepath.Join(dir, "fonts"))
|
|
||||||
}
|
|
||||||
|
|
||||||
baseDirs.applications = pathutil.Unique(appDirs)
|
|
||||||
baseDirs.fonts = pathutil.Unique(fontDirs)
|
|
||||||
}
|
|
||||||
|
|
||||||
func initUserDirs(home, configHome string) {
|
|
||||||
dirs, err := userdirs.ParseConfigFile(filepath.Join(configHome, "user-dirs.dirs"))
|
|
||||||
if err != nil {
|
|
||||||
dirs = &UserDirectories{}
|
|
||||||
}
|
|
||||||
|
|
||||||
UserDirs.Desktop = pathutil.EnvPath(userdirs.EnvDesktopDir, dirs.Desktop, filepath.Join(home, "Desktop"))
|
|
||||||
UserDirs.Download = pathutil.EnvPath(userdirs.EnvDownloadDir, dirs.Download, filepath.Join(home, "Downloads"))
|
|
||||||
UserDirs.Documents = pathutil.EnvPath(userdirs.EnvDocumentsDir, dirs.Documents, filepath.Join(home, "Documents"))
|
|
||||||
UserDirs.Music = pathutil.EnvPath(userdirs.EnvMusicDir, dirs.Music, filepath.Join(home, "Music"))
|
|
||||||
UserDirs.Pictures = pathutil.EnvPath(userdirs.EnvPicturesDir, dirs.Pictures, filepath.Join(home, "Pictures"))
|
|
||||||
UserDirs.Videos = pathutil.EnvPath(userdirs.EnvVideosDir, dirs.Videos, filepath.Join(home, "Videos"))
|
|
||||||
UserDirs.Templates = pathutil.EnvPath(userdirs.EnvTemplatesDir, dirs.Templates, filepath.Join(home, "Templates"))
|
|
||||||
UserDirs.PublicShare = pathutil.EnvPath(userdirs.EnvPublicShareDir, dirs.PublicShare, filepath.Join(home, "Public"))
|
|
||||||
}
|
|
||||||
196
vendor/github.com/adrg/xdg/paths_windows.go
generated
vendored
196
vendor/github.com/adrg/xdg/paths_windows.go
generated
vendored
@@ -1,196 +0,0 @@
|
|||||||
package xdg
|
|
||||||
|
|
||||||
import (
|
|
||||||
"path/filepath"
|
|
||||||
|
|
||||||
"github.com/adrg/xdg/internal/pathutil"
|
|
||||||
"github.com/adrg/xdg/internal/userdirs"
|
|
||||||
"golang.org/x/sys/windows"
|
|
||||||
)
|
|
||||||
|
|
||||||
func initDirs(home string) {
|
|
||||||
kf := initKnownFolders(home)
|
|
||||||
initBaseDirs(home, kf)
|
|
||||||
initUserDirs(home, kf)
|
|
||||||
}
|
|
||||||
|
|
||||||
func initBaseDirs(home string, kf *knownFolders) {
|
|
||||||
// Initialize standard directories.
|
|
||||||
baseDirs.dataHome = pathutil.EnvPath(envDataHome, kf.localAppData)
|
|
||||||
baseDirs.data = pathutil.EnvPathList(envDataDirs, kf.roamingAppData, kf.programData)
|
|
||||||
baseDirs.configHome = pathutil.EnvPath(envConfigHome, kf.localAppData)
|
|
||||||
baseDirs.config = pathutil.EnvPathList(envConfigDirs, kf.programData, kf.roamingAppData)
|
|
||||||
baseDirs.stateHome = pathutil.EnvPath(envStateHome, kf.localAppData)
|
|
||||||
baseDirs.cacheHome = pathutil.EnvPath(envCacheHome, filepath.Join(kf.localAppData, "cache"))
|
|
||||||
baseDirs.runtime = pathutil.EnvPath(envRuntimeDir, kf.localAppData)
|
|
||||||
|
|
||||||
// Initialize non-standard directories.
|
|
||||||
baseDirs.binHome = pathutil.EnvPath(envBinHome, kf.userProgramFiles)
|
|
||||||
|
|
||||||
baseDirs.applications = []string{
|
|
||||||
kf.programs,
|
|
||||||
kf.commonPrograms,
|
|
||||||
kf.programFiles,
|
|
||||||
kf.programFilesCommon,
|
|
||||||
kf.userProgramFiles,
|
|
||||||
kf.userProgramFilesCommon,
|
|
||||||
}
|
|
||||||
|
|
||||||
baseDirs.fonts = []string{
|
|
||||||
kf.fonts,
|
|
||||||
filepath.Join(kf.localAppData, "Microsoft", "Windows", "Fonts"),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func initUserDirs(home string, kf *knownFolders) {
|
|
||||||
UserDirs.Desktop = pathutil.EnvPath(userdirs.EnvDesktopDir, kf.desktop)
|
|
||||||
UserDirs.Download = pathutil.EnvPath(userdirs.EnvDownloadDir, kf.downloads)
|
|
||||||
UserDirs.Documents = pathutil.EnvPath(userdirs.EnvDocumentsDir, kf.documents)
|
|
||||||
UserDirs.Music = pathutil.EnvPath(userdirs.EnvMusicDir, kf.music)
|
|
||||||
UserDirs.Pictures = pathutil.EnvPath(userdirs.EnvPicturesDir, kf.pictures)
|
|
||||||
UserDirs.Videos = pathutil.EnvPath(userdirs.EnvVideosDir, kf.videos)
|
|
||||||
UserDirs.Templates = pathutil.EnvPath(userdirs.EnvTemplatesDir, kf.templates)
|
|
||||||
UserDirs.PublicShare = pathutil.EnvPath(userdirs.EnvPublicShareDir, kf.public)
|
|
||||||
}
|
|
||||||
|
|
||||||
type knownFolders struct {
|
|
||||||
systemDrive string
|
|
||||||
systemRoot string
|
|
||||||
programData string
|
|
||||||
userProfile string
|
|
||||||
userProfiles string
|
|
||||||
roamingAppData string
|
|
||||||
localAppData string
|
|
||||||
desktop string
|
|
||||||
downloads string
|
|
||||||
documents string
|
|
||||||
music string
|
|
||||||
pictures string
|
|
||||||
videos string
|
|
||||||
templates string
|
|
||||||
public string
|
|
||||||
fonts string
|
|
||||||
programs string
|
|
||||||
commonPrograms string
|
|
||||||
programFiles string
|
|
||||||
programFilesCommon string
|
|
||||||
userProgramFiles string
|
|
||||||
userProgramFilesCommon string
|
|
||||||
}
|
|
||||||
|
|
||||||
func initKnownFolders(home string) *knownFolders {
|
|
||||||
kf := &knownFolders{
|
|
||||||
userProfile: home,
|
|
||||||
}
|
|
||||||
kf.systemDrive = filepath.VolumeName(pathutil.KnownFolder(
|
|
||||||
windows.FOLDERID_Windows,
|
|
||||||
[]string{"SystemDrive", "SystemRoot", "windir"},
|
|
||||||
[]string{home, `C:`},
|
|
||||||
)) + string(filepath.Separator)
|
|
||||||
kf.systemRoot = pathutil.KnownFolder(
|
|
||||||
windows.FOLDERID_Windows,
|
|
||||||
[]string{"SystemRoot", "windir"},
|
|
||||||
[]string{filepath.Join(kf.systemDrive, "Windows")},
|
|
||||||
)
|
|
||||||
kf.programData = pathutil.KnownFolder(
|
|
||||||
windows.FOLDERID_ProgramData,
|
|
||||||
[]string{"ProgramData", "ALLUSERSPROFILE"},
|
|
||||||
[]string{filepath.Join(kf.systemDrive, "ProgramData")},
|
|
||||||
)
|
|
||||||
kf.userProfiles = pathutil.KnownFolder(
|
|
||||||
windows.FOLDERID_UserProfiles,
|
|
||||||
nil,
|
|
||||||
[]string{filepath.Join(kf.systemDrive, "Users")},
|
|
||||||
)
|
|
||||||
kf.roamingAppData = pathutil.KnownFolder(
|
|
||||||
windows.FOLDERID_RoamingAppData,
|
|
||||||
[]string{"APPDATA"},
|
|
||||||
[]string{filepath.Join(home, "AppData", "Roaming")},
|
|
||||||
)
|
|
||||||
kf.localAppData = pathutil.KnownFolder(
|
|
||||||
windows.FOLDERID_LocalAppData,
|
|
||||||
[]string{"LOCALAPPDATA"},
|
|
||||||
[]string{filepath.Join(home, "AppData", "Local")},
|
|
||||||
)
|
|
||||||
kf.desktop = pathutil.KnownFolder(
|
|
||||||
windows.FOLDERID_Desktop,
|
|
||||||
nil,
|
|
||||||
[]string{filepath.Join(home, "Desktop")},
|
|
||||||
)
|
|
||||||
kf.downloads = pathutil.KnownFolder(
|
|
||||||
windows.FOLDERID_Downloads,
|
|
||||||
nil,
|
|
||||||
[]string{filepath.Join(home, "Downloads")},
|
|
||||||
)
|
|
||||||
kf.documents = pathutil.KnownFolder(
|
|
||||||
windows.FOLDERID_Documents,
|
|
||||||
nil,
|
|
||||||
[]string{filepath.Join(home, "Documents")},
|
|
||||||
)
|
|
||||||
kf.music = pathutil.KnownFolder(
|
|
||||||
windows.FOLDERID_Music,
|
|
||||||
nil,
|
|
||||||
[]string{filepath.Join(home, "Music")},
|
|
||||||
)
|
|
||||||
kf.pictures = pathutil.KnownFolder(
|
|
||||||
windows.FOLDERID_Pictures,
|
|
||||||
nil,
|
|
||||||
[]string{filepath.Join(home, "Pictures")},
|
|
||||||
)
|
|
||||||
kf.videos = pathutil.KnownFolder(
|
|
||||||
windows.FOLDERID_Videos,
|
|
||||||
nil,
|
|
||||||
[]string{filepath.Join(home, "Videos")},
|
|
||||||
)
|
|
||||||
kf.templates = pathutil.KnownFolder(
|
|
||||||
windows.FOLDERID_Templates,
|
|
||||||
nil,
|
|
||||||
[]string{filepath.Join(kf.roamingAppData, "Microsoft", "Windows", "Templates")},
|
|
||||||
)
|
|
||||||
kf.public = pathutil.KnownFolder(
|
|
||||||
windows.FOLDERID_Public,
|
|
||||||
[]string{"PUBLIC"},
|
|
||||||
[]string{filepath.Join(kf.userProfiles, "Public")},
|
|
||||||
)
|
|
||||||
kf.fonts = pathutil.KnownFolder(
|
|
||||||
windows.FOLDERID_Fonts,
|
|
||||||
nil,
|
|
||||||
[]string{filepath.Join(kf.systemRoot, "Fonts")},
|
|
||||||
)
|
|
||||||
kf.programs = pathutil.KnownFolder(
|
|
||||||
windows.FOLDERID_Programs,
|
|
||||||
nil,
|
|
||||||
[]string{filepath.Join(kf.roamingAppData, "Microsoft", "Windows", "Start Menu", "Programs")},
|
|
||||||
)
|
|
||||||
kf.commonPrograms = pathutil.KnownFolder(
|
|
||||||
windows.FOLDERID_CommonPrograms,
|
|
||||||
nil,
|
|
||||||
[]string{filepath.Join(kf.programData, "Microsoft", "Windows", "Start Menu", "Programs")},
|
|
||||||
)
|
|
||||||
kf.programFiles = pathutil.KnownFolder(
|
|
||||||
windows.FOLDERID_ProgramFiles,
|
|
||||||
[]string{"ProgramFiles"},
|
|
||||||
[]string{filepath.Join(kf.systemDrive, "Program Files")},
|
|
||||||
)
|
|
||||||
kf.programFilesCommon = pathutil.KnownFolder(
|
|
||||||
windows.FOLDERID_ProgramFilesCommon,
|
|
||||||
nil,
|
|
||||||
[]string{filepath.Join(kf.programFiles, "Common Files")},
|
|
||||||
)
|
|
||||||
kf.userProgramFiles = pathutil.KnownFolder(
|
|
||||||
windows.FOLDERID_UserProgramFiles,
|
|
||||||
nil,
|
|
||||||
[]string{
|
|
||||||
filepath.Join(kf.localAppData, "Programs"),
|
|
||||||
},
|
|
||||||
)
|
|
||||||
kf.userProgramFilesCommon = pathutil.KnownFolder(
|
|
||||||
windows.FOLDERID_UserProgramFilesCommon,
|
|
||||||
nil,
|
|
||||||
[]string{
|
|
||||||
filepath.Join(kf.userProgramFiles, "Common"),
|
|
||||||
},
|
|
||||||
)
|
|
||||||
|
|
||||||
return kf
|
|
||||||
}
|
|
||||||
212
vendor/github.com/adrg/xdg/xdg.go
generated
vendored
212
vendor/github.com/adrg/xdg/xdg.go
generated
vendored
@@ -1,212 +0,0 @@
|
|||||||
package xdg
|
|
||||||
|
|
||||||
import (
|
|
||||||
"github.com/adrg/xdg/internal/pathutil"
|
|
||||||
"github.com/adrg/xdg/internal/userdirs"
|
|
||||||
)
|
|
||||||
|
|
||||||
// UserDirectories defines the locations of well known user directories.
|
|
||||||
type UserDirectories = userdirs.Directories
|
|
||||||
|
|
||||||
var (
|
|
||||||
// Home contains the path of the user's home directory.
|
|
||||||
Home string
|
|
||||||
|
|
||||||
// DataHome defines the base directory relative to which user-specific
|
|
||||||
// data files should be stored. This directory is defined by the
|
|
||||||
// $XDG_DATA_HOME environment variable. If the variable is not set,
|
|
||||||
// a default equal to $HOME/.local/share should be used.
|
|
||||||
DataHome string
|
|
||||||
|
|
||||||
// DataDirs defines the preference-ordered set of base directories to
|
|
||||||
// search for data files in addition to the DataHome base directory.
|
|
||||||
// This set of directories is defined by the $XDG_DATA_DIRS environment
|
|
||||||
// variable. If the variable is not set, the default directories
|
|
||||||
// to be used are /usr/local/share and /usr/share, in that order. The
|
|
||||||
// DataHome directory is considered more important than any of the
|
|
||||||
// directories defined by DataDirs. Therefore, user data files should be
|
|
||||||
// written relative to the DataHome directory, if possible.
|
|
||||||
DataDirs []string
|
|
||||||
|
|
||||||
// ConfigHome defines the base directory relative to which user-specific
|
|
||||||
// configuration files should be written. This directory is defined by
|
|
||||||
// the $XDG_CONFIG_HOME environment variable. If the variable is
|
|
||||||
// not set, a default equal to $HOME/.config should be used.
|
|
||||||
ConfigHome string
|
|
||||||
|
|
||||||
// ConfigDirs defines the preference-ordered set of base directories to
|
|
||||||
// search for configuration files in addition to the ConfigHome base
|
|
||||||
// directory. This set of directories is defined by the $XDG_CONFIG_DIRS
|
|
||||||
// environment variable. If the variable is not set, a default equal
|
|
||||||
// to /etc/xdg should be used. The ConfigHome directory is considered
|
|
||||||
// more important than any of the directories defined by ConfigDirs.
|
|
||||||
// Therefore, user config files should be written relative to the
|
|
||||||
// ConfigHome directory, if possible.
|
|
||||||
ConfigDirs []string
|
|
||||||
|
|
||||||
// StateHome defines the base directory relative to which user-specific
|
|
||||||
// state files should be stored. This directory is defined by the
|
|
||||||
// $XDG_STATE_HOME environment variable. If the variable is not set,
|
|
||||||
// a default equal to ~/.local/state should be used.
|
|
||||||
StateHome string
|
|
||||||
|
|
||||||
// CacheHome defines the base directory relative to which user-specific
|
|
||||||
// non-essential (cached) data should be written. This directory is
|
|
||||||
// defined by the $XDG_CACHE_HOME environment variable. If the variable
|
|
||||||
// is not set, a default equal to $HOME/.cache should be used.
|
|
||||||
CacheHome string
|
|
||||||
|
|
||||||
// RuntimeDir defines the base directory relative to which user-specific
|
|
||||||
// non-essential runtime files and other file objects (such as sockets,
|
|
||||||
// named pipes, etc.) should be stored. This directory is defined by the
|
|
||||||
// $XDG_RUNTIME_DIR environment variable. If the variable is not set,
|
|
||||||
// applications should fall back to a replacement directory with similar
|
|
||||||
// capabilities. Applications should use this directory for communication
|
|
||||||
// and synchronization purposes and should not place larger files in it,
|
|
||||||
// since it might reside in runtime memory and cannot necessarily be
|
|
||||||
// swapped out to disk.
|
|
||||||
RuntimeDir string
|
|
||||||
|
|
||||||
// BinHome defines the base directory relative to which user-specific
|
|
||||||
// binary files should be written. This directory is defined by
|
|
||||||
// the non-standard $XDG_BIN_HOME environment variable. If the variable is
|
|
||||||
// not set, a default equal to $HOME/.local/bin should be used.
|
|
||||||
BinHome string
|
|
||||||
|
|
||||||
// UserDirs defines the locations of well known user directories.
|
|
||||||
UserDirs UserDirectories
|
|
||||||
|
|
||||||
// FontDirs defines the common locations where font files are stored.
|
|
||||||
FontDirs []string
|
|
||||||
|
|
||||||
// ApplicationDirs defines the common locations of applications.
|
|
||||||
ApplicationDirs []string
|
|
||||||
|
|
||||||
// baseDirs defines the locations of base directories.
|
|
||||||
baseDirs baseDirectories
|
|
||||||
)
|
|
||||||
|
|
||||||
func init() {
|
|
||||||
Reload()
|
|
||||||
}
|
|
||||||
|
|
||||||
// Reload refreshes base and user directories by reading the environment.
|
|
||||||
// Defaults are applied for XDG variables which are empty or not present
|
|
||||||
// in the environment.
|
|
||||||
func Reload() {
|
|
||||||
// Initialize home directory.
|
|
||||||
Home = pathutil.UserHomeDir()
|
|
||||||
|
|
||||||
// Initialize base and user directories.
|
|
||||||
initDirs(Home)
|
|
||||||
|
|
||||||
// Set standard directories.
|
|
||||||
DataHome = baseDirs.dataHome
|
|
||||||
DataDirs = baseDirs.data
|
|
||||||
ConfigHome = baseDirs.configHome
|
|
||||||
ConfigDirs = baseDirs.config
|
|
||||||
StateHome = baseDirs.stateHome
|
|
||||||
CacheHome = baseDirs.cacheHome
|
|
||||||
RuntimeDir = baseDirs.runtime
|
|
||||||
|
|
||||||
// Set non-standard directories.
|
|
||||||
BinHome = baseDirs.binHome
|
|
||||||
FontDirs = baseDirs.fonts
|
|
||||||
ApplicationDirs = baseDirs.applications
|
|
||||||
}
|
|
||||||
|
|
||||||
// DataFile returns a suitable location for the specified data file.
|
|
||||||
// The relPath parameter must contain the name of the data file, and
|
|
||||||
// optionally, a set of parent directories (e.g. appname/app.data).
|
|
||||||
// If the specified directories do not exist, they will be created relative
|
|
||||||
// to the base data directory. On failure, an error containing the
|
|
||||||
// attempted paths is returned.
|
|
||||||
func DataFile(relPath string) (string, error) {
|
|
||||||
return baseDirs.dataFile(relPath)
|
|
||||||
}
|
|
||||||
|
|
||||||
// ConfigFile returns a suitable location for the specified config file.
|
|
||||||
// The relPath parameter must contain the name of the config file, and
|
|
||||||
// optionally, a set of parent directories (e.g. appname/app.yaml).
|
|
||||||
// If the specified directories do not exist, they will be created relative
|
|
||||||
// to the base config directory. On failure, an error containing the
|
|
||||||
// attempted paths is returned.
|
|
||||||
func ConfigFile(relPath string) (string, error) {
|
|
||||||
return baseDirs.configFile(relPath)
|
|
||||||
}
|
|
||||||
|
|
||||||
// StateFile returns a suitable location for the specified state file. State
|
|
||||||
// files are usually volatile data files, not suitable to be stored relative
|
|
||||||
// to the $XDG_DATA_HOME directory.
|
|
||||||
// The relPath parameter must contain the name of the state file, and
|
|
||||||
// optionally, a set of parent directories (e.g. appname/app.state).
|
|
||||||
// If the specified directories do not exist, they will be created relative
|
|
||||||
// to the base state directory. On failure, an error containing the
|
|
||||||
// attempted paths is returned.
|
|
||||||
func StateFile(relPath string) (string, error) {
|
|
||||||
return baseDirs.stateFile(relPath)
|
|
||||||
}
|
|
||||||
|
|
||||||
// CacheFile returns a suitable location for the specified cache file.
|
|
||||||
// The relPath parameter must contain the name of the cache file, and
|
|
||||||
// optionally, a set of parent directories (e.g. appname/app.cache).
|
|
||||||
// If the specified directories do not exist, they will be created relative
|
|
||||||
// to the base cache directory. On failure, an error containing the
|
|
||||||
// attempted paths is returned.
|
|
||||||
func CacheFile(relPath string) (string, error) {
|
|
||||||
return baseDirs.cacheFile(relPath)
|
|
||||||
}
|
|
||||||
|
|
||||||
// RuntimeFile returns a suitable location for the specified runtime file.
|
|
||||||
// The relPath parameter must contain the name of the runtime file, and
|
|
||||||
// optionally, a set of parent directories (e.g. appname/app.pid).
|
|
||||||
// If the specified directories do not exist, they will be created relative
|
|
||||||
// to the base runtime directory. If the base runtime directory does not exist,
|
|
||||||
// the operating system's temporary directory is used as a fallback. On failure,
|
|
||||||
// an error containing the attempted paths is returned.
|
|
||||||
func RuntimeFile(relPath string) (string, error) {
|
|
||||||
return baseDirs.runtimeFile(relPath)
|
|
||||||
}
|
|
||||||
|
|
||||||
// SearchDataFile searches for specified file in the data search paths.
|
|
||||||
// The relPath parameter must contain the name of the data file, and
|
|
||||||
// optionally, a set of parent directories (e.g. appname/app.data). If the
|
|
||||||
// file cannot be found, an error specifying the searched paths is returned.
|
|
||||||
func SearchDataFile(relPath string) (string, error) {
|
|
||||||
return baseDirs.searchDataFile(relPath)
|
|
||||||
}
|
|
||||||
|
|
||||||
// SearchConfigFile searches for the specified file in config search paths.
|
|
||||||
// The relPath parameter must contain the name of the config file, and
|
|
||||||
// optionally, a set of parent directories (e.g. appname/app.yaml). If the
|
|
||||||
// file cannot be found, an error specifying the searched paths is returned.
|
|
||||||
func SearchConfigFile(relPath string) (string, error) {
|
|
||||||
return baseDirs.searchConfigFile(relPath)
|
|
||||||
}
|
|
||||||
|
|
||||||
// SearchStateFile searches for the specified file in the state search path.
|
|
||||||
// The relPath parameter must contain the name of the state file, and
|
|
||||||
// optionally, a set of parent directories (e.g. appname/app.state). If the
|
|
||||||
// file cannot be found, an error specifying the searched path is returned.
|
|
||||||
func SearchStateFile(relPath string) (string, error) {
|
|
||||||
return baseDirs.searchStateFile(relPath)
|
|
||||||
}
|
|
||||||
|
|
||||||
// SearchCacheFile searches for the specified file in the cache search path.
|
|
||||||
// The relPath parameter must contain the name of the cache file, and
|
|
||||||
// optionally, a set of parent directories (e.g. appname/app.cache). If the
|
|
||||||
// file cannot be found, an error specifying the searched path is returned.
|
|
||||||
func SearchCacheFile(relPath string) (string, error) {
|
|
||||||
return baseDirs.searchCacheFile(relPath)
|
|
||||||
}
|
|
||||||
|
|
||||||
// SearchRuntimeFile searches for the specified file in the runtime search path.
|
|
||||||
// The relPath parameter must contain the name of the runtime file, and
|
|
||||||
// optionally, a set of parent directories (e.g. appname/app.pid). The runtime
|
|
||||||
// file is also searched in the operating system's temporary directory in order
|
|
||||||
// to cover cases in which the runtime base directory does not exist or is not
|
|
||||||
// accessible. If the file cannot be found, an error specifying the searched
|
|
||||||
// paths is returned.
|
|
||||||
func SearchRuntimeFile(relPath string) (string, error) {
|
|
||||||
return baseDirs.searchRuntimeFile(relPath)
|
|
||||||
}
|
|
||||||
13
vendor/github.com/coder/websocket/LICENSE.txt
generated
vendored
13
vendor/github.com/coder/websocket/LICENSE.txt
generated
vendored
@@ -1,13 +0,0 @@
|
|||||||
Copyright (c) 2025 Coder
|
|
||||||
|
|
||||||
Permission to use, copy, modify, and distribute this software for any
|
|
||||||
purpose with or without fee is hereby granted, provided that the above
|
|
||||||
copyright notice and this permission notice appear in all copies.
|
|
||||||
|
|
||||||
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
|
|
||||||
WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
|
|
||||||
MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
|
|
||||||
ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
|
|
||||||
WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
|
|
||||||
ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
|
|
||||||
OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
|
|
||||||
18
vendor/github.com/coder/websocket/Makefile
generated
vendored
18
vendor/github.com/coder/websocket/Makefile
generated
vendored
@@ -1,18 +0,0 @@
|
|||||||
.PHONY: all
|
|
||||||
all: fmt lint test
|
|
||||||
|
|
||||||
.PHONY: fmt
|
|
||||||
fmt:
|
|
||||||
./ci/fmt.sh
|
|
||||||
|
|
||||||
.PHONY: lint
|
|
||||||
lint:
|
|
||||||
./ci/lint.sh
|
|
||||||
|
|
||||||
.PHONY: test
|
|
||||||
test:
|
|
||||||
./ci/test.sh
|
|
||||||
|
|
||||||
.PHONY: bench
|
|
||||||
bench:
|
|
||||||
./ci/bench.sh
|
|
||||||
162
vendor/github.com/coder/websocket/README.md
generated
vendored
162
vendor/github.com/coder/websocket/README.md
generated
vendored
@@ -1,162 +0,0 @@
|
|||||||
# websocket
|
|
||||||
|
|
||||||
[](https://pkg.go.dev/github.com/coder/websocket)
|
|
||||||
[](https://coder.github.io/websocket/coverage.html)
|
|
||||||
|
|
||||||
websocket is a minimal and idiomatic WebSocket library for Go.
|
|
||||||
|
|
||||||
## Install
|
|
||||||
|
|
||||||
```sh
|
|
||||||
go get github.com/coder/websocket
|
|
||||||
```
|
|
||||||
|
|
||||||
> [!NOTE]
|
|
||||||
> Coder now maintains this project as explained in [this blog post](https://coder.com/blog/websocket).
|
|
||||||
> We're grateful to [nhooyr](https://github.com/nhooyr) for authoring and maintaining this project from
|
|
||||||
> 2019 to 2024.
|
|
||||||
|
|
||||||
## Highlights
|
|
||||||
|
|
||||||
- Minimal and idiomatic API
|
|
||||||
- First class [context.Context](https://blog.golang.org/context) support
|
|
||||||
- Fully passes the WebSocket [autobahn-testsuite](https://github.com/crossbario/autobahn-testsuite)
|
|
||||||
- [Zero dependencies](https://pkg.go.dev/github.com/coder/websocket?tab=imports)
|
|
||||||
- JSON helpers in the [wsjson](https://pkg.go.dev/github.com/coder/websocket/wsjson) subpackage
|
|
||||||
- Zero alloc reads and writes
|
|
||||||
- Concurrent writes
|
|
||||||
- [Close handshake](https://pkg.go.dev/github.com/coder/websocket#Conn.Close)
|
|
||||||
- [net.Conn](https://pkg.go.dev/github.com/coder/websocket#NetConn) wrapper
|
|
||||||
- [Ping pong](https://pkg.go.dev/github.com/coder/websocket#Conn.Ping) API
|
|
||||||
- [RFC 7692](https://tools.ietf.org/html/rfc7692) permessage-deflate compression
|
|
||||||
- [CloseRead](https://pkg.go.dev/github.com/coder/websocket#Conn.CloseRead) helper for write only connections
|
|
||||||
- Compile to [Wasm](https://pkg.go.dev/github.com/coder/websocket#hdr-Wasm)
|
|
||||||
|
|
||||||
## Roadmap
|
|
||||||
|
|
||||||
See GitHub issues for minor issues but the major future enhancements are:
|
|
||||||
|
|
||||||
- [ ] Perfect examples [#217](https://github.com/nhooyr/websocket/issues/217)
|
|
||||||
- [ ] wstest.Pipe for in memory testing [#340](https://github.com/nhooyr/websocket/issues/340)
|
|
||||||
- [ ] Ping pong heartbeat helper [#267](https://github.com/nhooyr/websocket/issues/267)
|
|
||||||
- [ ] Ping pong instrumentation callbacks [#246](https://github.com/nhooyr/websocket/issues/246)
|
|
||||||
- [ ] Graceful shutdown helpers [#209](https://github.com/nhooyr/websocket/issues/209)
|
|
||||||
- [ ] Assembly for WebSocket masking [#16](https://github.com/nhooyr/websocket/issues/16)
|
|
||||||
- WIP at [#326](https://github.com/nhooyr/websocket/pull/326), about 3x faster
|
|
||||||
- [ ] HTTP/2 [#4](https://github.com/nhooyr/websocket/issues/4)
|
|
||||||
- [ ] The holy grail [#402](https://github.com/nhooyr/websocket/issues/402)
|
|
||||||
|
|
||||||
## Examples
|
|
||||||
|
|
||||||
For a production quality example that demonstrates the complete API, see the
|
|
||||||
[echo example](./internal/examples/echo).
|
|
||||||
|
|
||||||
For a full stack example, see the [chat example](./internal/examples/chat).
|
|
||||||
|
|
||||||
### Server
|
|
||||||
|
|
||||||
```go
|
|
||||||
http.HandlerFunc(func (w http.ResponseWriter, r *http.Request) {
|
|
||||||
c, err := websocket.Accept(w, r, nil)
|
|
||||||
if err != nil {
|
|
||||||
// ...
|
|
||||||
}
|
|
||||||
defer c.CloseNow()
|
|
||||||
|
|
||||||
// Set the context as needed. Use of r.Context() is not recommended
|
|
||||||
// to avoid surprising behavior (see http.Hijacker).
|
|
||||||
ctx, cancel := context.WithTimeout(context.Background(), time.Second*10)
|
|
||||||
defer cancel()
|
|
||||||
|
|
||||||
var v any
|
|
||||||
err = wsjson.Read(ctx, c, &v)
|
|
||||||
if err != nil {
|
|
||||||
// ...
|
|
||||||
}
|
|
||||||
|
|
||||||
log.Printf("received: %v", v)
|
|
||||||
|
|
||||||
c.Close(websocket.StatusNormalClosure, "")
|
|
||||||
})
|
|
||||||
```
|
|
||||||
|
|
||||||
### Client
|
|
||||||
|
|
||||||
```go
|
|
||||||
ctx, cancel := context.WithTimeout(context.Background(), time.Minute)
|
|
||||||
defer cancel()
|
|
||||||
|
|
||||||
c, _, err := websocket.Dial(ctx, "ws://localhost:8080", nil)
|
|
||||||
if err != nil {
|
|
||||||
// ...
|
|
||||||
}
|
|
||||||
defer c.CloseNow()
|
|
||||||
|
|
||||||
err = wsjson.Write(ctx, c, "hi")
|
|
||||||
if err != nil {
|
|
||||||
// ...
|
|
||||||
}
|
|
||||||
|
|
||||||
c.Close(websocket.StatusNormalClosure, "")
|
|
||||||
```
|
|
||||||
|
|
||||||
## Comparison
|
|
||||||
|
|
||||||
### gorilla/websocket
|
|
||||||
|
|
||||||
Advantages of [gorilla/websocket](https://github.com/gorilla/websocket):
|
|
||||||
|
|
||||||
- Mature and widely used
|
|
||||||
- [Prepared writes](https://pkg.go.dev/github.com/gorilla/websocket#PreparedMessage)
|
|
||||||
- Configurable [buffer sizes](https://pkg.go.dev/github.com/gorilla/websocket#hdr-Buffers)
|
|
||||||
- No extra goroutine per connection to support cancellation with context.Context. This costs github.com/coder/websocket 2 KB of memory per connection.
|
|
||||||
- Will be removed soon with [context.AfterFunc](https://github.com/golang/go/issues/57928). See [#411](https://github.com/nhooyr/websocket/issues/411)
|
|
||||||
|
|
||||||
Advantages of github.com/coder/websocket:
|
|
||||||
|
|
||||||
- Minimal and idiomatic API
|
|
||||||
- Compare godoc of [github.com/coder/websocket](https://pkg.go.dev/github.com/coder/websocket) with [gorilla/websocket](https://pkg.go.dev/github.com/gorilla/websocket) side by side.
|
|
||||||
- [net.Conn](https://pkg.go.dev/github.com/coder/websocket#NetConn) wrapper
|
|
||||||
- Zero alloc reads and writes ([gorilla/websocket#535](https://github.com/gorilla/websocket/issues/535))
|
|
||||||
- Full [context.Context](https://blog.golang.org/context) support
|
|
||||||
- Dial uses [net/http.Client](https://golang.org/pkg/net/http/#Client)
|
|
||||||
- Will enable easy HTTP/2 support in the future
|
|
||||||
- Gorilla writes directly to a net.Conn and so duplicates features of net/http.Client.
|
|
||||||
- Concurrent writes
|
|
||||||
- Close handshake ([gorilla/websocket#448](https://github.com/gorilla/websocket/issues/448))
|
|
||||||
- Idiomatic [ping pong](https://pkg.go.dev/github.com/coder/websocket#Conn.Ping) API
|
|
||||||
- Gorilla requires registering a pong callback before sending a Ping
|
|
||||||
- Can target Wasm ([gorilla/websocket#432](https://github.com/gorilla/websocket/issues/432))
|
|
||||||
- Transparent message buffer reuse with [wsjson](https://pkg.go.dev/github.com/coder/websocket/wsjson) subpackage
|
|
||||||
- [1.75x](https://github.com/nhooyr/websocket/releases/tag/v1.7.4) faster WebSocket masking implementation in pure Go
|
|
||||||
- Gorilla's implementation is slower and uses [unsafe](https://golang.org/pkg/unsafe/).
|
|
||||||
Soon we'll have assembly and be 3x faster [#326](https://github.com/nhooyr/websocket/pull/326)
|
|
||||||
- Full [permessage-deflate](https://tools.ietf.org/html/rfc7692) compression extension support
|
|
||||||
- Gorilla only supports no context takeover mode
|
|
||||||
- [CloseRead](https://pkg.go.dev/github.com/coder/websocket#Conn.CloseRead) helper for write only connections ([gorilla/websocket#492](https://github.com/gorilla/websocket/issues/492))
|
|
||||||
|
|
||||||
#### golang.org/x/net/websocket
|
|
||||||
|
|
||||||
[golang.org/x/net/websocket](https://pkg.go.dev/golang.org/x/net/websocket) is deprecated.
|
|
||||||
See [golang/go/issues/18152](https://github.com/golang/go/issues/18152).
|
|
||||||
|
|
||||||
The [net.Conn](https://pkg.go.dev/github.com/coder/websocket#NetConn) can help in transitioning
|
|
||||||
to github.com/coder/websocket.
|
|
||||||
|
|
||||||
#### gobwas/ws
|
|
||||||
|
|
||||||
[gobwas/ws](https://github.com/gobwas/ws) has an extremely flexible API that allows it to be used
|
|
||||||
in an event driven style for performance. See the author's [blog post](https://medium.freecodecamp.org/million-websockets-and-go-cc58418460bb).
|
|
||||||
|
|
||||||
However it is quite bloated. See https://pkg.go.dev/github.com/gobwas/ws
|
|
||||||
|
|
||||||
When writing idiomatic Go, github.com/coder/websocket will be faster and easier to use.
|
|
||||||
|
|
||||||
#### lesismal/nbio
|
|
||||||
|
|
||||||
[lesismal/nbio](https://github.com/lesismal/nbio) is similar to gobwas/ws in that the API is
|
|
||||||
event driven for performance reasons.
|
|
||||||
|
|
||||||
However it is quite bloated. See https://pkg.go.dev/github.com/lesismal/nbio
|
|
||||||
|
|
||||||
When writing idiomatic Go, github.com/coder/websocket will be faster and easier to use.
|
|
||||||
378
vendor/github.com/coder/websocket/accept.go
generated
vendored
378
vendor/github.com/coder/websocket/accept.go
generated
vendored
@@ -1,378 +0,0 @@
|
|||||||
//go:build !js
|
|
||||||
|
|
||||||
package websocket
|
|
||||||
|
|
||||||
import (
|
|
||||||
"bytes"
|
|
||||||
"context"
|
|
||||||
"crypto/sha1"
|
|
||||||
"encoding/base64"
|
|
||||||
"errors"
|
|
||||||
"fmt"
|
|
||||||
"io"
|
|
||||||
"log"
|
|
||||||
"net/http"
|
|
||||||
"net/textproto"
|
|
||||||
"net/url"
|
|
||||||
"path"
|
|
||||||
"strings"
|
|
||||||
|
|
||||||
"github.com/coder/websocket/internal/errd"
|
|
||||||
)
|
|
||||||
|
|
||||||
// AcceptOptions represents Accept's options.
|
|
||||||
type AcceptOptions struct {
|
|
||||||
// Subprotocols lists the WebSocket subprotocols that Accept will negotiate with the client.
|
|
||||||
// The empty subprotocol will always be negotiated as per RFC 6455. If you would like to
|
|
||||||
// reject it, close the connection when c.Subprotocol() == "".
|
|
||||||
Subprotocols []string
|
|
||||||
|
|
||||||
// InsecureSkipVerify is used to disable Accept's origin verification behaviour.
|
|
||||||
//
|
|
||||||
// You probably want to use OriginPatterns instead.
|
|
||||||
InsecureSkipVerify bool
|
|
||||||
|
|
||||||
// OriginPatterns lists the host patterns for authorized origins.
|
|
||||||
// The request host is always authorized.
|
|
||||||
// Use this to enable cross origin WebSockets.
|
|
||||||
//
|
|
||||||
// i.e javascript running on example.com wants to access a WebSocket server at chat.example.com.
|
|
||||||
// In such a case, example.com is the origin and chat.example.com is the request host.
|
|
||||||
// One would set this field to []string{"example.com"} to authorize example.com to connect.
|
|
||||||
//
|
|
||||||
// Each pattern is matched case insensitively with path.Match (see
|
|
||||||
// https://golang.org/pkg/path/#Match). By default, it is matched
|
|
||||||
// against the request origin host. If the pattern contains a URI
|
|
||||||
// scheme ("://"), it will be matched against "scheme://host".
|
|
||||||
//
|
|
||||||
// Please ensure you understand the ramifications of enabling this.
|
|
||||||
// If used incorrectly your WebSocket server will be open to CSRF attacks.
|
|
||||||
//
|
|
||||||
// Do not use * as a pattern to allow any origin, prefer to use InsecureSkipVerify instead
|
|
||||||
// to bring attention to the danger of such a setting.
|
|
||||||
OriginPatterns []string
|
|
||||||
|
|
||||||
// CompressionMode controls the compression mode.
|
|
||||||
// Defaults to CompressionDisabled.
|
|
||||||
//
|
|
||||||
// See docs on CompressionMode for details.
|
|
||||||
CompressionMode CompressionMode
|
|
||||||
|
|
||||||
// CompressionThreshold controls the minimum size of a message before compression is applied.
|
|
||||||
//
|
|
||||||
// Defaults to 512 bytes for CompressionNoContextTakeover and 128 bytes
|
|
||||||
// for CompressionContextTakeover.
|
|
||||||
CompressionThreshold int
|
|
||||||
|
|
||||||
// OnPingReceived is an optional callback invoked synchronously when a ping frame is received.
|
|
||||||
//
|
|
||||||
// The payload contains the application data of the ping frame.
|
|
||||||
// If the callback returns false, the subsequent pong frame will not be sent.
|
|
||||||
// To avoid blocking, any expensive processing should be performed asynchronously using a goroutine.
|
|
||||||
OnPingReceived func(ctx context.Context, payload []byte) bool
|
|
||||||
|
|
||||||
// OnPongReceived is an optional callback invoked synchronously when a pong frame is received.
|
|
||||||
//
|
|
||||||
// The payload contains the application data of the pong frame.
|
|
||||||
// To avoid blocking, any expensive processing should be performed asynchronously using a goroutine.
|
|
||||||
//
|
|
||||||
// Unlike OnPingReceived, this callback does not return a value because a pong frame
|
|
||||||
// is a response to a ping and does not trigger any further frame transmission.
|
|
||||||
OnPongReceived func(ctx context.Context, payload []byte)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (opts *AcceptOptions) cloneWithDefaults() *AcceptOptions {
|
|
||||||
var o AcceptOptions
|
|
||||||
if opts != nil {
|
|
||||||
o = *opts
|
|
||||||
}
|
|
||||||
return &o
|
|
||||||
}
|
|
||||||
|
|
||||||
// Accept accepts a WebSocket handshake from a client and upgrades the
|
|
||||||
// the connection to a WebSocket.
|
|
||||||
//
|
|
||||||
// Accept will not allow cross origin requests by default.
|
|
||||||
// See the InsecureSkipVerify and OriginPatterns options to allow cross origin requests.
|
|
||||||
//
|
|
||||||
// Accept will write a response to w on all errors.
|
|
||||||
//
|
|
||||||
// Note that using the http.Request Context after Accept returns may lead to
|
|
||||||
// unexpected behavior (see http.Hijacker).
|
|
||||||
func Accept(w http.ResponseWriter, r *http.Request, opts *AcceptOptions) (*Conn, error) {
|
|
||||||
return accept(w, r, opts)
|
|
||||||
}
|
|
||||||
|
|
||||||
func accept(w http.ResponseWriter, r *http.Request, opts *AcceptOptions) (_ *Conn, err error) {
|
|
||||||
defer errd.Wrap(&err, "failed to accept WebSocket connection")
|
|
||||||
|
|
||||||
errCode, err := verifyClientRequest(w, r)
|
|
||||||
if err != nil {
|
|
||||||
http.Error(w, err.Error(), errCode)
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
opts = opts.cloneWithDefaults()
|
|
||||||
if !opts.InsecureSkipVerify {
|
|
||||||
err = authenticateOrigin(r, opts.OriginPatterns)
|
|
||||||
if err != nil {
|
|
||||||
if errors.Is(err, path.ErrBadPattern) {
|
|
||||||
log.Printf("websocket: %v", err)
|
|
||||||
err = errors.New(http.StatusText(http.StatusForbidden))
|
|
||||||
}
|
|
||||||
http.Error(w, err.Error(), http.StatusForbidden)
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
hj, ok := hijacker(w)
|
|
||||||
if !ok {
|
|
||||||
err = errors.New("http.ResponseWriter does not implement http.Hijacker")
|
|
||||||
http.Error(w, http.StatusText(http.StatusNotImplemented), http.StatusNotImplemented)
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
w.Header().Set("Upgrade", "websocket")
|
|
||||||
w.Header().Set("Connection", "Upgrade")
|
|
||||||
|
|
||||||
key := r.Header.Get("Sec-WebSocket-Key")
|
|
||||||
w.Header().Set("Sec-WebSocket-Accept", secWebSocketAccept(key))
|
|
||||||
|
|
||||||
subproto := selectSubprotocol(r, opts.Subprotocols)
|
|
||||||
if subproto != "" {
|
|
||||||
w.Header().Set("Sec-WebSocket-Protocol", subproto)
|
|
||||||
}
|
|
||||||
|
|
||||||
copts, ok := selectDeflate(websocketExtensions(r.Header), opts.CompressionMode)
|
|
||||||
if ok {
|
|
||||||
w.Header().Set("Sec-WebSocket-Extensions", copts.String())
|
|
||||||
}
|
|
||||||
|
|
||||||
w.WriteHeader(http.StatusSwitchingProtocols)
|
|
||||||
// See https://github.com/nhooyr/websocket/issues/166
|
|
||||||
if ginWriter, ok := w.(interface {
|
|
||||||
WriteHeaderNow()
|
|
||||||
}); ok {
|
|
||||||
ginWriter.WriteHeaderNow()
|
|
||||||
}
|
|
||||||
|
|
||||||
netConn, brw, err := hj.Hijack()
|
|
||||||
if err != nil {
|
|
||||||
err = fmt.Errorf("failed to hijack connection: %w", err)
|
|
||||||
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
// https://github.com/golang/go/issues/32314
|
|
||||||
b, _ := brw.Reader.Peek(brw.Reader.Buffered())
|
|
||||||
brw.Reader.Reset(io.MultiReader(bytes.NewReader(b), netConn))
|
|
||||||
|
|
||||||
return newConn(connConfig{
|
|
||||||
subprotocol: w.Header().Get("Sec-WebSocket-Protocol"),
|
|
||||||
rwc: netConn,
|
|
||||||
client: false,
|
|
||||||
copts: copts,
|
|
||||||
flateThreshold: opts.CompressionThreshold,
|
|
||||||
onPingReceived: opts.OnPingReceived,
|
|
||||||
onPongReceived: opts.OnPongReceived,
|
|
||||||
|
|
||||||
br: brw.Reader,
|
|
||||||
bw: brw.Writer,
|
|
||||||
}), nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func verifyClientRequest(w http.ResponseWriter, r *http.Request) (errCode int, _ error) {
|
|
||||||
if !r.ProtoAtLeast(1, 1) {
|
|
||||||
return http.StatusUpgradeRequired, fmt.Errorf("WebSocket protocol violation: handshake request must be at least HTTP/1.1: %q", r.Proto)
|
|
||||||
}
|
|
||||||
|
|
||||||
if !headerContainsTokenIgnoreCase(r.Header, "Connection", "Upgrade") {
|
|
||||||
w.Header().Set("Connection", "Upgrade")
|
|
||||||
w.Header().Set("Upgrade", "websocket")
|
|
||||||
return http.StatusUpgradeRequired, fmt.Errorf("WebSocket protocol violation: Connection header %q does not contain Upgrade", r.Header.Get("Connection"))
|
|
||||||
}
|
|
||||||
|
|
||||||
if !headerContainsTokenIgnoreCase(r.Header, "Upgrade", "websocket") {
|
|
||||||
w.Header().Set("Connection", "Upgrade")
|
|
||||||
w.Header().Set("Upgrade", "websocket")
|
|
||||||
return http.StatusUpgradeRequired, fmt.Errorf("WebSocket protocol violation: Upgrade header %q does not contain websocket", r.Header.Get("Upgrade"))
|
|
||||||
}
|
|
||||||
|
|
||||||
if r.Method != "GET" {
|
|
||||||
return http.StatusMethodNotAllowed, fmt.Errorf("WebSocket protocol violation: handshake request method is not GET but %q", r.Method)
|
|
||||||
}
|
|
||||||
|
|
||||||
if r.Header.Get("Sec-WebSocket-Version") != "13" {
|
|
||||||
w.Header().Set("Sec-WebSocket-Version", "13")
|
|
||||||
return http.StatusBadRequest, fmt.Errorf("unsupported WebSocket protocol version (only 13 is supported): %q", r.Header.Get("Sec-WebSocket-Version"))
|
|
||||||
}
|
|
||||||
|
|
||||||
websocketSecKeys := r.Header.Values("Sec-WebSocket-Key")
|
|
||||||
if len(websocketSecKeys) == 0 {
|
|
||||||
return http.StatusBadRequest, errors.New("WebSocket protocol violation: missing Sec-WebSocket-Key")
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(websocketSecKeys) > 1 {
|
|
||||||
return http.StatusBadRequest, errors.New("WebSocket protocol violation: multiple Sec-WebSocket-Key headers")
|
|
||||||
}
|
|
||||||
|
|
||||||
// The RFC states to remove any leading or trailing whitespace.
|
|
||||||
websocketSecKey := strings.TrimSpace(websocketSecKeys[0])
|
|
||||||
if v, err := base64.StdEncoding.DecodeString(websocketSecKey); err != nil || len(v) != 16 {
|
|
||||||
return http.StatusBadRequest, fmt.Errorf("WebSocket protocol violation: invalid Sec-WebSocket-Key %q, must be a 16 byte base64 encoded string", websocketSecKey)
|
|
||||||
}
|
|
||||||
|
|
||||||
return 0, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func authenticateOrigin(r *http.Request, originHosts []string) error {
|
|
||||||
origin := r.Header.Get("Origin")
|
|
||||||
if origin == "" {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
u, err := url.Parse(origin)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("failed to parse Origin header %q: %w", origin, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if strings.EqualFold(r.Host, u.Host) {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, hostPattern := range originHosts {
|
|
||||||
target := u.Host
|
|
||||||
if strings.Contains(hostPattern, "://") {
|
|
||||||
target = u.Scheme + "://" + u.Host
|
|
||||||
}
|
|
||||||
matched, err := match(hostPattern, target)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("failed to parse path pattern %q: %w", hostPattern, err)
|
|
||||||
}
|
|
||||||
if matched {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if u.Host == "" {
|
|
||||||
return fmt.Errorf("request Origin %q is not a valid URL with a host", origin)
|
|
||||||
}
|
|
||||||
return fmt.Errorf("request Origin %q is not authorized for Host %q", u.Host, r.Host)
|
|
||||||
}
|
|
||||||
|
|
||||||
func match(pattern, s string) (bool, error) {
|
|
||||||
return path.Match(strings.ToLower(pattern), strings.ToLower(s))
|
|
||||||
}
|
|
||||||
|
|
||||||
func selectSubprotocol(r *http.Request, subprotocols []string) string {
|
|
||||||
cps := headerTokens(r.Header, "Sec-WebSocket-Protocol")
|
|
||||||
for _, sp := range subprotocols {
|
|
||||||
for _, cp := range cps {
|
|
||||||
if strings.EqualFold(sp, cp) {
|
|
||||||
return cp
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return ""
|
|
||||||
}
|
|
||||||
|
|
||||||
func selectDeflate(extensions []websocketExtension, mode CompressionMode) (*compressionOptions, bool) {
|
|
||||||
if mode == CompressionDisabled {
|
|
||||||
return nil, false
|
|
||||||
}
|
|
||||||
for _, ext := range extensions {
|
|
||||||
switch ext.name {
|
|
||||||
// We used to implement x-webkit-deflate-frame too for Safari but Safari has bugs...
|
|
||||||
// See https://github.com/nhooyr/websocket/issues/218
|
|
||||||
case "permessage-deflate":
|
|
||||||
copts, ok := acceptDeflate(ext, mode)
|
|
||||||
if ok {
|
|
||||||
return copts, true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return nil, false
|
|
||||||
}
|
|
||||||
|
|
||||||
func acceptDeflate(ext websocketExtension, mode CompressionMode) (*compressionOptions, bool) {
|
|
||||||
copts := mode.opts()
|
|
||||||
for _, p := range ext.params {
|
|
||||||
switch p {
|
|
||||||
case "client_no_context_takeover":
|
|
||||||
copts.clientNoContextTakeover = true
|
|
||||||
continue
|
|
||||||
case "server_no_context_takeover":
|
|
||||||
copts.serverNoContextTakeover = true
|
|
||||||
continue
|
|
||||||
case "client_max_window_bits",
|
|
||||||
"server_max_window_bits=15":
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
if strings.HasPrefix(p, "client_max_window_bits=") {
|
|
||||||
// We can't adjust the deflate window, but decoding with a larger window is acceptable.
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
return nil, false
|
|
||||||
}
|
|
||||||
return copts, true
|
|
||||||
}
|
|
||||||
|
|
||||||
func headerContainsTokenIgnoreCase(h http.Header, key, token string) bool {
|
|
||||||
for _, t := range headerTokens(h, key) {
|
|
||||||
if strings.EqualFold(t, token) {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
type websocketExtension struct {
|
|
||||||
name string
|
|
||||||
params []string
|
|
||||||
}
|
|
||||||
|
|
||||||
func websocketExtensions(h http.Header) []websocketExtension {
|
|
||||||
var exts []websocketExtension
|
|
||||||
extStrs := headerTokens(h, "Sec-WebSocket-Extensions")
|
|
||||||
for _, extStr := range extStrs {
|
|
||||||
if extStr == "" {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
vals := strings.Split(extStr, ";")
|
|
||||||
for i := range vals {
|
|
||||||
vals[i] = strings.TrimSpace(vals[i])
|
|
||||||
}
|
|
||||||
|
|
||||||
e := websocketExtension{
|
|
||||||
name: vals[0],
|
|
||||||
params: vals[1:],
|
|
||||||
}
|
|
||||||
|
|
||||||
exts = append(exts, e)
|
|
||||||
}
|
|
||||||
return exts
|
|
||||||
}
|
|
||||||
|
|
||||||
func headerTokens(h http.Header, key string) []string {
|
|
||||||
key = textproto.CanonicalMIMEHeaderKey(key)
|
|
||||||
var tokens []string
|
|
||||||
for _, v := range h[key] {
|
|
||||||
v = strings.TrimSpace(v)
|
|
||||||
for _, t := range strings.Split(v, ",") {
|
|
||||||
t = strings.TrimSpace(t)
|
|
||||||
tokens = append(tokens, t)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return tokens
|
|
||||||
}
|
|
||||||
|
|
||||||
var keyGUID = []byte("258EAFA5-E914-47DA-95CA-C5AB0DC85B11")
|
|
||||||
|
|
||||||
func secWebSocketAccept(secWebSocketKey string) string {
|
|
||||||
h := sha1.New()
|
|
||||||
h.Write([]byte(secWebSocketKey))
|
|
||||||
h.Write(keyGUID)
|
|
||||||
|
|
||||||
return base64.StdEncoding.EncodeToString(h.Sum(nil))
|
|
||||||
}
|
|
||||||
335
vendor/github.com/coder/websocket/close.go
generated
vendored
335
vendor/github.com/coder/websocket/close.go
generated
vendored
@@ -1,335 +0,0 @@
|
|||||||
//go:build !js
|
|
||||||
|
|
||||||
package websocket
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"encoding/binary"
|
|
||||||
"errors"
|
|
||||||
"fmt"
|
|
||||||
"net"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/coder/websocket/internal/errd"
|
|
||||||
)
|
|
||||||
|
|
||||||
// StatusCode represents a WebSocket status code.
|
|
||||||
// https://tools.ietf.org/html/rfc6455#section-7.4
|
|
||||||
type StatusCode int
|
|
||||||
|
|
||||||
// https://www.iana.org/assignments/websocket/websocket.xhtml#close-code-number
|
|
||||||
//
|
|
||||||
// These are only the status codes defined by the protocol.
|
|
||||||
//
|
|
||||||
// You can define custom codes in the 3000-4999 range.
|
|
||||||
// The 3000-3999 range is reserved for use by libraries, frameworks and applications.
|
|
||||||
// The 4000-4999 range is reserved for private use.
|
|
||||||
const (
|
|
||||||
StatusNormalClosure StatusCode = 1000
|
|
||||||
StatusGoingAway StatusCode = 1001
|
|
||||||
StatusProtocolError StatusCode = 1002
|
|
||||||
StatusUnsupportedData StatusCode = 1003
|
|
||||||
|
|
||||||
// 1004 is reserved and so unexported.
|
|
||||||
statusReserved StatusCode = 1004
|
|
||||||
|
|
||||||
// StatusNoStatusRcvd cannot be sent in a close message.
|
|
||||||
// It is reserved for when a close message is received without
|
|
||||||
// a status code.
|
|
||||||
StatusNoStatusRcvd StatusCode = 1005
|
|
||||||
|
|
||||||
// StatusAbnormalClosure is exported for use only with Wasm.
|
|
||||||
// In non Wasm Go, the returned error will indicate whether the
|
|
||||||
// connection was closed abnormally.
|
|
||||||
StatusAbnormalClosure StatusCode = 1006
|
|
||||||
|
|
||||||
StatusInvalidFramePayloadData StatusCode = 1007
|
|
||||||
StatusPolicyViolation StatusCode = 1008
|
|
||||||
StatusMessageTooBig StatusCode = 1009
|
|
||||||
StatusMandatoryExtension StatusCode = 1010
|
|
||||||
StatusInternalError StatusCode = 1011
|
|
||||||
StatusServiceRestart StatusCode = 1012
|
|
||||||
StatusTryAgainLater StatusCode = 1013
|
|
||||||
StatusBadGateway StatusCode = 1014
|
|
||||||
|
|
||||||
// StatusTLSHandshake is only exported for use with Wasm.
|
|
||||||
// In non Wasm Go, the returned error will indicate whether there was
|
|
||||||
// a TLS handshake failure.
|
|
||||||
StatusTLSHandshake StatusCode = 1015
|
|
||||||
)
|
|
||||||
|
|
||||||
// CloseError is returned when the connection is closed with a status and reason.
|
|
||||||
//
|
|
||||||
// Use Go 1.13's errors.As to check for this error.
|
|
||||||
// Also see the CloseStatus helper.
|
|
||||||
type CloseError struct {
|
|
||||||
Code StatusCode
|
|
||||||
Reason string
|
|
||||||
}
|
|
||||||
|
|
||||||
func (ce CloseError) Error() string {
|
|
||||||
return fmt.Sprintf("status = %v and reason = %q", ce.Code, ce.Reason)
|
|
||||||
}
|
|
||||||
|
|
||||||
// CloseStatus is a convenience wrapper around Go 1.13's errors.As to grab
|
|
||||||
// the status code from a CloseError.
|
|
||||||
//
|
|
||||||
// -1 will be returned if the passed error is nil or not a CloseError.
|
|
||||||
func CloseStatus(err error) StatusCode {
|
|
||||||
var ce CloseError
|
|
||||||
if errors.As(err, &ce) {
|
|
||||||
return ce.Code
|
|
||||||
}
|
|
||||||
return -1
|
|
||||||
}
|
|
||||||
|
|
||||||
// Close performs the WebSocket close handshake with the given status code and reason.
|
|
||||||
//
|
|
||||||
// It will write a WebSocket close frame with a timeout of 5s and then wait 5s for
|
|
||||||
// the peer to send a close frame.
|
|
||||||
// All data messages received from the peer during the close handshake will be discarded.
|
|
||||||
//
|
|
||||||
// The connection can only be closed once. Additional calls to Close
|
|
||||||
// are no-ops.
|
|
||||||
//
|
|
||||||
// The maximum length of reason must be 125 bytes. Avoid sending a dynamic reason.
|
|
||||||
//
|
|
||||||
// Close will unblock all goroutines interacting with the connection once
|
|
||||||
// complete.
|
|
||||||
func (c *Conn) Close(code StatusCode, reason string) (err error) {
|
|
||||||
defer errd.Wrap(&err, "failed to close WebSocket")
|
|
||||||
|
|
||||||
if c.casClosing() {
|
|
||||||
err = c.waitGoroutines()
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
return net.ErrClosed
|
|
||||||
}
|
|
||||||
defer func() {
|
|
||||||
if errors.Is(err, net.ErrClosed) {
|
|
||||||
err = nil
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
|
|
||||||
err = c.closeHandshake(code, reason)
|
|
||||||
|
|
||||||
err2 := c.close()
|
|
||||||
if err == nil && err2 != nil {
|
|
||||||
err = err2
|
|
||||||
}
|
|
||||||
|
|
||||||
err2 = c.waitGoroutines()
|
|
||||||
if err == nil && err2 != nil {
|
|
||||||
err = err2
|
|
||||||
}
|
|
||||||
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
// CloseNow closes the WebSocket connection without attempting a close handshake.
|
|
||||||
// Use when you do not want the overhead of the close handshake.
|
|
||||||
func (c *Conn) CloseNow() (err error) {
|
|
||||||
defer errd.Wrap(&err, "failed to immediately close WebSocket")
|
|
||||||
|
|
||||||
if c.casClosing() {
|
|
||||||
err = c.waitGoroutines()
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
return net.ErrClosed
|
|
||||||
}
|
|
||||||
defer func() {
|
|
||||||
if errors.Is(err, net.ErrClosed) {
|
|
||||||
err = nil
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
|
|
||||||
err = c.close()
|
|
||||||
|
|
||||||
err2 := c.waitGoroutines()
|
|
||||||
if err == nil && err2 != nil {
|
|
||||||
err = err2
|
|
||||||
}
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *Conn) closeHandshake(code StatusCode, reason string) error {
|
|
||||||
err := c.writeClose(code, reason)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
err = c.waitCloseHandshake()
|
|
||||||
if CloseStatus(err) != code {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *Conn) writeClose(code StatusCode, reason string) error {
|
|
||||||
ce := CloseError{
|
|
||||||
Code: code,
|
|
||||||
Reason: reason,
|
|
||||||
}
|
|
||||||
|
|
||||||
var p []byte
|
|
||||||
var err error
|
|
||||||
if ce.Code != StatusNoStatusRcvd {
|
|
||||||
p, err = ce.bytes()
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
ctx, cancel := context.WithTimeout(context.Background(), time.Second*5)
|
|
||||||
defer cancel()
|
|
||||||
|
|
||||||
err = c.writeControl(ctx, opClose, p)
|
|
||||||
// If the connection closed as we're writing we ignore the error as we might
|
|
||||||
// have written the close frame, the peer responded and then someone else read it
|
|
||||||
// and closed the connection.
|
|
||||||
if err != nil && !errors.Is(err, net.ErrClosed) {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *Conn) waitCloseHandshake() error {
|
|
||||||
ctx, cancel := context.WithTimeout(context.Background(), time.Second*5)
|
|
||||||
defer cancel()
|
|
||||||
|
|
||||||
err := c.readMu.lock(ctx)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
defer c.readMu.unlock()
|
|
||||||
|
|
||||||
for i := int64(0); i < c.msgReader.payloadLength; i++ {
|
|
||||||
_, err := c.br.ReadByte()
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
for {
|
|
||||||
h, err := c.readLoop(ctx)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
for i := int64(0); i < h.payloadLength; i++ {
|
|
||||||
_, err := c.br.ReadByte()
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *Conn) waitGoroutines() error {
|
|
||||||
t := time.NewTimer(time.Second * 15)
|
|
||||||
defer t.Stop()
|
|
||||||
|
|
||||||
c.closeReadMu.Lock()
|
|
||||||
closeRead := c.closeReadCtx != nil
|
|
||||||
c.closeReadMu.Unlock()
|
|
||||||
if closeRead {
|
|
||||||
select {
|
|
||||||
case <-c.closeReadDone:
|
|
||||||
case <-t.C:
|
|
||||||
return errors.New("failed to wait for close read goroutine to exit")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
select {
|
|
||||||
case <-c.closed:
|
|
||||||
case <-t.C:
|
|
||||||
return errors.New("failed to wait for connection to be closed")
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func parseClosePayload(p []byte) (CloseError, error) {
|
|
||||||
if len(p) == 0 {
|
|
||||||
return CloseError{
|
|
||||||
Code: StatusNoStatusRcvd,
|
|
||||||
}, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(p) < 2 {
|
|
||||||
return CloseError{}, fmt.Errorf("close payload %q too small, cannot even contain the 2 byte status code", p)
|
|
||||||
}
|
|
||||||
|
|
||||||
ce := CloseError{
|
|
||||||
Code: StatusCode(binary.BigEndian.Uint16(p)),
|
|
||||||
Reason: string(p[2:]),
|
|
||||||
}
|
|
||||||
|
|
||||||
if !validWireCloseCode(ce.Code) {
|
|
||||||
return CloseError{}, fmt.Errorf("invalid status code %v", ce.Code)
|
|
||||||
}
|
|
||||||
|
|
||||||
return ce, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// See http://www.iana.org/assignments/websocket/websocket.xhtml#close-code-number
|
|
||||||
// and https://tools.ietf.org/html/rfc6455#section-7.4.1
|
|
||||||
func validWireCloseCode(code StatusCode) bool {
|
|
||||||
switch code {
|
|
||||||
case statusReserved, StatusNoStatusRcvd, StatusAbnormalClosure, StatusTLSHandshake:
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
if code >= StatusNormalClosure && code <= StatusBadGateway {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
if code >= 3000 && code <= 4999 {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
func (ce CloseError) bytes() ([]byte, error) {
|
|
||||||
p, err := ce.bytesErr()
|
|
||||||
if err != nil {
|
|
||||||
err = fmt.Errorf("failed to marshal close frame: %w", err)
|
|
||||||
ce = CloseError{
|
|
||||||
Code: StatusInternalError,
|
|
||||||
}
|
|
||||||
p, _ = ce.bytesErr()
|
|
||||||
}
|
|
||||||
return p, err
|
|
||||||
}
|
|
||||||
|
|
||||||
const maxCloseReason = maxControlPayload - 2
|
|
||||||
|
|
||||||
func (ce CloseError) bytesErr() ([]byte, error) {
|
|
||||||
if len(ce.Reason) > maxCloseReason {
|
|
||||||
return nil, fmt.Errorf("reason string max is %v but got %q with length %v", maxCloseReason, ce.Reason, len(ce.Reason))
|
|
||||||
}
|
|
||||||
|
|
||||||
if !validWireCloseCode(ce.Code) {
|
|
||||||
return nil, fmt.Errorf("status code %v cannot be set", ce.Code)
|
|
||||||
}
|
|
||||||
|
|
||||||
buf := make([]byte, 2+len(ce.Reason))
|
|
||||||
binary.BigEndian.PutUint16(buf, uint16(ce.Code))
|
|
||||||
copy(buf[2:], ce.Reason)
|
|
||||||
return buf, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *Conn) casClosing() bool {
|
|
||||||
return c.closing.Swap(true)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *Conn) isClosed() bool {
|
|
||||||
select {
|
|
||||||
case <-c.closed:
|
|
||||||
return true
|
|
||||||
default:
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
234
vendor/github.com/coder/websocket/compress.go
generated
vendored
234
vendor/github.com/coder/websocket/compress.go
generated
vendored
@@ -1,234 +0,0 @@
|
|||||||
//go:build !js
|
|
||||||
|
|
||||||
package websocket
|
|
||||||
|
|
||||||
import (
|
|
||||||
"compress/flate"
|
|
||||||
"io"
|
|
||||||
"sync"
|
|
||||||
)
|
|
||||||
|
|
||||||
// CompressionMode represents the modes available to the permessage-deflate extension.
|
|
||||||
// See https://tools.ietf.org/html/rfc7692
|
|
||||||
//
|
|
||||||
// Works in all modern browsers except Safari which does not implement the permessage-deflate extension.
|
|
||||||
//
|
|
||||||
// Compression is only used if the peer supports the mode selected.
|
|
||||||
type CompressionMode int
|
|
||||||
|
|
||||||
const (
|
|
||||||
// CompressionDisabled disables the negotiation of the permessage-deflate extension.
|
|
||||||
//
|
|
||||||
// This is the default. Do not enable compression without benchmarking for your particular use case first.
|
|
||||||
CompressionDisabled CompressionMode = iota
|
|
||||||
|
|
||||||
// CompressionContextTakeover compresses each message greater than 128 bytes reusing the 32 KB sliding window from
|
|
||||||
// previous messages. i.e compression context across messages is preserved.
|
|
||||||
//
|
|
||||||
// As most WebSocket protocols are text based and repetitive, this compression mode can be very efficient.
|
|
||||||
//
|
|
||||||
// The memory overhead is a fixed 32 KB sliding window, a fixed 1.2 MB flate.Writer and a sync.Pool of 40 KB flate.Reader's
|
|
||||||
// that are used when reading and then returned.
|
|
||||||
//
|
|
||||||
// Thus, it uses more memory than CompressionNoContextTakeover but compresses more efficiently.
|
|
||||||
//
|
|
||||||
// If the peer does not support CompressionContextTakeover then we will fall back to CompressionNoContextTakeover.
|
|
||||||
CompressionContextTakeover
|
|
||||||
|
|
||||||
// CompressionNoContextTakeover compresses each message greater than 512 bytes. Each message is compressed with
|
|
||||||
// a new 1.2 MB flate.Writer pulled from a sync.Pool. Each message is read with a 40 KB flate.Reader pulled from
|
|
||||||
// a sync.Pool.
|
|
||||||
//
|
|
||||||
// This means less efficient compression as the sliding window from previous messages will not be used but the
|
|
||||||
// memory overhead will be lower as there will be no fixed cost for the flate.Writer nor the 32 KB sliding window.
|
|
||||||
// Especially if the connections are long lived and seldom written to.
|
|
||||||
//
|
|
||||||
// Thus, it uses less memory than CompressionContextTakeover but compresses less efficiently.
|
|
||||||
//
|
|
||||||
// If the peer does not support CompressionNoContextTakeover then we will fall back to CompressionDisabled.
|
|
||||||
CompressionNoContextTakeover
|
|
||||||
)
|
|
||||||
|
|
||||||
func (m CompressionMode) opts() *compressionOptions {
|
|
||||||
return &compressionOptions{
|
|
||||||
clientNoContextTakeover: m == CompressionNoContextTakeover,
|
|
||||||
serverNoContextTakeover: m == CompressionNoContextTakeover,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
type compressionOptions struct {
|
|
||||||
clientNoContextTakeover bool
|
|
||||||
serverNoContextTakeover bool
|
|
||||||
}
|
|
||||||
|
|
||||||
func (copts *compressionOptions) String() string {
|
|
||||||
s := "permessage-deflate"
|
|
||||||
if copts.clientNoContextTakeover {
|
|
||||||
s += "; client_no_context_takeover"
|
|
||||||
}
|
|
||||||
if copts.serverNoContextTakeover {
|
|
||||||
s += "; server_no_context_takeover"
|
|
||||||
}
|
|
||||||
return s
|
|
||||||
}
|
|
||||||
|
|
||||||
// These bytes are required to get flate.Reader to return.
|
|
||||||
// They are removed when sending to avoid the overhead as
|
|
||||||
// WebSocket framing tell's when the message has ended but then
|
|
||||||
// we need to add them back otherwise flate.Reader keeps
|
|
||||||
// trying to read more bytes.
|
|
||||||
const deflateMessageTail = "\x00\x00\xff\xff"
|
|
||||||
|
|
||||||
type trimLastFourBytesWriter struct {
|
|
||||||
w io.Writer
|
|
||||||
tail []byte
|
|
||||||
}
|
|
||||||
|
|
||||||
func (tw *trimLastFourBytesWriter) reset() {
|
|
||||||
if tw != nil && tw.tail != nil {
|
|
||||||
tw.tail = tw.tail[:0]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (tw *trimLastFourBytesWriter) Write(p []byte) (int, error) {
|
|
||||||
if tw.tail == nil {
|
|
||||||
tw.tail = make([]byte, 0, 4)
|
|
||||||
}
|
|
||||||
|
|
||||||
extra := len(tw.tail) + len(p) - 4
|
|
||||||
|
|
||||||
if extra <= 0 {
|
|
||||||
tw.tail = append(tw.tail, p...)
|
|
||||||
return len(p), nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Now we need to write as many extra bytes as we can from the previous tail.
|
|
||||||
if extra > len(tw.tail) {
|
|
||||||
extra = len(tw.tail)
|
|
||||||
}
|
|
||||||
if extra > 0 {
|
|
||||||
_, err := tw.w.Write(tw.tail[:extra])
|
|
||||||
if err != nil {
|
|
||||||
return 0, err
|
|
||||||
}
|
|
||||||
|
|
||||||
// Shift remaining bytes in tail over.
|
|
||||||
n := copy(tw.tail, tw.tail[extra:])
|
|
||||||
tw.tail = tw.tail[:n]
|
|
||||||
}
|
|
||||||
|
|
||||||
// If p is less than or equal to 4 bytes,
|
|
||||||
// all of it is is part of the tail.
|
|
||||||
if len(p) <= 4 {
|
|
||||||
tw.tail = append(tw.tail, p...)
|
|
||||||
return len(p), nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Otherwise, only the last 4 bytes are.
|
|
||||||
tw.tail = append(tw.tail, p[len(p)-4:]...)
|
|
||||||
|
|
||||||
p = p[:len(p)-4]
|
|
||||||
n, err := tw.w.Write(p)
|
|
||||||
return n + 4, err
|
|
||||||
}
|
|
||||||
|
|
||||||
var flateReaderPool sync.Pool
|
|
||||||
|
|
||||||
func getFlateReader(r io.Reader, dict []byte) io.Reader {
|
|
||||||
fr, ok := flateReaderPool.Get().(io.Reader)
|
|
||||||
if !ok {
|
|
||||||
return flate.NewReaderDict(r, dict)
|
|
||||||
}
|
|
||||||
fr.(flate.Resetter).Reset(r, dict)
|
|
||||||
return fr
|
|
||||||
}
|
|
||||||
|
|
||||||
func putFlateReader(fr io.Reader) {
|
|
||||||
flateReaderPool.Put(fr)
|
|
||||||
}
|
|
||||||
|
|
||||||
var flateWriterPool sync.Pool
|
|
||||||
|
|
||||||
func getFlateWriter(w io.Writer) *flate.Writer {
|
|
||||||
fw, ok := flateWriterPool.Get().(*flate.Writer)
|
|
||||||
if !ok {
|
|
||||||
fw, _ = flate.NewWriter(w, flate.BestSpeed)
|
|
||||||
return fw
|
|
||||||
}
|
|
||||||
fw.Reset(w)
|
|
||||||
return fw
|
|
||||||
}
|
|
||||||
|
|
||||||
func putFlateWriter(w *flate.Writer) {
|
|
||||||
flateWriterPool.Put(w)
|
|
||||||
}
|
|
||||||
|
|
||||||
type slidingWindow struct {
|
|
||||||
buf []byte
|
|
||||||
}
|
|
||||||
|
|
||||||
var (
|
|
||||||
swPoolMu sync.RWMutex
|
|
||||||
swPool = map[int]*sync.Pool{}
|
|
||||||
)
|
|
||||||
|
|
||||||
func slidingWindowPool(n int) *sync.Pool {
|
|
||||||
swPoolMu.RLock()
|
|
||||||
p, ok := swPool[n]
|
|
||||||
swPoolMu.RUnlock()
|
|
||||||
if ok {
|
|
||||||
return p
|
|
||||||
}
|
|
||||||
|
|
||||||
p = &sync.Pool{}
|
|
||||||
|
|
||||||
swPoolMu.Lock()
|
|
||||||
swPool[n] = p
|
|
||||||
swPoolMu.Unlock()
|
|
||||||
|
|
||||||
return p
|
|
||||||
}
|
|
||||||
|
|
||||||
func (sw *slidingWindow) init(n int) {
|
|
||||||
if sw.buf != nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if n == 0 {
|
|
||||||
n = 32768
|
|
||||||
}
|
|
||||||
|
|
||||||
p := slidingWindowPool(n)
|
|
||||||
sw2, ok := p.Get().(*slidingWindow)
|
|
||||||
if ok {
|
|
||||||
*sw = *sw2
|
|
||||||
} else {
|
|
||||||
sw.buf = make([]byte, 0, n)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (sw *slidingWindow) close() {
|
|
||||||
sw.buf = sw.buf[:0]
|
|
||||||
swPoolMu.Lock()
|
|
||||||
swPool[cap(sw.buf)].Put(sw)
|
|
||||||
swPoolMu.Unlock()
|
|
||||||
}
|
|
||||||
|
|
||||||
func (sw *slidingWindow) write(p []byte) {
|
|
||||||
if len(p) >= cap(sw.buf) {
|
|
||||||
sw.buf = sw.buf[:cap(sw.buf)]
|
|
||||||
p = p[len(p)-cap(sw.buf):]
|
|
||||||
copy(sw.buf, p)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
left := cap(sw.buf) - len(sw.buf)
|
|
||||||
if left < len(p) {
|
|
||||||
// We need to shift spaceNeeded bytes from the end to make room for p at the end.
|
|
||||||
spaceNeeded := len(p) - left
|
|
||||||
copy(sw.buf, sw.buf[spaceNeeded:])
|
|
||||||
sw.buf = sw.buf[:len(sw.buf)-spaceNeeded]
|
|
||||||
}
|
|
||||||
|
|
||||||
sw.buf = append(sw.buf, p...)
|
|
||||||
}
|
|
||||||
306
vendor/github.com/coder/websocket/conn.go
generated
vendored
306
vendor/github.com/coder/websocket/conn.go
generated
vendored
@@ -1,306 +0,0 @@
|
|||||||
//go:build !js
|
|
||||||
|
|
||||||
package websocket
|
|
||||||
|
|
||||||
import (
|
|
||||||
"bufio"
|
|
||||||
"context"
|
|
||||||
"fmt"
|
|
||||||
"io"
|
|
||||||
"net"
|
|
||||||
"runtime"
|
|
||||||
"strconv"
|
|
||||||
"sync"
|
|
||||||
"sync/atomic"
|
|
||||||
)
|
|
||||||
|
|
||||||
// MessageType represents the type of a WebSocket message.
|
|
||||||
// See https://tools.ietf.org/html/rfc6455#section-5.6
|
|
||||||
type MessageType int
|
|
||||||
|
|
||||||
// MessageType constants.
|
|
||||||
const (
|
|
||||||
// MessageText is for UTF-8 encoded text messages like JSON.
|
|
||||||
MessageText MessageType = iota + 1
|
|
||||||
// MessageBinary is for binary messages like protobufs.
|
|
||||||
MessageBinary
|
|
||||||
)
|
|
||||||
|
|
||||||
// Conn represents a WebSocket connection.
|
|
||||||
// All methods may be called concurrently except for Reader and Read.
|
|
||||||
//
|
|
||||||
// You must always read from the connection. Otherwise control
|
|
||||||
// frames will not be handled. See Reader and CloseRead.
|
|
||||||
//
|
|
||||||
// Be sure to call Close on the connection when you
|
|
||||||
// are finished with it to release associated resources.
|
|
||||||
//
|
|
||||||
// On any error from any method, the connection is closed
|
|
||||||
// with an appropriate reason.
|
|
||||||
//
|
|
||||||
// This applies to context expirations as well unfortunately.
|
|
||||||
// See https://github.com/nhooyr/websocket/issues/242#issuecomment-633182220
|
|
||||||
type Conn struct {
|
|
||||||
noCopy noCopy
|
|
||||||
|
|
||||||
subprotocol string
|
|
||||||
rwc io.ReadWriteCloser
|
|
||||||
client bool
|
|
||||||
copts *compressionOptions
|
|
||||||
flateThreshold int
|
|
||||||
br *bufio.Reader
|
|
||||||
bw *bufio.Writer
|
|
||||||
|
|
||||||
readTimeoutStop atomic.Pointer[func() bool]
|
|
||||||
writeTimeoutStop atomic.Pointer[func() bool]
|
|
||||||
|
|
||||||
// Read state.
|
|
||||||
readMu *mu
|
|
||||||
readHeaderBuf [8]byte
|
|
||||||
readControlBuf [maxControlPayload]byte
|
|
||||||
msgReader *msgReader
|
|
||||||
|
|
||||||
// Write state.
|
|
||||||
msgWriter *msgWriter
|
|
||||||
writeFrameMu *mu
|
|
||||||
writeBuf []byte
|
|
||||||
writeHeaderBuf [8]byte
|
|
||||||
writeHeader header
|
|
||||||
|
|
||||||
// Close handshake state.
|
|
||||||
closeStateMu sync.RWMutex
|
|
||||||
closeReceivedErr error
|
|
||||||
closeSentErr error
|
|
||||||
|
|
||||||
// CloseRead state.
|
|
||||||
closeReadMu sync.Mutex
|
|
||||||
closeReadCtx context.Context
|
|
||||||
closeReadDone chan struct{}
|
|
||||||
|
|
||||||
closing atomic.Bool
|
|
||||||
closeMu sync.Mutex // Protects following.
|
|
||||||
closed chan struct{}
|
|
||||||
|
|
||||||
pingCounter atomic.Int64
|
|
||||||
activePingsMu sync.Mutex
|
|
||||||
activePings map[string]chan<- struct{}
|
|
||||||
onPingReceived func(context.Context, []byte) bool
|
|
||||||
onPongReceived func(context.Context, []byte)
|
|
||||||
}
|
|
||||||
|
|
||||||
type connConfig struct {
|
|
||||||
subprotocol string
|
|
||||||
rwc io.ReadWriteCloser
|
|
||||||
client bool
|
|
||||||
copts *compressionOptions
|
|
||||||
flateThreshold int
|
|
||||||
onPingReceived func(context.Context, []byte) bool
|
|
||||||
onPongReceived func(context.Context, []byte)
|
|
||||||
|
|
||||||
br *bufio.Reader
|
|
||||||
bw *bufio.Writer
|
|
||||||
}
|
|
||||||
|
|
||||||
func newConn(cfg connConfig) *Conn {
|
|
||||||
c := &Conn{
|
|
||||||
subprotocol: cfg.subprotocol,
|
|
||||||
rwc: cfg.rwc,
|
|
||||||
client: cfg.client,
|
|
||||||
copts: cfg.copts,
|
|
||||||
flateThreshold: cfg.flateThreshold,
|
|
||||||
|
|
||||||
br: cfg.br,
|
|
||||||
bw: cfg.bw,
|
|
||||||
|
|
||||||
closed: make(chan struct{}),
|
|
||||||
activePings: make(map[string]chan<- struct{}),
|
|
||||||
onPingReceived: cfg.onPingReceived,
|
|
||||||
onPongReceived: cfg.onPongReceived,
|
|
||||||
}
|
|
||||||
|
|
||||||
c.readMu = newMu(c)
|
|
||||||
c.writeFrameMu = newMu(c)
|
|
||||||
|
|
||||||
c.msgReader = newMsgReader(c)
|
|
||||||
|
|
||||||
c.msgWriter = newMsgWriter(c)
|
|
||||||
if c.client {
|
|
||||||
c.writeBuf = extractBufioWriterBuf(c.bw, c.rwc)
|
|
||||||
}
|
|
||||||
|
|
||||||
if c.flate() && c.flateThreshold == 0 {
|
|
||||||
c.flateThreshold = 128
|
|
||||||
if !c.msgWriter.flateContextTakeover() {
|
|
||||||
c.flateThreshold = 512
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
runtime.SetFinalizer(c, func(c *Conn) {
|
|
||||||
c.close()
|
|
||||||
})
|
|
||||||
|
|
||||||
return c
|
|
||||||
}
|
|
||||||
|
|
||||||
// Subprotocol returns the negotiated subprotocol.
|
|
||||||
// An empty string means the default protocol.
|
|
||||||
func (c *Conn) Subprotocol() string {
|
|
||||||
return c.subprotocol
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *Conn) close() error {
|
|
||||||
c.closeMu.Lock()
|
|
||||||
defer c.closeMu.Unlock()
|
|
||||||
|
|
||||||
if c.isClosed() {
|
|
||||||
return net.ErrClosed
|
|
||||||
}
|
|
||||||
runtime.SetFinalizer(c, nil)
|
|
||||||
close(c.closed)
|
|
||||||
|
|
||||||
// Have to close after c.closed is closed to ensure any goroutine that wakes up
|
|
||||||
// from the connection being closed also sees that c.closed is closed and returns
|
|
||||||
// closeErr.
|
|
||||||
err := c.rwc.Close()
|
|
||||||
// With the close of rwc, these become safe to close.
|
|
||||||
c.msgWriter.close()
|
|
||||||
c.msgReader.close()
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *Conn) setupWriteTimeout(ctx context.Context) {
|
|
||||||
stop := context.AfterFunc(ctx, func() {
|
|
||||||
c.clearWriteTimeout()
|
|
||||||
c.close()
|
|
||||||
})
|
|
||||||
swapTimeoutStop(&c.writeTimeoutStop, &stop)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *Conn) clearWriteTimeout() {
|
|
||||||
swapTimeoutStop(&c.writeTimeoutStop, nil)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *Conn) setupReadTimeout(ctx context.Context) {
|
|
||||||
stop := context.AfterFunc(ctx, func() {
|
|
||||||
c.clearReadTimeout()
|
|
||||||
c.close()
|
|
||||||
})
|
|
||||||
swapTimeoutStop(&c.readTimeoutStop, &stop)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *Conn) clearReadTimeout() {
|
|
||||||
swapTimeoutStop(&c.readTimeoutStop, nil)
|
|
||||||
}
|
|
||||||
|
|
||||||
func swapTimeoutStop(p *atomic.Pointer[func() bool], newStop *func() bool) {
|
|
||||||
oldStop := p.Swap(newStop)
|
|
||||||
if oldStop != nil {
|
|
||||||
(*oldStop)()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *Conn) flate() bool {
|
|
||||||
return c.copts != nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Ping sends a ping to the peer and waits for a pong.
|
|
||||||
// Use this to measure latency or ensure the peer is responsive.
|
|
||||||
// Ping must be called concurrently with Reader as it does
|
|
||||||
// not read from the connection but instead waits for a Reader call
|
|
||||||
// to read the pong.
|
|
||||||
//
|
|
||||||
// TCP Keepalives should suffice for most use cases.
|
|
||||||
func (c *Conn) Ping(ctx context.Context) error {
|
|
||||||
p := c.pingCounter.Add(1)
|
|
||||||
|
|
||||||
err := c.ping(ctx, strconv.FormatInt(p, 10))
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("failed to ping: %w", err)
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *Conn) ping(ctx context.Context, p string) error {
|
|
||||||
pong := make(chan struct{}, 1)
|
|
||||||
|
|
||||||
c.activePingsMu.Lock()
|
|
||||||
c.activePings[p] = pong
|
|
||||||
c.activePingsMu.Unlock()
|
|
||||||
|
|
||||||
defer func() {
|
|
||||||
c.activePingsMu.Lock()
|
|
||||||
delete(c.activePings, p)
|
|
||||||
c.activePingsMu.Unlock()
|
|
||||||
}()
|
|
||||||
|
|
||||||
err := c.writeControl(ctx, opPing, []byte(p))
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
select {
|
|
||||||
case <-c.closed:
|
|
||||||
return net.ErrClosed
|
|
||||||
case <-ctx.Done():
|
|
||||||
return fmt.Errorf("failed to wait for pong: %w", ctx.Err())
|
|
||||||
case <-pong:
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
type mu struct {
|
|
||||||
c *Conn
|
|
||||||
ch chan struct{}
|
|
||||||
}
|
|
||||||
|
|
||||||
func newMu(c *Conn) *mu {
|
|
||||||
return &mu{
|
|
||||||
c: c,
|
|
||||||
ch: make(chan struct{}, 1),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *mu) forceLock() {
|
|
||||||
m.ch <- struct{}{}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *mu) tryLock() bool {
|
|
||||||
select {
|
|
||||||
case m.ch <- struct{}{}:
|
|
||||||
return true
|
|
||||||
default:
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *mu) lock(ctx context.Context) error {
|
|
||||||
select {
|
|
||||||
case <-m.c.closed:
|
|
||||||
return net.ErrClosed
|
|
||||||
case <-ctx.Done():
|
|
||||||
return fmt.Errorf("failed to acquire lock: %w", ctx.Err())
|
|
||||||
case m.ch <- struct{}{}:
|
|
||||||
// To make sure the connection is certainly alive.
|
|
||||||
// As it's possible the send on m.ch was selected
|
|
||||||
// over the receive on closed.
|
|
||||||
select {
|
|
||||||
case <-m.c.closed:
|
|
||||||
// Make sure to release.
|
|
||||||
m.unlock()
|
|
||||||
return net.ErrClosed
|
|
||||||
default:
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *mu) unlock() {
|
|
||||||
select {
|
|
||||||
case <-m.ch:
|
|
||||||
default:
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
type noCopy struct{}
|
|
||||||
|
|
||||||
func (*noCopy) Lock() {}
|
|
||||||
347
vendor/github.com/coder/websocket/dial.go
generated
vendored
347
vendor/github.com/coder/websocket/dial.go
generated
vendored
@@ -1,347 +0,0 @@
|
|||||||
//go:build !js
|
|
||||||
|
|
||||||
package websocket
|
|
||||||
|
|
||||||
import (
|
|
||||||
"bufio"
|
|
||||||
"bytes"
|
|
||||||
"context"
|
|
||||||
"crypto/rand"
|
|
||||||
"encoding/base64"
|
|
||||||
"fmt"
|
|
||||||
"io"
|
|
||||||
"net/http"
|
|
||||||
"net/url"
|
|
||||||
"strings"
|
|
||||||
"sync"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/coder/websocket/internal/errd"
|
|
||||||
)
|
|
||||||
|
|
||||||
// DialOptions represents Dial's options.
|
|
||||||
type DialOptions struct {
|
|
||||||
// HTTPClient is used for the connection.
|
|
||||||
// Its Transport must return writable bodies for WebSocket handshakes.
|
|
||||||
// http.Transport does beginning with Go 1.12.
|
|
||||||
HTTPClient *http.Client
|
|
||||||
|
|
||||||
// HTTPHeader specifies the HTTP headers included in the handshake request.
|
|
||||||
HTTPHeader http.Header
|
|
||||||
|
|
||||||
// Host optionally overrides the Host HTTP header to send. If empty, the value
|
|
||||||
// of URL.Host will be used.
|
|
||||||
Host string
|
|
||||||
|
|
||||||
// Subprotocols lists the WebSocket subprotocols to negotiate with the server.
|
|
||||||
Subprotocols []string
|
|
||||||
|
|
||||||
// CompressionMode controls the compression mode.
|
|
||||||
// Defaults to CompressionDisabled.
|
|
||||||
//
|
|
||||||
// See docs on CompressionMode for details.
|
|
||||||
CompressionMode CompressionMode
|
|
||||||
|
|
||||||
// CompressionThreshold controls the minimum size of a message before compression is applied.
|
|
||||||
//
|
|
||||||
// Defaults to 512 bytes for CompressionNoContextTakeover and 128 bytes
|
|
||||||
// for CompressionContextTakeover.
|
|
||||||
CompressionThreshold int
|
|
||||||
|
|
||||||
// OnPingReceived is an optional callback invoked synchronously when a ping frame is received.
|
|
||||||
//
|
|
||||||
// The payload contains the application data of the ping frame.
|
|
||||||
// If the callback returns false, the subsequent pong frame will not be sent.
|
|
||||||
// To avoid blocking, any expensive processing should be performed asynchronously using a goroutine.
|
|
||||||
OnPingReceived func(ctx context.Context, payload []byte) bool
|
|
||||||
|
|
||||||
// OnPongReceived is an optional callback invoked synchronously when a pong frame is received.
|
|
||||||
//
|
|
||||||
// The payload contains the application data of the pong frame.
|
|
||||||
// To avoid blocking, any expensive processing should be performed asynchronously using a goroutine.
|
|
||||||
//
|
|
||||||
// Unlike OnPingReceived, this callback does not return a value because a pong frame
|
|
||||||
// is a response to a ping and does not trigger any further frame transmission.
|
|
||||||
OnPongReceived func(ctx context.Context, payload []byte)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (opts *DialOptions) cloneWithDefaults(ctx context.Context) (context.Context, context.CancelFunc, *DialOptions) {
|
|
||||||
var cancel context.CancelFunc
|
|
||||||
|
|
||||||
var o DialOptions
|
|
||||||
if opts != nil {
|
|
||||||
o = *opts
|
|
||||||
}
|
|
||||||
if o.HTTPClient == nil {
|
|
||||||
o.HTTPClient = http.DefaultClient
|
|
||||||
}
|
|
||||||
if o.HTTPClient.Timeout > 0 {
|
|
||||||
ctx, cancel = context.WithTimeout(ctx, o.HTTPClient.Timeout)
|
|
||||||
|
|
||||||
newClient := *o.HTTPClient
|
|
||||||
newClient.Timeout = 0
|
|
||||||
o.HTTPClient = &newClient
|
|
||||||
}
|
|
||||||
if o.HTTPHeader == nil {
|
|
||||||
o.HTTPHeader = http.Header{}
|
|
||||||
}
|
|
||||||
newClient := *o.HTTPClient
|
|
||||||
oldCheckRedirect := o.HTTPClient.CheckRedirect
|
|
||||||
newClient.CheckRedirect = func(req *http.Request, via []*http.Request) error {
|
|
||||||
switch req.URL.Scheme {
|
|
||||||
case "ws":
|
|
||||||
req.URL.Scheme = "http"
|
|
||||||
case "wss":
|
|
||||||
req.URL.Scheme = "https"
|
|
||||||
}
|
|
||||||
if oldCheckRedirect != nil {
|
|
||||||
return oldCheckRedirect(req, via)
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
o.HTTPClient = &newClient
|
|
||||||
|
|
||||||
return ctx, cancel, &o
|
|
||||||
}
|
|
||||||
|
|
||||||
// Dial performs a WebSocket handshake on url.
|
|
||||||
//
|
|
||||||
// The response is the WebSocket handshake response from the server.
|
|
||||||
// You never need to close resp.Body yourself.
|
|
||||||
//
|
|
||||||
// If an error occurs, the returned response may be non nil.
|
|
||||||
// However, you can only read the first 1024 bytes of the body.
|
|
||||||
//
|
|
||||||
// This function requires at least Go 1.12 as it uses a new feature
|
|
||||||
// in net/http to perform WebSocket handshakes.
|
|
||||||
// See docs on the HTTPClient option and https://github.com/golang/go/issues/26937#issuecomment-415855861
|
|
||||||
//
|
|
||||||
// URLs with http/https schemes will work and are interpreted as ws/wss.
|
|
||||||
func Dial(ctx context.Context, u string, opts *DialOptions) (*Conn, *http.Response, error) {
|
|
||||||
return dial(ctx, u, opts, nil)
|
|
||||||
}
|
|
||||||
|
|
||||||
func dial(ctx context.Context, urls string, opts *DialOptions, rand io.Reader) (_ *Conn, _ *http.Response, err error) {
|
|
||||||
defer errd.Wrap(&err, "failed to WebSocket dial")
|
|
||||||
|
|
||||||
var cancel context.CancelFunc
|
|
||||||
ctx, cancel, opts = opts.cloneWithDefaults(ctx)
|
|
||||||
if cancel != nil {
|
|
||||||
defer cancel()
|
|
||||||
}
|
|
||||||
|
|
||||||
secWebSocketKey, err := secWebSocketKey(rand)
|
|
||||||
if err != nil {
|
|
||||||
return nil, nil, fmt.Errorf("failed to generate Sec-WebSocket-Key: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
var copts *compressionOptions
|
|
||||||
if opts.CompressionMode != CompressionDisabled {
|
|
||||||
copts = opts.CompressionMode.opts()
|
|
||||||
}
|
|
||||||
|
|
||||||
resp, err := handshakeRequest(ctx, urls, opts, copts, secWebSocketKey)
|
|
||||||
if err != nil {
|
|
||||||
return nil, resp, err
|
|
||||||
}
|
|
||||||
respBody := resp.Body
|
|
||||||
resp.Body = nil
|
|
||||||
defer func() {
|
|
||||||
if err != nil {
|
|
||||||
// We read a bit of the body for easier debugging.
|
|
||||||
r := io.LimitReader(respBody, 1024)
|
|
||||||
|
|
||||||
timer := time.AfterFunc(time.Second*3, func() {
|
|
||||||
respBody.Close()
|
|
||||||
})
|
|
||||||
defer timer.Stop()
|
|
||||||
|
|
||||||
b, _ := io.ReadAll(r)
|
|
||||||
respBody.Close()
|
|
||||||
resp.Body = io.NopCloser(bytes.NewReader(b))
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
|
|
||||||
copts, err = verifyServerResponse(opts, copts, secWebSocketKey, resp)
|
|
||||||
if err != nil {
|
|
||||||
return nil, resp, err
|
|
||||||
}
|
|
||||||
|
|
||||||
rwc, ok := respBody.(io.ReadWriteCloser)
|
|
||||||
if !ok {
|
|
||||||
return nil, resp, fmt.Errorf("response body is not a io.ReadWriteCloser: %T", respBody)
|
|
||||||
}
|
|
||||||
|
|
||||||
return newConn(connConfig{
|
|
||||||
subprotocol: resp.Header.Get("Sec-WebSocket-Protocol"),
|
|
||||||
rwc: rwc,
|
|
||||||
client: true,
|
|
||||||
copts: copts,
|
|
||||||
flateThreshold: opts.CompressionThreshold,
|
|
||||||
onPingReceived: opts.OnPingReceived,
|
|
||||||
onPongReceived: opts.OnPongReceived,
|
|
||||||
br: getBufioReader(rwc),
|
|
||||||
bw: getBufioWriter(rwc),
|
|
||||||
}), resp, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func handshakeRequest(ctx context.Context, urls string, opts *DialOptions, copts *compressionOptions, secWebSocketKey string) (*http.Response, error) {
|
|
||||||
u, err := url.Parse(urls)
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("failed to parse url: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
switch u.Scheme {
|
|
||||||
case "ws":
|
|
||||||
u.Scheme = "http"
|
|
||||||
case "wss":
|
|
||||||
u.Scheme = "https"
|
|
||||||
case "http", "https":
|
|
||||||
default:
|
|
||||||
return nil, fmt.Errorf("unexpected url scheme: %q", u.Scheme)
|
|
||||||
}
|
|
||||||
|
|
||||||
req, err := http.NewRequestWithContext(ctx, "GET", u.String(), nil)
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("failed to create new http request: %w", err)
|
|
||||||
}
|
|
||||||
if len(opts.Host) > 0 {
|
|
||||||
req.Host = opts.Host
|
|
||||||
}
|
|
||||||
req.Header = opts.HTTPHeader.Clone()
|
|
||||||
req.Header.Set("Connection", "Upgrade")
|
|
||||||
req.Header.Set("Upgrade", "websocket")
|
|
||||||
req.Header.Set("Sec-WebSocket-Version", "13")
|
|
||||||
req.Header.Set("Sec-WebSocket-Key", secWebSocketKey)
|
|
||||||
if len(opts.Subprotocols) > 0 {
|
|
||||||
req.Header.Set("Sec-WebSocket-Protocol", strings.Join(opts.Subprotocols, ","))
|
|
||||||
}
|
|
||||||
if copts != nil {
|
|
||||||
req.Header.Set("Sec-WebSocket-Extensions", copts.String())
|
|
||||||
}
|
|
||||||
|
|
||||||
resp, err := opts.HTTPClient.Do(req)
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("failed to send handshake request: %w", err)
|
|
||||||
}
|
|
||||||
return resp, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func secWebSocketKey(rr io.Reader) (string, error) {
|
|
||||||
if rr == nil {
|
|
||||||
rr = rand.Reader
|
|
||||||
}
|
|
||||||
b := make([]byte, 16)
|
|
||||||
_, err := io.ReadFull(rr, b)
|
|
||||||
if err != nil {
|
|
||||||
return "", fmt.Errorf("failed to read random data from rand.Reader: %w", err)
|
|
||||||
}
|
|
||||||
return base64.StdEncoding.EncodeToString(b), nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func verifyServerResponse(opts *DialOptions, copts *compressionOptions, secWebSocketKey string, resp *http.Response) (*compressionOptions, error) {
|
|
||||||
if resp.StatusCode != http.StatusSwitchingProtocols {
|
|
||||||
return nil, fmt.Errorf("expected handshake response status code %v but got %v", http.StatusSwitchingProtocols, resp.StatusCode)
|
|
||||||
}
|
|
||||||
|
|
||||||
if !headerContainsTokenIgnoreCase(resp.Header, "Connection", "Upgrade") {
|
|
||||||
return nil, fmt.Errorf("WebSocket protocol violation: Connection header %q does not contain Upgrade", resp.Header.Get("Connection"))
|
|
||||||
}
|
|
||||||
|
|
||||||
if !headerContainsTokenIgnoreCase(resp.Header, "Upgrade", "WebSocket") {
|
|
||||||
return nil, fmt.Errorf("WebSocket protocol violation: Upgrade header %q does not contain websocket", resp.Header.Get("Upgrade"))
|
|
||||||
}
|
|
||||||
|
|
||||||
if resp.Header.Get("Sec-WebSocket-Accept") != secWebSocketAccept(secWebSocketKey) {
|
|
||||||
return nil, fmt.Errorf("WebSocket protocol violation: invalid Sec-WebSocket-Accept %q, key %q",
|
|
||||||
resp.Header.Get("Sec-WebSocket-Accept"),
|
|
||||||
secWebSocketKey,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
err := verifySubprotocol(opts.Subprotocols, resp)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
return verifyServerExtensions(copts, resp.Header)
|
|
||||||
}
|
|
||||||
|
|
||||||
func verifySubprotocol(subprotos []string, resp *http.Response) error {
|
|
||||||
proto := resp.Header.Get("Sec-WebSocket-Protocol")
|
|
||||||
if proto == "" {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, sp2 := range subprotos {
|
|
||||||
if strings.EqualFold(sp2, proto) {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return fmt.Errorf("WebSocket protocol violation: unexpected Sec-WebSocket-Protocol from server: %q", proto)
|
|
||||||
}
|
|
||||||
|
|
||||||
func verifyServerExtensions(copts *compressionOptions, h http.Header) (*compressionOptions, error) {
|
|
||||||
exts := websocketExtensions(h)
|
|
||||||
if len(exts) == 0 {
|
|
||||||
return nil, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
ext := exts[0]
|
|
||||||
if ext.name != "permessage-deflate" || len(exts) > 1 || copts == nil {
|
|
||||||
return nil, fmt.Errorf("WebSocket protcol violation: unsupported extensions from server: %+v", exts[1:])
|
|
||||||
}
|
|
||||||
|
|
||||||
_copts := *copts
|
|
||||||
copts = &_copts
|
|
||||||
|
|
||||||
for _, p := range ext.params {
|
|
||||||
switch p {
|
|
||||||
case "client_no_context_takeover":
|
|
||||||
copts.clientNoContextTakeover = true
|
|
||||||
continue
|
|
||||||
case "server_no_context_takeover":
|
|
||||||
copts.serverNoContextTakeover = true
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
if strings.HasPrefix(p, "server_max_window_bits=") {
|
|
||||||
// We can't adjust the deflate window, but decoding with a larger window is acceptable.
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil, fmt.Errorf("unsupported permessage-deflate parameter: %q", p)
|
|
||||||
}
|
|
||||||
|
|
||||||
return copts, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
var bufioReaderPool sync.Pool
|
|
||||||
|
|
||||||
func getBufioReader(r io.Reader) *bufio.Reader {
|
|
||||||
br, ok := bufioReaderPool.Get().(*bufio.Reader)
|
|
||||||
if !ok {
|
|
||||||
return bufio.NewReader(r)
|
|
||||||
}
|
|
||||||
br.Reset(r)
|
|
||||||
return br
|
|
||||||
}
|
|
||||||
|
|
||||||
func putBufioReader(br *bufio.Reader) {
|
|
||||||
bufioReaderPool.Put(br)
|
|
||||||
}
|
|
||||||
|
|
||||||
var bufioWriterPool sync.Pool
|
|
||||||
|
|
||||||
func getBufioWriter(w io.Writer) *bufio.Writer {
|
|
||||||
bw, ok := bufioWriterPool.Get().(*bufio.Writer)
|
|
||||||
if !ok {
|
|
||||||
return bufio.NewWriter(w)
|
|
||||||
}
|
|
||||||
bw.Reset(w)
|
|
||||||
return bw
|
|
||||||
}
|
|
||||||
|
|
||||||
func putBufioWriter(bw *bufio.Writer) {
|
|
||||||
bufioWriterPool.Put(bw)
|
|
||||||
}
|
|
||||||
33
vendor/github.com/coder/websocket/doc.go
generated
vendored
33
vendor/github.com/coder/websocket/doc.go
generated
vendored
@@ -1,33 +0,0 @@
|
|||||||
//go:build !js
|
|
||||||
|
|
||||||
// Package websocket implements the RFC 6455 WebSocket protocol.
|
|
||||||
//
|
|
||||||
// https://tools.ietf.org/html/rfc6455
|
|
||||||
//
|
|
||||||
// Use Dial to dial a WebSocket server.
|
|
||||||
//
|
|
||||||
// Use Accept to accept a WebSocket client.
|
|
||||||
//
|
|
||||||
// Conn represents the resulting WebSocket connection.
|
|
||||||
//
|
|
||||||
// The examples are the best way to understand how to correctly use the library.
|
|
||||||
//
|
|
||||||
// The wsjson subpackage contain helpers for JSON and protobuf messages.
|
|
||||||
//
|
|
||||||
// More documentation at https://github.com/coder/websocket.
|
|
||||||
//
|
|
||||||
// # Wasm
|
|
||||||
//
|
|
||||||
// The client side supports compiling to Wasm.
|
|
||||||
// It wraps the WebSocket browser API.
|
|
||||||
//
|
|
||||||
// See https://developer.mozilla.org/en-US/docs/Web/API/WebSocket
|
|
||||||
//
|
|
||||||
// Some important caveats to be aware of:
|
|
||||||
//
|
|
||||||
// - Accept always errors out
|
|
||||||
// - Conn.Ping is no-op
|
|
||||||
// - Conn.CloseNow is Close(StatusGoingAway, "")
|
|
||||||
// - HTTPClient, HTTPHeader and CompressionMode in DialOptions are no-op
|
|
||||||
// - *http.Response from Dial is &http.Response{} with a 101 status code on success
|
|
||||||
package websocket // import "github.com/coder/websocket"
|
|
||||||
8
vendor/github.com/coder/websocket/errors.go
generated
vendored
8
vendor/github.com/coder/websocket/errors.go
generated
vendored
@@ -1,8 +0,0 @@
|
|||||||
package websocket
|
|
||||||
|
|
||||||
import (
|
|
||||||
"errors"
|
|
||||||
)
|
|
||||||
|
|
||||||
// ErrMessageTooBig is returned when a message exceeds the read limit.
|
|
||||||
var ErrMessageTooBig = errors.New("websocket: message too big")
|
|
||||||
173
vendor/github.com/coder/websocket/frame.go
generated
vendored
173
vendor/github.com/coder/websocket/frame.go
generated
vendored
@@ -1,173 +0,0 @@
|
|||||||
//go:build !js
|
|
||||||
|
|
||||||
package websocket
|
|
||||||
|
|
||||||
import (
|
|
||||||
"bufio"
|
|
||||||
"encoding/binary"
|
|
||||||
"fmt"
|
|
||||||
"io"
|
|
||||||
"math"
|
|
||||||
|
|
||||||
"github.com/coder/websocket/internal/errd"
|
|
||||||
)
|
|
||||||
|
|
||||||
// opcode represents a WebSocket opcode.
|
|
||||||
type opcode int
|
|
||||||
|
|
||||||
// https://tools.ietf.org/html/rfc6455#section-11.8.
|
|
||||||
const (
|
|
||||||
opContinuation opcode = iota
|
|
||||||
opText
|
|
||||||
opBinary
|
|
||||||
// 3 - 7 are reserved for further non-control frames.
|
|
||||||
_
|
|
||||||
_
|
|
||||||
_
|
|
||||||
_
|
|
||||||
_
|
|
||||||
opClose
|
|
||||||
opPing
|
|
||||||
opPong
|
|
||||||
// 11-16 are reserved for further control frames.
|
|
||||||
)
|
|
||||||
|
|
||||||
// header represents a WebSocket frame header.
|
|
||||||
// See https://tools.ietf.org/html/rfc6455#section-5.2.
|
|
||||||
type header struct {
|
|
||||||
fin bool
|
|
||||||
rsv1 bool
|
|
||||||
rsv2 bool
|
|
||||||
rsv3 bool
|
|
||||||
opcode opcode
|
|
||||||
|
|
||||||
payloadLength int64
|
|
||||||
|
|
||||||
masked bool
|
|
||||||
maskKey uint32
|
|
||||||
}
|
|
||||||
|
|
||||||
// readFrameHeader reads a header from the reader.
|
|
||||||
// See https://tools.ietf.org/html/rfc6455#section-5.2.
|
|
||||||
func readFrameHeader(r *bufio.Reader, readBuf []byte) (h header, err error) {
|
|
||||||
defer errd.Wrap(&err, "failed to read frame header")
|
|
||||||
|
|
||||||
b, err := r.ReadByte()
|
|
||||||
if err != nil {
|
|
||||||
return header{}, err
|
|
||||||
}
|
|
||||||
|
|
||||||
h.fin = b&(1<<7) != 0
|
|
||||||
h.rsv1 = b&(1<<6) != 0
|
|
||||||
h.rsv2 = b&(1<<5) != 0
|
|
||||||
h.rsv3 = b&(1<<4) != 0
|
|
||||||
|
|
||||||
h.opcode = opcode(b & 0xf)
|
|
||||||
|
|
||||||
b, err = r.ReadByte()
|
|
||||||
if err != nil {
|
|
||||||
return header{}, err
|
|
||||||
}
|
|
||||||
|
|
||||||
h.masked = b&(1<<7) != 0
|
|
||||||
|
|
||||||
payloadLength := b &^ (1 << 7)
|
|
||||||
switch {
|
|
||||||
case payloadLength < 126:
|
|
||||||
h.payloadLength = int64(payloadLength)
|
|
||||||
case payloadLength == 126:
|
|
||||||
_, err = io.ReadFull(r, readBuf[:2])
|
|
||||||
h.payloadLength = int64(binary.BigEndian.Uint16(readBuf))
|
|
||||||
case payloadLength == 127:
|
|
||||||
_, err = io.ReadFull(r, readBuf)
|
|
||||||
h.payloadLength = int64(binary.BigEndian.Uint64(readBuf))
|
|
||||||
}
|
|
||||||
if err != nil {
|
|
||||||
return header{}, err
|
|
||||||
}
|
|
||||||
|
|
||||||
if h.payloadLength < 0 {
|
|
||||||
return header{}, fmt.Errorf("received negative payload length: %v", h.payloadLength)
|
|
||||||
}
|
|
||||||
|
|
||||||
if h.masked {
|
|
||||||
_, err = io.ReadFull(r, readBuf[:4])
|
|
||||||
if err != nil {
|
|
||||||
return header{}, err
|
|
||||||
}
|
|
||||||
h.maskKey = binary.LittleEndian.Uint32(readBuf)
|
|
||||||
}
|
|
||||||
|
|
||||||
return h, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// maxControlPayload is the maximum length of a control frame payload.
|
|
||||||
// See https://tools.ietf.org/html/rfc6455#section-5.5.
|
|
||||||
const maxControlPayload = 125
|
|
||||||
|
|
||||||
// writeFrameHeader writes the bytes of the header to w.
|
|
||||||
// See https://tools.ietf.org/html/rfc6455#section-5.2
|
|
||||||
func writeFrameHeader(h header, w *bufio.Writer, buf []byte) (err error) {
|
|
||||||
defer errd.Wrap(&err, "failed to write frame header")
|
|
||||||
|
|
||||||
var b byte
|
|
||||||
if h.fin {
|
|
||||||
b |= 1 << 7
|
|
||||||
}
|
|
||||||
if h.rsv1 {
|
|
||||||
b |= 1 << 6
|
|
||||||
}
|
|
||||||
if h.rsv2 {
|
|
||||||
b |= 1 << 5
|
|
||||||
}
|
|
||||||
if h.rsv3 {
|
|
||||||
b |= 1 << 4
|
|
||||||
}
|
|
||||||
|
|
||||||
b |= byte(h.opcode)
|
|
||||||
|
|
||||||
err = w.WriteByte(b)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
lengthByte := byte(0)
|
|
||||||
if h.masked {
|
|
||||||
lengthByte |= 1 << 7
|
|
||||||
}
|
|
||||||
|
|
||||||
switch {
|
|
||||||
case h.payloadLength > math.MaxUint16:
|
|
||||||
lengthByte |= 127
|
|
||||||
case h.payloadLength > 125:
|
|
||||||
lengthByte |= 126
|
|
||||||
case h.payloadLength >= 0:
|
|
||||||
lengthByte |= byte(h.payloadLength)
|
|
||||||
}
|
|
||||||
err = w.WriteByte(lengthByte)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
switch {
|
|
||||||
case h.payloadLength > math.MaxUint16:
|
|
||||||
binary.BigEndian.PutUint64(buf, uint64(h.payloadLength))
|
|
||||||
_, err = w.Write(buf)
|
|
||||||
case h.payloadLength > 125:
|
|
||||||
binary.BigEndian.PutUint16(buf, uint16(h.payloadLength))
|
|
||||||
_, err = w.Write(buf[:2])
|
|
||||||
}
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
if h.masked {
|
|
||||||
binary.LittleEndian.PutUint32(buf, h.maskKey)
|
|
||||||
_, err = w.Write(buf[:4])
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
33
vendor/github.com/coder/websocket/hijack.go
generated
vendored
33
vendor/github.com/coder/websocket/hijack.go
generated
vendored
@@ -1,33 +0,0 @@
|
|||||||
//go:build !js
|
|
||||||
|
|
||||||
package websocket
|
|
||||||
|
|
||||||
import (
|
|
||||||
"net/http"
|
|
||||||
)
|
|
||||||
|
|
||||||
type rwUnwrapper interface {
|
|
||||||
Unwrap() http.ResponseWriter
|
|
||||||
}
|
|
||||||
|
|
||||||
// hijacker returns the Hijacker interface of the http.ResponseWriter.
|
|
||||||
// It follows the Unwrap method of the http.ResponseWriter if available,
|
|
||||||
// matching the behavior of http.ResponseController. If the Hijacker
|
|
||||||
// interface is not found, it returns false.
|
|
||||||
//
|
|
||||||
// Since the http.ResponseController is not available in Go 1.19, and
|
|
||||||
// does not support checking the presence of the Hijacker interface,
|
|
||||||
// this function is used to provide a consistent way to check for the
|
|
||||||
// Hijacker interface across Go versions.
|
|
||||||
func hijacker(rw http.ResponseWriter) (http.Hijacker, bool) {
|
|
||||||
for {
|
|
||||||
switch t := rw.(type) {
|
|
||||||
case http.Hijacker:
|
|
||||||
return t, true
|
|
||||||
case rwUnwrapper:
|
|
||||||
rw = t.Unwrap()
|
|
||||||
default:
|
|
||||||
return nil, false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
25
vendor/github.com/coder/websocket/internal/bpool/bpool.go
generated
vendored
25
vendor/github.com/coder/websocket/internal/bpool/bpool.go
generated
vendored
@@ -1,25 +0,0 @@
|
|||||||
package bpool
|
|
||||||
|
|
||||||
import (
|
|
||||||
"bytes"
|
|
||||||
"sync"
|
|
||||||
)
|
|
||||||
|
|
||||||
var bpool = sync.Pool{
|
|
||||||
New: func() any {
|
|
||||||
return &bytes.Buffer{}
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get returns a buffer from the pool or creates a new one if
|
|
||||||
// the pool is empty.
|
|
||||||
func Get() *bytes.Buffer {
|
|
||||||
b := bpool.Get()
|
|
||||||
return b.(*bytes.Buffer)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Put returns a buffer into the pool.
|
|
||||||
func Put(b *bytes.Buffer) {
|
|
||||||
b.Reset()
|
|
||||||
bpool.Put(b)
|
|
||||||
}
|
|
||||||
14
vendor/github.com/coder/websocket/internal/errd/wrap.go
generated
vendored
14
vendor/github.com/coder/websocket/internal/errd/wrap.go
generated
vendored
@@ -1,14 +0,0 @@
|
|||||||
package errd
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
)
|
|
||||||
|
|
||||||
// Wrap wraps err with fmt.Errorf if err is non nil.
|
|
||||||
// Intended for use with defer and a named error return.
|
|
||||||
// Inspired by https://github.com/golang/go/issues/32676.
|
|
||||||
func Wrap(err *error, f string, v ...any) {
|
|
||||||
if *err != nil {
|
|
||||||
*err = fmt.Errorf(f+": %w", append(v, *err)...)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
15
vendor/github.com/coder/websocket/internal/util/util.go
generated
vendored
15
vendor/github.com/coder/websocket/internal/util/util.go
generated
vendored
@@ -1,15 +0,0 @@
|
|||||||
package util
|
|
||||||
|
|
||||||
// WriterFunc is used to implement one off io.Writers.
|
|
||||||
type WriterFunc func(p []byte) (int, error)
|
|
||||||
|
|
||||||
func (f WriterFunc) Write(p []byte) (int, error) {
|
|
||||||
return f(p)
|
|
||||||
}
|
|
||||||
|
|
||||||
// ReaderFunc is used to implement one off io.Readers.
|
|
||||||
type ReaderFunc func(p []byte) (int, error)
|
|
||||||
|
|
||||||
func (f ReaderFunc) Read(p []byte) (int, error) {
|
|
||||||
return f(p)
|
|
||||||
}
|
|
||||||
169
vendor/github.com/coder/websocket/internal/wsjs/wsjs_js.go
generated
vendored
169
vendor/github.com/coder/websocket/internal/wsjs/wsjs_js.go
generated
vendored
@@ -1,169 +0,0 @@
|
|||||||
//go:build js
|
|
||||||
// +build js
|
|
||||||
|
|
||||||
// Package wsjs implements typed access to the browser javascript WebSocket API.
|
|
||||||
//
|
|
||||||
// https://developer.mozilla.org/en-US/docs/Web/API/WebSocket
|
|
||||||
package wsjs
|
|
||||||
|
|
||||||
import (
|
|
||||||
"syscall/js"
|
|
||||||
)
|
|
||||||
|
|
||||||
func handleJSError(err *error, onErr func()) {
|
|
||||||
r := recover()
|
|
||||||
|
|
||||||
if jsErr, ok := r.(js.Error); ok {
|
|
||||||
*err = jsErr
|
|
||||||
|
|
||||||
if onErr != nil {
|
|
||||||
onErr()
|
|
||||||
}
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if r != nil {
|
|
||||||
panic(r)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// New is a wrapper around the javascript WebSocket constructor.
|
|
||||||
func New(url string, protocols []string) (c WebSocket, err error) {
|
|
||||||
defer handleJSError(&err, func() {
|
|
||||||
c = WebSocket{}
|
|
||||||
})
|
|
||||||
|
|
||||||
jsProtocols := make([]any, len(protocols))
|
|
||||||
for i, p := range protocols {
|
|
||||||
jsProtocols[i] = p
|
|
||||||
}
|
|
||||||
|
|
||||||
c = WebSocket{
|
|
||||||
v: js.Global().Get("WebSocket").New(url, jsProtocols),
|
|
||||||
}
|
|
||||||
|
|
||||||
c.setBinaryType("arraybuffer")
|
|
||||||
|
|
||||||
return c, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// WebSocket is a wrapper around a javascript WebSocket object.
|
|
||||||
type WebSocket struct {
|
|
||||||
v js.Value
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c WebSocket) setBinaryType(typ string) {
|
|
||||||
c.v.Set("binaryType", string(typ))
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c WebSocket) addEventListener(eventType string, fn func(e js.Value)) func() {
|
|
||||||
f := js.FuncOf(func(this js.Value, args []js.Value) any {
|
|
||||||
fn(args[0])
|
|
||||||
return nil
|
|
||||||
})
|
|
||||||
c.v.Call("addEventListener", eventType, f)
|
|
||||||
|
|
||||||
return func() {
|
|
||||||
c.v.Call("removeEventListener", eventType, f)
|
|
||||||
f.Release()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// CloseEvent is the type passed to a WebSocket close handler.
|
|
||||||
type CloseEvent struct {
|
|
||||||
Code uint16
|
|
||||||
Reason string
|
|
||||||
WasClean bool
|
|
||||||
}
|
|
||||||
|
|
||||||
// OnClose registers a function to be called when the WebSocket is closed.
|
|
||||||
func (c WebSocket) OnClose(fn func(CloseEvent)) (remove func()) {
|
|
||||||
return c.addEventListener("close", func(e js.Value) {
|
|
||||||
ce := CloseEvent{
|
|
||||||
Code: uint16(e.Get("code").Int()),
|
|
||||||
Reason: e.Get("reason").String(),
|
|
||||||
WasClean: e.Get("wasClean").Bool(),
|
|
||||||
}
|
|
||||||
fn(ce)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// OnError registers a function to be called when there is an error
|
|
||||||
// with the WebSocket.
|
|
||||||
func (c WebSocket) OnError(fn func(e js.Value)) (remove func()) {
|
|
||||||
return c.addEventListener("error", fn)
|
|
||||||
}
|
|
||||||
|
|
||||||
// MessageEvent is the type passed to a message handler.
|
|
||||||
type MessageEvent struct {
|
|
||||||
// string or []byte.
|
|
||||||
Data any
|
|
||||||
|
|
||||||
// There are more fields to the interface but we don't use them.
|
|
||||||
// See https://developer.mozilla.org/en-US/docs/Web/API/MessageEvent
|
|
||||||
}
|
|
||||||
|
|
||||||
// OnMessage registers a function to be called when the WebSocket receives a message.
|
|
||||||
func (c WebSocket) OnMessage(fn func(m MessageEvent)) (remove func()) {
|
|
||||||
return c.addEventListener("message", func(e js.Value) {
|
|
||||||
var data any
|
|
||||||
|
|
||||||
arrayBuffer := e.Get("data")
|
|
||||||
if arrayBuffer.Type() == js.TypeString {
|
|
||||||
data = arrayBuffer.String()
|
|
||||||
} else {
|
|
||||||
data = extractArrayBuffer(arrayBuffer)
|
|
||||||
}
|
|
||||||
|
|
||||||
me := MessageEvent{
|
|
||||||
Data: data,
|
|
||||||
}
|
|
||||||
fn(me)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// Subprotocol returns the WebSocket subprotocol in use.
|
|
||||||
func (c WebSocket) Subprotocol() string {
|
|
||||||
return c.v.Get("protocol").String()
|
|
||||||
}
|
|
||||||
|
|
||||||
// OnOpen registers a function to be called when the WebSocket is opened.
|
|
||||||
func (c WebSocket) OnOpen(fn func(e js.Value)) (remove func()) {
|
|
||||||
return c.addEventListener("open", fn)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Close closes the WebSocket with the given code and reason.
|
|
||||||
func (c WebSocket) Close(code int, reason string) (err error) {
|
|
||||||
defer handleJSError(&err, nil)
|
|
||||||
c.v.Call("close", code, reason)
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
// SendText sends the given string as a text message
|
|
||||||
// on the WebSocket.
|
|
||||||
func (c WebSocket) SendText(v string) (err error) {
|
|
||||||
defer handleJSError(&err, nil)
|
|
||||||
c.v.Call("send", v)
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
// SendBytes sends the given message as a binary message
|
|
||||||
// on the WebSocket.
|
|
||||||
func (c WebSocket) SendBytes(v []byte) (err error) {
|
|
||||||
defer handleJSError(&err, nil)
|
|
||||||
c.v.Call("send", uint8Array(v))
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
func extractArrayBuffer(arrayBuffer js.Value) []byte {
|
|
||||||
uint8Array := js.Global().Get("Uint8Array").New(arrayBuffer)
|
|
||||||
dst := make([]byte, uint8Array.Length())
|
|
||||||
js.CopyBytesToGo(dst, uint8Array)
|
|
||||||
return dst
|
|
||||||
}
|
|
||||||
|
|
||||||
func uint8Array(src []byte) js.Value {
|
|
||||||
uint8Array := js.Global().Get("Uint8Array").New(len(src))
|
|
||||||
js.CopyBytesToJS(uint8Array, src)
|
|
||||||
return uint8Array
|
|
||||||
}
|
|
||||||
128
vendor/github.com/coder/websocket/mask.go
generated
vendored
128
vendor/github.com/coder/websocket/mask.go
generated
vendored
@@ -1,128 +0,0 @@
|
|||||||
package websocket
|
|
||||||
|
|
||||||
import (
|
|
||||||
"encoding/binary"
|
|
||||||
"math/bits"
|
|
||||||
)
|
|
||||||
|
|
||||||
// maskGo applies the WebSocket masking algorithm to p
|
|
||||||
// with the given key.
|
|
||||||
// See https://tools.ietf.org/html/rfc6455#section-5.3
|
|
||||||
//
|
|
||||||
// The returned value is the correctly rotated key to
|
|
||||||
// to continue to mask/unmask the message.
|
|
||||||
//
|
|
||||||
// It is optimized for LittleEndian and expects the key
|
|
||||||
// to be in little endian.
|
|
||||||
//
|
|
||||||
// See https://github.com/golang/go/issues/31586
|
|
||||||
func maskGo(b []byte, key uint32) uint32 {
|
|
||||||
if len(b) >= 8 {
|
|
||||||
key64 := uint64(key)<<32 | uint64(key)
|
|
||||||
|
|
||||||
// At some point in the future we can clean these unrolled loops up.
|
|
||||||
// See https://github.com/golang/go/issues/31586#issuecomment-487436401
|
|
||||||
|
|
||||||
// Then we xor until b is less than 128 bytes.
|
|
||||||
for len(b) >= 128 {
|
|
||||||
v := binary.LittleEndian.Uint64(b)
|
|
||||||
binary.LittleEndian.PutUint64(b, v^key64)
|
|
||||||
v = binary.LittleEndian.Uint64(b[8:16])
|
|
||||||
binary.LittleEndian.PutUint64(b[8:16], v^key64)
|
|
||||||
v = binary.LittleEndian.Uint64(b[16:24])
|
|
||||||
binary.LittleEndian.PutUint64(b[16:24], v^key64)
|
|
||||||
v = binary.LittleEndian.Uint64(b[24:32])
|
|
||||||
binary.LittleEndian.PutUint64(b[24:32], v^key64)
|
|
||||||
v = binary.LittleEndian.Uint64(b[32:40])
|
|
||||||
binary.LittleEndian.PutUint64(b[32:40], v^key64)
|
|
||||||
v = binary.LittleEndian.Uint64(b[40:48])
|
|
||||||
binary.LittleEndian.PutUint64(b[40:48], v^key64)
|
|
||||||
v = binary.LittleEndian.Uint64(b[48:56])
|
|
||||||
binary.LittleEndian.PutUint64(b[48:56], v^key64)
|
|
||||||
v = binary.LittleEndian.Uint64(b[56:64])
|
|
||||||
binary.LittleEndian.PutUint64(b[56:64], v^key64)
|
|
||||||
v = binary.LittleEndian.Uint64(b[64:72])
|
|
||||||
binary.LittleEndian.PutUint64(b[64:72], v^key64)
|
|
||||||
v = binary.LittleEndian.Uint64(b[72:80])
|
|
||||||
binary.LittleEndian.PutUint64(b[72:80], v^key64)
|
|
||||||
v = binary.LittleEndian.Uint64(b[80:88])
|
|
||||||
binary.LittleEndian.PutUint64(b[80:88], v^key64)
|
|
||||||
v = binary.LittleEndian.Uint64(b[88:96])
|
|
||||||
binary.LittleEndian.PutUint64(b[88:96], v^key64)
|
|
||||||
v = binary.LittleEndian.Uint64(b[96:104])
|
|
||||||
binary.LittleEndian.PutUint64(b[96:104], v^key64)
|
|
||||||
v = binary.LittleEndian.Uint64(b[104:112])
|
|
||||||
binary.LittleEndian.PutUint64(b[104:112], v^key64)
|
|
||||||
v = binary.LittleEndian.Uint64(b[112:120])
|
|
||||||
binary.LittleEndian.PutUint64(b[112:120], v^key64)
|
|
||||||
v = binary.LittleEndian.Uint64(b[120:128])
|
|
||||||
binary.LittleEndian.PutUint64(b[120:128], v^key64)
|
|
||||||
b = b[128:]
|
|
||||||
}
|
|
||||||
|
|
||||||
// Then we xor until b is less than 64 bytes.
|
|
||||||
for len(b) >= 64 {
|
|
||||||
v := binary.LittleEndian.Uint64(b)
|
|
||||||
binary.LittleEndian.PutUint64(b, v^key64)
|
|
||||||
v = binary.LittleEndian.Uint64(b[8:16])
|
|
||||||
binary.LittleEndian.PutUint64(b[8:16], v^key64)
|
|
||||||
v = binary.LittleEndian.Uint64(b[16:24])
|
|
||||||
binary.LittleEndian.PutUint64(b[16:24], v^key64)
|
|
||||||
v = binary.LittleEndian.Uint64(b[24:32])
|
|
||||||
binary.LittleEndian.PutUint64(b[24:32], v^key64)
|
|
||||||
v = binary.LittleEndian.Uint64(b[32:40])
|
|
||||||
binary.LittleEndian.PutUint64(b[32:40], v^key64)
|
|
||||||
v = binary.LittleEndian.Uint64(b[40:48])
|
|
||||||
binary.LittleEndian.PutUint64(b[40:48], v^key64)
|
|
||||||
v = binary.LittleEndian.Uint64(b[48:56])
|
|
||||||
binary.LittleEndian.PutUint64(b[48:56], v^key64)
|
|
||||||
v = binary.LittleEndian.Uint64(b[56:64])
|
|
||||||
binary.LittleEndian.PutUint64(b[56:64], v^key64)
|
|
||||||
b = b[64:]
|
|
||||||
}
|
|
||||||
|
|
||||||
// Then we xor until b is less than 32 bytes.
|
|
||||||
for len(b) >= 32 {
|
|
||||||
v := binary.LittleEndian.Uint64(b)
|
|
||||||
binary.LittleEndian.PutUint64(b, v^key64)
|
|
||||||
v = binary.LittleEndian.Uint64(b[8:16])
|
|
||||||
binary.LittleEndian.PutUint64(b[8:16], v^key64)
|
|
||||||
v = binary.LittleEndian.Uint64(b[16:24])
|
|
||||||
binary.LittleEndian.PutUint64(b[16:24], v^key64)
|
|
||||||
v = binary.LittleEndian.Uint64(b[24:32])
|
|
||||||
binary.LittleEndian.PutUint64(b[24:32], v^key64)
|
|
||||||
b = b[32:]
|
|
||||||
}
|
|
||||||
|
|
||||||
// Then we xor until b is less than 16 bytes.
|
|
||||||
for len(b) >= 16 {
|
|
||||||
v := binary.LittleEndian.Uint64(b)
|
|
||||||
binary.LittleEndian.PutUint64(b, v^key64)
|
|
||||||
v = binary.LittleEndian.Uint64(b[8:16])
|
|
||||||
binary.LittleEndian.PutUint64(b[8:16], v^key64)
|
|
||||||
b = b[16:]
|
|
||||||
}
|
|
||||||
|
|
||||||
// Then we xor until b is less than 8 bytes.
|
|
||||||
for len(b) >= 8 {
|
|
||||||
v := binary.LittleEndian.Uint64(b)
|
|
||||||
binary.LittleEndian.PutUint64(b, v^key64)
|
|
||||||
b = b[8:]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Then we xor until b is less than 4 bytes.
|
|
||||||
for len(b) >= 4 {
|
|
||||||
v := binary.LittleEndian.Uint32(b)
|
|
||||||
binary.LittleEndian.PutUint32(b, v^key)
|
|
||||||
b = b[4:]
|
|
||||||
}
|
|
||||||
|
|
||||||
// xor remaining bytes.
|
|
||||||
for i := range b {
|
|
||||||
b[i] ^= byte(key)
|
|
||||||
key = bits.RotateLeft32(key, -8)
|
|
||||||
}
|
|
||||||
|
|
||||||
return key
|
|
||||||
}
|
|
||||||
127
vendor/github.com/coder/websocket/mask_amd64.s
generated
vendored
127
vendor/github.com/coder/websocket/mask_amd64.s
generated
vendored
@@ -1,127 +0,0 @@
|
|||||||
#include "textflag.h"
|
|
||||||
|
|
||||||
// func maskAsm(b *byte, len int, key uint32)
|
|
||||||
TEXT ·maskAsm(SB), NOSPLIT, $0-28
|
|
||||||
// AX = b
|
|
||||||
// CX = len (left length)
|
|
||||||
// SI = key (uint32)
|
|
||||||
// DI = uint64(SI) | uint64(SI)<<32
|
|
||||||
MOVQ b+0(FP), AX
|
|
||||||
MOVQ len+8(FP), CX
|
|
||||||
MOVL key+16(FP), SI
|
|
||||||
|
|
||||||
// calculate the DI
|
|
||||||
// DI = SI<<32 | SI
|
|
||||||
MOVL SI, DI
|
|
||||||
MOVQ DI, DX
|
|
||||||
SHLQ $32, DI
|
|
||||||
ORQ DX, DI
|
|
||||||
|
|
||||||
CMPQ CX, $15
|
|
||||||
JLE less_than_16
|
|
||||||
CMPQ CX, $63
|
|
||||||
JLE less_than_64
|
|
||||||
CMPQ CX, $128
|
|
||||||
JLE sse
|
|
||||||
TESTQ $31, AX
|
|
||||||
JNZ unaligned
|
|
||||||
|
|
||||||
unaligned_loop_1byte:
|
|
||||||
XORB SI, (AX)
|
|
||||||
INCQ AX
|
|
||||||
DECQ CX
|
|
||||||
ROLL $24, SI
|
|
||||||
TESTQ $7, AX
|
|
||||||
JNZ unaligned_loop_1byte
|
|
||||||
|
|
||||||
// calculate DI again since SI was modified
|
|
||||||
// DI = SI<<32 | SI
|
|
||||||
MOVL SI, DI
|
|
||||||
MOVQ DI, DX
|
|
||||||
SHLQ $32, DI
|
|
||||||
ORQ DX, DI
|
|
||||||
|
|
||||||
TESTQ $31, AX
|
|
||||||
JZ sse
|
|
||||||
|
|
||||||
unaligned:
|
|
||||||
TESTQ $7, AX // AND $7 & len, if not zero jump to loop_1b.
|
|
||||||
JNZ unaligned_loop_1byte
|
|
||||||
|
|
||||||
unaligned_loop:
|
|
||||||
// we don't need to check the CX since we know it's above 128
|
|
||||||
XORQ DI, (AX)
|
|
||||||
ADDQ $8, AX
|
|
||||||
SUBQ $8, CX
|
|
||||||
TESTQ $31, AX
|
|
||||||
JNZ unaligned_loop
|
|
||||||
JMP sse
|
|
||||||
|
|
||||||
sse:
|
|
||||||
CMPQ CX, $0x40
|
|
||||||
JL less_than_64
|
|
||||||
MOVQ DI, X0
|
|
||||||
PUNPCKLQDQ X0, X0
|
|
||||||
|
|
||||||
sse_loop:
|
|
||||||
MOVOU 0*16(AX), X1
|
|
||||||
MOVOU 1*16(AX), X2
|
|
||||||
MOVOU 2*16(AX), X3
|
|
||||||
MOVOU 3*16(AX), X4
|
|
||||||
PXOR X0, X1
|
|
||||||
PXOR X0, X2
|
|
||||||
PXOR X0, X3
|
|
||||||
PXOR X0, X4
|
|
||||||
MOVOU X1, 0*16(AX)
|
|
||||||
MOVOU X2, 1*16(AX)
|
|
||||||
MOVOU X3, 2*16(AX)
|
|
||||||
MOVOU X4, 3*16(AX)
|
|
||||||
ADDQ $0x40, AX
|
|
||||||
SUBQ $0x40, CX
|
|
||||||
CMPQ CX, $0x40
|
|
||||||
JAE sse_loop
|
|
||||||
|
|
||||||
less_than_64:
|
|
||||||
TESTQ $32, CX
|
|
||||||
JZ less_than_32
|
|
||||||
XORQ DI, (AX)
|
|
||||||
XORQ DI, 8(AX)
|
|
||||||
XORQ DI, 16(AX)
|
|
||||||
XORQ DI, 24(AX)
|
|
||||||
ADDQ $32, AX
|
|
||||||
|
|
||||||
less_than_32:
|
|
||||||
TESTQ $16, CX
|
|
||||||
JZ less_than_16
|
|
||||||
XORQ DI, (AX)
|
|
||||||
XORQ DI, 8(AX)
|
|
||||||
ADDQ $16, AX
|
|
||||||
|
|
||||||
less_than_16:
|
|
||||||
TESTQ $8, CX
|
|
||||||
JZ less_than_8
|
|
||||||
XORQ DI, (AX)
|
|
||||||
ADDQ $8, AX
|
|
||||||
|
|
||||||
less_than_8:
|
|
||||||
TESTQ $4, CX
|
|
||||||
JZ less_than_4
|
|
||||||
XORL SI, (AX)
|
|
||||||
ADDQ $4, AX
|
|
||||||
|
|
||||||
less_than_4:
|
|
||||||
TESTQ $2, CX
|
|
||||||
JZ less_than_2
|
|
||||||
XORW SI, (AX)
|
|
||||||
ROLL $16, SI
|
|
||||||
ADDQ $2, AX
|
|
||||||
|
|
||||||
less_than_2:
|
|
||||||
TESTQ $1, CX
|
|
||||||
JZ done
|
|
||||||
XORB SI, (AX)
|
|
||||||
ROLL $24, SI
|
|
||||||
|
|
||||||
done:
|
|
||||||
MOVL SI, ret+24(FP)
|
|
||||||
RET
|
|
||||||
72
vendor/github.com/coder/websocket/mask_arm64.s
generated
vendored
72
vendor/github.com/coder/websocket/mask_arm64.s
generated
vendored
@@ -1,72 +0,0 @@
|
|||||||
#include "textflag.h"
|
|
||||||
|
|
||||||
// func maskAsm(b *byte, len int, key uint32)
|
|
||||||
TEXT ·maskAsm(SB), NOSPLIT, $0-28
|
|
||||||
// R0 = b
|
|
||||||
// R1 = len
|
|
||||||
// R3 = key (uint32)
|
|
||||||
// R2 = uint64(key)<<32 | uint64(key)
|
|
||||||
MOVD b_ptr+0(FP), R0
|
|
||||||
MOVD b_len+8(FP), R1
|
|
||||||
MOVWU key+16(FP), R3
|
|
||||||
MOVD R3, R2
|
|
||||||
ORR R2<<32, R2, R2
|
|
||||||
VDUP R2, V0.D2
|
|
||||||
CMP $64, R1
|
|
||||||
BLT less_than_64
|
|
||||||
|
|
||||||
loop_64:
|
|
||||||
VLD1 (R0), [V1.B16, V2.B16, V3.B16, V4.B16]
|
|
||||||
VEOR V1.B16, V0.B16, V1.B16
|
|
||||||
VEOR V2.B16, V0.B16, V2.B16
|
|
||||||
VEOR V3.B16, V0.B16, V3.B16
|
|
||||||
VEOR V4.B16, V0.B16, V4.B16
|
|
||||||
VST1.P [V1.B16, V2.B16, V3.B16, V4.B16], 64(R0)
|
|
||||||
SUBS $64, R1
|
|
||||||
CMP $64, R1
|
|
||||||
BGE loop_64
|
|
||||||
|
|
||||||
less_than_64:
|
|
||||||
CBZ R1, end
|
|
||||||
TBZ $5, R1, less_than_32
|
|
||||||
VLD1 (R0), [V1.B16, V2.B16]
|
|
||||||
VEOR V1.B16, V0.B16, V1.B16
|
|
||||||
VEOR V2.B16, V0.B16, V2.B16
|
|
||||||
VST1.P [V1.B16, V2.B16], 32(R0)
|
|
||||||
|
|
||||||
less_than_32:
|
|
||||||
TBZ $4, R1, less_than_16
|
|
||||||
LDP (R0), (R11, R12)
|
|
||||||
EOR R11, R2, R11
|
|
||||||
EOR R12, R2, R12
|
|
||||||
STP.P (R11, R12), 16(R0)
|
|
||||||
|
|
||||||
less_than_16:
|
|
||||||
TBZ $3, R1, less_than_8
|
|
||||||
MOVD (R0), R11
|
|
||||||
EOR R2, R11, R11
|
|
||||||
MOVD.P R11, 8(R0)
|
|
||||||
|
|
||||||
less_than_8:
|
|
||||||
TBZ $2, R1, less_than_4
|
|
||||||
MOVWU (R0), R11
|
|
||||||
EORW R2, R11, R11
|
|
||||||
MOVWU.P R11, 4(R0)
|
|
||||||
|
|
||||||
less_than_4:
|
|
||||||
TBZ $1, R1, less_than_2
|
|
||||||
MOVHU (R0), R11
|
|
||||||
EORW R3, R11, R11
|
|
||||||
MOVHU.P R11, 2(R0)
|
|
||||||
RORW $16, R3
|
|
||||||
|
|
||||||
less_than_2:
|
|
||||||
TBZ $0, R1, end
|
|
||||||
MOVBU (R0), R11
|
|
||||||
EORW R3, R11, R11
|
|
||||||
MOVBU.P R11, 1(R0)
|
|
||||||
RORW $8, R3
|
|
||||||
|
|
||||||
end:
|
|
||||||
MOVWU R3, ret+24(FP)
|
|
||||||
RET
|
|
||||||
26
vendor/github.com/coder/websocket/mask_asm.go
generated
vendored
26
vendor/github.com/coder/websocket/mask_asm.go
generated
vendored
@@ -1,26 +0,0 @@
|
|||||||
//go:build amd64 || arm64
|
|
||||||
|
|
||||||
package websocket
|
|
||||||
|
|
||||||
func mask(b []byte, key uint32) uint32 {
|
|
||||||
// TODO: Will enable in v1.9.0.
|
|
||||||
return maskGo(b, key)
|
|
||||||
/*
|
|
||||||
if len(b) > 0 {
|
|
||||||
return maskAsm(&b[0], len(b), key)
|
|
||||||
}
|
|
||||||
return key
|
|
||||||
*/
|
|
||||||
}
|
|
||||||
|
|
||||||
// @nhooyr: I am not confident that the amd64 or the arm64 implementations of this
|
|
||||||
// function are perfect. There are almost certainly missing optimizations or
|
|
||||||
// opportunities for simplification. I'm confident there are no bugs though.
|
|
||||||
// For example, the arm64 implementation doesn't align memory like the amd64.
|
|
||||||
// Or the amd64 implementation could use AVX512 instead of just AVX2.
|
|
||||||
// The AVX2 code I had to disable anyway as it wasn't performing as expected.
|
|
||||||
// See https://github.com/nhooyr/websocket/pull/326#issuecomment-1771138049
|
|
||||||
//
|
|
||||||
//go:noescape
|
|
||||||
//lint:ignore U1000 disabled till v1.9.0
|
|
||||||
func maskAsm(b *byte, len int, key uint32) uint32
|
|
||||||
7
vendor/github.com/coder/websocket/mask_go.go
generated
vendored
7
vendor/github.com/coder/websocket/mask_go.go
generated
vendored
@@ -1,7 +0,0 @@
|
|||||||
//go:build !amd64 && !arm64 && !js
|
|
||||||
|
|
||||||
package websocket
|
|
||||||
|
|
||||||
func mask(b []byte, key uint32) uint32 {
|
|
||||||
return maskGo(b, key)
|
|
||||||
}
|
|
||||||
233
vendor/github.com/coder/websocket/netconn.go
generated
vendored
233
vendor/github.com/coder/websocket/netconn.go
generated
vendored
@@ -1,233 +0,0 @@
|
|||||||
package websocket
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"fmt"
|
|
||||||
"io"
|
|
||||||
"math"
|
|
||||||
"net"
|
|
||||||
"sync/atomic"
|
|
||||||
"time"
|
|
||||||
)
|
|
||||||
|
|
||||||
// NetConn converts a *websocket.Conn into a net.Conn.
|
|
||||||
//
|
|
||||||
// It's for tunneling arbitrary protocols over WebSockets.
|
|
||||||
// Few users of the library will need this but it's tricky to implement
|
|
||||||
// correctly and so provided in the library.
|
|
||||||
// See https://github.com/nhooyr/websocket/issues/100.
|
|
||||||
//
|
|
||||||
// Every Write to the net.Conn will correspond to a message write of
|
|
||||||
// the given type on *websocket.Conn.
|
|
||||||
//
|
|
||||||
// The passed ctx bounds the lifetime of the net.Conn. If cancelled,
|
|
||||||
// all reads and writes on the net.Conn will be cancelled.
|
|
||||||
//
|
|
||||||
// If a message is read that is not of the correct type, the connection
|
|
||||||
// will be closed with StatusUnsupportedData and an error will be returned.
|
|
||||||
//
|
|
||||||
// Close will close the *websocket.Conn with StatusNormalClosure.
|
|
||||||
//
|
|
||||||
// When a deadline is hit and there is an active read or write goroutine, the
|
|
||||||
// connection will be closed. This is different from most net.Conn implementations
|
|
||||||
// where only the reading/writing goroutines are interrupted but the connection
|
|
||||||
// is kept alive.
|
|
||||||
//
|
|
||||||
// The Addr methods will return the real addresses for connections obtained
|
|
||||||
// from websocket.Accept. But for connections obtained from websocket.Dial, a mock net.Addr
|
|
||||||
// will be returned that gives "websocket" for Network() and "websocket/unknown-addr" for
|
|
||||||
// String(). This is because websocket.Dial only exposes a io.ReadWriteCloser instead of the
|
|
||||||
// full net.Conn to us.
|
|
||||||
//
|
|
||||||
// When running as WASM, the Addr methods will always return the mock address described above.
|
|
||||||
//
|
|
||||||
// A received StatusNormalClosure or StatusGoingAway close frame will be translated to
|
|
||||||
// io.EOF when reading.
|
|
||||||
//
|
|
||||||
// Furthermore, the ReadLimit is set to -1 to disable it.
|
|
||||||
func NetConn(ctx context.Context, c *Conn, msgType MessageType) net.Conn {
|
|
||||||
c.SetReadLimit(-1)
|
|
||||||
|
|
||||||
nc := &netConn{
|
|
||||||
c: c,
|
|
||||||
msgType: msgType,
|
|
||||||
readMu: newMu(c),
|
|
||||||
writeMu: newMu(c),
|
|
||||||
}
|
|
||||||
|
|
||||||
nc.writeCtx, nc.writeCancel = context.WithCancel(ctx)
|
|
||||||
nc.readCtx, nc.readCancel = context.WithCancel(ctx)
|
|
||||||
|
|
||||||
nc.writeTimer = time.AfterFunc(math.MaxInt64, func() {
|
|
||||||
if !nc.writeMu.tryLock() {
|
|
||||||
// If the lock cannot be acquired, then there is an
|
|
||||||
// active write goroutine and so we should cancel the context.
|
|
||||||
nc.writeCancel()
|
|
||||||
return
|
|
||||||
}
|
|
||||||
defer nc.writeMu.unlock()
|
|
||||||
|
|
||||||
// Prevents future writes from writing until the deadline is reset.
|
|
||||||
nc.writeExpired.Store(1)
|
|
||||||
})
|
|
||||||
if !nc.writeTimer.Stop() {
|
|
||||||
<-nc.writeTimer.C
|
|
||||||
}
|
|
||||||
|
|
||||||
nc.readTimer = time.AfterFunc(math.MaxInt64, func() {
|
|
||||||
if !nc.readMu.tryLock() {
|
|
||||||
// If the lock cannot be acquired, then there is an
|
|
||||||
// active read goroutine and so we should cancel the context.
|
|
||||||
nc.readCancel()
|
|
||||||
return
|
|
||||||
}
|
|
||||||
defer nc.readMu.unlock()
|
|
||||||
|
|
||||||
// Prevents future reads from reading until the deadline is reset.
|
|
||||||
nc.readExpired.Store(1)
|
|
||||||
})
|
|
||||||
if !nc.readTimer.Stop() {
|
|
||||||
<-nc.readTimer.C
|
|
||||||
}
|
|
||||||
|
|
||||||
return nc
|
|
||||||
}
|
|
||||||
|
|
||||||
type netConn struct {
|
|
||||||
c *Conn
|
|
||||||
msgType MessageType
|
|
||||||
|
|
||||||
writeTimer *time.Timer
|
|
||||||
writeMu *mu
|
|
||||||
writeExpired atomic.Int64
|
|
||||||
writeCtx context.Context
|
|
||||||
writeCancel context.CancelFunc
|
|
||||||
|
|
||||||
readTimer *time.Timer
|
|
||||||
readMu *mu
|
|
||||||
readExpired atomic.Int64
|
|
||||||
readCtx context.Context
|
|
||||||
readCancel context.CancelFunc
|
|
||||||
readEOFed bool
|
|
||||||
reader io.Reader
|
|
||||||
}
|
|
||||||
|
|
||||||
var _ net.Conn = &netConn{}
|
|
||||||
|
|
||||||
func (nc *netConn) Close() error {
|
|
||||||
nc.writeTimer.Stop()
|
|
||||||
nc.writeCancel()
|
|
||||||
nc.readTimer.Stop()
|
|
||||||
nc.readCancel()
|
|
||||||
return nc.c.Close(StatusNormalClosure, "")
|
|
||||||
}
|
|
||||||
|
|
||||||
func (nc *netConn) Write(p []byte) (int, error) {
|
|
||||||
nc.writeMu.forceLock()
|
|
||||||
defer nc.writeMu.unlock()
|
|
||||||
|
|
||||||
if nc.writeExpired.Load() == 1 {
|
|
||||||
return 0, fmt.Errorf("failed to write: %w", context.DeadlineExceeded)
|
|
||||||
}
|
|
||||||
|
|
||||||
err := nc.c.Write(nc.writeCtx, nc.msgType, p)
|
|
||||||
if err != nil {
|
|
||||||
return 0, err
|
|
||||||
}
|
|
||||||
return len(p), nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (nc *netConn) Read(p []byte) (int, error) {
|
|
||||||
nc.readMu.forceLock()
|
|
||||||
defer nc.readMu.unlock()
|
|
||||||
|
|
||||||
for {
|
|
||||||
n, err := nc.read(p)
|
|
||||||
if err != nil {
|
|
||||||
return n, err
|
|
||||||
}
|
|
||||||
if n == 0 {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
return n, nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (nc *netConn) read(p []byte) (int, error) {
|
|
||||||
if nc.readExpired.Load() == 1 {
|
|
||||||
return 0, fmt.Errorf("failed to read: %w", context.DeadlineExceeded)
|
|
||||||
}
|
|
||||||
|
|
||||||
if nc.readEOFed {
|
|
||||||
return 0, io.EOF
|
|
||||||
}
|
|
||||||
|
|
||||||
if nc.reader == nil {
|
|
||||||
typ, r, err := nc.c.Reader(nc.readCtx)
|
|
||||||
if err != nil {
|
|
||||||
switch CloseStatus(err) {
|
|
||||||
case StatusNormalClosure, StatusGoingAway:
|
|
||||||
nc.readEOFed = true
|
|
||||||
return 0, io.EOF
|
|
||||||
}
|
|
||||||
return 0, err
|
|
||||||
}
|
|
||||||
if typ != nc.msgType {
|
|
||||||
err := fmt.Errorf("unexpected frame type read (expected %v): %v", nc.msgType, typ)
|
|
||||||
nc.c.Close(StatusUnsupportedData, err.Error())
|
|
||||||
return 0, err
|
|
||||||
}
|
|
||||||
nc.reader = r
|
|
||||||
}
|
|
||||||
|
|
||||||
n, err := nc.reader.Read(p)
|
|
||||||
if err == io.EOF {
|
|
||||||
nc.reader = nil
|
|
||||||
err = nil
|
|
||||||
}
|
|
||||||
return n, err
|
|
||||||
}
|
|
||||||
|
|
||||||
type websocketAddr struct{}
|
|
||||||
|
|
||||||
func (a websocketAddr) Network() string {
|
|
||||||
return "websocket"
|
|
||||||
}
|
|
||||||
|
|
||||||
func (a websocketAddr) String() string {
|
|
||||||
return "websocket/unknown-addr"
|
|
||||||
}
|
|
||||||
|
|
||||||
func (nc *netConn) SetDeadline(t time.Time) error {
|
|
||||||
nc.SetWriteDeadline(t)
|
|
||||||
nc.SetReadDeadline(t)
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (nc *netConn) SetWriteDeadline(t time.Time) error {
|
|
||||||
nc.writeExpired.Store(0)
|
|
||||||
if t.IsZero() {
|
|
||||||
nc.writeTimer.Stop()
|
|
||||||
} else {
|
|
||||||
dur := time.Until(t)
|
|
||||||
if dur <= 0 {
|
|
||||||
dur = 1
|
|
||||||
}
|
|
||||||
nc.writeTimer.Reset(dur)
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (nc *netConn) SetReadDeadline(t time.Time) error {
|
|
||||||
nc.readExpired.Store(0)
|
|
||||||
if t.IsZero() {
|
|
||||||
nc.readTimer.Stop()
|
|
||||||
} else {
|
|
||||||
dur := time.Until(t)
|
|
||||||
if dur <= 0 {
|
|
||||||
dur = 1
|
|
||||||
}
|
|
||||||
nc.readTimer.Reset(dur)
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
11
vendor/github.com/coder/websocket/netconn_js.go
generated
vendored
11
vendor/github.com/coder/websocket/netconn_js.go
generated
vendored
@@ -1,11 +0,0 @@
|
|||||||
package websocket
|
|
||||||
|
|
||||||
import "net"
|
|
||||||
|
|
||||||
func (nc *netConn) RemoteAddr() net.Addr {
|
|
||||||
return websocketAddr{}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (nc *netConn) LocalAddr() net.Addr {
|
|
||||||
return websocketAddr{}
|
|
||||||
}
|
|
||||||
19
vendor/github.com/coder/websocket/netconn_notjs.go
generated
vendored
19
vendor/github.com/coder/websocket/netconn_notjs.go
generated
vendored
@@ -1,19 +0,0 @@
|
|||||||
//go:build !js
|
|
||||||
|
|
||||||
package websocket
|
|
||||||
|
|
||||||
import "net"
|
|
||||||
|
|
||||||
func (nc *netConn) RemoteAddr() net.Addr {
|
|
||||||
if unc, ok := nc.c.rwc.(net.Conn); ok {
|
|
||||||
return unc.RemoteAddr()
|
|
||||||
}
|
|
||||||
return websocketAddr{}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (nc *netConn) LocalAddr() net.Addr {
|
|
||||||
if unc, ok := nc.c.rwc.(net.Conn); ok {
|
|
||||||
return unc.LocalAddr()
|
|
||||||
}
|
|
||||||
return websocketAddr{}
|
|
||||||
}
|
|
||||||
540
vendor/github.com/coder/websocket/read.go
generated
vendored
540
vendor/github.com/coder/websocket/read.go
generated
vendored
@@ -1,540 +0,0 @@
|
|||||||
//go:build !js
|
|
||||||
|
|
||||||
package websocket
|
|
||||||
|
|
||||||
import (
|
|
||||||
"bufio"
|
|
||||||
"context"
|
|
||||||
"errors"
|
|
||||||
"fmt"
|
|
||||||
"io"
|
|
||||||
"net"
|
|
||||||
"strings"
|
|
||||||
"sync/atomic"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/coder/websocket/internal/errd"
|
|
||||||
"github.com/coder/websocket/internal/util"
|
|
||||||
)
|
|
||||||
|
|
||||||
// Reader reads from the connection until there is a WebSocket
|
|
||||||
// data message to be read. It will handle ping, pong and close frames as appropriate.
|
|
||||||
//
|
|
||||||
// It returns the type of the message and an io.Reader to read it.
|
|
||||||
// The passed context will also bound the reader.
|
|
||||||
// Ensure you read to EOF otherwise the connection will hang.
|
|
||||||
//
|
|
||||||
// Call CloseRead if you do not expect any data messages from the peer.
|
|
||||||
//
|
|
||||||
// Only one Reader may be open at a time.
|
|
||||||
//
|
|
||||||
// If you need a separate timeout on the Reader call and the Read itself,
|
|
||||||
// use time.AfterFunc to cancel the context passed in.
|
|
||||||
// See https://github.com/nhooyr/websocket/issues/87#issue-451703332
|
|
||||||
// Most users should not need this.
|
|
||||||
func (c *Conn) Reader(ctx context.Context) (MessageType, io.Reader, error) {
|
|
||||||
return c.reader(ctx)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Read is a convenience method around Reader to read a single message
|
|
||||||
// from the connection.
|
|
||||||
func (c *Conn) Read(ctx context.Context) (MessageType, []byte, error) {
|
|
||||||
typ, r, err := c.Reader(ctx)
|
|
||||||
if err != nil {
|
|
||||||
return 0, nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
b, err := io.ReadAll(r)
|
|
||||||
return typ, b, err
|
|
||||||
}
|
|
||||||
|
|
||||||
// CloseRead starts a goroutine to read from the connection until it is closed
|
|
||||||
// or a data message is received.
|
|
||||||
//
|
|
||||||
// Once CloseRead is called you cannot read any messages from the connection.
|
|
||||||
// The returned context will be cancelled when the connection is closed.
|
|
||||||
//
|
|
||||||
// If a data message is received, the connection will be closed with StatusPolicyViolation.
|
|
||||||
//
|
|
||||||
// Call CloseRead when you do not expect to read any more messages.
|
|
||||||
// Since it actively reads from the connection, it will ensure that ping, pong and close
|
|
||||||
// frames are responded to. This means c.Ping and c.Close will still work as expected.
|
|
||||||
//
|
|
||||||
// This function is idempotent.
|
|
||||||
func (c *Conn) CloseRead(ctx context.Context) context.Context {
|
|
||||||
c.closeReadMu.Lock()
|
|
||||||
ctx2 := c.closeReadCtx
|
|
||||||
if ctx2 != nil {
|
|
||||||
c.closeReadMu.Unlock()
|
|
||||||
return ctx2
|
|
||||||
}
|
|
||||||
ctx, cancel := context.WithCancel(ctx)
|
|
||||||
c.closeReadCtx = ctx
|
|
||||||
c.closeReadDone = make(chan struct{})
|
|
||||||
c.closeReadMu.Unlock()
|
|
||||||
|
|
||||||
go func() {
|
|
||||||
defer close(c.closeReadDone)
|
|
||||||
defer cancel()
|
|
||||||
defer c.close()
|
|
||||||
_, _, err := c.Reader(ctx)
|
|
||||||
if err == nil {
|
|
||||||
c.Close(StatusPolicyViolation, "unexpected data message")
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
return ctx
|
|
||||||
}
|
|
||||||
|
|
||||||
// SetReadLimit sets the max number of bytes to read for a single message.
|
|
||||||
// It applies to the Reader and Read methods.
|
|
||||||
//
|
|
||||||
// By default, the connection has a message read limit of 32768 bytes.
|
|
||||||
//
|
|
||||||
// When the limit is hit, reads return an error wrapping ErrMessageTooBig and
|
|
||||||
// the connection is closed with StatusMessageTooBig.
|
|
||||||
//
|
|
||||||
// Set to -1 to disable.
|
|
||||||
func (c *Conn) SetReadLimit(n int64) {
|
|
||||||
if n >= 0 {
|
|
||||||
// We read one more byte than the limit in case
|
|
||||||
// there is a fin frame that needs to be read.
|
|
||||||
n++
|
|
||||||
}
|
|
||||||
|
|
||||||
c.msgReader.limitReader.limit.Store(n)
|
|
||||||
}
|
|
||||||
|
|
||||||
const defaultReadLimit = 32768
|
|
||||||
|
|
||||||
func newMsgReader(c *Conn) *msgReader {
|
|
||||||
mr := &msgReader{
|
|
||||||
c: c,
|
|
||||||
fin: true,
|
|
||||||
}
|
|
||||||
mr.readFunc = mr.read
|
|
||||||
|
|
||||||
mr.limitReader = newLimitReader(c, mr.readFunc, defaultReadLimit+1)
|
|
||||||
return mr
|
|
||||||
}
|
|
||||||
|
|
||||||
func (mr *msgReader) resetFlate() {
|
|
||||||
if mr.flateContextTakeover() {
|
|
||||||
if mr.dict == nil {
|
|
||||||
mr.dict = &slidingWindow{}
|
|
||||||
}
|
|
||||||
mr.dict.init(32768)
|
|
||||||
}
|
|
||||||
if mr.flateBufio == nil {
|
|
||||||
mr.flateBufio = getBufioReader(mr.readFunc)
|
|
||||||
}
|
|
||||||
|
|
||||||
if mr.flateContextTakeover() {
|
|
||||||
mr.flateReader = getFlateReader(mr.flateBufio, mr.dict.buf)
|
|
||||||
} else {
|
|
||||||
mr.flateReader = getFlateReader(mr.flateBufio, nil)
|
|
||||||
}
|
|
||||||
mr.limitReader.r = mr.flateReader
|
|
||||||
mr.flateTail.Reset(deflateMessageTail)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (mr *msgReader) putFlateReader() {
|
|
||||||
if mr.flateReader != nil {
|
|
||||||
putFlateReader(mr.flateReader)
|
|
||||||
mr.flateReader = nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (mr *msgReader) close() {
|
|
||||||
mr.c.readMu.forceLock()
|
|
||||||
mr.putFlateReader()
|
|
||||||
if mr.dict != nil {
|
|
||||||
mr.dict.close()
|
|
||||||
mr.dict = nil
|
|
||||||
}
|
|
||||||
if mr.flateBufio != nil {
|
|
||||||
putBufioReader(mr.flateBufio)
|
|
||||||
}
|
|
||||||
|
|
||||||
if mr.c.client {
|
|
||||||
putBufioReader(mr.c.br)
|
|
||||||
mr.c.br = nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (mr *msgReader) flateContextTakeover() bool {
|
|
||||||
if mr.c.client {
|
|
||||||
return !mr.c.copts.serverNoContextTakeover
|
|
||||||
}
|
|
||||||
return !mr.c.copts.clientNoContextTakeover
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *Conn) readRSV1Illegal(h header) bool {
|
|
||||||
// If compression is disabled, rsv1 is illegal.
|
|
||||||
if !c.flate() {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
// rsv1 is only allowed on data frames beginning messages.
|
|
||||||
if h.opcode != opText && h.opcode != opBinary {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *Conn) readLoop(ctx context.Context) (header, error) {
|
|
||||||
for {
|
|
||||||
h, err := c.readFrameHeader(ctx)
|
|
||||||
if err != nil {
|
|
||||||
return header{}, err
|
|
||||||
}
|
|
||||||
|
|
||||||
if h.rsv1 && c.readRSV1Illegal(h) || h.rsv2 || h.rsv3 {
|
|
||||||
err := fmt.Errorf("received header with unexpected rsv bits set: %v:%v:%v", h.rsv1, h.rsv2, h.rsv3)
|
|
||||||
c.writeError(StatusProtocolError, err)
|
|
||||||
return header{}, err
|
|
||||||
}
|
|
||||||
|
|
||||||
if !c.client && !h.masked {
|
|
||||||
return header{}, errors.New("received unmasked frame from client")
|
|
||||||
}
|
|
||||||
|
|
||||||
switch h.opcode {
|
|
||||||
case opClose, opPing, opPong:
|
|
||||||
err = c.handleControl(ctx, h)
|
|
||||||
if err != nil {
|
|
||||||
// Pass through CloseErrors when receiving a close frame.
|
|
||||||
if h.opcode == opClose && CloseStatus(err) != -1 {
|
|
||||||
return header{}, err
|
|
||||||
}
|
|
||||||
return header{}, fmt.Errorf("failed to handle control frame %v: %w", h.opcode, err)
|
|
||||||
}
|
|
||||||
case opContinuation, opText, opBinary:
|
|
||||||
return h, nil
|
|
||||||
default:
|
|
||||||
err := fmt.Errorf("received unknown opcode %v", h.opcode)
|
|
||||||
c.writeError(StatusProtocolError, err)
|
|
||||||
return header{}, err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// prepareRead sets the readTimeout context and returns a done function
|
|
||||||
// to be called after the read is done. It also returns an error if the
|
|
||||||
// connection is closed. The reference to the error is used to assign
|
|
||||||
// an error depending on if the connection closed or the context timed
|
|
||||||
// out during use. Typically, the referenced error is a named return
|
|
||||||
// variable of the function calling this method.
|
|
||||||
func (c *Conn) prepareRead(ctx context.Context, err *error) (func(), error) {
|
|
||||||
select {
|
|
||||||
case <-c.closed:
|
|
||||||
return nil, net.ErrClosed
|
|
||||||
default:
|
|
||||||
}
|
|
||||||
c.setupReadTimeout(ctx)
|
|
||||||
|
|
||||||
done := func() {
|
|
||||||
c.clearReadTimeout()
|
|
||||||
select {
|
|
||||||
case <-c.closed:
|
|
||||||
if *err != nil {
|
|
||||||
*err = net.ErrClosed
|
|
||||||
}
|
|
||||||
default:
|
|
||||||
}
|
|
||||||
if *err != nil && ctx.Err() != nil {
|
|
||||||
*err = ctx.Err()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
c.closeStateMu.Lock()
|
|
||||||
closeReceivedErr := c.closeReceivedErr
|
|
||||||
c.closeStateMu.Unlock()
|
|
||||||
if closeReceivedErr != nil {
|
|
||||||
defer done()
|
|
||||||
return nil, closeReceivedErr
|
|
||||||
}
|
|
||||||
|
|
||||||
return done, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *Conn) readFrameHeader(ctx context.Context) (_ header, err error) {
|
|
||||||
readDone, err := c.prepareRead(ctx, &err)
|
|
||||||
if err != nil {
|
|
||||||
return header{}, err
|
|
||||||
}
|
|
||||||
defer readDone()
|
|
||||||
|
|
||||||
h, err := readFrameHeader(c.br, c.readHeaderBuf[:])
|
|
||||||
if err != nil {
|
|
||||||
return header{}, err
|
|
||||||
}
|
|
||||||
|
|
||||||
return h, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *Conn) readFramePayload(ctx context.Context, p []byte) (_ int, err error) {
|
|
||||||
readDone, err := c.prepareRead(ctx, &err)
|
|
||||||
if err != nil {
|
|
||||||
return 0, err
|
|
||||||
}
|
|
||||||
defer readDone()
|
|
||||||
|
|
||||||
n, err := io.ReadFull(c.br, p)
|
|
||||||
if err != nil {
|
|
||||||
return n, fmt.Errorf("failed to read frame payload: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
return n, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *Conn) handleControl(ctx context.Context, h header) (err error) {
|
|
||||||
if h.payloadLength < 0 || h.payloadLength > maxControlPayload {
|
|
||||||
err := fmt.Errorf("received control frame payload with invalid length: %d", h.payloadLength)
|
|
||||||
c.writeError(StatusProtocolError, err)
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
if !h.fin {
|
|
||||||
err := errors.New("received fragmented control frame")
|
|
||||||
c.writeError(StatusProtocolError, err)
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
ctx, cancel := context.WithTimeout(ctx, time.Second*5)
|
|
||||||
defer cancel()
|
|
||||||
|
|
||||||
b := c.readControlBuf[:h.payloadLength]
|
|
||||||
_, err = c.readFramePayload(ctx, b)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
if h.masked {
|
|
||||||
mask(b, h.maskKey)
|
|
||||||
}
|
|
||||||
|
|
||||||
switch h.opcode {
|
|
||||||
case opPing:
|
|
||||||
if c.onPingReceived != nil {
|
|
||||||
if !c.onPingReceived(ctx, b) {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return c.writeControl(ctx, opPong, b)
|
|
||||||
case opPong:
|
|
||||||
if c.onPongReceived != nil {
|
|
||||||
c.onPongReceived(ctx, b)
|
|
||||||
}
|
|
||||||
c.activePingsMu.Lock()
|
|
||||||
pong, ok := c.activePings[string(b)]
|
|
||||||
c.activePingsMu.Unlock()
|
|
||||||
if ok {
|
|
||||||
select {
|
|
||||||
case pong <- struct{}{}:
|
|
||||||
default:
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// opClose
|
|
||||||
|
|
||||||
ce, err := parseClosePayload(b)
|
|
||||||
if err != nil {
|
|
||||||
err = fmt.Errorf("received invalid close payload: %w", err)
|
|
||||||
c.writeError(StatusProtocolError, err)
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
err = fmt.Errorf("received close frame: %w", ce)
|
|
||||||
c.closeStateMu.Lock()
|
|
||||||
c.closeReceivedErr = err
|
|
||||||
closeSent := c.closeSentErr != nil
|
|
||||||
c.closeStateMu.Unlock()
|
|
||||||
|
|
||||||
// Only unlock readMu if this connection is being closed becaue
|
|
||||||
// c.close will try to acquire the readMu lock. We unlock for
|
|
||||||
// writeClose as well because it may also call c.close.
|
|
||||||
if !closeSent {
|
|
||||||
c.readMu.unlock()
|
|
||||||
_ = c.writeClose(ce.Code, ce.Reason)
|
|
||||||
}
|
|
||||||
if !c.casClosing() {
|
|
||||||
c.readMu.unlock()
|
|
||||||
_ = c.close()
|
|
||||||
}
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *Conn) reader(ctx context.Context) (_ MessageType, _ io.Reader, err error) {
|
|
||||||
defer errd.Wrap(&err, "failed to get reader")
|
|
||||||
|
|
||||||
err = c.readMu.lock(ctx)
|
|
||||||
if err != nil {
|
|
||||||
return 0, nil, err
|
|
||||||
}
|
|
||||||
defer c.readMu.unlock()
|
|
||||||
|
|
||||||
if !c.msgReader.fin {
|
|
||||||
return 0, nil, errors.New("previous message not read to completion")
|
|
||||||
}
|
|
||||||
|
|
||||||
h, err := c.readLoop(ctx)
|
|
||||||
if err != nil {
|
|
||||||
return 0, nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
if h.opcode == opContinuation {
|
|
||||||
err := errors.New("received continuation frame without text or binary frame")
|
|
||||||
c.writeError(StatusProtocolError, err)
|
|
||||||
return 0, nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
c.msgReader.reset(ctx, h)
|
|
||||||
|
|
||||||
return MessageType(h.opcode), c.msgReader, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
type msgReader struct {
|
|
||||||
c *Conn
|
|
||||||
|
|
||||||
ctx context.Context
|
|
||||||
flate bool
|
|
||||||
flateReader io.Reader
|
|
||||||
flateBufio *bufio.Reader
|
|
||||||
flateTail strings.Reader
|
|
||||||
limitReader *limitReader
|
|
||||||
dict *slidingWindow
|
|
||||||
|
|
||||||
fin bool
|
|
||||||
payloadLength int64
|
|
||||||
maskKey uint32
|
|
||||||
|
|
||||||
// util.ReaderFunc(mr.Read) to avoid continuous allocations.
|
|
||||||
readFunc util.ReaderFunc
|
|
||||||
}
|
|
||||||
|
|
||||||
func (mr *msgReader) reset(ctx context.Context, h header) {
|
|
||||||
mr.ctx = ctx
|
|
||||||
mr.flate = h.rsv1
|
|
||||||
mr.limitReader.reset(mr.readFunc)
|
|
||||||
|
|
||||||
if mr.flate {
|
|
||||||
mr.resetFlate()
|
|
||||||
}
|
|
||||||
|
|
||||||
mr.setFrame(h)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (mr *msgReader) setFrame(h header) {
|
|
||||||
mr.fin = h.fin
|
|
||||||
mr.payloadLength = h.payloadLength
|
|
||||||
mr.maskKey = h.maskKey
|
|
||||||
}
|
|
||||||
|
|
||||||
func (mr *msgReader) Read(p []byte) (n int, err error) {
|
|
||||||
err = mr.c.readMu.lock(mr.ctx)
|
|
||||||
if err != nil {
|
|
||||||
return 0, fmt.Errorf("failed to read: %w", err)
|
|
||||||
}
|
|
||||||
defer mr.c.readMu.unlock()
|
|
||||||
|
|
||||||
n, err = mr.limitReader.Read(p)
|
|
||||||
if mr.flate && mr.flateContextTakeover() {
|
|
||||||
p = p[:n]
|
|
||||||
mr.dict.write(p)
|
|
||||||
}
|
|
||||||
if errors.Is(err, io.EOF) || errors.Is(err, io.ErrUnexpectedEOF) && mr.fin && mr.flate {
|
|
||||||
mr.putFlateReader()
|
|
||||||
return n, io.EOF
|
|
||||||
}
|
|
||||||
if err != nil {
|
|
||||||
return n, fmt.Errorf("failed to read: %w", err)
|
|
||||||
}
|
|
||||||
return n, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (mr *msgReader) read(p []byte) (int, error) {
|
|
||||||
for {
|
|
||||||
if mr.payloadLength == 0 {
|
|
||||||
if mr.fin {
|
|
||||||
if mr.flate {
|
|
||||||
return mr.flateTail.Read(p)
|
|
||||||
}
|
|
||||||
return 0, io.EOF
|
|
||||||
}
|
|
||||||
|
|
||||||
h, err := mr.c.readLoop(mr.ctx)
|
|
||||||
if err != nil {
|
|
||||||
return 0, err
|
|
||||||
}
|
|
||||||
if h.opcode != opContinuation {
|
|
||||||
err := errors.New("received new data message without finishing the previous message")
|
|
||||||
mr.c.writeError(StatusProtocolError, err)
|
|
||||||
return 0, err
|
|
||||||
}
|
|
||||||
mr.setFrame(h)
|
|
||||||
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
if int64(len(p)) > mr.payloadLength {
|
|
||||||
p = p[:mr.payloadLength]
|
|
||||||
}
|
|
||||||
|
|
||||||
n, err := mr.c.readFramePayload(mr.ctx, p)
|
|
||||||
if err != nil {
|
|
||||||
return n, err
|
|
||||||
}
|
|
||||||
|
|
||||||
mr.payloadLength -= int64(n)
|
|
||||||
|
|
||||||
if !mr.c.client {
|
|
||||||
mr.maskKey = mask(p, mr.maskKey)
|
|
||||||
}
|
|
||||||
|
|
||||||
return n, nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
type limitReader struct {
|
|
||||||
c *Conn
|
|
||||||
r io.Reader
|
|
||||||
limit atomic.Int64
|
|
||||||
n int64
|
|
||||||
}
|
|
||||||
|
|
||||||
func newLimitReader(c *Conn, r io.Reader, limit int64) *limitReader {
|
|
||||||
lr := &limitReader{
|
|
||||||
c: c,
|
|
||||||
}
|
|
||||||
lr.limit.Store(limit)
|
|
||||||
lr.reset(r)
|
|
||||||
return lr
|
|
||||||
}
|
|
||||||
|
|
||||||
func (lr *limitReader) reset(r io.Reader) {
|
|
||||||
lr.n = lr.limit.Load()
|
|
||||||
lr.r = r
|
|
||||||
}
|
|
||||||
|
|
||||||
func (lr *limitReader) Read(p []byte) (int, error) {
|
|
||||||
if lr.n < 0 {
|
|
||||||
return lr.r.Read(p)
|
|
||||||
}
|
|
||||||
|
|
||||||
if lr.n == 0 {
|
|
||||||
reason := fmt.Errorf("read limited at %d bytes", lr.limit.Load())
|
|
||||||
lr.c.writeError(StatusMessageTooBig, reason)
|
|
||||||
return 0, fmt.Errorf("%w: %v", ErrMessageTooBig, reason)
|
|
||||||
}
|
|
||||||
|
|
||||||
if int64(len(p)) > lr.n {
|
|
||||||
p = p[:lr.n]
|
|
||||||
}
|
|
||||||
n, err := lr.r.Read(p)
|
|
||||||
lr.n -= int64(n)
|
|
||||||
if lr.n < 0 {
|
|
||||||
lr.n = 0
|
|
||||||
}
|
|
||||||
return n, err
|
|
||||||
}
|
|
||||||
91
vendor/github.com/coder/websocket/stringer.go
generated
vendored
91
vendor/github.com/coder/websocket/stringer.go
generated
vendored
@@ -1,91 +0,0 @@
|
|||||||
// Code generated by "stringer -type=opcode,MessageType,StatusCode -output=stringer.go"; DO NOT EDIT.
|
|
||||||
|
|
||||||
package websocket
|
|
||||||
|
|
||||||
import "strconv"
|
|
||||||
|
|
||||||
func _() {
|
|
||||||
// An "invalid array index" compiler error signifies that the constant values have changed.
|
|
||||||
// Re-run the stringer command to generate them again.
|
|
||||||
var x [1]struct{}
|
|
||||||
_ = x[opContinuation-0]
|
|
||||||
_ = x[opText-1]
|
|
||||||
_ = x[opBinary-2]
|
|
||||||
_ = x[opClose-8]
|
|
||||||
_ = x[opPing-9]
|
|
||||||
_ = x[opPong-10]
|
|
||||||
}
|
|
||||||
|
|
||||||
const (
|
|
||||||
_opcode_name_0 = "opContinuationopTextopBinary"
|
|
||||||
_opcode_name_1 = "opCloseopPingopPong"
|
|
||||||
)
|
|
||||||
|
|
||||||
var (
|
|
||||||
_opcode_index_0 = [...]uint8{0, 14, 20, 28}
|
|
||||||
_opcode_index_1 = [...]uint8{0, 7, 13, 19}
|
|
||||||
)
|
|
||||||
|
|
||||||
func (i opcode) String() string {
|
|
||||||
switch {
|
|
||||||
case 0 <= i && i <= 2:
|
|
||||||
return _opcode_name_0[_opcode_index_0[i]:_opcode_index_0[i+1]]
|
|
||||||
case 8 <= i && i <= 10:
|
|
||||||
i -= 8
|
|
||||||
return _opcode_name_1[_opcode_index_1[i]:_opcode_index_1[i+1]]
|
|
||||||
default:
|
|
||||||
return "opcode(" + strconv.FormatInt(int64(i), 10) + ")"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
func _() {
|
|
||||||
// An "invalid array index" compiler error signifies that the constant values have changed.
|
|
||||||
// Re-run the stringer command to generate them again.
|
|
||||||
var x [1]struct{}
|
|
||||||
_ = x[MessageText-1]
|
|
||||||
_ = x[MessageBinary-2]
|
|
||||||
}
|
|
||||||
|
|
||||||
const _MessageType_name = "MessageTextMessageBinary"
|
|
||||||
|
|
||||||
var _MessageType_index = [...]uint8{0, 11, 24}
|
|
||||||
|
|
||||||
func (i MessageType) String() string {
|
|
||||||
i -= 1
|
|
||||||
if i < 0 || i >= MessageType(len(_MessageType_index)-1) {
|
|
||||||
return "MessageType(" + strconv.FormatInt(int64(i+1), 10) + ")"
|
|
||||||
}
|
|
||||||
return _MessageType_name[_MessageType_index[i]:_MessageType_index[i+1]]
|
|
||||||
}
|
|
||||||
func _() {
|
|
||||||
// An "invalid array index" compiler error signifies that the constant values have changed.
|
|
||||||
// Re-run the stringer command to generate them again.
|
|
||||||
var x [1]struct{}
|
|
||||||
_ = x[StatusNormalClosure-1000]
|
|
||||||
_ = x[StatusGoingAway-1001]
|
|
||||||
_ = x[StatusProtocolError-1002]
|
|
||||||
_ = x[StatusUnsupportedData-1003]
|
|
||||||
_ = x[statusReserved-1004]
|
|
||||||
_ = x[StatusNoStatusRcvd-1005]
|
|
||||||
_ = x[StatusAbnormalClosure-1006]
|
|
||||||
_ = x[StatusInvalidFramePayloadData-1007]
|
|
||||||
_ = x[StatusPolicyViolation-1008]
|
|
||||||
_ = x[StatusMessageTooBig-1009]
|
|
||||||
_ = x[StatusMandatoryExtension-1010]
|
|
||||||
_ = x[StatusInternalError-1011]
|
|
||||||
_ = x[StatusServiceRestart-1012]
|
|
||||||
_ = x[StatusTryAgainLater-1013]
|
|
||||||
_ = x[StatusBadGateway-1014]
|
|
||||||
_ = x[StatusTLSHandshake-1015]
|
|
||||||
}
|
|
||||||
|
|
||||||
const _StatusCode_name = "StatusNormalClosureStatusGoingAwayStatusProtocolErrorStatusUnsupportedDatastatusReservedStatusNoStatusRcvdStatusAbnormalClosureStatusInvalidFramePayloadDataStatusPolicyViolationStatusMessageTooBigStatusMandatoryExtensionStatusInternalErrorStatusServiceRestartStatusTryAgainLaterStatusBadGatewayStatusTLSHandshake"
|
|
||||||
|
|
||||||
var _StatusCode_index = [...]uint16{0, 19, 34, 53, 74, 88, 106, 127, 156, 177, 196, 220, 239, 259, 278, 294, 312}
|
|
||||||
|
|
||||||
func (i StatusCode) String() string {
|
|
||||||
i -= 1000
|
|
||||||
if i < 0 || i >= StatusCode(len(_StatusCode_index)-1) {
|
|
||||||
return "StatusCode(" + strconv.FormatInt(int64(i+1000), 10) + ")"
|
|
||||||
}
|
|
||||||
return _StatusCode_name[_StatusCode_index[i]:_StatusCode_index[i+1]]
|
|
||||||
}
|
|
||||||
384
vendor/github.com/coder/websocket/write.go
generated
vendored
384
vendor/github.com/coder/websocket/write.go
generated
vendored
@@ -1,384 +0,0 @@
|
|||||||
//go:build !js
|
|
||||||
|
|
||||||
package websocket
|
|
||||||
|
|
||||||
import (
|
|
||||||
"bufio"
|
|
||||||
"compress/flate"
|
|
||||||
"context"
|
|
||||||
"crypto/rand"
|
|
||||||
"encoding/binary"
|
|
||||||
"errors"
|
|
||||||
"fmt"
|
|
||||||
"io"
|
|
||||||
"net"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/coder/websocket/internal/errd"
|
|
||||||
"github.com/coder/websocket/internal/util"
|
|
||||||
)
|
|
||||||
|
|
||||||
// Writer returns a writer bounded by the context that will write
|
|
||||||
// a WebSocket message of type dataType to the connection.
|
|
||||||
//
|
|
||||||
// You must close the writer once you have written the entire message.
|
|
||||||
//
|
|
||||||
// Only one writer can be open at a time, multiple calls will block until the previous writer
|
|
||||||
// is closed.
|
|
||||||
func (c *Conn) Writer(ctx context.Context, typ MessageType) (io.WriteCloser, error) {
|
|
||||||
w, err := c.writer(ctx, typ)
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("failed to get writer: %w", err)
|
|
||||||
}
|
|
||||||
return w, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Write writes a message to the connection.
|
|
||||||
//
|
|
||||||
// See the Writer method if you want to stream a message.
|
|
||||||
//
|
|
||||||
// If compression is disabled or the compression threshold is not met, then it
|
|
||||||
// will write the message in a single frame.
|
|
||||||
func (c *Conn) Write(ctx context.Context, typ MessageType, p []byte) error {
|
|
||||||
_, err := c.write(ctx, typ, p)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("failed to write msg: %w", err)
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
type msgWriter struct {
|
|
||||||
c *Conn
|
|
||||||
|
|
||||||
mu *mu
|
|
||||||
writeMu *mu
|
|
||||||
closed bool
|
|
||||||
|
|
||||||
ctx context.Context
|
|
||||||
opcode opcode
|
|
||||||
flate bool
|
|
||||||
|
|
||||||
trimWriter *trimLastFourBytesWriter
|
|
||||||
flateWriter *flate.Writer
|
|
||||||
}
|
|
||||||
|
|
||||||
func newMsgWriter(c *Conn) *msgWriter {
|
|
||||||
mw := &msgWriter{
|
|
||||||
c: c,
|
|
||||||
mu: newMu(c),
|
|
||||||
writeMu: newMu(c),
|
|
||||||
}
|
|
||||||
return mw
|
|
||||||
}
|
|
||||||
|
|
||||||
func (mw *msgWriter) ensureFlate() {
|
|
||||||
if mw.trimWriter == nil {
|
|
||||||
mw.trimWriter = &trimLastFourBytesWriter{
|
|
||||||
w: util.WriterFunc(mw.write),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if mw.flateWriter == nil {
|
|
||||||
mw.flateWriter = getFlateWriter(mw.trimWriter)
|
|
||||||
}
|
|
||||||
mw.flate = true
|
|
||||||
}
|
|
||||||
|
|
||||||
func (mw *msgWriter) flateContextTakeover() bool {
|
|
||||||
if mw.c.client {
|
|
||||||
return !mw.c.copts.clientNoContextTakeover
|
|
||||||
}
|
|
||||||
return !mw.c.copts.serverNoContextTakeover
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *Conn) writer(ctx context.Context, typ MessageType) (io.WriteCloser, error) {
|
|
||||||
err := c.msgWriter.reset(ctx, typ)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
return c.msgWriter, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *Conn) write(ctx context.Context, typ MessageType, p []byte) (int, error) {
|
|
||||||
mw, err := c.writer(ctx, typ)
|
|
||||||
if err != nil {
|
|
||||||
return 0, err
|
|
||||||
}
|
|
||||||
|
|
||||||
if !c.flate() {
|
|
||||||
defer c.msgWriter.mu.unlock()
|
|
||||||
return c.writeFrame(ctx, true, false, c.msgWriter.opcode, p)
|
|
||||||
}
|
|
||||||
|
|
||||||
n, err := mw.Write(p)
|
|
||||||
if err != nil {
|
|
||||||
return n, err
|
|
||||||
}
|
|
||||||
|
|
||||||
err = mw.Close()
|
|
||||||
return n, err
|
|
||||||
}
|
|
||||||
|
|
||||||
func (mw *msgWriter) reset(ctx context.Context, typ MessageType) error {
|
|
||||||
err := mw.mu.lock(ctx)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
mw.ctx = ctx
|
|
||||||
mw.opcode = opcode(typ)
|
|
||||||
mw.flate = false
|
|
||||||
mw.closed = false
|
|
||||||
|
|
||||||
mw.trimWriter.reset()
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (mw *msgWriter) putFlateWriter() {
|
|
||||||
if mw.flateWriter != nil {
|
|
||||||
putFlateWriter(mw.flateWriter)
|
|
||||||
mw.flateWriter = nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Write writes the given bytes to the WebSocket connection.
|
|
||||||
func (mw *msgWriter) Write(p []byte) (_ int, err error) {
|
|
||||||
err = mw.writeMu.lock(mw.ctx)
|
|
||||||
if err != nil {
|
|
||||||
return 0, fmt.Errorf("failed to write: %w", err)
|
|
||||||
}
|
|
||||||
defer mw.writeMu.unlock()
|
|
||||||
|
|
||||||
if mw.closed {
|
|
||||||
return 0, errors.New("cannot use closed writer")
|
|
||||||
}
|
|
||||||
|
|
||||||
defer func() {
|
|
||||||
if err != nil {
|
|
||||||
err = fmt.Errorf("failed to write: %w", err)
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
|
|
||||||
if mw.c.flate() {
|
|
||||||
// Only enables flate if the length crosses the
|
|
||||||
// threshold on the first frame
|
|
||||||
if mw.opcode != opContinuation && len(p) >= mw.c.flateThreshold {
|
|
||||||
mw.ensureFlate()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if mw.flate {
|
|
||||||
return mw.flateWriter.Write(p)
|
|
||||||
}
|
|
||||||
|
|
||||||
return mw.write(p)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (mw *msgWriter) write(p []byte) (int, error) {
|
|
||||||
n, err := mw.c.writeFrame(mw.ctx, false, mw.flate, mw.opcode, p)
|
|
||||||
if err != nil {
|
|
||||||
return n, fmt.Errorf("failed to write data frame: %w", err)
|
|
||||||
}
|
|
||||||
mw.opcode = opContinuation
|
|
||||||
return n, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Close flushes the frame to the connection.
|
|
||||||
func (mw *msgWriter) Close() (err error) {
|
|
||||||
defer errd.Wrap(&err, "failed to close writer")
|
|
||||||
|
|
||||||
err = mw.writeMu.lock(mw.ctx)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
defer mw.writeMu.unlock()
|
|
||||||
|
|
||||||
if mw.closed {
|
|
||||||
return errors.New("writer already closed")
|
|
||||||
}
|
|
||||||
mw.closed = true
|
|
||||||
|
|
||||||
if mw.flate {
|
|
||||||
err = mw.flateWriter.Flush()
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("failed to flush flate: %w", err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
_, err = mw.c.writeFrame(mw.ctx, true, mw.flate, mw.opcode, nil)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("failed to write fin frame: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if mw.flate && !mw.flateContextTakeover() {
|
|
||||||
mw.putFlateWriter()
|
|
||||||
}
|
|
||||||
mw.mu.unlock()
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (mw *msgWriter) close() {
|
|
||||||
if mw.c.client {
|
|
||||||
mw.c.writeFrameMu.forceLock()
|
|
||||||
putBufioWriter(mw.c.bw)
|
|
||||||
}
|
|
||||||
|
|
||||||
mw.writeMu.forceLock()
|
|
||||||
mw.putFlateWriter()
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *Conn) writeControl(ctx context.Context, opcode opcode, p []byte) error {
|
|
||||||
ctx, cancel := context.WithTimeout(ctx, time.Second*5)
|
|
||||||
defer cancel()
|
|
||||||
|
|
||||||
_, err := c.writeFrame(ctx, true, false, opcode, p)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("failed to write control frame %v: %w", opcode, err)
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// writeFrame handles all writes to the connection.
|
|
||||||
func (c *Conn) writeFrame(ctx context.Context, fin bool, flate bool, opcode opcode, p []byte) (_ int, err error) {
|
|
||||||
err = c.writeFrameMu.lock(ctx)
|
|
||||||
if err != nil {
|
|
||||||
return 0, err
|
|
||||||
}
|
|
||||||
defer c.writeFrameMu.unlock()
|
|
||||||
|
|
||||||
defer func() {
|
|
||||||
if c.isClosed() && opcode == opClose {
|
|
||||||
err = nil
|
|
||||||
}
|
|
||||||
if err != nil {
|
|
||||||
if ctx.Err() != nil {
|
|
||||||
err = ctx.Err()
|
|
||||||
} else if c.isClosed() {
|
|
||||||
err = net.ErrClosed
|
|
||||||
}
|
|
||||||
err = fmt.Errorf("failed to write frame: %w", err)
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
|
|
||||||
c.closeStateMu.Lock()
|
|
||||||
closeSentErr := c.closeSentErr
|
|
||||||
c.closeStateMu.Unlock()
|
|
||||||
if closeSentErr != nil {
|
|
||||||
return 0, net.ErrClosed
|
|
||||||
}
|
|
||||||
|
|
||||||
select {
|
|
||||||
case <-c.closed:
|
|
||||||
return 0, net.ErrClosed
|
|
||||||
default:
|
|
||||||
}
|
|
||||||
c.setupWriteTimeout(ctx)
|
|
||||||
defer c.clearWriteTimeout()
|
|
||||||
|
|
||||||
c.writeHeader.fin = fin
|
|
||||||
c.writeHeader.opcode = opcode
|
|
||||||
c.writeHeader.payloadLength = int64(len(p))
|
|
||||||
|
|
||||||
if c.client {
|
|
||||||
c.writeHeader.masked = true
|
|
||||||
_, err = io.ReadFull(rand.Reader, c.writeHeaderBuf[:4])
|
|
||||||
if err != nil {
|
|
||||||
return 0, fmt.Errorf("failed to generate masking key: %w", err)
|
|
||||||
}
|
|
||||||
c.writeHeader.maskKey = binary.LittleEndian.Uint32(c.writeHeaderBuf[:])
|
|
||||||
}
|
|
||||||
|
|
||||||
c.writeHeader.rsv1 = false
|
|
||||||
if flate && (opcode == opText || opcode == opBinary) {
|
|
||||||
c.writeHeader.rsv1 = true
|
|
||||||
}
|
|
||||||
|
|
||||||
err = writeFrameHeader(c.writeHeader, c.bw, c.writeHeaderBuf[:])
|
|
||||||
if err != nil {
|
|
||||||
return 0, err
|
|
||||||
}
|
|
||||||
|
|
||||||
n, err := c.writeFramePayload(p)
|
|
||||||
if err != nil {
|
|
||||||
return n, err
|
|
||||||
}
|
|
||||||
|
|
||||||
if c.writeHeader.fin {
|
|
||||||
err = c.bw.Flush()
|
|
||||||
if err != nil {
|
|
||||||
return n, fmt.Errorf("failed to flush: %w", err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if opcode == opClose {
|
|
||||||
c.closeStateMu.Lock()
|
|
||||||
c.closeSentErr = fmt.Errorf("sent close frame: %w", net.ErrClosed)
|
|
||||||
closeReceived := c.closeReceivedErr != nil
|
|
||||||
c.closeStateMu.Unlock()
|
|
||||||
|
|
||||||
if closeReceived && !c.casClosing() {
|
|
||||||
c.writeFrameMu.unlock()
|
|
||||||
_ = c.close()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return n, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *Conn) writeFramePayload(p []byte) (n int, err error) {
|
|
||||||
defer errd.Wrap(&err, "failed to write frame payload")
|
|
||||||
|
|
||||||
if !c.writeHeader.masked {
|
|
||||||
return c.bw.Write(p)
|
|
||||||
}
|
|
||||||
|
|
||||||
maskKey := c.writeHeader.maskKey
|
|
||||||
for len(p) > 0 {
|
|
||||||
// If the buffer is full, we need to flush.
|
|
||||||
if c.bw.Available() == 0 {
|
|
||||||
err = c.bw.Flush()
|
|
||||||
if err != nil {
|
|
||||||
return n, err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Start of next write in the buffer.
|
|
||||||
i := c.bw.Buffered()
|
|
||||||
|
|
||||||
j := min(len(p), c.bw.Available())
|
|
||||||
|
|
||||||
_, err := c.bw.Write(p[:j])
|
|
||||||
if err != nil {
|
|
||||||
return n, err
|
|
||||||
}
|
|
||||||
|
|
||||||
maskKey = mask(c.writeBuf[i:c.bw.Buffered()], maskKey)
|
|
||||||
|
|
||||||
p = p[j:]
|
|
||||||
n += j
|
|
||||||
}
|
|
||||||
|
|
||||||
return n, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// extractBufioWriterBuf grabs the []byte backing a *bufio.Writer
|
|
||||||
// and returns it.
|
|
||||||
func extractBufioWriterBuf(bw *bufio.Writer, w io.Writer) []byte {
|
|
||||||
var writeBuf []byte
|
|
||||||
bw.Reset(util.WriterFunc(func(p2 []byte) (int, error) {
|
|
||||||
writeBuf = p2[:cap(p2)]
|
|
||||||
return len(p2), nil
|
|
||||||
}))
|
|
||||||
|
|
||||||
bw.WriteByte(0)
|
|
||||||
bw.Flush()
|
|
||||||
|
|
||||||
bw.Reset(w)
|
|
||||||
|
|
||||||
return writeBuf
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *Conn) writeError(code StatusCode, err error) {
|
|
||||||
c.writeClose(code, err.Error())
|
|
||||||
}
|
|
||||||
598
vendor/github.com/coder/websocket/ws_js.go
generated
vendored
598
vendor/github.com/coder/websocket/ws_js.go
generated
vendored
@@ -1,598 +0,0 @@
|
|||||||
package websocket // import "github.com/coder/websocket"
|
|
||||||
|
|
||||||
import (
|
|
||||||
"bytes"
|
|
||||||
"context"
|
|
||||||
"errors"
|
|
||||||
"fmt"
|
|
||||||
"io"
|
|
||||||
"net"
|
|
||||||
"net/http"
|
|
||||||
"reflect"
|
|
||||||
"runtime"
|
|
||||||
"strings"
|
|
||||||
"sync"
|
|
||||||
"sync/atomic"
|
|
||||||
"syscall/js"
|
|
||||||
|
|
||||||
"github.com/coder/websocket/internal/bpool"
|
|
||||||
"github.com/coder/websocket/internal/wsjs"
|
|
||||||
)
|
|
||||||
|
|
||||||
// opcode represents a WebSocket opcode.
|
|
||||||
type opcode int
|
|
||||||
|
|
||||||
// https://tools.ietf.org/html/rfc6455#section-11.8.
|
|
||||||
const (
|
|
||||||
opContinuation opcode = iota
|
|
||||||
opText
|
|
||||||
opBinary
|
|
||||||
// 3 - 7 are reserved for further non-control frames.
|
|
||||||
_
|
|
||||||
_
|
|
||||||
_
|
|
||||||
_
|
|
||||||
_
|
|
||||||
opClose
|
|
||||||
opPing
|
|
||||||
opPong
|
|
||||||
// 11-16 are reserved for further control frames.
|
|
||||||
)
|
|
||||||
|
|
||||||
// Conn provides a wrapper around the browser WebSocket API.
|
|
||||||
type Conn struct {
|
|
||||||
noCopy noCopy
|
|
||||||
ws wsjs.WebSocket
|
|
||||||
|
|
||||||
// read limit for a message in bytes.
|
|
||||||
msgReadLimit atomic.Int64
|
|
||||||
|
|
||||||
closeReadMu sync.Mutex
|
|
||||||
closeReadCtx context.Context
|
|
||||||
|
|
||||||
closingMu sync.Mutex
|
|
||||||
closeOnce sync.Once
|
|
||||||
closed chan struct{}
|
|
||||||
closeErrOnce sync.Once
|
|
||||||
closeErr error
|
|
||||||
closeWasClean bool
|
|
||||||
|
|
||||||
releaseOnClose func()
|
|
||||||
releaseOnError func()
|
|
||||||
releaseOnMessage func()
|
|
||||||
|
|
||||||
readSignal chan struct{}
|
|
||||||
readBufMu sync.Mutex
|
|
||||||
readBuf []wsjs.MessageEvent
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *Conn) close(err error, wasClean bool) {
|
|
||||||
c.closeOnce.Do(func() {
|
|
||||||
runtime.SetFinalizer(c, nil)
|
|
||||||
|
|
||||||
if !wasClean {
|
|
||||||
err = fmt.Errorf("unclean connection close: %w", err)
|
|
||||||
}
|
|
||||||
c.setCloseErr(err)
|
|
||||||
c.closeWasClean = wasClean
|
|
||||||
close(c.closed)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *Conn) init() {
|
|
||||||
c.closed = make(chan struct{})
|
|
||||||
c.readSignal = make(chan struct{}, 1)
|
|
||||||
|
|
||||||
c.msgReadLimit.Store(32768)
|
|
||||||
|
|
||||||
c.releaseOnClose = c.ws.OnClose(func(e wsjs.CloseEvent) {
|
|
||||||
err := CloseError{
|
|
||||||
Code: StatusCode(e.Code),
|
|
||||||
Reason: e.Reason,
|
|
||||||
}
|
|
||||||
// We do not know if we sent or received this close as
|
|
||||||
// its possible the browser triggered it without us
|
|
||||||
// explicitly sending it.
|
|
||||||
c.close(err, e.WasClean)
|
|
||||||
|
|
||||||
c.releaseOnClose()
|
|
||||||
c.releaseOnError()
|
|
||||||
c.releaseOnMessage()
|
|
||||||
})
|
|
||||||
|
|
||||||
c.releaseOnError = c.ws.OnError(func(v js.Value) {
|
|
||||||
c.setCloseErr(errors.New(v.Get("message").String()))
|
|
||||||
c.closeWithInternal()
|
|
||||||
})
|
|
||||||
|
|
||||||
c.releaseOnMessage = c.ws.OnMessage(func(e wsjs.MessageEvent) {
|
|
||||||
c.readBufMu.Lock()
|
|
||||||
defer c.readBufMu.Unlock()
|
|
||||||
|
|
||||||
c.readBuf = append(c.readBuf, e)
|
|
||||||
|
|
||||||
// Lets the read goroutine know there is definitely something in readBuf.
|
|
||||||
select {
|
|
||||||
case c.readSignal <- struct{}{}:
|
|
||||||
default:
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
runtime.SetFinalizer(c, func(c *Conn) {
|
|
||||||
c.setCloseErr(errors.New("connection garbage collected"))
|
|
||||||
c.closeWithInternal()
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *Conn) closeWithInternal() {
|
|
||||||
c.Close(StatusInternalError, "something went wrong")
|
|
||||||
}
|
|
||||||
|
|
||||||
// Read attempts to read a message from the connection.
|
|
||||||
// The maximum time spent waiting is bounded by the context.
|
|
||||||
func (c *Conn) Read(ctx context.Context) (MessageType, []byte, error) {
|
|
||||||
c.closeReadMu.Lock()
|
|
||||||
closedRead := c.closeReadCtx != nil
|
|
||||||
c.closeReadMu.Unlock()
|
|
||||||
if closedRead {
|
|
||||||
return 0, nil, errors.New("WebSocket connection read closed")
|
|
||||||
}
|
|
||||||
|
|
||||||
typ, p, err := c.read(ctx)
|
|
||||||
if err != nil {
|
|
||||||
return 0, nil, fmt.Errorf("failed to read: %w", err)
|
|
||||||
}
|
|
||||||
readLimit := c.msgReadLimit.Load()
|
|
||||||
if readLimit >= 0 && int64(len(p)) > readLimit {
|
|
||||||
reason := fmt.Errorf("read limited at %d bytes", c.msgReadLimit.Load())
|
|
||||||
c.Close(StatusMessageTooBig, reason.Error())
|
|
||||||
return 0, nil, fmt.Errorf("%w: %v", ErrMessageTooBig, reason)
|
|
||||||
}
|
|
||||||
return typ, p, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *Conn) read(ctx context.Context) (MessageType, []byte, error) {
|
|
||||||
select {
|
|
||||||
case <-ctx.Done():
|
|
||||||
c.Close(StatusPolicyViolation, "read timed out")
|
|
||||||
return 0, nil, ctx.Err()
|
|
||||||
case <-c.readSignal:
|
|
||||||
case <-c.closed:
|
|
||||||
return 0, nil, net.ErrClosed
|
|
||||||
}
|
|
||||||
|
|
||||||
c.readBufMu.Lock()
|
|
||||||
defer c.readBufMu.Unlock()
|
|
||||||
|
|
||||||
me := c.readBuf[0]
|
|
||||||
// We copy the messages forward and decrease the size
|
|
||||||
// of the slice to avoid reallocating.
|
|
||||||
copy(c.readBuf, c.readBuf[1:])
|
|
||||||
c.readBuf = c.readBuf[:len(c.readBuf)-1]
|
|
||||||
|
|
||||||
if len(c.readBuf) > 0 {
|
|
||||||
// Next time we read, we'll grab the message.
|
|
||||||
select {
|
|
||||||
case c.readSignal <- struct{}{}:
|
|
||||||
default:
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
switch p := me.Data.(type) {
|
|
||||||
case string:
|
|
||||||
return MessageText, []byte(p), nil
|
|
||||||
case []byte:
|
|
||||||
return MessageBinary, p, nil
|
|
||||||
default:
|
|
||||||
panic("websocket: unexpected data type from wsjs OnMessage: " + reflect.TypeOf(me.Data).String())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Ping is mocked out for Wasm.
|
|
||||||
func (c *Conn) Ping(ctx context.Context) error {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Write writes a message of the given type to the connection.
|
|
||||||
// Always non blocking.
|
|
||||||
func (c *Conn) Write(ctx context.Context, typ MessageType, p []byte) error {
|
|
||||||
err := c.write(typ, p)
|
|
||||||
if err != nil {
|
|
||||||
// Have to ensure the WebSocket is closed after a write error
|
|
||||||
// to match the Go API. It can only error if the message type
|
|
||||||
// is unexpected or the passed bytes contain invalid UTF-8 for
|
|
||||||
// MessageText.
|
|
||||||
err := fmt.Errorf("failed to write: %w", err)
|
|
||||||
c.setCloseErr(err)
|
|
||||||
c.closeWithInternal()
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *Conn) write(typ MessageType, p []byte) error {
|
|
||||||
if c.isClosed() {
|
|
||||||
return net.ErrClosed
|
|
||||||
}
|
|
||||||
switch typ {
|
|
||||||
case MessageBinary:
|
|
||||||
return c.ws.SendBytes(p)
|
|
||||||
case MessageText:
|
|
||||||
return c.ws.SendText(string(p))
|
|
||||||
default:
|
|
||||||
return fmt.Errorf("unexpected message type: %v", typ)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Close closes the WebSocket with the given code and reason.
|
|
||||||
// It will wait until the peer responds with a close frame
|
|
||||||
// or the connection is closed.
|
|
||||||
// It thus performs the full WebSocket close handshake.
|
|
||||||
func (c *Conn) Close(code StatusCode, reason string) error {
|
|
||||||
err := c.exportedClose(code, reason)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("failed to close WebSocket: %w", err)
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// CloseNow closes the WebSocket connection without attempting a close handshake.
|
|
||||||
// Use when you do not want the overhead of the close handshake.
|
|
||||||
//
|
|
||||||
// note: No different from Close(StatusGoingAway, "") in WASM as there is no way to close
|
|
||||||
// a WebSocket without the close handshake.
|
|
||||||
func (c *Conn) CloseNow() error {
|
|
||||||
return c.Close(StatusGoingAway, "")
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *Conn) exportedClose(code StatusCode, reason string) error {
|
|
||||||
c.closingMu.Lock()
|
|
||||||
defer c.closingMu.Unlock()
|
|
||||||
|
|
||||||
if c.isClosed() {
|
|
||||||
return net.ErrClosed
|
|
||||||
}
|
|
||||||
|
|
||||||
ce := fmt.Errorf("sent close: %w", CloseError{
|
|
||||||
Code: code,
|
|
||||||
Reason: reason,
|
|
||||||
})
|
|
||||||
|
|
||||||
c.setCloseErr(ce)
|
|
||||||
err := c.ws.Close(int(code), reason)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
<-c.closed
|
|
||||||
if !c.closeWasClean {
|
|
||||||
return c.closeErr
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Subprotocol returns the negotiated subprotocol.
|
|
||||||
// An empty string means the default protocol.
|
|
||||||
func (c *Conn) Subprotocol() string {
|
|
||||||
return c.ws.Subprotocol()
|
|
||||||
}
|
|
||||||
|
|
||||||
// DialOptions represents the options available to pass to Dial.
|
|
||||||
type DialOptions struct {
|
|
||||||
// Subprotocols lists the subprotocols to negotiate with the server.
|
|
||||||
Subprotocols []string
|
|
||||||
}
|
|
||||||
|
|
||||||
// Dial creates a new WebSocket connection to the given url with the given options.
|
|
||||||
// The passed context bounds the maximum time spent waiting for the connection to open.
|
|
||||||
// The returned *http.Response is always nil or a mock. It's only in the signature
|
|
||||||
// to match the core API.
|
|
||||||
func Dial(ctx context.Context, url string, opts *DialOptions) (*Conn, *http.Response, error) {
|
|
||||||
c, resp, err := dial(ctx, url, opts)
|
|
||||||
if err != nil {
|
|
||||||
return nil, nil, fmt.Errorf("failed to WebSocket dial %q: %w", url, err)
|
|
||||||
}
|
|
||||||
return c, resp, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func dial(ctx context.Context, url string, opts *DialOptions) (*Conn, *http.Response, error) {
|
|
||||||
if opts == nil {
|
|
||||||
opts = &DialOptions{}
|
|
||||||
}
|
|
||||||
|
|
||||||
url = strings.Replace(url, "http://", "ws://", 1)
|
|
||||||
url = strings.Replace(url, "https://", "wss://", 1)
|
|
||||||
|
|
||||||
ws, err := wsjs.New(url, opts.Subprotocols)
|
|
||||||
if err != nil {
|
|
||||||
return nil, nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
c := &Conn{
|
|
||||||
ws: ws,
|
|
||||||
}
|
|
||||||
c.init()
|
|
||||||
|
|
||||||
opench := make(chan struct{})
|
|
||||||
releaseOpen := ws.OnOpen(func(e js.Value) {
|
|
||||||
close(opench)
|
|
||||||
})
|
|
||||||
defer releaseOpen()
|
|
||||||
|
|
||||||
select {
|
|
||||||
case <-ctx.Done():
|
|
||||||
c.Close(StatusPolicyViolation, "dial timed out")
|
|
||||||
return nil, nil, ctx.Err()
|
|
||||||
case <-opench:
|
|
||||||
return c, &http.Response{
|
|
||||||
StatusCode: http.StatusSwitchingProtocols,
|
|
||||||
}, nil
|
|
||||||
case <-c.closed:
|
|
||||||
return nil, nil, net.ErrClosed
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Reader attempts to read a message from the connection.
|
|
||||||
// The maximum time spent waiting is bounded by the context.
|
|
||||||
func (c *Conn) Reader(ctx context.Context) (MessageType, io.Reader, error) {
|
|
||||||
typ, p, err := c.Read(ctx)
|
|
||||||
if err != nil {
|
|
||||||
return 0, nil, err
|
|
||||||
}
|
|
||||||
return typ, bytes.NewReader(p), nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Writer returns a writer to write a WebSocket data message to the connection.
|
|
||||||
// It buffers the entire message in memory and then sends it when the writer
|
|
||||||
// is closed.
|
|
||||||
func (c *Conn) Writer(ctx context.Context, typ MessageType) (io.WriteCloser, error) {
|
|
||||||
return &writer{
|
|
||||||
c: c,
|
|
||||||
ctx: ctx,
|
|
||||||
typ: typ,
|
|
||||||
b: bpool.Get(),
|
|
||||||
}, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
type writer struct {
|
|
||||||
closed bool
|
|
||||||
|
|
||||||
c *Conn
|
|
||||||
ctx context.Context
|
|
||||||
typ MessageType
|
|
||||||
|
|
||||||
b *bytes.Buffer
|
|
||||||
}
|
|
||||||
|
|
||||||
func (w *writer) Write(p []byte) (int, error) {
|
|
||||||
if w.closed {
|
|
||||||
return 0, errors.New("cannot write to closed writer")
|
|
||||||
}
|
|
||||||
n, err := w.b.Write(p)
|
|
||||||
if err != nil {
|
|
||||||
return n, fmt.Errorf("failed to write message: %w", err)
|
|
||||||
}
|
|
||||||
return n, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (w *writer) Close() error {
|
|
||||||
if w.closed {
|
|
||||||
return errors.New("cannot close closed writer")
|
|
||||||
}
|
|
||||||
w.closed = true
|
|
||||||
defer bpool.Put(w.b)
|
|
||||||
|
|
||||||
err := w.c.Write(w.ctx, w.typ, w.b.Bytes())
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("failed to close writer: %w", err)
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// CloseRead implements *Conn.CloseRead for wasm.
|
|
||||||
func (c *Conn) CloseRead(ctx context.Context) context.Context {
|
|
||||||
c.closeReadMu.Lock()
|
|
||||||
ctx2 := c.closeReadCtx
|
|
||||||
if ctx2 != nil {
|
|
||||||
c.closeReadMu.Unlock()
|
|
||||||
return ctx2
|
|
||||||
}
|
|
||||||
ctx, cancel := context.WithCancel(ctx)
|
|
||||||
c.closeReadCtx = ctx
|
|
||||||
c.closeReadMu.Unlock()
|
|
||||||
|
|
||||||
go func() {
|
|
||||||
defer cancel()
|
|
||||||
defer c.CloseNow()
|
|
||||||
_, _, err := c.read(ctx)
|
|
||||||
if err != nil {
|
|
||||||
c.Close(StatusPolicyViolation, "unexpected data message")
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
return ctx
|
|
||||||
}
|
|
||||||
|
|
||||||
// SetReadLimit implements *Conn.SetReadLimit for wasm.
|
|
||||||
func (c *Conn) SetReadLimit(n int64) {
|
|
||||||
c.msgReadLimit.Store(n)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *Conn) setCloseErr(err error) {
|
|
||||||
c.closeErrOnce.Do(func() {
|
|
||||||
c.closeErr = fmt.Errorf("WebSocket closed: %w", err)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *Conn) isClosed() bool {
|
|
||||||
select {
|
|
||||||
case <-c.closed:
|
|
||||||
return true
|
|
||||||
default:
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// AcceptOptions represents Accept's options.
|
|
||||||
type AcceptOptions struct {
|
|
||||||
Subprotocols []string
|
|
||||||
InsecureSkipVerify bool
|
|
||||||
OriginPatterns []string
|
|
||||||
CompressionMode CompressionMode
|
|
||||||
CompressionThreshold int
|
|
||||||
}
|
|
||||||
|
|
||||||
// Accept is stubbed out for Wasm.
|
|
||||||
func Accept(w http.ResponseWriter, r *http.Request, opts *AcceptOptions) (*Conn, error) {
|
|
||||||
return nil, errors.New("unimplemented")
|
|
||||||
}
|
|
||||||
|
|
||||||
// StatusCode represents a WebSocket status code.
|
|
||||||
// https://tools.ietf.org/html/rfc6455#section-7.4
|
|
||||||
type StatusCode int
|
|
||||||
|
|
||||||
// https://www.iana.org/assignments/websocket/websocket.xhtml#close-code-number
|
|
||||||
//
|
|
||||||
// These are only the status codes defined by the protocol.
|
|
||||||
//
|
|
||||||
// You can define custom codes in the 3000-4999 range.
|
|
||||||
// The 3000-3999 range is reserved for use by libraries, frameworks and applications.
|
|
||||||
// The 4000-4999 range is reserved for private use.
|
|
||||||
const (
|
|
||||||
StatusNormalClosure StatusCode = 1000
|
|
||||||
StatusGoingAway StatusCode = 1001
|
|
||||||
StatusProtocolError StatusCode = 1002
|
|
||||||
StatusUnsupportedData StatusCode = 1003
|
|
||||||
|
|
||||||
// 1004 is reserved and so unexported.
|
|
||||||
statusReserved StatusCode = 1004
|
|
||||||
|
|
||||||
// StatusNoStatusRcvd cannot be sent in a close message.
|
|
||||||
// It is reserved for when a close message is received without
|
|
||||||
// a status code.
|
|
||||||
StatusNoStatusRcvd StatusCode = 1005
|
|
||||||
|
|
||||||
// StatusAbnormalClosure is exported for use only with Wasm.
|
|
||||||
// In non Wasm Go, the returned error will indicate whether the
|
|
||||||
// connection was closed abnormally.
|
|
||||||
StatusAbnormalClosure StatusCode = 1006
|
|
||||||
|
|
||||||
StatusInvalidFramePayloadData StatusCode = 1007
|
|
||||||
StatusPolicyViolation StatusCode = 1008
|
|
||||||
StatusMessageTooBig StatusCode = 1009
|
|
||||||
StatusMandatoryExtension StatusCode = 1010
|
|
||||||
StatusInternalError StatusCode = 1011
|
|
||||||
StatusServiceRestart StatusCode = 1012
|
|
||||||
StatusTryAgainLater StatusCode = 1013
|
|
||||||
StatusBadGateway StatusCode = 1014
|
|
||||||
|
|
||||||
// StatusTLSHandshake is only exported for use with Wasm.
|
|
||||||
// In non Wasm Go, the returned error will indicate whether there was
|
|
||||||
// a TLS handshake failure.
|
|
||||||
StatusTLSHandshake StatusCode = 1015
|
|
||||||
)
|
|
||||||
|
|
||||||
// CloseError is returned when the connection is closed with a status and reason.
|
|
||||||
//
|
|
||||||
// Use Go 1.13's errors.As to check for this error.
|
|
||||||
// Also see the CloseStatus helper.
|
|
||||||
type CloseError struct {
|
|
||||||
Code StatusCode
|
|
||||||
Reason string
|
|
||||||
}
|
|
||||||
|
|
||||||
func (ce CloseError) Error() string {
|
|
||||||
return fmt.Sprintf("status = %v and reason = %q", ce.Code, ce.Reason)
|
|
||||||
}
|
|
||||||
|
|
||||||
// CloseStatus is a convenience wrapper around Go 1.13's errors.As to grab
|
|
||||||
// the status code from a CloseError.
|
|
||||||
//
|
|
||||||
// -1 will be returned if the passed error is nil or not a CloseError.
|
|
||||||
func CloseStatus(err error) StatusCode {
|
|
||||||
var ce CloseError
|
|
||||||
if errors.As(err, &ce) {
|
|
||||||
return ce.Code
|
|
||||||
}
|
|
||||||
return -1
|
|
||||||
}
|
|
||||||
|
|
||||||
// CompressionMode represents the modes available to the deflate extension.
|
|
||||||
// See https://tools.ietf.org/html/rfc7692
|
|
||||||
// Works in all browsers except Safari which does not implement the deflate extension.
|
|
||||||
type CompressionMode int
|
|
||||||
|
|
||||||
const (
|
|
||||||
// CompressionNoContextTakeover grabs a new flate.Reader and flate.Writer as needed
|
|
||||||
// for every message. This applies to both server and client side.
|
|
||||||
//
|
|
||||||
// This means less efficient compression as the sliding window from previous messages
|
|
||||||
// will not be used but the memory overhead will be lower if the connections
|
|
||||||
// are long lived and seldom used.
|
|
||||||
//
|
|
||||||
// The message will only be compressed if greater than 512 bytes.
|
|
||||||
CompressionNoContextTakeover CompressionMode = iota
|
|
||||||
|
|
||||||
// CompressionContextTakeover uses a flate.Reader and flate.Writer per connection.
|
|
||||||
// This enables reusing the sliding window from previous messages.
|
|
||||||
// As most WebSocket protocols are repetitive, this can be very efficient.
|
|
||||||
// It carries an overhead of 8 kB for every connection compared to CompressionNoContextTakeover.
|
|
||||||
//
|
|
||||||
// If the peer negotiates NoContextTakeover on the client or server side, it will be
|
|
||||||
// used instead as this is required by the RFC.
|
|
||||||
CompressionContextTakeover
|
|
||||||
|
|
||||||
// CompressionDisabled disables the deflate extension.
|
|
||||||
//
|
|
||||||
// Use this if you are using a predominantly binary protocol with very
|
|
||||||
// little duplication in between messages or CPU and memory are more
|
|
||||||
// important than bandwidth.
|
|
||||||
CompressionDisabled
|
|
||||||
)
|
|
||||||
|
|
||||||
// MessageType represents the type of a WebSocket message.
|
|
||||||
// See https://tools.ietf.org/html/rfc6455#section-5.6
|
|
||||||
type MessageType int
|
|
||||||
|
|
||||||
// MessageType constants.
|
|
||||||
const (
|
|
||||||
// MessageText is for UTF-8 encoded text messages like JSON.
|
|
||||||
MessageText MessageType = iota + 1
|
|
||||||
// MessageBinary is for binary messages like protobufs.
|
|
||||||
MessageBinary
|
|
||||||
)
|
|
||||||
|
|
||||||
type mu struct {
|
|
||||||
c *Conn
|
|
||||||
ch chan struct{}
|
|
||||||
}
|
|
||||||
|
|
||||||
func newMu(c *Conn) *mu {
|
|
||||||
return &mu{
|
|
||||||
c: c,
|
|
||||||
ch: make(chan struct{}, 1),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *mu) forceLock() {
|
|
||||||
m.ch <- struct{}{}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *mu) tryLock() bool {
|
|
||||||
select {
|
|
||||||
case m.ch <- struct{}{}:
|
|
||||||
return true
|
|
||||||
default:
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *mu) unlock() {
|
|
||||||
select {
|
|
||||||
case <-m.ch:
|
|
||||||
default:
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
type noCopy struct{}
|
|
||||||
|
|
||||||
func (*noCopy) Lock() {}
|
|
||||||
68
vendor/github.com/coder/websocket/wsjson/wsjson.go
generated
vendored
68
vendor/github.com/coder/websocket/wsjson/wsjson.go
generated
vendored
@@ -1,68 +0,0 @@
|
|||||||
// Package wsjson provides helpers for reading and writing JSON messages.
|
|
||||||
package wsjson // import "github.com/coder/websocket/wsjson"
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"encoding/json"
|
|
||||||
"fmt"
|
|
||||||
|
|
||||||
"github.com/coder/websocket"
|
|
||||||
"github.com/coder/websocket/internal/bpool"
|
|
||||||
"github.com/coder/websocket/internal/errd"
|
|
||||||
"github.com/coder/websocket/internal/util"
|
|
||||||
)
|
|
||||||
|
|
||||||
// Read reads a JSON message from c into v.
|
|
||||||
// It will reuse buffers in between calls to avoid allocations.
|
|
||||||
func Read(ctx context.Context, c *websocket.Conn, v any) error {
|
|
||||||
return read(ctx, c, v)
|
|
||||||
}
|
|
||||||
|
|
||||||
func read(ctx context.Context, c *websocket.Conn, v any) (err error) {
|
|
||||||
defer errd.Wrap(&err, "failed to read JSON message")
|
|
||||||
|
|
||||||
_, r, err := c.Reader(ctx)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
b := bpool.Get()
|
|
||||||
defer bpool.Put(b)
|
|
||||||
|
|
||||||
_, err = b.ReadFrom(r)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
err = json.Unmarshal(b.Bytes(), v)
|
|
||||||
if err != nil {
|
|
||||||
c.Close(websocket.StatusInvalidFramePayloadData, "failed to unmarshal JSON")
|
|
||||||
return fmt.Errorf("failed to unmarshal JSON: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Write writes the JSON message v to c.
|
|
||||||
// It will reuse buffers in between calls to avoid allocations.
|
|
||||||
func Write(ctx context.Context, c *websocket.Conn, v any) error {
|
|
||||||
return write(ctx, c, v)
|
|
||||||
}
|
|
||||||
|
|
||||||
func write(ctx context.Context, c *websocket.Conn, v any) (err error) {
|
|
||||||
defer errd.Wrap(&err, "failed to write JSON message")
|
|
||||||
|
|
||||||
// json.Marshal cannot reuse buffers between calls as it has to return
|
|
||||||
// a copy of the byte slice but Encoder does as it directly writes to w.
|
|
||||||
err = json.NewEncoder(util.WriterFunc(func(p []byte) (int, error) {
|
|
||||||
err := c.Write(ctx, websocket.MessageText, p)
|
|
||||||
if err != nil {
|
|
||||||
return 0, err
|
|
||||||
}
|
|
||||||
return len(p), nil
|
|
||||||
})).Encode(v)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("failed to marshal JSON: %w", err)
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
1
vendor/github.com/danieljoos/wincred/.gitattributes
generated
vendored
1
vendor/github.com/danieljoos/wincred/.gitattributes
generated
vendored
@@ -1 +0,0 @@
|
|||||||
*.go text eol=lf
|
|
||||||
25
vendor/github.com/danieljoos/wincred/.gitignore
generated
vendored
25
vendor/github.com/danieljoos/wincred/.gitignore
generated
vendored
@@ -1,25 +0,0 @@
|
|||||||
# Compiled Object files, Static and Dynamic libs (Shared Objects)
|
|
||||||
*.o
|
|
||||||
*.a
|
|
||||||
*.so
|
|
||||||
|
|
||||||
# Folders
|
|
||||||
_obj
|
|
||||||
_test
|
|
||||||
|
|
||||||
# Architecture specific extensions/prefixes
|
|
||||||
*.[568vq]
|
|
||||||
[568vq].out
|
|
||||||
|
|
||||||
*.cgo1.go
|
|
||||||
*.cgo2.c
|
|
||||||
_cgo_defun.c
|
|
||||||
_cgo_gotypes.go
|
|
||||||
_cgo_export.*
|
|
||||||
|
|
||||||
_testmain.go
|
|
||||||
|
|
||||||
*.exe
|
|
||||||
*.test
|
|
||||||
|
|
||||||
coverage.txt
|
|
||||||
21
vendor/github.com/danieljoos/wincred/LICENSE
generated
vendored
21
vendor/github.com/danieljoos/wincred/LICENSE
generated
vendored
@@ -1,21 +0,0 @@
|
|||||||
The MIT License (MIT)
|
|
||||||
|
|
||||||
Copyright (c) 2014 Daniel Joos
|
|
||||||
|
|
||||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
||||||
of this software and associated documentation files (the "Software"), to deal
|
|
||||||
in the Software without restriction, including without limitation the rights
|
|
||||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
||||||
copies of the Software, and to permit persons to whom the Software is
|
|
||||||
furnished to do so, subject to the following conditions:
|
|
||||||
|
|
||||||
The above copyright notice and this permission notice shall be included in all
|
|
||||||
copies or substantial portions of the Software.
|
|
||||||
|
|
||||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
||||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
||||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
||||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
||||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
||||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
||||||
SOFTWARE.
|
|
||||||
145
vendor/github.com/danieljoos/wincred/README.md
generated
vendored
145
vendor/github.com/danieljoos/wincred/README.md
generated
vendored
@@ -1,145 +0,0 @@
|
|||||||
wincred
|
|
||||||
=======
|
|
||||||
|
|
||||||
Go wrapper around the Windows Credential Manager API functions.
|
|
||||||
|
|
||||||
[](https://github.com/danieljoos/wincred/releases/latest)
|
|
||||||
[](https://github.com/danieljoos/wincred/actions?query=workflow%3Atest)
|
|
||||||
[](https://goreportcard.com/report/github.com/danieljoos/wincred)
|
|
||||||
[](https://codecov.io/gh/danieljoos/wincred)
|
|
||||||
[](https://pkg.go.dev/github.com/danieljoos/wincred)
|
|
||||||
|
|
||||||
Installation
|
|
||||||
------------
|
|
||||||
|
|
||||||
```Go
|
|
||||||
go get github.com/danieljoos/wincred
|
|
||||||
```
|
|
||||||
|
|
||||||
|
|
||||||
Usage
|
|
||||||
-----
|
|
||||||
|
|
||||||
See the following examples:
|
|
||||||
|
|
||||||
### Create and store a new generic credential object
|
|
||||||
```Go
|
|
||||||
package main
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"github.com/danieljoos/wincred"
|
|
||||||
)
|
|
||||||
|
|
||||||
func main() {
|
|
||||||
cred := wincred.NewGenericCredential("myGoApplication")
|
|
||||||
cred.CredentialBlob = []byte("my secret")
|
|
||||||
err := cred.Write()
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
fmt.Println(err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### Retrieve a credential object
|
|
||||||
```Go
|
|
||||||
package main
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"github.com/danieljoos/wincred"
|
|
||||||
)
|
|
||||||
|
|
||||||
func main() {
|
|
||||||
cred, err := wincred.GetGenericCredential("myGoApplication")
|
|
||||||
if err == nil {
|
|
||||||
fmt.Println(string(cred.CredentialBlob))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### Remove a credential object
|
|
||||||
```Go
|
|
||||||
package main
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"github.com/danieljoos/wincred"
|
|
||||||
)
|
|
||||||
|
|
||||||
func main() {
|
|
||||||
cred, err := wincred.GetGenericCredential("myGoApplication")
|
|
||||||
if err != nil {
|
|
||||||
fmt.Println(err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
cred.Delete()
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### List all available credentials
|
|
||||||
```Go
|
|
||||||
package main
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"github.com/danieljoos/wincred"
|
|
||||||
)
|
|
||||||
|
|
||||||
func main() {
|
|
||||||
creds, err := wincred.List()
|
|
||||||
if err != nil {
|
|
||||||
fmt.Println(err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
for i := range(creds) {
|
|
||||||
fmt.Println(creds[i].TargetName)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
Hints
|
|
||||||
-----
|
|
||||||
|
|
||||||
### Encoding
|
|
||||||
|
|
||||||
The credential objects simply store byte arrays without specific meaning or encoding.
|
|
||||||
For sharing between different applications, it might make sense to apply an explicit string encoding - for example **UTF-16 LE** (used nearly everywhere in the Win32 API).
|
|
||||||
|
|
||||||
```Go
|
|
||||||
package main
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"os"
|
|
||||||
|
|
||||||
"github.com/danieljoos/wincred"
|
|
||||||
"golang.org/x/text/encoding/unicode"
|
|
||||||
"golang.org/x/text/transform"
|
|
||||||
)
|
|
||||||
|
|
||||||
func main() {
|
|
||||||
cred := wincred.NewGenericCredential("myGoApplication")
|
|
||||||
|
|
||||||
encoder := unicode.UTF16(unicode.LittleEndian, unicode.IgnoreBOM).NewEncoder()
|
|
||||||
blob, _, err := transform.Bytes(encoder, []byte("mysecret"))
|
|
||||||
if err != nil {
|
|
||||||
fmt.Println(err)
|
|
||||||
os.Exit(1)
|
|
||||||
}
|
|
||||||
|
|
||||||
cred.CredentialBlob = blob
|
|
||||||
err = cred.Write()
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
fmt.Println(err)
|
|
||||||
os.Exit(1)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
```
|
|
||||||
|
|
||||||
### Limitations
|
|
||||||
|
|
||||||
The size of a credential blob is limited to **2560 Bytes** by the Windows API.
|
|
||||||
110
vendor/github.com/danieljoos/wincred/conversion.go
generated
vendored
110
vendor/github.com/danieljoos/wincred/conversion.go
generated
vendored
@@ -1,110 +0,0 @@
|
|||||||
// +build windows
|
|
||||||
|
|
||||||
package wincred
|
|
||||||
|
|
||||||
import (
|
|
||||||
"encoding/binary"
|
|
||||||
"reflect"
|
|
||||||
"time"
|
|
||||||
"unsafe"
|
|
||||||
|
|
||||||
syscall "golang.org/x/sys/windows"
|
|
||||||
)
|
|
||||||
|
|
||||||
// utf16ToByte creates a byte array from a given UTF 16 char array.
|
|
||||||
func utf16ToByte(wstr []uint16) (result []byte) {
|
|
||||||
result = make([]byte, len(wstr)*2)
|
|
||||||
for i := range wstr {
|
|
||||||
binary.LittleEndian.PutUint16(result[(i*2):(i*2)+2], wstr[i])
|
|
||||||
}
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// utf16FromString creates a UTF16 char array from a string.
|
|
||||||
func utf16FromString(str string) []uint16 {
|
|
||||||
res, err := syscall.UTF16FromString(str)
|
|
||||||
if err != nil {
|
|
||||||
return []uint16{}
|
|
||||||
}
|
|
||||||
return res
|
|
||||||
}
|
|
||||||
|
|
||||||
// goBytes copies the given C byte array to a Go byte array (see `C.GoBytes`).
|
|
||||||
// This function avoids having cgo as dependency.
|
|
||||||
func goBytes(src *byte, len uint32) []byte {
|
|
||||||
if src == nil || len == 0 {
|
|
||||||
return []byte{}
|
|
||||||
}
|
|
||||||
rv := make([]byte, len)
|
|
||||||
copy(rv, *(*[]byte)(unsafe.Pointer(&reflect.SliceHeader{
|
|
||||||
Data: uintptr(unsafe.Pointer(src)),
|
|
||||||
Len: int(len),
|
|
||||||
Cap: int(len),
|
|
||||||
})))
|
|
||||||
return rv
|
|
||||||
}
|
|
||||||
|
|
||||||
// Convert the given CREDENTIAL struct to a more usable structure
|
|
||||||
func sysToCredential(cred *sysCREDENTIAL) (result *Credential) {
|
|
||||||
if cred == nil {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
result = new(Credential)
|
|
||||||
result.Comment = syscall.UTF16PtrToString(cred.Comment)
|
|
||||||
result.TargetName = syscall.UTF16PtrToString(cred.TargetName)
|
|
||||||
result.TargetAlias = syscall.UTF16PtrToString(cred.TargetAlias)
|
|
||||||
result.UserName = syscall.UTF16PtrToString(cred.UserName)
|
|
||||||
result.LastWritten = time.Unix(0, cred.LastWritten.Nanoseconds())
|
|
||||||
result.Persist = CredentialPersistence(cred.Persist)
|
|
||||||
result.CredentialBlob = goBytes(cred.CredentialBlob, cred.CredentialBlobSize)
|
|
||||||
result.Attributes = make([]CredentialAttribute, cred.AttributeCount)
|
|
||||||
attrSlice := *(*[]sysCREDENTIAL_ATTRIBUTE)(unsafe.Pointer(&reflect.SliceHeader{
|
|
||||||
Data: uintptr(unsafe.Pointer(cred.Attributes)),
|
|
||||||
Len: int(cred.AttributeCount),
|
|
||||||
Cap: int(cred.AttributeCount),
|
|
||||||
}))
|
|
||||||
for i, attr := range attrSlice {
|
|
||||||
resultAttr := &result.Attributes[i]
|
|
||||||
resultAttr.Keyword = syscall.UTF16PtrToString(attr.Keyword)
|
|
||||||
resultAttr.Value = goBytes(attr.Value, attr.ValueSize)
|
|
||||||
}
|
|
||||||
return result
|
|
||||||
}
|
|
||||||
|
|
||||||
// Convert the given Credential object back to a CREDENTIAL struct, which can be used for calling the
|
|
||||||
// Windows APIs
|
|
||||||
func sysFromCredential(cred *Credential) (result *sysCREDENTIAL) {
|
|
||||||
if cred == nil {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
result = new(sysCREDENTIAL)
|
|
||||||
result.Flags = 0
|
|
||||||
result.Type = 0
|
|
||||||
result.TargetName, _ = syscall.UTF16PtrFromString(cred.TargetName)
|
|
||||||
result.Comment, _ = syscall.UTF16PtrFromString(cred.Comment)
|
|
||||||
result.LastWritten = syscall.NsecToFiletime(cred.LastWritten.UnixNano())
|
|
||||||
result.CredentialBlobSize = uint32(len(cred.CredentialBlob))
|
|
||||||
if len(cred.CredentialBlob) > 0 {
|
|
||||||
result.CredentialBlob = &cred.CredentialBlob[0]
|
|
||||||
}
|
|
||||||
result.Persist = uint32(cred.Persist)
|
|
||||||
result.AttributeCount = uint32(len(cred.Attributes))
|
|
||||||
attributes := make([]sysCREDENTIAL_ATTRIBUTE, len(cred.Attributes))
|
|
||||||
if len(attributes) > 0 {
|
|
||||||
result.Attributes = &attributes[0]
|
|
||||||
}
|
|
||||||
for i := range cred.Attributes {
|
|
||||||
inAttr := &cred.Attributes[i]
|
|
||||||
outAttr := &attributes[i]
|
|
||||||
outAttr.Keyword, _ = syscall.UTF16PtrFromString(inAttr.Keyword)
|
|
||||||
outAttr.Flags = 0
|
|
||||||
outAttr.ValueSize = uint32(len(inAttr.Value))
|
|
||||||
if len(inAttr.Value) > 0 {
|
|
||||||
outAttr.Value = &inAttr.Value[0]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
result.TargetAlias, _ = syscall.UTF16PtrFromString(cred.TargetAlias)
|
|
||||||
result.UserName, _ = syscall.UTF16PtrFromString(cred.UserName)
|
|
||||||
|
|
||||||
return
|
|
||||||
}
|
|
||||||
11
vendor/github.com/danieljoos/wincred/conversion_unsupported.go
generated
vendored
11
vendor/github.com/danieljoos/wincred/conversion_unsupported.go
generated
vendored
@@ -1,11 +0,0 @@
|
|||||||
// +build !windows
|
|
||||||
|
|
||||||
package wincred
|
|
||||||
|
|
||||||
func utf16ToByte(...interface{}) []byte {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func utf16FromString(...interface{}) []uint16 {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
151
vendor/github.com/danieljoos/wincred/sys.go
generated
vendored
151
vendor/github.com/danieljoos/wincred/sys.go
generated
vendored
@@ -1,151 +0,0 @@
|
|||||||
//go:build windows
|
|
||||||
// +build windows
|
|
||||||
|
|
||||||
package wincred
|
|
||||||
|
|
||||||
import (
|
|
||||||
"reflect"
|
|
||||||
"runtime"
|
|
||||||
"syscall"
|
|
||||||
"unsafe"
|
|
||||||
|
|
||||||
"golang.org/x/sys/windows"
|
|
||||||
)
|
|
||||||
|
|
||||||
var (
|
|
||||||
modadvapi32 = windows.NewLazySystemDLL("advapi32.dll")
|
|
||||||
procCredRead = modadvapi32.NewProc("CredReadW")
|
|
||||||
procCredWrite proc = modadvapi32.NewProc("CredWriteW")
|
|
||||||
procCredDelete proc = modadvapi32.NewProc("CredDeleteW")
|
|
||||||
procCredFree proc = modadvapi32.NewProc("CredFree")
|
|
||||||
procCredEnumerate = modadvapi32.NewProc("CredEnumerateW")
|
|
||||||
)
|
|
||||||
|
|
||||||
// Interface for syscall.Proc: helps testing
|
|
||||||
type proc interface {
|
|
||||||
Call(a ...uintptr) (r1, r2 uintptr, lastErr error)
|
|
||||||
}
|
|
||||||
|
|
||||||
// https://docs.microsoft.com/en-us/windows/desktop/api/wincred/ns-wincred-_credentialw
|
|
||||||
type sysCREDENTIAL struct {
|
|
||||||
Flags uint32
|
|
||||||
Type uint32
|
|
||||||
TargetName *uint16
|
|
||||||
Comment *uint16
|
|
||||||
LastWritten windows.Filetime
|
|
||||||
CredentialBlobSize uint32
|
|
||||||
CredentialBlob *byte
|
|
||||||
Persist uint32
|
|
||||||
AttributeCount uint32
|
|
||||||
Attributes *sysCREDENTIAL_ATTRIBUTE
|
|
||||||
TargetAlias *uint16
|
|
||||||
UserName *uint16
|
|
||||||
}
|
|
||||||
|
|
||||||
// https://docs.microsoft.com/en-us/windows/desktop/api/wincred/ns-wincred-_credential_attributew
|
|
||||||
type sysCREDENTIAL_ATTRIBUTE struct {
|
|
||||||
Keyword *uint16
|
|
||||||
Flags uint32
|
|
||||||
ValueSize uint32
|
|
||||||
Value *byte
|
|
||||||
}
|
|
||||||
|
|
||||||
// https://docs.microsoft.com/en-us/windows/desktop/api/wincred/ns-wincred-_credentialw
|
|
||||||
type sysCRED_TYPE uint32
|
|
||||||
|
|
||||||
const (
|
|
||||||
sysCRED_TYPE_GENERIC sysCRED_TYPE = 0x1
|
|
||||||
sysCRED_TYPE_DOMAIN_PASSWORD sysCRED_TYPE = 0x2
|
|
||||||
sysCRED_TYPE_DOMAIN_CERTIFICATE sysCRED_TYPE = 0x3
|
|
||||||
sysCRED_TYPE_DOMAIN_VISIBLE_PASSWORD sysCRED_TYPE = 0x4
|
|
||||||
sysCRED_TYPE_GENERIC_CERTIFICATE sysCRED_TYPE = 0x5
|
|
||||||
sysCRED_TYPE_DOMAIN_EXTENDED sysCRED_TYPE = 0x6
|
|
||||||
|
|
||||||
// https://docs.microsoft.com/en-us/windows/desktop/Debug/system-error-codes
|
|
||||||
sysERROR_NOT_FOUND = windows.Errno(1168)
|
|
||||||
sysERROR_INVALID_PARAMETER = windows.Errno(87)
|
|
||||||
sysERROR_BAD_USERNAME = windows.Errno(2202)
|
|
||||||
)
|
|
||||||
|
|
||||||
// https://docs.microsoft.com/en-us/windows/desktop/api/wincred/nf-wincred-credreadw
|
|
||||||
func sysCredRead(targetName string, typ sysCRED_TYPE) (*Credential, error) {
|
|
||||||
var pcred *sysCREDENTIAL
|
|
||||||
targetNamePtr, _ := windows.UTF16PtrFromString(targetName)
|
|
||||||
ret, _, err := syscall.SyscallN(
|
|
||||||
procCredRead.Addr(),
|
|
||||||
uintptr(unsafe.Pointer(targetNamePtr)),
|
|
||||||
uintptr(typ),
|
|
||||||
0,
|
|
||||||
uintptr(unsafe.Pointer(&pcred)),
|
|
||||||
)
|
|
||||||
if ret == 0 {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
defer procCredFree.Call(uintptr(unsafe.Pointer(pcred)))
|
|
||||||
|
|
||||||
return sysToCredential(pcred), nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// https://docs.microsoft.com/en-us/windows/desktop/api/wincred/nf-wincred-credwritew
|
|
||||||
func sysCredWrite(cred *Credential, typ sysCRED_TYPE) error {
|
|
||||||
ncred := sysFromCredential(cred)
|
|
||||||
ncred.Type = uint32(typ)
|
|
||||||
ret, _, err := procCredWrite.Call(
|
|
||||||
uintptr(unsafe.Pointer(ncred)),
|
|
||||||
0,
|
|
||||||
)
|
|
||||||
// Make sure everything reachable from ncred stays alive through the call.
|
|
||||||
runtime.KeepAlive(ncred)
|
|
||||||
if ret == 0 {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// https://docs.microsoft.com/en-us/windows/desktop/api/wincred/nf-wincred-creddeletew
|
|
||||||
func sysCredDelete(cred *Credential, typ sysCRED_TYPE) error {
|
|
||||||
targetNamePtr, _ := windows.UTF16PtrFromString(cred.TargetName)
|
|
||||||
ret, _, err := procCredDelete.Call(
|
|
||||||
uintptr(unsafe.Pointer(targetNamePtr)),
|
|
||||||
uintptr(typ),
|
|
||||||
0,
|
|
||||||
)
|
|
||||||
if ret == 0 {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// https://docs.microsoft.com/en-us/windows/desktop/api/wincred/nf-wincred-credenumeratew
|
|
||||||
func sysCredEnumerate(filter string, all bool) ([]*Credential, error) {
|
|
||||||
var count int
|
|
||||||
var pcreds uintptr
|
|
||||||
var filterPtr *uint16
|
|
||||||
if !all {
|
|
||||||
filterPtr, _ = windows.UTF16PtrFromString(filter)
|
|
||||||
}
|
|
||||||
ret, _, err := syscall.SyscallN(
|
|
||||||
procCredEnumerate.Addr(),
|
|
||||||
uintptr(unsafe.Pointer(filterPtr)),
|
|
||||||
0,
|
|
||||||
uintptr(unsafe.Pointer(&count)),
|
|
||||||
uintptr(unsafe.Pointer(&pcreds)),
|
|
||||||
)
|
|
||||||
if ret == 0 {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
defer procCredFree.Call(pcreds)
|
|
||||||
credsSlice := *(*[]*sysCREDENTIAL)(unsafe.Pointer(&reflect.SliceHeader{
|
|
||||||
Data: pcreds,
|
|
||||||
Len: count,
|
|
||||||
Cap: count,
|
|
||||||
}))
|
|
||||||
creds := make([]*Credential, count, count)
|
|
||||||
for i, cred := range credsSlice {
|
|
||||||
creds[i] = sysToCredential(cred)
|
|
||||||
}
|
|
||||||
|
|
||||||
return creds, nil
|
|
||||||
}
|
|
||||||
38
vendor/github.com/danieljoos/wincred/sys_unsupported.go
generated
vendored
38
vendor/github.com/danieljoos/wincred/sys_unsupported.go
generated
vendored
@@ -1,38 +0,0 @@
|
|||||||
//go:build !windows
|
|
||||||
// +build !windows
|
|
||||||
|
|
||||||
package wincred
|
|
||||||
|
|
||||||
import (
|
|
||||||
"errors"
|
|
||||||
"syscall"
|
|
||||||
)
|
|
||||||
|
|
||||||
const (
|
|
||||||
sysCRED_TYPE_GENERIC = 0
|
|
||||||
sysCRED_TYPE_DOMAIN_PASSWORD = 0
|
|
||||||
sysCRED_TYPE_DOMAIN_CERTIFICATE = 0
|
|
||||||
sysCRED_TYPE_DOMAIN_VISIBLE_PASSWORD = 0
|
|
||||||
sysCRED_TYPE_GENERIC_CERTIFICATE = 0
|
|
||||||
sysCRED_TYPE_DOMAIN_EXTENDED = 0
|
|
||||||
|
|
||||||
sysERROR_NOT_FOUND = syscall.Errno(1)
|
|
||||||
sysERROR_INVALID_PARAMETER = syscall.Errno(1)
|
|
||||||
sysERROR_BAD_USERNAME = syscall.Errno(1)
|
|
||||||
)
|
|
||||||
|
|
||||||
func sysCredRead(...interface{}) (*Credential, error) {
|
|
||||||
return nil, errors.New("Operation not supported")
|
|
||||||
}
|
|
||||||
|
|
||||||
func sysCredWrite(...interface{}) error {
|
|
||||||
return errors.New("Operation not supported")
|
|
||||||
}
|
|
||||||
|
|
||||||
func sysCredDelete(...interface{}) error {
|
|
||||||
return errors.New("Operation not supported")
|
|
||||||
}
|
|
||||||
|
|
||||||
func sysCredEnumerate(...interface{}) ([]*Credential, error) {
|
|
||||||
return nil, errors.New("Operation not supported")
|
|
||||||
}
|
|
||||||
69
vendor/github.com/danieljoos/wincred/types.go
generated
vendored
69
vendor/github.com/danieljoos/wincred/types.go
generated
vendored
@@ -1,69 +0,0 @@
|
|||||||
package wincred
|
|
||||||
|
|
||||||
import (
|
|
||||||
"time"
|
|
||||||
)
|
|
||||||
|
|
||||||
// CredentialPersistence describes one of three persistence modes of a credential.
|
|
||||||
// A detailed description of the available modes can be found on
|
|
||||||
// Docs: https://docs.microsoft.com/en-us/windows/desktop/api/wincred/ns-wincred-_credentialw
|
|
||||||
type CredentialPersistence uint32
|
|
||||||
|
|
||||||
const (
|
|
||||||
// PersistSession indicates that the credential only persists for the life
|
|
||||||
// of the current Windows login session. Such a credential is not visible in
|
|
||||||
// any other logon session, even from the same user.
|
|
||||||
PersistSession CredentialPersistence = 0x1
|
|
||||||
|
|
||||||
// PersistLocalMachine indicates that the credential persists for this and
|
|
||||||
// all subsequent logon sessions on this local machine/computer. It is
|
|
||||||
// however not visible for logon sessions of this user on a different
|
|
||||||
// machine.
|
|
||||||
PersistLocalMachine CredentialPersistence = 0x2
|
|
||||||
|
|
||||||
// PersistEnterprise indicates that the credential persists for this and all
|
|
||||||
// subsequent logon sessions for this user. It is also visible for logon
|
|
||||||
// sessions on different computers.
|
|
||||||
PersistEnterprise CredentialPersistence = 0x3
|
|
||||||
)
|
|
||||||
|
|
||||||
// CredentialAttribute represents an application-specific attribute of a credential.
|
|
||||||
type CredentialAttribute struct {
|
|
||||||
Keyword string
|
|
||||||
Value []byte
|
|
||||||
}
|
|
||||||
|
|
||||||
// Credential is the basic credential structure.
|
|
||||||
// A credential is identified by its target name.
|
|
||||||
// The actual credential secret is available in the CredentialBlob field.
|
|
||||||
type Credential struct {
|
|
||||||
TargetName string
|
|
||||||
Comment string
|
|
||||||
LastWritten time.Time
|
|
||||||
CredentialBlob []byte
|
|
||||||
Attributes []CredentialAttribute
|
|
||||||
TargetAlias string
|
|
||||||
UserName string
|
|
||||||
Persist CredentialPersistence
|
|
||||||
}
|
|
||||||
|
|
||||||
// GenericCredential holds a credential for generic usage.
|
|
||||||
// It is typically defined and used by applications that need to manage user
|
|
||||||
// secrets.
|
|
||||||
//
|
|
||||||
// More information about the available kinds of credentials of the Windows
|
|
||||||
// Credential Management API can be found on Docs:
|
|
||||||
// https://docs.microsoft.com/en-us/windows/desktop/SecAuthN/kinds-of-credentials
|
|
||||||
type GenericCredential struct {
|
|
||||||
Credential
|
|
||||||
}
|
|
||||||
|
|
||||||
// DomainPassword holds a domain credential that is typically used by the
|
|
||||||
// operating system for user logon.
|
|
||||||
//
|
|
||||||
// More information about the available kinds of credentials of the Windows
|
|
||||||
// Credential Management API can be found on Docs:
|
|
||||||
// https://docs.microsoft.com/en-us/windows/desktop/SecAuthN/kinds-of-credentials
|
|
||||||
type DomainPassword struct {
|
|
||||||
Credential
|
|
||||||
}
|
|
||||||
114
vendor/github.com/danieljoos/wincred/wincred.go
generated
vendored
114
vendor/github.com/danieljoos/wincred/wincred.go
generated
vendored
@@ -1,114 +0,0 @@
|
|||||||
// Package wincred provides primitives for accessing the Windows Credentials Management API.
|
|
||||||
// This includes functions for retrieval, listing and storage of credentials as well as Go structures for convenient access to the credential data.
|
|
||||||
//
|
|
||||||
// A more detailed description of Windows Credentials Management can be found on
|
|
||||||
// Docs: https://docs.microsoft.com/en-us/windows/desktop/SecAuthN/credentials-management
|
|
||||||
package wincred
|
|
||||||
|
|
||||||
import "errors"
|
|
||||||
|
|
||||||
const (
|
|
||||||
// ErrElementNotFound is the error that is returned if a requested element cannot be found.
|
|
||||||
// This error constant can be used to check if a credential could not be found.
|
|
||||||
ErrElementNotFound = sysERROR_NOT_FOUND
|
|
||||||
|
|
||||||
// ErrInvalidParameter is the error that is returned for invalid parameters.
|
|
||||||
// This error constant can be used to check if the given function parameters were invalid.
|
|
||||||
// For example when trying to create a new generic credential with an empty target name.
|
|
||||||
ErrInvalidParameter = sysERROR_INVALID_PARAMETER
|
|
||||||
|
|
||||||
// ErrBadUsername is returned when the credential's username is invalid.
|
|
||||||
ErrBadUsername = sysERROR_BAD_USERNAME
|
|
||||||
)
|
|
||||||
|
|
||||||
// GetGenericCredential fetches the generic credential with the given name from Windows credential manager.
|
|
||||||
// It returns nil and an error if the credential could not be found or an error occurred.
|
|
||||||
func GetGenericCredential(targetName string) (*GenericCredential, error) {
|
|
||||||
cred, err := sysCredRead(targetName, sysCRED_TYPE_GENERIC)
|
|
||||||
if cred != nil {
|
|
||||||
return &GenericCredential{Credential: *cred}, err
|
|
||||||
}
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
// NewGenericCredential creates a new generic credential object with the given name.
|
|
||||||
// The persist mode of the newly created object is set to a default value that indicates local-machine-wide storage.
|
|
||||||
// The credential object is NOT yet persisted to the Windows credential vault.
|
|
||||||
func NewGenericCredential(targetName string) (result *GenericCredential) {
|
|
||||||
result = new(GenericCredential)
|
|
||||||
result.TargetName = targetName
|
|
||||||
result.Persist = PersistLocalMachine
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Write persists the generic credential object to Windows credential manager.
|
|
||||||
func (t *GenericCredential) Write() (err error) {
|
|
||||||
err = sysCredWrite(&t.Credential, sysCRED_TYPE_GENERIC)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Delete removes the credential object from Windows credential manager.
|
|
||||||
func (t *GenericCredential) Delete() (err error) {
|
|
||||||
err = sysCredDelete(&t.Credential, sysCRED_TYPE_GENERIC)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetDomainPassword fetches the domain-password credential with the given target host name from Windows credential manager.
|
|
||||||
// It returns nil and an error if the credential could not be found or an error occurred.
|
|
||||||
func GetDomainPassword(targetName string) (*DomainPassword, error) {
|
|
||||||
cred, err := sysCredRead(targetName, sysCRED_TYPE_DOMAIN_PASSWORD)
|
|
||||||
if cred != nil {
|
|
||||||
return &DomainPassword{Credential: *cred}, err
|
|
||||||
}
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
// NewDomainPassword creates a new domain-password credential used for login to the given target host name.
|
|
||||||
// The persist mode of the newly created object is set to a default value that indicates local-machine-wide storage.
|
|
||||||
// The credential object is NOT yet persisted to the Windows credential vault.
|
|
||||||
func NewDomainPassword(targetName string) (result *DomainPassword) {
|
|
||||||
result = new(DomainPassword)
|
|
||||||
result.TargetName = targetName
|
|
||||||
result.Persist = PersistLocalMachine
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Write persists the domain-password credential to Windows credential manager.
|
|
||||||
func (t *DomainPassword) Write() (err error) {
|
|
||||||
err = sysCredWrite(&t.Credential, sysCRED_TYPE_DOMAIN_PASSWORD)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Delete removes the domain-password credential from Windows credential manager.
|
|
||||||
func (t *DomainPassword) Delete() (err error) {
|
|
||||||
err = sysCredDelete(&t.Credential, sysCRED_TYPE_DOMAIN_PASSWORD)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// SetPassword sets the CredentialBlob field of a domain password credential to the given string.
|
|
||||||
func (t *DomainPassword) SetPassword(pw string) {
|
|
||||||
t.CredentialBlob = utf16ToByte(utf16FromString(pw))
|
|
||||||
}
|
|
||||||
|
|
||||||
// List retrieves all credentials of the Credentials store.
|
|
||||||
func List() ([]*Credential, error) {
|
|
||||||
creds, err := sysCredEnumerate("", true)
|
|
||||||
if err != nil && errors.Is(err, ErrElementNotFound) {
|
|
||||||
// Ignore ERROR_NOT_FOUND and return an empty list instead
|
|
||||||
creds = []*Credential{}
|
|
||||||
err = nil
|
|
||||||
}
|
|
||||||
return creds, err
|
|
||||||
}
|
|
||||||
|
|
||||||
// FilteredList retrieves the list of credentials from the Credentials store that match the given filter.
|
|
||||||
// The filter string defines the prefix followed by an asterisk for the `TargetName` attribute of the credentials.
|
|
||||||
func FilteredList(filter string) ([]*Credential, error) {
|
|
||||||
creds, err := sysCredEnumerate(filter, false)
|
|
||||||
if err != nil && errors.Is(err, ErrElementNotFound) {
|
|
||||||
// Ignore ERROR_NOT_FOUND and return an empty list instead
|
|
||||||
creds = []*Credential{}
|
|
||||||
err = nil
|
|
||||||
}
|
|
||||||
return creds, err
|
|
||||||
}
|
|
||||||
8
vendor/github.com/go-ole/go-ole/.travis.yml
generated
vendored
8
vendor/github.com/go-ole/go-ole/.travis.yml
generated
vendored
@@ -1,8 +0,0 @@
|
|||||||
language: go
|
|
||||||
sudo: false
|
|
||||||
|
|
||||||
go:
|
|
||||||
- 1.9.x
|
|
||||||
- 1.10.x
|
|
||||||
- 1.11.x
|
|
||||||
- tip
|
|
||||||
49
vendor/github.com/go-ole/go-ole/ChangeLog.md
generated
vendored
49
vendor/github.com/go-ole/go-ole/ChangeLog.md
generated
vendored
@@ -1,49 +0,0 @@
|
|||||||
# Version 1.x.x
|
|
||||||
|
|
||||||
* **Add more test cases and reference new test COM server project.** (Placeholder for future additions)
|
|
||||||
|
|
||||||
# Version 1.2.0-alphaX
|
|
||||||
|
|
||||||
**Minimum supported version is now Go 1.4. Go 1.1 support is deprecated, but should still build.**
|
|
||||||
|
|
||||||
* Added CI configuration for Travis-CI and AppVeyor.
|
|
||||||
* Added test InterfaceID and ClassID for the COM Test Server project.
|
|
||||||
* Added more inline documentation (#83).
|
|
||||||
* Added IEnumVARIANT implementation (#88).
|
|
||||||
* Added IEnumVARIANT test cases (#99, #100, #101).
|
|
||||||
* Added support for retrieving `time.Time` from VARIANT (#92).
|
|
||||||
* Added test case for IUnknown (#64).
|
|
||||||
* Added test case for IDispatch (#64).
|
|
||||||
* Added test cases for scalar variants (#64, #76).
|
|
||||||
|
|
||||||
# Version 1.1.1
|
|
||||||
|
|
||||||
* Fixes for Linux build.
|
|
||||||
* Fixes for Windows build.
|
|
||||||
|
|
||||||
# Version 1.1.0
|
|
||||||
|
|
||||||
The change to provide building on all platforms is a new feature. The increase in minor version reflects that and allows those who wish to stay on 1.0.x to continue to do so. Support for 1.0.x will be limited to bug fixes.
|
|
||||||
|
|
||||||
* Move GUID out of variables.go into its own file to make new documentation available.
|
|
||||||
* Move OleError out of ole.go into its own file to make new documentation available.
|
|
||||||
* Add documentation to utility functions.
|
|
||||||
* Add documentation to variant receiver functions.
|
|
||||||
* Add documentation to ole structures.
|
|
||||||
* Make variant available to other systems outside of Windows.
|
|
||||||
* Make OLE structures available to other systems outside of Windows.
|
|
||||||
|
|
||||||
## New Features
|
|
||||||
|
|
||||||
* Library should now be built on all platforms supported by Go. Library will NOOP on any platform that is not Windows.
|
|
||||||
* More functions are now documented and available on godoc.org.
|
|
||||||
|
|
||||||
# Version 1.0.1
|
|
||||||
|
|
||||||
1. Fix package references from repository location change.
|
|
||||||
|
|
||||||
# Version 1.0.0
|
|
||||||
|
|
||||||
This version is stable enough for use. The COM API is still incomplete, but provides enough functionality for accessing COM servers using IDispatch interface.
|
|
||||||
|
|
||||||
There is no changelog for this version. Check commits for history.
|
|
||||||
21
vendor/github.com/go-ole/go-ole/LICENSE
generated
vendored
21
vendor/github.com/go-ole/go-ole/LICENSE
generated
vendored
@@ -1,21 +0,0 @@
|
|||||||
The MIT License (MIT)
|
|
||||||
|
|
||||||
Copyright © 2013-2017 Yasuhiro Matsumoto, <mattn.jp@gmail.com>
|
|
||||||
|
|
||||||
Permission is hereby granted, free of charge, to any person obtaining a copy of
|
|
||||||
this software and associated documentation files (the “Software”), to deal in
|
|
||||||
the Software without restriction, including without limitation the rights to
|
|
||||||
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
|
|
||||||
of the Software, and to permit persons to whom the Software is furnished to do
|
|
||||||
so, subject to the following conditions:
|
|
||||||
|
|
||||||
The above copyright notice and this permission notice shall be included in all
|
|
||||||
copies or substantial portions of the Software.
|
|
||||||
|
|
||||||
THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
||||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
||||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
||||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
||||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
||||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
||||||
SOFTWARE.
|
|
||||||
46
vendor/github.com/go-ole/go-ole/README.md
generated
vendored
46
vendor/github.com/go-ole/go-ole/README.md
generated
vendored
@@ -1,46 +0,0 @@
|
|||||||
# Go OLE
|
|
||||||
|
|
||||||
[](https://ci.appveyor.com/project/jacobsantos/go-ole-jgs28)
|
|
||||||
[](https://travis-ci.org/go-ole/go-ole)
|
|
||||||
[](https://godoc.org/github.com/go-ole/go-ole)
|
|
||||||
|
|
||||||
Go bindings for Windows COM using shared libraries instead of cgo.
|
|
||||||
|
|
||||||
By Yasuhiro Matsumoto.
|
|
||||||
|
|
||||||
## Install
|
|
||||||
|
|
||||||
To experiment with go-ole, you can just compile and run the example program:
|
|
||||||
|
|
||||||
```
|
|
||||||
go get github.com/go-ole/go-ole
|
|
||||||
cd /path/to/go-ole/
|
|
||||||
go test
|
|
||||||
|
|
||||||
cd /path/to/go-ole/example/excel
|
|
||||||
go run excel.go
|
|
||||||
```
|
|
||||||
|
|
||||||
## Continuous Integration
|
|
||||||
|
|
||||||
Continuous integration configuration has been added for both Travis-CI and AppVeyor. You will have to add these to your own account for your fork in order for it to run.
|
|
||||||
|
|
||||||
**Travis-CI**
|
|
||||||
|
|
||||||
Travis-CI was added to check builds on Linux to ensure that `go get` works when cross building. Currently, Travis-CI is not used to test cross-building, but this may be changed in the future. It is also not currently possible to test the library on Linux, since COM API is specific to Windows and it is not currently possible to run a COM server on Linux or even connect to a remote COM server.
|
|
||||||
|
|
||||||
**AppVeyor**
|
|
||||||
|
|
||||||
AppVeyor is used to build on Windows using the (in-development) test COM server. It is currently only used to test the build and ensure that the code works on Windows. It will be used to register a COM server and then run the test cases based on the test COM server.
|
|
||||||
|
|
||||||
The tests currently do run and do pass and this should be maintained with commits.
|
|
||||||
|
|
||||||
## Versioning
|
|
||||||
|
|
||||||
Go OLE uses [semantic versioning](http://semver.org) for version numbers, which is similar to the version contract of the Go language. Which means that the major version will always maintain backwards compatibility with minor versions. Minor versions will only add new additions and changes. Fixes will always be in patch.
|
|
||||||
|
|
||||||
This contract should allow you to upgrade to new minor and patch versions without breakage or modifications to your existing code. Leave a ticket, if there is breakage, so that it could be fixed.
|
|
||||||
|
|
||||||
## LICENSE
|
|
||||||
|
|
||||||
Under the MIT License: http://mattn.mit-license.org/2013
|
|
||||||
13
vendor/github.com/go-ole/go-ole/SECURITY.md
generated
vendored
13
vendor/github.com/go-ole/go-ole/SECURITY.md
generated
vendored
@@ -1,13 +0,0 @@
|
|||||||
# Security Policy
|
|
||||||
|
|
||||||
## Supported Versions
|
|
||||||
|
|
||||||
Security updates are applied only to the latest release.
|
|
||||||
|
|
||||||
## Reporting a Vulnerability
|
|
||||||
|
|
||||||
If you have discovered a security vulnerability in this project, please report it privately. **Do not disclose it as a public issue.** This gives us time to work with you to fix the issue before public exposure, reducing the chance that the exploit will be used before a patch is released.
|
|
||||||
|
|
||||||
Please disclose it at [security advisory](https://github.com/go-ole/go-ole/security/advisories/new).
|
|
||||||
|
|
||||||
This project is maintained by a team of volunteers on a reasonable-effort basis. As such, please give us at least 90 days to work on a fix before public exposure.
|
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user