Compare commits

..

98 Commits

Author SHA1 Message Date
yusing
54450924dc fix: build error 2026-02-16 10:23:59 +08:00
yusing
e4e6f6b3e8 v0.26.0 2026-02-16 09:04:56 +08:00
yusing
15b9635ee1 fix(Makefile): exclude specific directories from gomod_paths search 2026-02-01 00:22:00 +08:00
yusing
33d512f550 fix(api): prevent timeout during agent verification
Send early HTTP 100 Continue response before processing to avoid
timeouts, and propagate request context through the verification flow
for proper cancellation handling.
2026-02-01 00:22:00 +08:00
yusing
c7a70f630a fix(autocert): rebuild SNI matcher after ObtainCertAll operations
The ObtainCertAll method was missing a call to rebuildSNIMatcher(),
which could leave the SNI configuration stale after certificate
renewals. Both ObtainCertIfNotExistsAll and ObtainCertAll now
consistently rebuild the SNI matcher after their operations.

This was introduced in 3ad6e98a17,
not a bug fix for previous version
2026-02-01 00:22:00 +08:00
yusing
12a8b9a1b4 feat(route): add YAML anchor exclusion reason
Add ExcludedReasonYAMLAnchor to explicitly identify routes with "x-" prefix
used for YAML anchors and references. These routes are removed before
validation.
2026-02-01 00:22:00 +08:00
yusing
4c62a4b365 fix(route): allow excluded routes to use localhost addresses
Routes marked for exclusion should bypass normal validation checks,
including the restriction on localhost/127.0.0.1 hostnames.
2026-02-01 00:22:00 +08:00
yusing
5c30f4a859 fix(health/check): validate URL port before dialing in Stream check
Add port validation to return an unhealthy result with descriptive
message when URL has no port specified, preventing potential dialing
errors on zero port.
2026-02-01 00:22:00 +08:00
yusing
c6e4e83fcd chore: disable godoxy health checking for socket-proxy 2026-02-01 00:22:00 +08:00
yusing
c54741aab6 feat(autocert): generate unique ACME key paths per CA directory URL
Previously, ACME keys were stored at a single default path regardless of
which CA directory URL was configured. This caused key conflicts when
using multiple different ACME CAs.

Now, the key path is derived from a SHA256 hash of the CA directory URL,
allowing each CA to have its own key file:
- Default CA (Let's Encrypt): certs/acme.key
- Custom CA: certs/acme_<url_hash_16chars>.key

This enables running certificates against multiple ACME providers without
key collision issues.
2026-02-01 00:21:59 +08:00
yusing
fb6b692f55 fix(autocert): correct ObtainCert error handling
- ObtainCertIfNotExistsAll longer fail on fs.ErrNotExists
- Separate public LoadCertAll (loads all providers) from private loadCert
- LoadCertAll now uses allProviders() for iteration
- Updated tests to use LoadCertAll
2026-02-01 00:21:59 +08:00
yusing
1319f72805 refactor: propagate context and standardize HTTP client timeouts
Add context parameter to TCP/UDP stream health checks and client constructors
for proper cancellation and deadline propagation. Switch from encoding/json
to sonic for faster JSON unmarshaling.

Standardize HTTP client timeouts to 5 seconds
across agent pool and health check.
2026-02-01 00:21:50 +08:00
yusing
52511f1618 Cherry-pick 372132b1da 2026-01-30 00:33:34 +08:00
yusing
ca2cbd3332 chore: upgrade dependencies 2026-01-30 00:32:39 +08:00
yusing
57f7b72923 chore(json): use stdlib json for compatibility 2026-01-30 00:30:48 +08:00
yusing
54b3008cce refactor: propagate context and standardize HTTP client timeouts
Add context parameter to TCP/UDP stream health checks and client constructors
for proper cancellation and deadline propagation. Switch from encoding/json
to sonic for faster JSON unmarshaling.

Standardize HTTP client timeouts to 5 seconds
across agent pool and health check.
2026-01-30 00:28:17 +08:00
yusing
d9abd65471 factor(route): make proxmox validation non-critical
Proxmox validation errors are now logged and ignored rather than
causing route validation to fail, allowing routes to function even
when proxmox integration encounters issues.

- Extract proxmox validation into dedicated validateProxmox() method
- Log warnings/errors instead of returning validation errors
- Add warning when proxmox config exists but no node/resource found
2026-01-30 00:28:00 +08:00
yusing
d5b9d2e6bc fix(serialization): correct validation parameter
- Fix bug in mapUnmarshalValidate where checkValidateTag parameter
  was incorrectly negated when passed to Convert()
- Remove obsolete validateWithValidator helper function
2026-01-30 00:28:00 +08:00
yusing
ec9cab05c7 chore(docs): update package docs for internal/serialization 2026-01-30 00:27:59 +08:00
yusing
6ff1346675 feat(api): add route validation endpoint with WebSocket support
Adds a new `/route/validate` endpoint that accepts YAML-encoded route
configurations for validation. Supports both synchronous HTTP requests
and real-time streaming via WebSocket for interactive validation workflows.

Changes:
- Implement Validate handler with YAML binding in route/validate.go
- Add WebSocket manager for streaming validation results
- Register GET/POST routes in handler.go
- Regenerate Swagger documentation
2026-01-30 00:27:58 +08:00
yusing
a75441aa8a refactor(serialization): generalize unmarshal/load functions with pluggable format handlers
Replace YAML-specific functions with generic ones accepting unmarshaler/marshaler
function parameters. This enables future support for JSON and other formats
while maintaining current YAML behavior.

- UnmarshalValidateYAML -> UnmarshalValidate(unmarshalFunc)
- UnmarshalValidateYAMLXSync -> UnmarshalValidateXSync(unmarshalFunc)
- SaveJSON -> SaveFile(marshalFunc)
- LoadJSONIfExist -> LoadFileIfExist(unmarshalFunc)
- Add UnmarshalValidateReader for reader-based decoding

Testing: all 12 staged test files updated to use new API
2026-01-30 00:27:38 +08:00
yusing
32971b45c3 chore(swagger): update API documentation annotations
- Change ValidateFile endpoint Accept type from text/plain to json
- Add Route struct name annotation for Swagger documentation
2026-01-30 00:27:07 +08:00
yusing
4bc7ce09c7 chore(docs): update package docs for internal/proxmox 2026-01-30 00:27:06 +08:00
yusing
b5946b34b8 refactor(proxmox): add struct level validation for node configuration services and files
Add Validate() method to NodeConfig that implements the CustomValidator
interface. The method checks all services and files for invalid shell
metacharacters (&, $(), etc.) to prevent shell injection attacks.

Testing: Added validation_test.go with 6 table-driven test cases covering
valid inputs and various shell metacharacter injection attempts.
2026-01-30 00:26:59 +08:00
yusing
442e4a0972 fix(proxmox): improve journalctl with log tailing fallback for non-systemd systems
- Format tail command with fallback retry logic
- Add /var/log/messages fallback when no services specified

Improves log viewing reliability on systems without systemd support.
2026-01-30 00:26:28 +08:00
yusing
d1c79acf87 fix(docker): improve error handling for missing Docker agent
Replaced panic with an error return in the NewClient
2026-01-28 17:23:53 +08:00
yusing
df70f7f63c refactor(proxmox): add validation for node name and VMID in provider initialization 2026-01-28 17:23:52 +08:00
yusing
6707143aaf refactor(logging): add non-blocking writer for high-volume logging
Replace synchronous log writing with zerolog's diode-based non-blocking
writer to prevent logging from blocking the main application during
log bursts. The diode writer buffers up to 1024 messages and logs a
warning when messages are dropped.

- Extract multi-writer logic into separate `multiWriter` function
- Wrap with `diode.NewWriter` for async buffering
- Update both `NewLogger` and `NewLoggerWithFixedLevel` to use diode
2026-01-28 17:23:52 +08:00
yusing
742a86d079 refactor(config): simplify route provider loading with improved error handling
Streamlined the `loadRouteProviders()` function by:
- Replacing channel-based concurrency with a simpler sequential registration pattern after agent initialization
- Using `gperr.NewGroup` and `gperr.NewBuilder` for more idiomatic error handling
- Adding mutex protection for concurrent result building
- Removing the `storeProvider` helper method
2026-01-28 17:23:51 +08:00
yusing
45f856b8d1 chore(config): make initialization timeout configurable via environment variable
Replaced hardcoded 10-second initialization timeout with a configurable `INIT_TIMEOUT` environment variable.
The new default is 1 minute, allowing operators to adjust startup behavior based on their infrastructure requirements.
2026-01-28 17:23:51 +08:00
yusing
044c7bf8a0 feat(proxmox): add session refresh loop to maintain Proxmox API session
Introduced a new session refresh mechanism in the Proxmox configuration to ensure the API session remains active. This includes:
- Added `SessionRefreshInterval` constant for configurable session refresh timing.
- Implemented `refreshSessionLoop` method to periodically refresh the session and handle errors with exponential backoff.

This enhancement improves the reliability of interactions with the Proxmox API by preventing session expiry.
2026-01-28 17:23:50 +08:00
yusing
e7b19c472b feat(proxmox): add tail endpoint and enhance journalctl with multi-service support
Add new `/proxmox/tail` API endpoint for streaming file contents from Proxmox
nodes and LXC containers via WebSocket. Extend journalctl endpoint to support
filtering by multiple services simultaneously.

Changes:
- Add `GET /proxmox/tail` endpoint supporting node-level and LXC container file tailing
- Change `service` parameter from string to array in journalctl endpoints
- Add input validation (`checkValidInput`) to prevent command injection
- Refactor command formatting with proper shell quoting

Security: All command inputs are validated for dangerous characters before
2026-01-28 17:23:45 +08:00
yusing
1ace5e641d feat(proxmox): better node-level routes auto-discovery with pointer VMID
- Add BaseURL field to Client for node-level route configuration
- Change VMID from int to *int to support three states:
  - nil: auto-discover node or VM from hostname/IP/alias
  - 0: node-level route (direct to Proxmox node API)
  - >0: LXC/QEMU resource route with container control
- Change Service string to Services []string for multi-service support
- Implement proper node-level route handling: HTTPS scheme,
  hostname from node BaseURL, default port 8006
- Move initial UpdateResources call to Init before starting loop
- Move proxmox auto-discovery earlier in route validation

BREAKING: NodeConfig.VMID is now a pointer type; NodeConfig.Service
renamed to Services (backward compatible via alias)
2026-01-28 17:23:29 +08:00
yusing
83976646db feat(api): support query parameters for proxmox journalctl endpoint
Refactored the journalctl API to accept `node`, `vmid`, and `service` parameters as query strings in addition to path parameters. Added a new route `/proxmox/journalctl` that accepts all parameters via query string while maintaining backward compatibility with existing path-parameter routes.

- Changed `JournalctlRequest` struct binding from URI-only to query+URI
- Simplified Swagger documentation by consolidating multiple route definitions
- Existing path-parameter routes remain functional for backward compatibility
2026-01-28 17:23:29 +08:00
yusing
8e1d75abd6 refactor(swagger): rename DockerConfig and ProxmoxNodeConfig to IdlewatcherDockerConfig and IdlewatcherProxmoxNodeConfig 2026-01-28 17:23:28 +08:00
yusing
5c341d4745 refactor: improve error handling, validation and proper cleanup 2026-01-28 17:23:28 +08:00
yusing
9f245a62f2 fix(proxmox): concurrent map write in UpdateResources 2026-01-25 18:05:36 +08:00
yusing
ecc2547eb1 chore: go mod tidy 2026-01-25 17:41:10 +08:00
yusing
e2a48fcff9 fix(api/docker): correct paramaters 2026-01-25 17:40:47 +08:00
yusing
6b09f54793 chore: upgrade dependencies 2026-01-25 17:31:29 +08:00
yusing
7b5ad3ab5e chore(docker): add go-proxmox module dependencies to Dockerfile 2026-01-25 17:30:23 +08:00
yusing
48790b4756 refactor(types): decouple Proxmox config from proxmox package
Decouple the types package from the internal/proxmox package by defining
a standalone ProxmoxConfig struct. This reduces circular dependencies
and allows the types package to define its own configuration structures
without importing the proxmox package.

The route validation logic now converts between types.ProxmoxConfig and
proxmox.NodeConfig where needed for internal operations.
2026-01-25 17:29:43 +08:00
yusing
727a3e9452 chore(docs): enhance README with Proxmox integration details
Added sections for Proxmox integration, including automatic route binding, WebUI management, and API endpoints. Updated existing content to reflect LXC lifecycle control and real-time logging capabilities for both Docker and Proxmox environments.
2026-01-25 17:29:42 +08:00
yusing
f8f1b54aaf fix(scripts/update-wiki): add "internal/go-proxmox/" to skipSubmodules list 2026-01-25 17:29:42 +08:00
yusing
cb8f405e76 feat(proxmox): add node-level stats endpoint with streaming support
Add new `/proxmox/stats/{node}` API endpoint for retrieving Proxmox node
statistics in JSON format. The endpoint returns kernel version, CPU
usage/model, memory usage, rootfs usage, uptime, and load averages.

The existing `/proxmox/stats/{node}/{vmid}` endpoint has been corrected `VMStats` to return`text/plain` instead of `application/json`.

Both endpoints support WebSocket streaming for real-time stats updates
with a 1-second poll interval.
2026-01-25 17:29:41 +08:00
yusing
51704829c6 refactor(proxmox): move NodeCommand to node_command.go 2026-01-25 17:29:36 +08:00
yusing
a1cc1d844d chore(docs): update package docs for proxmox and route/routes 2026-01-25 17:29:01 +08:00
yusing
633deb85ca feat(proxmox): support node-level routes and journalctl access
This change enables Proxmox node-level operations without requiring a specific
LXC container VMID.

**Features added:**
- New `/proxmox/journalctl/{node}` API endpoint for streaming node journalctl
- Route configuration support for Proxmox nodes (VMID = 0)
- `ReverseLookupNode` function for node discovery by hostname/IP/alias
- `NodeJournalctl` method for executing journalctl on nodes

**Behavior changes:**
- VMID parameter in journalctl endpoints is now optional
- Routes targeting nodes (without specific containers) are now valid

**Bug fixes:**
- Fixed error message variable reference in route validation
2026-01-25 17:29:00 +08:00
yusing
ee1c375fd9 refactor(proxmox): extract websocket command execution into reusable NodeCommand method
The LXCCommand method contained duplicate websocket handling logic for connecting to Proxmox's VNC terminal proxy. This refactoring extracts the common websocket connection, streaming, and cleanup logic into a new NodeCommand method on the Node type, allowing LXCCommand to simply format the pct command and delegate.

The go-proxmox submodule was also updated to access the NewNode constructor, which provides a cleaner API for creating node instances with the HTTP client.

- Moves ~100 lines of websocket handling from lxc_command.go to node.go
- Adds reusable NodeCommand method for executing commands via VNC websocket
- LXCCommand now simply calls NodeCommand with formatted command
- Maintains identical behavior and output streaming semantics
2026-01-25 17:28:58 +08:00
yusing
2713f5282e fix(proxmox): prevent goroutine leaks by closing idle HTTP connections
Added a function to close idle HTTP connections in the LXCCommand method. This addresses potential goroutine leaks caused by the go-proxmox library's TermWebSocket not closing underlying HTTP/2 connections. The websocket closer is now wrapped to ensure proper cleanup of transport connections when the command execution is finished.
2026-01-25 17:28:17 +08:00
yusing
f70cca0d00 fix(swagger): remove /api/v1 prefix from Proxmox endpoints
Streamline Proxmox API route paths by removing incorrect /api/v1 prefix.

Changed endpoints:
- /api/v1/proxmox/journalctl/{node}/{vmid} → /proxmox/journalctl/{node}/{vmid}
- /api/v1/proxmox/journalctl/{node}/{vmid}/{service} → /proxmox/journalctl/{node}/{vmid}/{service}
- /api/v1/proxmox/lxc/:node/:vmid/restart → /proxmox/lxc/:node/:vmid/restart
- /api/v1/proxmox/lxc/:node/:vmid/start → /proxmox/lxc/:node/:vmid/start
- /api/v1/proxmox/lxc/:node/:vmid/stop → /proxmox/lxc/:node/:vmid/stop
- /api/v1/proxmox/stats/{node}/{vmid} → /proxmox/stats/{node}/{vmid}

Updated:
- Swagger annotations in 5 Go source files
- Generated swagger.json and swagger.yaml documentation
2026-01-25 17:28:16 +08:00
yusing
3adefe4202 fix: add startup timeout guard to prevent indefinite hangs
Add a 10-second timeout mechanism during application initialization. If initialization
fails to complete within the timeout window, the application logs a fatal error and exits.
This prevents the proxy from becoming unresponsive during startup due to blocking operations
in parallel initialization tasks (DNS providers, icon cache, system info poller, middleware
loading, Docker client, API server, debug server, config watcher).

The timeout guard uses a background goroutine that listens for either a completion signal
(via closing the done channel) or the timeout expiration, providing a safety net for
long-running or blocked initialization scenarios.
2026-01-25 17:28:15 +08:00
yusing
966c873013 feat(proxmox): add LXC container control endpoints
Add start, stop, and restart endpoints for LXC containers via the Proxmox API:
- POST /api/v1/proxmox/lxc/:node/:vmid/start
- POST /api/v1/proxmox/lxc/:node/:vmid/stop
- POST /api/v1/proxmox/lxc/:node/:vmid/restart
2026-01-25 17:28:15 +08:00
yusing
b8c61c37dc feat(proxmox): add journalctl endpoint without service; add limit parameter
Added new Proxmox journalctl endpoint `/journalctl/:node/:vmid` for viewing all
journalctl output without requiring a service name. Made the service parameter
optional across both endpoints.

Introduced configurable `limit` query parameter (1-1000, default 100) to both
proxmox journalctl and docker logs APIs, replacing hardcoded 100-line tail.

Added container status check in LXCCommand to prevent command execution on
stopped containers, returning a clear status message instead.

Refactored route validation to use pre-fetched IPs and improved References()
method for proxmox routes with better alias handling.
2026-01-25 17:28:14 +08:00
yusing
22582cd32f chore(docs): update proxmox package docs 2026-01-25 17:28:14 +08:00
yusing
a46573cab3 feat(proxmox): enhance VM resource tracking with auto-discovery and cached IPs
- Add VMResource wrapper type with cached IP addresses for efficient lookups
- Implement concurrent IP fetching during resource updates (limited concurrency)
- Add ReverseLookupResource for discovering VMs by IP, hostname, or alias
- Prioritize interfaces API over config for IP retrieval (offline container fallback)
- Enable routes to auto-discover Proxmox resources when no explicit config provided
- Fix configuration type from value to pointer slice for correct proxmox client retrievel
- Ensure Proxmox providers are initialized before route validation
2026-01-25 17:28:13 +08:00
yusing
64e380cc40 feat(proxmox): add LXC container stats endpoint with streaming support
Implement a new API endpoint to retrieve real-time statistics for Proxmox
LXC containers, similar to `docker stats` functionality.

Changes:
- Add `GET /api/v1/proxmox/stats/:node/:vmid` endpoint with HTTP and WebSocket support
- Implement resource polling loop to cache VM metadata every 3 seconds
- Create `LXCStats()` method with streaming (websocket) and single-shot modes
- Format output as: STATUS|CPU%|MEM USAGE/LIMIT|MEM%|NET I/O|BLOCK I/O
- Add `GetResource()` method for efficient VM resource lookup by kind and ID
- Fix task creation bug using correct client reference

Example response:
  running|31.1%|9.6GiB/20GiB|48.87%|4.7GiB/3.3GiB|25GiB/36GiB
2026-01-25 17:28:05 +08:00
yusing
2f68c7c386 fix(proxmox): enhance LXCCommand skip logic
Updated the LXCCommand function to skip until`\x1b[H` and `\x1b[?2004`, ensuring no garbage output.
2026-01-25 17:26:47 +08:00
yusing
e3001a70ed refactor(proxmox): consolidate NodeConfig and add service field
Centralize Proxmox node configuration by moving `ProxmoxConfig` from `internal/types/idlewatcher.go` to a new `NodeConfig` struct in `internal/proxmox/node.go`.

- Add `proxmox` field to route; allowing `proxy.app.proxmox` labels and corresponding route file config
- Added `service` optional field to NodeConfig for service identification
- Integrated Proxmox config directly into Route struct with proper validation
- Propagate Proxmox settings to Idlewatcher during route validation
- Updated swagger documentation to reflect schema changes
2026-01-25 17:26:46 +08:00
yusing
ea6543b3f9 feat(proxmox): add journalctl streaming API endpoint for LXC containers
Add new /api/v1/proxmox/journalctl/:node/:vmid/:service endpoint that
streams real-time journalctl output from Proxmox LXC containers via
WebSocket connection. This enables live monitoring of container services
from the GoDoxy WebUI.

Implementation includes:
- New proxmox API handler with path parameter validation
- WebSocket upgrade for streaming output
- LXCCommand helper for executing commands over Proxmox VNC websocket
- LXCJournalctl wrapper for convenient journalctl -u service -f invocation
- Updated API documentation with proxmox integration
2026-01-25 17:26:46 +08:00
yusing
1793dd629f Requires authenticated Proxmox session with username/password configured.
refactor(proxmox): support for PAM authentication

- Added support for username and password authentication alongside existing token-based authentication.
- Updated validation rules to require either token or username/password for authentication.
- Modified the Init function to handle session creation based on the selected authentication method.
- Increased timeout duration for context in the Init function.
2026-01-25 17:26:45 +08:00
yusing
edf8c6ea32 feat(proxmox): add go-proxmox submodule for customized Proxmox integration
Add the go-proxmox library as a Git submodule to enable Proxmox
integration for container/VM management.

Submodule: https://github.com/yusing/go-proxmox
2026-01-25 17:26:45 +08:00
yusing
60cdffcf3c refactor(metrics): remove unused fields from RouteAggregate and update related documentation
- Removed `display_name`, `is_docker`, and `is_excluded` fields from the `RouteAggregate` struct and corresponding Swagger documentation.
- Updated references in the README and code to reflect the removal of these fields, ensuring consistency across the codebase.
2026-01-25 17:26:44 +08:00
yusing
717ed04b38 refactor(query): remove SearchRoute function and related documentation 2026-01-25 17:26:43 +08:00
yusing
80a6b21ff9 refactor(routes): replace route retrieval with GetIncludeExcluded
- Updated route retrieval in the API and idle watcher to use GetIncludeExcluded, allowing for the inclusion of excluded routes.
- Simplified the route status aggregation logic by directly using GetIncludeExcluded for display name resolution.
- Removed redundant code that separately handled excluded routes, streamlining the route management process.
2026-01-25 17:26:43 +08:00
yusing
e48c3f57dd feat(api): add endpoint to retrieve container stats
- Introduced a new GET endpoint `/docker/stats/:id` to fetch statistics for a specified container by its ID or route alias.
- Implemented the `Stats` function in the `dockerapi` package to handle the request and return container stats in both JSON and WebSocket formats.
- Added error handling for invalid requests and container not found scenarios.
2026-01-25 17:26:42 +08:00
yusing
39f427555f feat(ci): separate cache for different tags; utilize gha cache 2026-01-22 16:23:12 +08:00
yusing
280e455198 feat(ci): pass BRANCH to Makefile for correct build tag 2026-01-22 16:16:03 +08:00
yusing
082991694b feat(ci): enhance Docker image workflow to compute version based on Git tags and branches
- Added a step to checkout the repository for accurate tag resolution.
- Implemented logic to determine the build version based on the Git reference type, supporting tags and branch names.
- Updated the Docker build arguments to use the computed version for better versioning in images.
2026-01-22 16:12:01 +08:00
yusing
4ec9d818bf fix(Makefile): no longer add sonic tag to compat build 2026-01-22 16:11:46 +08:00
yusing
258d8921ad chore(deps): upgrade dependencies 2026-01-22 15:41:01 +08:00
yusing
2b8d416625 refactor(watcher): simplify config file watcher initialization using sync.Once 2026-01-22 15:36:48 +08:00
yusing
b4e9613efe refactor(memlogger): remove HTTP/WebSocket handler and simplify buffer management
Removes the embedded HTTP handler and WebSocket streaming capability from the
in-memory logger, leaving only the core io.Writer interface and event subscription
via Events(). Simplifies buffer management by eliminating position-based tracking
and using slices.Clone() for safe message passing to listeners.

- Removes HandlerFunc(), ServeHTTP(), wsInitial(), wsStreamLog() methods
- Removes logEntryRange struct and connChans map (no longer needed)
- Refactors buffer field from embedded to explicit buf with named mutexes
- Adds buffered channel (64) for event listeners to prevent blocking
- Improves concurrency with double-checked locking in truncation logic
2026-01-22 15:36:48 +08:00
yusing
25605208e4 fix(config): update JSON tags in ACL and access log configurations to omit empty values
Modified JSON tags in the Notify struct of ACL config and the ConfigBase and Retention structs in access log config to include 'omitempty'
2026-01-22 15:36:47 +08:00
yusing
29036bed18 fix(loadbalancer): change pool type from value to pointer 2026-01-22 15:36:47 +08:00
yusing
6d78f14cc7 fix(logging): update JSON tags in access log configuration to omit zero values
Modified JSON tags in the Filters and Fields structs to include 'omitzero', ensuring that zero values are not included in the serialized output.
2026-01-22 15:36:46 +08:00
yusing
4730753cb3 fix(logging): correct variable shadowing in NewLoggerWithFixedLevel causing incorrect log level being assigned 2026-01-22 15:36:46 +08:00
yusing
4fa2ef41aa feat(pool): introduce tombstone-based deletion with soft-delete mechanism
Refactored the pool implementation to use a tombstone-based deletion strategy
instead of immediate removal. This allows correct logging "reload"
instead of "removed" + "added" when an item is quickly deleted
and re-added within a short time window.

Changes:
- Items are now marked as tombstones upon deletion and retained for 1 second
- Added `PurgeExpiredTombs()` method for cleanup of expired tombstones
- Updated `Get`, `Iter`, and `Slice` to skip tombstoned entries
- Updated `Del` and `DelKey` to cleanup tombstones when exceeding threshold
- `AddIfNotExists` can now "reload" recently deleted items within the TTL
- Added tomb counter for tracking active tombstones and triggering purge
2026-01-22 15:36:45 +08:00
yusing
732376328c fix(acl): correctly marshal matchers instead of plain '{}'
- Introduced a raw field in the Matcher struct to store the original string representation.
- Implemented MarshalText method for Matcher
2026-01-22 15:36:45 +08:00
yusing
110fe4b0aa feat(api): enhance API handler to support unauthenticated local access
- Updated NewHandler function to accept a requireAuth parameter for authentication control.
- Introduced a new local API server that allows unauthenticated access when LocalAPIHTTPAddr is set.
- Adjusted server startup logic to handle both authenticated and unauthenticated API routes.
2026-01-22 15:36:36 +08:00
yusing
45ba8d447a fix(config): no longer show "http_route: added <route>" on startup 2026-01-21 14:32:28 +00:00
yusing
108417ca9d fix(websocket): log errors only for non-normal closure codes 2026-01-21 14:32:28 +00:00
yusing
bd1ff9731d refactor(accesslog): restructure access logging; enhance console output format
Major refactoring of the access logging infrastructure to improve code organization and add proper console/stdout logging support.

- Renamed `Writer` interface to `File` and consolidated with `SupportRotate`
- Renamed `Log(req, res)` to `LogRequest(req, res)` for clarity
- Added new `ConsoleLogger` with zerolog console writer for formatted stdout output
- Moved type definitions to new `types.go` file
- Changed buffer handling from `[]byte` returns to `*bytes.Buffer` parameters
- Renamed internal files for clarity (`access_logger.go` → `file_access_logger.go`)
- Fixed fileserver access logging timing: moved logging after handler execution with defer
- Correct response handling in Fileserver
- Remove deprecated field `buffer_size`
- Simplify and removed unnecessary code

All callers have been updated to use the new APIs.
2026-01-21 14:32:28 +00:00
yusing
235af71343 perf(accesslog): use buffer pool in BackScanner to reduce allocations
Replace per-scan byte slice allocations with a sized buffer pool,
significantly reducing memory pressure during log file scanning.

- Add Release() method to return buffers to pool (callers must invoke)
- Remove Reset() method - create new scanner instead for simpler lifecycle
- Refactor chunk prepending to reuse pooled buffers instead of append

Benchmark results show allocations dropped from ~26k to 1 per scan
for small chunk sizes, with better throughput.

BREAKING CHANGE: Reset() removed; callers must call Release() and
create a new BackScanner instance instead.
2026-01-21 14:32:28 +00:00
yusing
059306c93b fix(config): rename initAccessLogger to initACL 2026-01-21 14:32:28 +00:00
yusing
d938e24cf5 fix(acl): ensure acl behind proxy protocol for TCP; fix acl not working for TCP/UDP by replacing ActiveConfig with context value 2026-01-21 14:32:28 +00:00
yusing
ab1881d02e fix(acl): deny rules now have higher precedence than allow rules 2026-01-21 14:32:28 +00:00
FrozenFrog
90a4922b79 feat(middleware): implement CrowdSec WAF bouncer middleware (#196)
* crowdsec middleware
2026-01-21 14:32:28 +00:00
yusing
2022a0db82 fix(docker): correct 89cbcfee8c 2026-01-18 00:53:51 +08:00
yusing
4f8bb40d3d chore(docs): update package docs for internal/homepage 2026-01-17 16:16:24 +08:00
yusing
8ea296c99f fix(config): replace ToggleLog with DisableLog for clearer intent in loadRouteProviders 2026-01-17 16:16:16 +08:00
yusing
ccefaf003d fix(route): correct URL construction for IPv6 host 2026-01-17 16:16:08 +08:00
yusing
61236e0ace fix(docker): add container name to network not found error 2026-01-17 16:16:03 +08:00
yusing
6a89ab77c8 fix(docker): fix incorrect network not found error 2026-01-17 16:15:56 +08:00
yusing
9f1c279698 fix(idlewatcher): remove duplicated w.readyNotifyCh notification 2026-01-17 16:14:44 +08:00
yusing
89cbcfee8c fix(docker): add back client.WithAPIVersionNegotiation() to ensure docker compatibility 2026-01-17 15:27:49 +08:00
Charles GTE
2da4bbedbf fix(script): correct sed command in setup.sh for macos (#194)
use `uname -s` for OS detection

---------

Co-authored-by: charlesgauthereau <charles.gauthereau@soluce-technologies.com>
Co-authored-by: yusing <yusing.wys@gmail.com>
2026-01-17 15:26:37 +08:00
yusing
d73272b8e0 Rebased from main: Old CPU / Old docker version compatibility 2026-01-16 21:27:37 +08:00
230 changed files with 2421 additions and 7963 deletions

View File

@@ -56,10 +56,6 @@ GODOXY_HTTP3_ENABLED=true
# API listening address
GODOXY_API_ADDR=127.0.0.1:8888
# Local API listening address (unauthenticated, optional)
# Useful for local development, debugging or automation
GODOXY_LOCAL_API_ADDR=
# Metrics
GODOXY_METRICS_DISABLE_CPU=false
GODOXY_METRICS_DISABLE_MEMORY=false

View File

@@ -1,60 +0,0 @@
name: GoDoxy CLI Binary
on:
pull_request:
paths:
- "cmd/cli/**"
- "internal/api/v1/docs/swagger.json"
- "Makefile"
- ".github/workflows/cli-binary.yml"
push:
branches:
- main
paths:
- "cmd/cli/**"
- "internal/api/v1/docs/swagger.json"
- "Makefile"
- ".github/workflows/cli-binary.yml"
tags:
- v*
workflow_dispatch:
jobs:
build:
strategy:
matrix:
include:
- runner: ubuntu-latest
platform: linux/amd64
binary_name: godoxy-cli-linux-amd64
- runner: ubuntu-24.04-arm
platform: linux/arm64
binary_name: godoxy-cli-linux-arm64
name: Build ${{ matrix.platform }}
runs-on: ${{ matrix.runner }}
steps:
- uses: actions/checkout@v4
with:
submodules: "recursive"
- uses: actions/setup-go@v5
with:
go-version-file: go.mod
- name: Verify dependencies
run: go mod verify
- name: Build CLI
run: |
make CLI_BIN_PATH=bin/${{ matrix.binary_name }} build-cli
- name: Check binary
run: |
file bin/${{ matrix.binary_name }}
- name: Upload artifact
uses: actions/upload-artifact@v4
with:
name: ${{ matrix.binary_name }}
path: bin/${{ matrix.binary_name }}

View File

@@ -0,0 +1,20 @@
name: Docker Image CI (compat)
on:
push:
branches:
- compat
jobs:
build-compat:
uses: ./.github/workflows/docker-image.yml
with:
image_name: ${{ github.repository_owner }}/godoxy
tag: latest-compat
target: main
build-compat-agent:
uses: ./.github/workflows/docker-image.yml
with:
image_name: ${{ github.repository_owner }}/godoxy-agent
tag: latest-compat
target: agent

View File

@@ -8,6 +8,7 @@ on:
- "**" # matches every branch
- "!dependabot/*"
- "!main" # excludes main
- "!compat" # excludes compat branch
jobs:
build-nightly:

5
.gitignore vendored
View File

@@ -40,7 +40,4 @@ CLAUDE.md
!.trunk/configs
# minified files
**/*-min.*
# generated CLI commands
cmd/cli/generated_commands.go
**/*-min.*

View File

@@ -1,11 +1,11 @@
{
"yaml.schemas": {
"https://github.com/yusing/godoxy-webui/raw/refs/heads/main/src/types/godoxy/config.schema.json": [
"https://github.com/yusing/godoxy-webui/raw/refs/heads/main/types/godoxy/config.schema.json": [
"config.example.yml",
"config.yml"
],
"https://github.com/yusing/godoxy-webui/raw/refs/heads/main/src/types/godoxy/routes.schema.json": [
"https://github.com/yusing/godoxy-webui/raw/refs/heads/main/types/godoxy/routes.schema.json": [
"providers.example.yml"
]
}
}
}

View File

@@ -1,128 +0,0 @@
# Contributor Covenant Code of Conduct
## Our Pledge
We as members, contributors, and leaders pledge to make participation in our
community a harassment-free experience for everyone, regardless of age, body
size, visible or invisible 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.
We pledge to act and interact in ways that contribute to an open, welcoming,
diverse, inclusive, and healthy community.
## Our Standards
Examples of behavior that contributes to a positive environment for our
community include:
* Demonstrating empathy and kindness toward other people
* Being respectful of differing opinions, viewpoints, and experiences
* Giving and gracefully accepting constructive feedback
* Accepting responsibility and apologizing to those affected by our mistakes,
and learning from the experience
* Focusing on what is best not just for us as individuals, but for the
overall community
Examples of unacceptable behavior include:
* The use of sexualized language or imagery, and sexual attention or
advances of any kind
* Trolling, insulting or derogatory comments, and personal or political attacks
* Public or private harassment
* Publishing others' private information, such as a physical or email
address, without their explicit permission
* Other conduct which could reasonably be considered inappropriate in a
professional setting
## Enforcement Responsibilities
Community leaders are responsible for clarifying and enforcing our standards of
acceptable behavior and will take appropriate and fair corrective action in
response to any behavior that they deem inappropriate, threatening, offensive,
or harmful.
Community leaders 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, and will communicate reasons for moderation
decisions when appropriate.
## Scope
This Code of Conduct applies within all community spaces, and also applies when
an individual is officially representing the community in public spaces.
Examples of representing our community include using an official e-mail address,
posting via an official social media account, or acting as an appointed
representative at an online or offline event.
## Enforcement
Instances of abusive, harassing, or otherwise unacceptable behavior may be
reported to the community leaders responsible for enforcement at
yusing@6uo.me.
All complaints will be reviewed and investigated promptly and fairly.
All community leaders are obligated to respect the privacy and security of the
reporter of any incident.
## Enforcement Guidelines
Community leaders will follow these Community Impact Guidelines in determining
the consequences for any action they deem in violation of this Code of Conduct:
### 1. Correction
**Community Impact**: Use of inappropriate language or other behavior deemed
unprofessional or unwelcome in the community.
**Consequence**: A private, written warning from community leaders, providing
clarity around the nature of the violation and an explanation of why the
behavior was inappropriate. A public apology may be requested.
### 2. Warning
**Community Impact**: A violation through a single incident or series
of actions.
**Consequence**: A warning with consequences for continued behavior. No
interaction with the people involved, including unsolicited interaction with
those enforcing the Code of Conduct, for a specified period of time. This
includes avoiding interactions in community spaces as well as external channels
like social media. Violating these terms may lead to a temporary or
permanent ban.
### 3. Temporary Ban
**Community Impact**: A serious violation of community standards, including
sustained inappropriate behavior.
**Consequence**: A temporary ban from any sort of interaction or public
communication with the community for a specified period of time. No public or
private interaction with the people involved, including unsolicited interaction
with those enforcing the Code of Conduct, is allowed during this period.
Violating these terms may lead to a permanent ban.
### 4. Permanent Ban
**Community Impact**: Demonstrating a pattern of violation of community
standards, including sustained inappropriate behavior, harassment of an
individual, or aggression toward or disparagement of classes of individuals.
**Consequence**: A permanent ban from any sort of public interaction within
the community.
## Attribution
This Code of Conduct is adapted from the [Contributor Covenant][homepage],
version 2.0, available at
https://www.contributor-covenant.org/version/2/0/code_of_conduct.html.
Community Impact Guidelines were inspired by [Mozilla's code of conduct
enforcement ladder](https://github.com/mozilla/diversity).
[homepage]: https://www.contributor-covenant.org
For answers to common questions about this code of conduct, see the FAQ at
https://www.contributor-covenant.org/faq. Translations are available at
https://www.contributor-covenant.org/translations.

View File

@@ -7,7 +7,7 @@ export GOOS = linux
REPO_URL ?= https://github.com/yusing/godoxy
WEBUI_DIR ?= ../godoxy-webui
DOCS_DIR ?= wiki
DOCS_DIR ?= ${WEBUI_DIR}/wiki
ifneq ($(BRANCH), compat)
GO_TAGS = sonic
@@ -58,7 +58,6 @@ endif
BUILD_FLAGS += -tags '$(GO_TAGS)' -ldflags='$(LDFLAGS)'
BIN_PATH := $(shell pwd)/bin/${NAME}
CLI_BIN_PATH ?= $(shell pwd)/bin/godoxy-cli
export NAME
export CGO_ENABLED
@@ -179,20 +178,16 @@ gen-swagger:
python3 scripts/fix-swagger-json.py
# we don't need this
rm internal/api/v1/docs/docs.go
cp internal/api/v1/docs/swagger.json ${DOCS_DIR}/public/api.json
gen-swagger-markdown: gen-swagger
# brew tap go-swagger/go-swagger && brew install go-swagger
swagger generate markdown -f internal/api/v1/docs/swagger.yaml --skip-validation --output ${DOCS_DIR}/src/API.md
gen-api-types: gen-swagger
# --disable-throw-on-error
bunx --bun swagger-typescript-api generate --sort-types --generate-union-enums --axios --add-readonly --route-types \
--responses -o ${WEBUI_DIR}/src/lib -n api.ts -p internal/api/v1/docs/swagger.json
gen-cli:
cd cmd/cli && go run ./gen
build-cli: gen-cli
mkdir -p $(shell dirname ${CLI_BIN_PATH})
go build -C cmd/cli -o ${CLI_BIN_PATH} .
.PHONY: gen-cli build-cli update-wiki
.PHONY: update-wiki
update-wiki:
DOCS_DIR=${DOCS_DIR} REPO_URL=${REPO_URL} bun --bun scripts/update-wiki/main.ts

View File

@@ -36,6 +36,7 @@ Have questions? Ask [ChatGPT](https://chatgpt.com/g/g-6825390374b481919ad482f2e4
- [Proxmox Integration](#proxmox-integration)
- [Automatic Route Binding](#automatic-route-binding)
- [WebUI Management](#webui-management)
- [API Endpoints](#api-endpoints)
- [Update / Uninstall system agent](#update--uninstall-system-agent)
- [Screenshots](#screenshots)
- [idlesleeper](#idlesleeper)
@@ -166,8 +167,23 @@ routes:
From the WebUI, you can:
- **LXC Lifecycle Control**: Start, stop, restart containers
- **Node Logs**: Stream real-time journalctl or log files output from nodes
- **LXC Logs**: Stream real-time journalctl or log files output from containers
- **Node Logs**: Stream real-time journalctl output from nodes
- **LXC Logs**: Stream real-time journalctl output from containers
### API Endpoints
```http
# Node journalctl (WebSocket)
GET /api/v1/proxmox/journalctl/:node
# LXC journalctl (WebSocket)
GET /api/v1/proxmox/journalctl/:node/:vmid
# LXC lifecycle control
POST /api/v1/proxmox/lxc/:node/:vmid/start
POST /api/v1/proxmox/lxc/:node/:vmid/stop
POST /api/v1/proxmox/lxc/:node/:vmid/restart
```
## Update / Uninstall system agent

View File

@@ -37,6 +37,7 @@
- [Proxmox 整合](#proxmox-整合)
- [自動路由綁定](#自動路由綁定)
- [WebUI 管理](#webui-管理)
- [API 端點](#api-端點)
- [更新 / 卸載系統代理 (System Agent)](#更新--卸載系統代理-system-agent)
- [截圖](#截圖)
- [閒置休眠](#閒置休眠)
@@ -182,8 +183,23 @@ routes:
您可以從 WebUI
- **LXC 生命週期控制**:啟動、停止、重新啟動容器
- **節點日誌**:串流節點的即時 journalctl 或日誌檔案輸出
- **LXC 日誌**:串流容器的即時 journalctl 或日誌檔案輸出
- **節點日誌**:串流來自節點的即時 journalctl 輸出
- **LXC 日誌**:串流來自容器的即時 journalctl 輸出
### API 端點
```http
# 節點 journalctl (WebSocket)
GET /api/v1/proxmox/journalctl/:node
# LXC journalctl (WebSocket)
GET /api/v1/proxmox/journalctl/:node/:vmid
# LXC 生命週期控制
POST /api/v1/proxmox/lxc/:node/:vmid/start
POST /api/v1/proxmox/lxc/:node/:vmid/stop
POST /api/v1/proxmox/lxc/:node/:vmid/restart
```
## 更新 / 卸載系統代理 (System Agent)

View File

@@ -2,11 +2,6 @@ module github.com/yusing/godoxy/agent
go 1.26.0
exclude (
github.com/moby/moby/api v1.53.0 // allow older daemon versions
github.com/moby/moby/client v0.2.2 // allow older daemon versions
)
replace (
github.com/shirou/gopsutil/v4 => ../internal/gopsutil
github.com/yusing/godoxy => ../
@@ -19,8 +14,9 @@ replace (
exclude github.com/containerd/nerdctl/mod/tigron v0.0.0
exclude github.com/yusing/godoxy/internal/utils v0.0.0-20250927032450-e2aeef3a863f
require (
github.com/bytedance/sonic v1.15.0
github.com/gin-gonic/gin v1.11.0
github.com/gorilla/websocket v1.5.3
github.com/pion/dtls/v3 v3.1.2
@@ -36,6 +32,7 @@ require (
github.com/Microsoft/go-winio v0.6.2 // indirect
github.com/andybalholm/brotli v1.2.0 // indirect
github.com/bytedance/gopkg v0.1.3 // indirect
github.com/bytedance/sonic v1.15.0 // indirect
github.com/bytedance/sonic/loader v0.5.0 // indirect
github.com/cespare/xxhash/v2 v2.3.0 // indirect
github.com/cloudwego/base64x v0.1.6 // indirect
@@ -44,6 +41,7 @@ require (
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
github.com/distribution/reference v0.6.0 // indirect
github.com/docker/cli v29.2.1+incompatible // indirect
github.com/docker/docker v28.5.2+incompatible // indirect
github.com/docker/go-connections v0.6.0 // indirect
github.com/docker/go-units v0.5.0 // indirect
github.com/ebitengine/purego v0.9.1 // indirect
@@ -63,12 +61,13 @@ require (
github.com/klauspost/compress v1.18.4 // indirect
github.com/klauspost/cpuid/v2 v2.3.0 // indirect
github.com/leodido/go-urn v1.4.0 // indirect
github.com/lufia/plan9stats v0.0.0-20260216142805-b3301c5f2a88 // indirect
github.com/lufia/plan9stats v0.0.0-20251013123823-9fd1530e3ec3 // indirect
github.com/mattn/go-colorable v0.1.14 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect
github.com/moby/docker-image-spec v1.3.1 // indirect
github.com/moby/moby/api v1.52.0 // indirect
github.com/moby/moby/client v0.2.1 // indirect
github.com/moby/moby/api v1.53.0 // indirect
github.com/moby/sys/sequential v0.6.0 // indirect
github.com/moby/term v0.5.2 // indirect
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
github.com/modern-go/reflect2 v1.0.2 // indirect
github.com/opencontainers/go-digest v1.0.0 // indirect
@@ -76,6 +75,7 @@ require (
github.com/pelletier/go-toml/v2 v2.2.4 // indirect
github.com/pion/logging v0.2.4 // indirect
github.com/pion/transport/v4 v4.0.1 // indirect
github.com/pkg/errors v0.9.1 // indirect
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55 // indirect
github.com/puzpuzpuz/xsync/v4 v4.4.0 // indirect
@@ -91,12 +91,13 @@ require (
github.com/valyala/fasthttp v1.69.0 // indirect
github.com/yusing/ds v0.4.1 // indirect
github.com/yusing/gointernals v0.2.0 // indirect
github.com/yusing/goutils/http/reverseproxy v0.0.0-20260218062549-0b0fa3a059ec // indirect
github.com/yusing/goutils/http/websocket v0.0.0-20260218062549-0b0fa3a059ec // indirect
github.com/yusing/goutils/http/reverseproxy v0.0.0-20260215081811-494ab85a33ae // indirect
github.com/yusing/goutils/http/websocket v0.0.0-20260215081811-494ab85a33ae // indirect
github.com/yusufpapurcu/wmi v1.2.4 // indirect
go.opentelemetry.io/auto/sdk v1.2.1 // indirect
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.65.0 // indirect
go.opentelemetry.io/otel v1.40.0 // indirect
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.38.0 // indirect
go.opentelemetry.io/otel/metric v1.40.0 // indirect
go.opentelemetry.io/otel/trace v1.40.0 // indirect
golang.org/x/arch v0.24.0 // indirect
@@ -104,6 +105,7 @@ require (
golang.org/x/net v0.50.0 // indirect
golang.org/x/sys v0.41.0 // indirect
golang.org/x/text v0.34.0 // indirect
google.golang.org/genproto/googleapis/api v0.0.0-20251222181119-0a764e51fe1b // indirect
google.golang.org/protobuf v1.36.11 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
)

View File

@@ -1,3 +1,5 @@
github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c h1:udKWzYgxTojEKWjV8V+WSxDXJ4NFATAsZjh8iIbsQIg=
github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E=
github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY=
github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU=
github.com/PuerkitoBio/goquery v1.11.0 h1:jZ7pwMQXIITcUXNH83LLk+txlaEy6NVOfTuP43xxfqw=
@@ -24,6 +26,8 @@ github.com/containerd/errdefs v1.0.0 h1:tg5yIfIlQIrxYtu9ajqY42W3lpS19XqdxRQeEwYG
github.com/containerd/errdefs v1.0.0/go.mod h1:+YBYIdtsnF4Iw6nWZhJcqGSg/dwvV7tyJ/kCkyJ2k+M=
github.com/containerd/errdefs/pkg v0.3.0 h1:9IKJ06FvyNlexW690DXuQNx2KA2cUJXx151Xdx3ZPPE=
github.com/containerd/errdefs/pkg v0.3.0/go.mod h1:NJw6s9HwNuRhnjJhM7pylWwMyAkmCQvQ4GpJHEqRLVk=
github.com/containerd/log v0.1.0 h1:TCJt7ioM2cr/tfR8GPbGf9/VRAX8D2B4PjzCpfX540I=
github.com/containerd/log v0.1.0/go.mod h1:VRRf09a7mHDIRezVKTRCrOq78v577GXq3bSa3EhrzVo=
github.com/coreos/go-oidc/v3 v3.17.0 h1:hWBGaQfbi0iVviX4ibC7bk8OKT5qNr4klBaCHVNvehc=
github.com/coreos/go-oidc/v3 v3.17.0/go.mod h1:wqPbKFrVnE90vty060SB40FCJ8fTHTxSwyXJqZH+sI8=
github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc=
@@ -39,6 +43,8 @@ github.com/djherbis/times v1.6.0 h1:w2ctJ92J8fBvWPxugmXIv7Nz7Q3iDMKNx9v5ocVH20c=
github.com/djherbis/times v1.6.0/go.mod h1:gOHeRAz2h+VJNZ5Gmc/o7iD9k4wW7NMVqieYCY99oc0=
github.com/docker/cli v29.2.1+incompatible h1:n3Jt0QVCN65eiVBoUTZQM9mcQICCJt3akW4pKAbKdJg=
github.com/docker/cli v29.2.1+incompatible/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8=
github.com/docker/docker v28.5.2+incompatible h1:DBX0Y0zAjZbSrm1uzOkdr1onVghKaftjlSWt4AFexzM=
github.com/docker/docker v28.5.2+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk=
github.com/docker/go-connections v0.6.0 h1:LlMG9azAe1TqfR7sO+NJttz1gy6KO7VJBh+pMmjSD94=
github.com/docker/go-connections v0.6.0/go.mod h1:AahvXYshr6JgfUJGdDCs2b5EZG/vmaMAntpSFH5BFKE=
github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4=
@@ -55,8 +61,8 @@ github.com/gin-contrib/sse v1.1.0 h1:n0w2GMuUpWDVp7qSpvze6fAu9iRxJY4Hmj6AmBOU05w
github.com/gin-contrib/sse v1.1.0/go.mod h1:hxRZ5gVpWMT7Z0B0gSNYqqsSCNIJMjzvm6fqCz9vjwM=
github.com/gin-gonic/gin v1.11.0 h1:OW/6PLjyusp2PPXtyxKHU0RbX6I/l28FTdDlae5ueWk=
github.com/gin-gonic/gin v1.11.0/go.mod h1:+iq/FyxlGzII0KHiBGjuNn4UNENUlKbGlNmc+W50Dls=
github.com/go-acme/lego/v4 v4.32.0 h1:z7Ss7aa1noabhKj+DBzhNCO2SM96xhE3b0ucVW3x8Tc=
github.com/go-acme/lego/v4 v4.32.0/go.mod h1:lI2fZNdgeM/ymf9xQ9YKbgZm6MeDuf91UrohMQE4DhI=
github.com/go-acme/lego/v4 v4.31.0 h1:gd4oUYdfs83PR1/SflkNdit9xY1iul2I4EystnU8NXM=
github.com/go-acme/lego/v4 v4.31.0/go.mod h1:m6zcfX/zcbMYDa8s6AnCMnoORWNP8Epnei+6NBCTUGs=
github.com/go-jose/go-jose/v4 v4.1.3 h1:CVLmWDhDVRa6Mi/IgCgaopNosCaHz7zrMeF9MlZRkrs=
github.com/go-jose/go-jose/v4 v4.1.3/go.mod h1:x4oUasVrzR7071A4TnHLGSPpNOm2a21K9Kf04k1rs08=
github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
@@ -95,6 +101,8 @@ github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aN
github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
github.com/gotify/server/v2 v2.9.0 h1:2zRCl28wkq0oc6YNbyJS2n0dDOOVvOS3Oez5AG2ij54=
github.com/gotify/server/v2 v2.9.0/go.mod h1:249wwlUqHTr0QsiKARGtFVqds0pNLIMjYLinHyMACdQ=
github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.3 h1:NmZ1PKzSTQbuGHw9DGPFomqkkLWMC+vZCkfs+FHv1Vg=
github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.3/go.mod h1:zQrxl1YP88HQlA6i9c63DSVPFklWpGX4OWAc9bFuaH4=
github.com/jinzhu/copier v0.4.0 h1:w3ciUoD19shMCRargcpm0cm91ytaBhDvuRpz1ODO/U8=
github.com/jinzhu/copier v0.4.0/go.mod h1:DfbEm0FYsaqBcKcFuvmOZb218JkPGtvSHsKg8S8hyyg=
github.com/json-iterator/go v1.1.13-0.20220915233716-71ac16282d12 h1:9Nu54bhS/H/Kgo2/7xNSUuC5G28VR8ljfrLKU2G4IjU=
@@ -111,10 +119,10 @@ github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ=
github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI=
github.com/lithammer/fuzzysearch v1.1.8 h1:/HIuJnjHuXS8bKaiTMeeDlW2/AyIWk2brx1V8LFgLN4=
github.com/lithammer/fuzzysearch v1.1.8/go.mod h1:IdqeyBClc3FFqSzYq/MXESsS4S0FsZ5ajtkr5xPLts4=
github.com/lufia/plan9stats v0.0.0-20260216142805-b3301c5f2a88 h1:PTw+yKnXcOFCR6+8hHTyWBeQ/P4Nb7dd4/0ohEcWQuM=
github.com/lufia/plan9stats v0.0.0-20260216142805-b3301c5f2a88/go.mod h1:autxFIvghDt3jPTLoqZ9OZ7s9qTGNAWmYCjVFWPX/zg=
github.com/luthermonson/go-proxmox v0.4.0 h1:LKXpG9d64zTaQF79wV0kfOnnSwIcdG39m7sc4ga+XZs=
github.com/luthermonson/go-proxmox v0.4.0/go.mod h1:U6dAkJ+iiwaeb1g/LMWpWuWN4nmvWeXhmoMuYJMumS4=
github.com/lufia/plan9stats v0.0.0-20251013123823-9fd1530e3ec3 h1:PwQumkgq4/acIiZhtifTV5OUqqiP82UAl0h87xj/l9k=
github.com/lufia/plan9stats v0.0.0-20251013123823-9fd1530e3ec3/go.mod h1:autxFIvghDt3jPTLoqZ9OZ7s9qTGNAWmYCjVFWPX/zg=
github.com/luthermonson/go-proxmox v0.3.2 h1:/zUg6FCl9cAABx0xU3OIgtDtClY0gVXxOCsrceDNylc=
github.com/luthermonson/go-proxmox v0.3.2/go.mod h1:oyFgg2WwTEIF0rP6ppjiixOHa5ebK1p8OaRiFhvICBQ=
github.com/magefile/mage v1.15.0 h1:BvGheCMAsG3bWUDbZ8AyXXpCNwU9u5CB6sM+HNb9HYg=
github.com/magefile/mage v1.15.0/go.mod h1:z5UZb/iS3GoOSn0JgWuiw7dxlurVYTu+/jHXqQg881A=
github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
@@ -128,15 +136,21 @@ github.com/miekg/dns v1.1.72 h1:vhmr+TF2A3tuoGNkLDFK9zi36F2LS+hKTRW0Uf8kbzI=
github.com/miekg/dns v1.1.72/go.mod h1:+EuEPhdHOsfk6Wk5TT2CzssZdqkmFhf8r+aVyDEToIs=
github.com/moby/docker-image-spec v1.3.1 h1:jMKff3w6PgbfSa69GfNg+zN/XLhfXJGnEx3Nl2EsFP0=
github.com/moby/docker-image-spec v1.3.1/go.mod h1:eKmb5VW8vQEh/BAr2yvVNvuiJuY6UIocYsFu/DxxRpo=
github.com/moby/moby/api v1.52.0 h1:00BtlJY4MXkkt84WhUZPRqt5TvPbgig2FZvTbe3igYg=
github.com/moby/moby/api v1.52.0/go.mod h1:8mb+ReTlisw4pS6BRzCMts5M49W5M7bKt1cJy/YbAqc=
github.com/moby/moby/client v0.2.1 h1:1Grh1552mvv6i+sYOdY+xKKVTvzJegcVMhuXocyDz/k=
github.com/moby/moby/client v0.2.1/go.mod h1:O+/tw5d4a1Ha/ZA/tPxIZJapJRUS6LNZ1wiVRxYHyUE=
github.com/moby/moby/api v1.53.0 h1:PihqG1ncw4W+8mZs69jlwGXdaYBeb5brF6BL7mPIS/w=
github.com/moby/moby/api v1.53.0/go.mod h1:8mb+ReTlisw4pS6BRzCMts5M49W5M7bKt1cJy/YbAqc=
github.com/moby/sys/atomicwriter v0.1.0 h1:kw5D/EqkBwsBFi0ss9v1VG3wIkVhzGvLklJ+w3A14Sw=
github.com/moby/sys/atomicwriter v0.1.0/go.mod h1:Ul8oqv2ZMNHOceF643P6FKPXeCmYtlQMvpizfsSoaWs=
github.com/moby/sys/sequential v0.6.0 h1:qrx7XFUd/5DxtqcoH1h438hF5TmOvzC/lspjy7zgvCU=
github.com/moby/sys/sequential v0.6.0/go.mod h1:uyv8EUTrca5PnDsdMGXhZe6CCe8U/UiTWd+lL+7b/Ko=
github.com/moby/term v0.5.2 h1:6qk3FJAFDs6i/q3W/pQ97SX192qKfZgGjCQqfCJkgzQ=
github.com/moby/term v0.5.2/go.mod h1:d3djjFCrjnB+fl8NJux+EJzu0msscUP+f8it8hPkFLc=
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M=
github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
github.com/morikuni/aec v1.0.0 h1:nP9CBfwrvYnBRgY6qfDQkygYDmYwOilePFkwzv4dU8A=
github.com/morikuni/aec v1.0.0/go.mod h1:BbKIizmSmc5MMPqRYbxO4ZU0S0+P200+tUnFx7PXmsc=
github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U=
github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM=
github.com/opencontainers/image-spec v1.1.1 h1:y0fUlFfIZhPF1W537XOLg0/fcx6zcHCJwooC2xJA040=
@@ -155,6 +169,7 @@ github.com/pion/transport/v4 v4.0.1 h1:sdROELU6BZ63Ab7FrOLn13M6YdJLY20wldXW2Cu2k
github.com/pion/transport/v4 v4.0.1/go.mod h1:nEuEA4AD5lPdcIegQDpVLgNoDGreqM/YqmEx3ovP4jM=
github.com/pires/go-proxyproto v0.11.0 h1:gUQpS85X/VJMdUsYyEgyn59uLJvGqPhJV5YvG68wXH4=
github.com/pires/go-proxyproto v0.11.0/go.mod h1:ZKAAyp3cgy5Y5Mo4n9AlScrkCZwUy0g3Jf+slqQVcuU=
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U=
@@ -220,6 +235,10 @@ go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.65.0 h1:7iP2uCb
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.65.0/go.mod h1:c7hN3ddxs/z6q9xwvfLPk+UHlWRQyaeR1LdgfL/66l0=
go.opentelemetry.io/otel v1.40.0 h1:oA5YeOcpRTXq6NN7frwmwFR0Cn3RhTVZvXsP4duvCms=
go.opentelemetry.io/otel v1.40.0/go.mod h1:IMb+uXZUKkMXdPddhwAHm6UfOwJyh4ct1ybIlV14J0g=
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.38.0 h1:GqRJVj7UmLjCVyVJ3ZFLdPRmhDUp2zFmQe3RHIOsw24=
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.38.0/go.mod h1:ri3aaHSmCTVYu2AWv44YMauwAQc0aqI9gHKIcSbI1pU=
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.38.0 h1:aTL7F04bJHUlztTsNGJ2l+6he8c+y/b//eR0jjjemT4=
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.38.0/go.mod h1:kldtb7jDTeol0l3ewcmd8SDvx3EmIE7lyvqbasU3QC4=
go.opentelemetry.io/otel/metric v1.40.0 h1:rcZe317KPftE2rstWIBitCdVp89A2HqjkxR3c11+p9g=
go.opentelemetry.io/otel/metric v1.40.0/go.mod h1:ib/crwQH7N3r5kfiBZQbwrTge743UDc7DTFVZrrXnqc=
go.opentelemetry.io/otel/sdk v1.40.0 h1:KHW/jUzgo6wsPh9At46+h4upjtccTmuZCFAc9OJ71f8=
@@ -228,6 +247,8 @@ go.opentelemetry.io/otel/sdk/metric v1.40.0 h1:mtmdVqgQkeRxHgRv4qhyJduP3fYJRMX4A
go.opentelemetry.io/otel/sdk/metric v1.40.0/go.mod h1:4Z2bGMf0KSK3uRjlczMOeMhKU2rhUqdWNoKcYrtcBPg=
go.opentelemetry.io/otel/trace v1.40.0 h1:WA4etStDttCSYuhwvEa8OP8I5EWu24lkOzp+ZYblVjw=
go.opentelemetry.io/otel/trace v1.40.0/go.mod h1:zeAhriXecNGP/s2SEG3+Y8X9ujcJOTqQ5RgdEJcawiA=
go.opentelemetry.io/proto/otlp v1.9.0 h1:l706jCMITVouPOqEnii2fIAuO3IVGBRPV5ICjceRb/A=
go.opentelemetry.io/proto/otlp v1.9.0/go.mod h1:xE+Cx5E/eEHw+ISFkwPLwCZefwVjY+pqKg1qcK03+/4=
go.uber.org/atomic v1.11.0 h1:ZvwS0R+56ePWxUNi+Atn9dWONBPp/AUETXlHW0DxSjE=
go.uber.org/atomic v1.11.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0=
go.uber.org/mock v0.5.2 h1:LbtPTcP8A5k9WPXj54PPPbjcI4Y6lhyOZXn+VS7wNko=
@@ -258,6 +279,12 @@ golang.org/x/time v0.14.0 h1:MRx4UaLrDotUKUdCIqzPC48t1Y9hANFKIRpNx+Te8PI=
golang.org/x/time v0.14.0/go.mod h1:eL/Oa2bBBK0TkX57Fyni+NgnyQQN4LitPmob2Hjnqw4=
golang.org/x/tools v0.42.0 h1:uNgphsn75Tdz5Ji2q36v/nsFSfR/9BRFvqhGBaJGd5k=
golang.org/x/tools v0.42.0/go.mod h1:Ma6lCIwGZvHK6XtgbswSoWroEkhugApmsXyrUmBhfr0=
google.golang.org/genproto/googleapis/api v0.0.0-20251222181119-0a764e51fe1b h1:uA40e2M6fYRBf0+8uN5mLlqUtV192iiksiICIBkYJ1E=
google.golang.org/genproto/googleapis/api v0.0.0-20251222181119-0a764e51fe1b/go.mod h1:Xa7le7qx2vmqB/SzWUBa7KdMjpdpAHlh5QCSnjessQk=
google.golang.org/genproto/googleapis/rpc v0.0.0-20260209200024-4cfbd4190f57 h1:mWPCjDEyshlQYzBpMNHaEof6UX1PmHcaUODUywQ0uac=
google.golang.org/genproto/googleapis/rpc v0.0.0-20260209200024-4cfbd4190f57/go.mod h1:j9x/tPzZkyxcgEFkiKEEGxfvyumM01BEtsW8xzOahRQ=
google.golang.org/grpc v1.79.1 h1:zGhSi45ODB9/p3VAawt9a+O/MULLl9dpizzNNpq7flY=
google.golang.org/grpc v1.79.1/go.mod h1:KmT0Kjez+0dde/v2j9vzwoAScgEPx/Bw1CYChhHLrHQ=
google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE=
google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
@@ -268,5 +295,3 @@ gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gotest.tools/v3 v3.5.2 h1:7koQfIKdy+I8UTetycgUqXWSDwpgv193Ka+qRsmBY8Q=
gotest.tools/v3 v3.5.2/go.mod h1:LtdLGcnqToBH83WByAAi/wiwSFCArdFIUV/xxN4pcjA=
pgregory.net/rapid v1.2.0 h1:keKAYRcjm+e1F0oAuU5F5+YPAWcyxNNRK2wud503Gnk=
pgregory.net/rapid v1.2.0/go.mod h1:PY5XlDGj0+V1FCq0o192FdRhpKHGTRIWBgqjDBTrq04=

View File

@@ -1,4 +1,4 @@
# agent/pkg/agent
# Agent Package
The `agent` package provides the client-side implementation for interacting with GoDoxy agents. It handles agent configuration, secure communication via TLS, and provides utilities for agent deployment and management.

View File

@@ -4,6 +4,7 @@ import (
"context"
"crypto/tls"
"crypto/x509"
"encoding/json"
"encoding/pem"
"errors"
"fmt"
@@ -15,7 +16,6 @@ import (
"strings"
"time"
"github.com/bytedance/sonic"
"github.com/rs/zerolog"
"github.com/rs/zerolog/log"
"github.com/yusing/godoxy/agent/pkg/agent/common"
@@ -366,7 +366,7 @@ func (cfg *AgentConfig) fetchJSON(ctx context.Context, endpoint string, out any)
return resp.StatusCode, nil
}
err = sonic.Unmarshal(data, out)
err = json.Unmarshal(data, out)
if err != nil {
return 0, err
}

View File

@@ -1,4 +1,4 @@
# agent/pkg/agent/stream
# Stream proxy protocol
This package implements a small header-based handshake that allows an authenticated client to request forwarding to a `(host, port)` destination. It supports both TCP-over-TLS and UDP-over-DTLS transports.

View File

@@ -21,10 +21,8 @@ const (
var version = [versionSize]byte{'0', '.', '1', '.', '0', 0, 0, 0}
var (
ErrInvalidHeader = errors.New("invalid header")
ErrCloseImmediately = errors.New("close immediately")
)
var ErrInvalidHeader = errors.New("invalid header")
var ErrCloseImmediately = errors.New("close immediately")
type FlagType uint8

View File

@@ -45,10 +45,9 @@ func TestTLSALPNMux_HTTPAndStreamShareOnePort(t *testing.T) {
defer func() { _ = tlsLn.Close() }()
// HTTP server
httpSrv := &http.Server{
Handler: http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
_, _ = w.Write([]byte("ok"))
}),
httpSrv := &http.Server{Handler: http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
_, _ = w.Write([]byte("ok"))
}),
TLSNextProto: map[string]func(*http.Server, *tls.Conn, http.Handler){
stream.StreamALPN: func(_ *http.Server, conn *tls.Conn, _ http.Handler) {
streamSrv.ServeConn(conn)

View File

@@ -2,11 +2,11 @@ package agentproxy
import (
"encoding/base64"
"encoding/json"
"net/http"
"strconv"
"time"
"github.com/bytedance/sonic"
route "github.com/yusing/godoxy/internal/route/types"
)
@@ -53,7 +53,7 @@ func proxyConfigFromHeaders(h http.Header) (cfg Config, err error) {
return cfg, err
}
err = sonic.Unmarshal(cfgJSON, &cfg)
err = json.Unmarshal(cfgJSON, &cfg)
return cfg, err
}
@@ -67,7 +67,7 @@ func (cfg *Config) SetAgentProxyConfigHeadersLegacy(h http.Header) {
func (cfg *Config) SetAgentProxyConfigHeaders(h http.Header) {
h.Set(HeaderXProxyHost, cfg.Host)
h.Set(HeaderXProxyScheme, string(cfg.Scheme))
cfgJSON, _ := sonic.Marshal(cfg.HTTPConfig)
cfgJSON, _ := json.Marshal(cfg.HTTPConfig)
cfgBase64 := base64.StdEncoding.EncodeToString(cfgJSON)
h.Set(HeaderXProxyConfig, cfgBase64)
}

View File

@@ -1,6 +1,7 @@
package handler
import (
"encoding/json"
"net"
"net/http"
"net/url"
@@ -8,7 +9,6 @@ import (
"strings"
"time"
"github.com/bytedance/sonic"
healthcheck "github.com/yusing/godoxy/internal/health/check"
"github.com/yusing/godoxy/internal/types"
)
@@ -73,7 +73,7 @@ func CheckHealth(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusOK)
sonic.ConfigDefault.NewEncoder(w).Encode(result)
json.NewEncoder(w).Encode(result)
}
func parseMsOrDefault(msStr string) time.Duration {

View File

@@ -1,9 +1,9 @@
package handler
import (
"encoding/json"
"net/http"
"github.com/bytedance/sonic"
"github.com/gin-gonic/gin"
"github.com/gorilla/websocket"
"github.com/yusing/godoxy/agent/pkg/agent"
@@ -51,7 +51,7 @@ func NewAgentHandler() http.Handler {
Runtime: env.Runtime,
}
w.Header().Set("Content-Type", "application/json")
sonic.ConfigDefault.NewEncoder(w).Encode(agentInfo)
json.NewEncoder(w).Encode(agentInfo)
})
mux.HandleEndpoint("GET", agent.EndpointHealth, CheckHealth)
mux.HandleEndpoint("GET", agent.EndpointSystemInfo, metricsHandler.ServeHTTP)

View File

@@ -2,14 +2,13 @@ package main
import (
"log"
"math/rand/v2"
"net/http"
"math/rand/v2"
)
var (
printables = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"
random = make([]byte, 4096)
)
var printables = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"
var random = make([]byte, 4096)
func init() {
for i := range random {

View File

@@ -1,730 +0,0 @@
package main
import (
"bytes"
"encoding/json"
"errors"
"flag"
"fmt"
"io"
"net/http"
"net/url"
"os"
"sort"
"strconv"
"strings"
"time"
"github.com/gorilla/websocket"
"github.com/yusing/goutils/env"
)
type config struct {
Addr string
}
type stringSliceFlag struct {
set bool
v []string
}
func (s *stringSliceFlag) String() string {
return strings.Join(s.v, ",")
}
func (s *stringSliceFlag) Set(value string) error {
s.set = true
if value == "" {
s.v = nil
return nil
}
s.v = strings.Split(value, ",")
return nil
}
func run(args []string) error {
cfg, rest, err := parseGlobal(args)
if err != nil {
return err
}
if len(rest) == 0 {
printHelp()
return nil
}
if rest[0] == "help" {
printHelp()
return nil
}
ep, matchedLen := findEndpoint(rest)
if ep == nil {
ep, matchedLen = findEndpointAlias(rest)
}
if ep == nil {
return unknownCommandError(rest)
}
cmdArgs := rest[matchedLen:]
return executeEndpoint(cfg.Addr, *ep, cmdArgs)
}
func parseGlobal(args []string) (config, []string, error) {
var cfg config
fs := flag.NewFlagSet("godoxy", flag.ContinueOnError)
fs.SetOutput(io.Discard)
fs.StringVar(&cfg.Addr, "addr", "", "API address, e.g. 127.0.0.1:8888 or http://127.0.0.1:8888")
if err := fs.Parse(args); err != nil {
return cfg, nil, err
}
return cfg, fs.Args(), nil
}
func resolveBaseURL(addrFlag string) (string, error) {
if addrFlag != "" {
return normalizeURL(addrFlag), nil
}
_, _, _, fullURL := env.GetAddrEnv("LOCAL_API_ADDR", "", "http")
if fullURL == "" {
return "", errors.New("missing LOCAL_API_ADDR (or GODOXY_LOCAL_API_ADDR). set env var or pass --addr")
}
return normalizeURL(fullURL), nil
}
func normalizeURL(addr string) string {
a := strings.TrimSpace(addr)
if strings.Contains(a, "://") {
return strings.TrimRight(a, "/")
}
return "http://" + strings.TrimRight(a, "/")
}
func findEndpoint(args []string) (*Endpoint, int) {
var best *Endpoint
bestLen := -1
for i := range generatedEndpoints {
ep := &generatedEndpoints[i]
if len(ep.CommandPath) > len(args) {
continue
}
ok := true
for j, tok := range ep.CommandPath {
if args[j] != tok {
ok = false
break
}
}
if ok && len(ep.CommandPath) > bestLen {
best = ep
bestLen = len(ep.CommandPath)
}
}
return best, bestLen
}
func executeEndpoint(addrFlag string, ep Endpoint, args []string) error {
fs := flag.NewFlagSet(strings.Join(ep.CommandPath, "-"), flag.ContinueOnError)
fs.SetOutput(io.Discard)
useWS := false
if ep.IsWebSocket {
fs.BoolVar(&useWS, "ws", false, "use websocket")
}
typedValues := make(map[string]any, len(ep.Params))
isSet := make(map[string]bool, len(ep.Params))
for _, p := range ep.Params {
switch p.Type {
case "integer":
v := new(int)
fs.IntVar(v, p.FlagName, 0, p.Description)
typedValues[p.FlagName] = v
case "number":
v := new(float64)
fs.Float64Var(v, p.FlagName, 0, p.Description)
typedValues[p.FlagName] = v
case "boolean":
v := new(bool)
fs.BoolVar(v, p.FlagName, false, p.Description)
typedValues[p.FlagName] = v
case "array":
v := &stringSliceFlag{}
fs.Var(v, p.FlagName, p.Description+" (comma-separated)")
typedValues[p.FlagName] = v
default:
v := new(string)
fs.StringVar(v, p.FlagName, "", p.Description)
typedValues[p.FlagName] = v
}
}
if err := fs.Parse(args); err != nil {
return fmt.Errorf("%w\n\n%s", err, formatEndpointHelp(ep))
}
if len(fs.Args()) > 0 {
return fmt.Errorf("unexpected args: %s\n\n%s", strings.Join(fs.Args(), " "), formatEndpointHelp(ep))
}
fs.Visit(func(f *flag.Flag) {
isSet[f.Name] = true
})
for _, p := range ep.Params {
if !p.Required {
continue
}
if !isSet[p.FlagName] {
return fmt.Errorf("missing required flag --%s\n\n%s", p.FlagName, formatEndpointHelp(ep))
}
}
baseURL, err := resolveBaseURL(addrFlag)
if err != nil {
return err
}
reqURL, body, err := buildRequest(ep, baseURL, typedValues, isSet)
if err != nil {
return err
}
if useWS {
if !ep.IsWebSocket {
return errors.New("--ws is only supported for websocket endpoints")
}
return execWebsocket(ep, reqURL)
}
return execHTTP(ep, reqURL, body)
}
func buildRequest(ep Endpoint, baseURL string, typedValues map[string]any, isSet map[string]bool) (string, []byte, error) {
path := ep.Path
for _, p := range ep.Params {
if p.In != "path" {
continue
}
raw, err := paramValueString(p, typedValues[p.FlagName], isSet[p.FlagName])
if err != nil {
return "", nil, err
}
if raw == "" {
continue
}
esc := url.PathEscape(raw)
path = strings.ReplaceAll(path, "{"+p.Name+"}", esc)
path = strings.ReplaceAll(path, ":"+p.Name, esc)
}
u, err := url.Parse(baseURL)
if err != nil {
return "", nil, fmt.Errorf("invalid base url: %w", err)
}
u.Path = strings.TrimRight(u.Path, "/") + path
q := u.Query()
for _, p := range ep.Params {
if p.In != "query" || !isSet[p.FlagName] {
continue
}
val, err := paramQueryValues(p, typedValues[p.FlagName])
if err != nil {
return "", nil, err
}
for _, v := range val {
q.Add(p.Name, v)
}
}
u.RawQuery = q.Encode()
bodyMap := map[string]any{}
rawBody := ""
for _, p := range ep.Params {
if p.In != "body" || !isSet[p.FlagName] {
continue
}
if p.Name == "file" {
s, err := paramValueString(p, typedValues[p.FlagName], true)
if err != nil {
return "", nil, err
}
rawBody = s
continue
}
v, err := paramBodyValue(p, typedValues[p.FlagName])
if err != nil {
return "", nil, err
}
bodyMap[p.Name] = v
}
if rawBody != "" {
return u.String(), []byte(rawBody), nil
}
if len(bodyMap) == 0 {
return u.String(), nil, nil
}
data, err := json.Marshal(bodyMap)
if err != nil {
return "", nil, fmt.Errorf("marshal body: %w", err)
}
return u.String(), data, nil
}
func paramValueString(p Param, raw any, wasSet bool) (string, error) {
if !wasSet {
return "", nil
}
switch v := raw.(type) {
case *string:
return *v, nil
case *int:
return strconv.Itoa(*v), nil
case *float64:
return strconv.FormatFloat(*v, 'f', -1, 64), nil
case *bool:
if *v {
return "true", nil
}
return "false", nil
case *stringSliceFlag:
return strings.Join(v.v, ","), nil
default:
return "", fmt.Errorf("unsupported flag value for %s", p.FlagName)
}
}
func paramQueryValues(p Param, raw any) ([]string, error) {
switch v := raw.(type) {
case *string:
return []string{*v}, nil
case *int:
return []string{strconv.Itoa(*v)}, nil
case *float64:
return []string{strconv.FormatFloat(*v, 'f', -1, 64)}, nil
case *bool:
if *v {
return []string{"true"}, nil
}
return []string{"false"}, nil
case *stringSliceFlag:
if len(v.v) == 0 {
return nil, nil
}
return v.v, nil
default:
return nil, fmt.Errorf("unsupported query flag type for %s", p.FlagName)
}
}
func paramBodyValue(p Param, raw any) (any, error) {
switch v := raw.(type) {
case *string:
if p.Type == "object" || p.Type == "array" {
var decoded any
if err := json.Unmarshal([]byte(*v), &decoded); err != nil {
return nil, fmt.Errorf("invalid JSON for --%s: %w", p.FlagName, err)
}
return decoded, nil
}
return *v, nil
case *int:
return *v, nil
case *float64:
return *v, nil
case *bool:
return *v, nil
case *stringSliceFlag:
return v.v, nil
default:
return nil, fmt.Errorf("unsupported body flag type for %s", p.FlagName)
}
}
func execHTTP(ep Endpoint, reqURL string, body []byte) error {
var r io.Reader
if body != nil {
r = bytes.NewReader(body)
}
req, err := http.NewRequest(ep.Method, reqURL, r)
if err != nil {
return err
}
if body != nil {
req.Header.Set("Content-Type", "application/json")
}
client := &http.Client{Timeout: 30 * time.Second}
resp, err := client.Do(req)
if err != nil {
return err
}
defer resp.Body.Close()
payload, err := io.ReadAll(resp.Body)
if err != nil {
return err
}
if resp.StatusCode < 200 || resp.StatusCode >= 300 {
if len(payload) == 0 {
return fmt.Errorf("%s %s failed: %s", ep.Method, ep.Path, resp.Status)
}
return fmt.Errorf("%s %s failed: %s: %s", ep.Method, ep.Path, resp.Status, strings.TrimSpace(string(payload)))
}
printJSON(payload)
return nil
}
func execWebsocket(ep Endpoint, reqURL string) error {
wsURL := strings.Replace(reqURL, "http://", "ws://", 1)
wsURL = strings.Replace(wsURL, "https://", "wss://", 1)
if strings.ToUpper(ep.Method) != http.MethodGet {
return fmt.Errorf("--ws requires GET endpoint, got %s", ep.Method)
}
c, _, err := websocket.DefaultDialer.Dial(wsURL, nil)
if err != nil {
return err
}
defer c.Close()
stopPing := make(chan struct{})
defer close(stopPing)
go func() {
ticker := time.NewTicker(3 * time.Second)
defer ticker.Stop()
for {
select {
case <-stopPing:
return
case <-ticker.C:
_ = c.SetWriteDeadline(time.Now().Add(2 * time.Second))
if err := c.WriteMessage(websocket.TextMessage, []byte("ping")); err != nil {
return
}
}
}
}()
for {
_, msg, err := c.ReadMessage()
if err != nil {
if websocket.IsCloseError(err, websocket.CloseNormalClosure, websocket.CloseGoingAway) || strings.Contains(err.Error(), "close") {
return nil
}
return err
}
if string(msg) == "pong" {
continue
}
fmt.Println(string(msg))
}
}
func printJSON(payload []byte) {
if len(payload) == 0 {
fmt.Println("null")
return
}
var v any
if err := json.Unmarshal(payload, &v); err != nil {
fmt.Println(strings.TrimSpace(string(payload)))
return
}
enc := json.NewEncoder(os.Stdout)
enc.SetIndent("", " ")
_ = enc.Encode(v)
}
func printHelp() {
fmt.Println("godoxy [--addr ADDR] <command>")
fmt.Println()
fmt.Println("Examples:")
fmt.Println(" godoxy version")
fmt.Println(" godoxy route list")
fmt.Println(" godoxy route route --which whoami")
fmt.Println()
printGroupedCommands()
}
func printGroupedCommands() {
grouped := map[string][]Endpoint{}
groupOrder := make([]string, 0)
seen := map[string]bool{}
for _, ep := range generatedEndpoints {
group := "root"
if len(ep.CommandPath) > 1 {
group = ep.CommandPath[0]
}
grouped[group] = append(grouped[group], ep)
if !seen[group] {
seen[group] = true
groupOrder = append(groupOrder, group)
}
}
sort.Strings(groupOrder)
for _, group := range groupOrder {
fmt.Printf("Commands (%s):\n", group)
sort.Slice(grouped[group], func(i, j int) bool {
li := strings.Join(grouped[group][i].CommandPath, " ")
lj := strings.Join(grouped[group][j].CommandPath, " ")
return li < lj
})
maxCmdWidth := 0
for _, ep := range grouped[group] {
cmd := strings.Join(ep.CommandPath, " ")
if len(cmd) > maxCmdWidth {
maxCmdWidth = len(cmd)
}
}
for _, ep := range grouped[group] {
cmd := strings.Join(ep.CommandPath, " ")
fmt.Printf(" %-*s %s\n", maxCmdWidth, cmd, ep.Summary)
}
fmt.Println()
}
}
func unknownCommandError(rest []string) error {
cmd := strings.Join(rest, " ")
var b strings.Builder
b.WriteString("unknown command: ")
b.WriteString(cmd)
if len(rest) > 0 && hasGroup(rest[0]) {
if len(rest) > 1 {
if hint := nearestForGroup(rest[0], rest[1]); hint != "" {
b.WriteString("\nDo you mean ")
b.WriteString(hint)
b.WriteString("?")
}
}
b.WriteString("\n\n")
b.WriteString(formatGroupHelp(rest[0]))
return errors.New(b.String())
}
if hint := nearestCommand(cmd); hint != "" {
b.WriteString("\nDo you mean ")
b.WriteString(hint)
b.WriteString("?")
}
b.WriteString("\n\n")
b.WriteString("Run `godoxy help` for available commands.")
return errors.New(b.String())
}
func findEndpointAlias(args []string) (*Endpoint, int) {
var best *Endpoint
bestLen := -1
for i := range generatedEndpoints {
alias := aliasCommandPath(generatedEndpoints[i])
if len(alias) == 0 || len(alias) > len(args) {
continue
}
ok := true
for j, tok := range alias {
if args[j] != tok {
ok = false
break
}
}
if ok && len(alias) > bestLen {
best = &generatedEndpoints[i]
bestLen = len(alias)
}
}
return best, bestLen
}
func aliasCommandPath(ep Endpoint) []string {
rawPath := strings.TrimPrefix(ep.Path, "/api/v1/")
rawPath = strings.Trim(rawPath, "/")
if rawPath == "" {
return nil
}
parts := strings.Split(rawPath, "/")
if len(parts) == 1 {
if isPathParam(parts[0]) {
return nil
}
return []string{toKebabToken(parts[0])}
}
if isPathParam(parts[0]) || isPathParam(parts[1]) {
return nil
}
return []string{toKebabToken(parts[0]), toKebabToken(parts[1])}
}
func isPathParam(s string) bool {
return strings.HasPrefix(s, "{") || strings.HasPrefix(s, ":")
}
func toKebabToken(s string) string {
s = strings.ReplaceAll(s, "_", "-")
return strings.ToLower(strings.Trim(s, "-"))
}
func hasGroup(group string) bool {
for _, ep := range generatedEndpoints {
if len(ep.CommandPath) > 1 && ep.CommandPath[0] == group {
return true
}
}
return false
}
func nearestCommand(input string) string {
commands := make([]string, 0, len(generatedEndpoints))
for _, ep := range generatedEndpoints {
commands = append(commands, strings.Join(ep.CommandPath, " "))
}
return nearestByDistance(input, commands)
}
func nearestForGroup(group, input string) string {
choiceSet := map[string]struct{}{}
for _, ep := range generatedEndpoints {
if len(ep.CommandPath) < 2 || ep.CommandPath[0] != group {
continue
}
choiceSet[ep.CommandPath[1]] = struct{}{}
alias := aliasCommandPath(ep)
if len(alias) == 2 && alias[0] == group {
choiceSet[alias[1]] = struct{}{}
}
}
choices := make([]string, 0, len(choiceSet))
for choice := range choiceSet {
choices = append(choices, choice)
}
if len(choices) == 0 {
return ""
}
return group + " " + nearestByDistance(input, choices)
}
func formatGroupHelp(group string) string {
commands := make([]Endpoint, 0)
for _, ep := range generatedEndpoints {
if len(ep.CommandPath) > 1 && ep.CommandPath[0] == group {
commands = append(commands, ep)
}
}
sort.Slice(commands, func(i, j int) bool {
return strings.Join(commands[i].CommandPath, " ") < strings.Join(commands[j].CommandPath, " ")
})
maxWidth := 0
for _, ep := range commands {
cmd := strings.Join(ep.CommandPath, " ")
if len(cmd) > maxWidth {
maxWidth = len(cmd)
}
}
var b strings.Builder
fmt.Fprintf(&b, "Available subcommands for %s:\n", group)
for _, ep := range commands {
cmd := strings.Join(ep.CommandPath, " ")
fmt.Fprintf(&b, " %-*s %s\n", maxWidth, cmd, ep.Summary)
}
return strings.TrimRight(b.String(), "\n")
}
func formatEndpointHelp(ep Endpoint) string {
cmd := "godoxy " + strings.Join(ep.CommandPath, " ")
var b strings.Builder
fmt.Fprintf(&b, "Usage: %s [flags]\n", cmd)
if ep.Summary != "" {
fmt.Fprintf(&b, "Summary: %s\n", ep.Summary)
}
if alias := aliasCommandPath(ep); len(alias) > 0 && strings.Join(alias, " ") != strings.Join(ep.CommandPath, " ") {
fmt.Fprintf(&b, "Alias: godoxy %s\n", strings.Join(alias, " "))
}
params := make([]Param, 0, len(ep.Params))
params = append(params, ep.Params...)
if ep.IsWebSocket {
params = append(params, Param{
FlagName: "ws",
Type: "boolean",
Description: "use websocket",
Required: false,
})
}
if len(params) == 0 {
return strings.TrimRight(b.String(), "\n")
}
b.WriteString("Flags:\n")
maxWidth := 0
flagNames := make([]string, 0, len(params))
for _, p := range params {
name := "--" + p.FlagName
if p.Required {
name += " (required)"
}
flagNames = append(flagNames, name)
if len(name) > maxWidth {
maxWidth = len(name)
}
}
for i, p := range params {
desc := p.Description
if desc == "" {
desc = p.In + " " + p.Type
}
fmt.Fprintf(&b, " %-*s %s\n", maxWidth, flagNames[i], desc)
}
return strings.TrimRight(b.String(), "\n")
}
func nearestByDistance(input string, choices []string) string {
if len(choices) == 0 {
return ""
}
nearest := choices[0]
minDistance := levenshteinDistance(input, nearest)
for _, choice := range choices[1:] {
d := levenshteinDistance(input, choice)
if d < minDistance {
minDistance = d
nearest = choice
}
}
return nearest
}
//nolint:intrange
func levenshteinDistance(a, b string) int {
if a == b {
return 0
}
if len(a) == 0 {
return len(b)
}
if len(b) == 0 {
return len(a)
}
v0 := make([]int, len(b)+1)
v1 := make([]int, len(b)+1)
for i := 0; i <= len(b); i++ {
v0[i] = i
}
for i := 0; i < len(a); i++ {
v1[0] = i + 1
for j := 0; j < len(b); j++ {
cost := 0
if a[i] != b[j] {
cost = 1
}
v1[j+1] = min3(v1[j]+1, v0[j+1]+1, v0[j]+cost)
}
for j := 0; j <= len(b); j++ {
v0[j] = v1[j]
}
}
return v1[len(b)]
}
func min3(a, b, c int) int {
if a < b && a < c {
return a
}
if b < a && b < c {
return b
}
return c
}

View File

@@ -1,366 +0,0 @@
package main
import (
"bytes"
"encoding/json"
"fmt"
"go/format"
"os"
"path/filepath"
"sort"
"strings"
"unicode"
)
type swaggerSpec struct {
BasePath string `json:"basePath"`
Paths map[string]map[string]operation `json:"paths"`
Definitions map[string]definition `json:"definitions"`
}
type operation struct {
OperationID string `json:"operationId"`
Summary string `json:"summary"`
Tags []string `json:"tags"`
Parameters []parameter `json:"parameters"`
}
type parameter struct {
Name string `json:"name"`
In string `json:"in"`
Required bool `json:"required"`
Type string `json:"type"`
Description string `json:"description"`
Schema *schemaRef `json:"schema"`
}
type schemaRef struct {
Ref string `json:"$ref"`
}
type definition struct {
Type string `json:"type"`
Required []string `json:"required"`
Properties map[string]definition `json:"properties"`
Items *definition `json:"items"`
}
type endpoint struct {
CommandPath []string
Method string
Path string
Summary string
IsWebSocket bool
Params []param
}
type param struct {
FlagName string
Name string
In string
Type string
Required bool
Description string
}
func main() {
root := filepath.Join("..", "..")
inPath := filepath.Join(root, "internal", "api", "v1", "docs", "swagger.json")
outPath := "generated_commands.go"
raw, err := os.ReadFile(inPath)
must(err)
var spec swaggerSpec
must(json.Unmarshal(raw, &spec))
eps := buildEndpoints(spec)
must(writeGenerated(outPath, eps))
}
func buildEndpoints(spec swaggerSpec) []endpoint {
byCommand := map[string]endpoint{}
pathKeys := make([]string, 0, len(spec.Paths))
for p := range spec.Paths {
pathKeys = append(pathKeys, p)
}
sort.Strings(pathKeys)
for _, p := range pathKeys {
methodMap := spec.Paths[p]
methods := make([]string, 0, len(methodMap))
for m := range methodMap {
methods = append(methods, strings.ToUpper(m))
}
sort.Strings(methods)
for _, method := range methods {
op := methodMap[strings.ToLower(method)]
if op.OperationID == "" {
continue
}
ep := endpoint{
CommandPath: commandPathFromOp(p, op.OperationID),
Method: method,
Path: ensureSlash(spec.BasePath) + normalizePath(p),
Summary: op.Summary,
IsWebSocket: hasTag(op.Tags, "websocket"),
Params: collectParams(spec, op),
}
key := strings.Join(ep.CommandPath, " ")
if existing, ok := byCommand[key]; ok {
if betterEndpoint(ep, existing) {
byCommand[key] = ep
}
continue
}
byCommand[key] = ep
}
}
out := make([]endpoint, 0, len(byCommand))
for _, ep := range byCommand {
out = append(out, ep)
}
sort.Slice(out, func(i, j int) bool {
ai := strings.Join(out[i].CommandPath, " ")
aj := strings.Join(out[j].CommandPath, " ")
return ai < aj
})
return out
}
func commandPathFromOp(path, opID string) []string {
parts := strings.Split(strings.Trim(path, "/"), "/")
if len(parts) == 0 {
return []string{toKebab(opID)}
}
if len(parts) == 1 {
return []string{toKebab(parts[0])}
}
group := toKebab(parts[0])
name := toKebab(opID)
if name == group {
name = "get"
}
if group == "v1" {
return []string{name}
}
return []string{group, name}
}
func collectParams(spec swaggerSpec, op operation) []param {
params := make([]param, 0)
for _, p := range op.Parameters {
switch p.In {
case "body":
if p.Schema != nil && p.Schema.Ref != "" {
defName := strings.TrimPrefix(p.Schema.Ref, "#/definitions/")
params = append(params, bodyParamsFromDef(spec.Definitions[defName])...)
continue
}
params = append(params, param{
FlagName: toKebab(p.Name),
Name: p.Name,
In: "body",
Type: defaultType(p.Type),
Required: p.Required,
Description: p.Description,
})
default:
params = append(params, param{
FlagName: toKebab(p.Name),
Name: p.Name,
In: p.In,
Type: defaultType(p.Type),
Required: p.Required,
Description: p.Description,
})
}
}
// Deduplicate by flag name, prefer required entries.
byFlag := map[string]param{}
for _, p := range params {
if cur, ok := byFlag[p.FlagName]; ok {
if !cur.Required && p.Required {
byFlag[p.FlagName] = p
}
continue
}
byFlag[p.FlagName] = p
}
out := make([]param, 0, len(byFlag))
for _, p := range byFlag {
out = append(out, p)
}
sort.Slice(out, func(i, j int) bool {
if out[i].In != out[j].In {
return out[i].In < out[j].In
}
return out[i].FlagName < out[j].FlagName
})
return out
}
func bodyParamsFromDef(def definition) []param {
if def.Type != "object" {
return nil
}
requiredSet := map[string]struct{}{}
for _, name := range def.Required {
requiredSet[name] = struct{}{}
}
keys := make([]string, 0, len(def.Properties))
for k := range def.Properties {
keys = append(keys, k)
}
sort.Strings(keys)
out := make([]param, 0, len(keys))
for _, k := range keys {
prop := def.Properties[k]
_, required := requiredSet[k]
t := defaultType(prop.Type)
if prop.Type == "array" {
t = "array"
}
if prop.Type == "object" {
t = "object"
}
out = append(out, param{
FlagName: toKebab(k),
Name: k,
In: "body",
Type: t,
Required: required,
})
}
return out
}
func betterEndpoint(a, b endpoint) bool {
// Prefer GET, then fewer path params, then shorter path.
if a.Method == "GET" && b.Method != "GET" {
return true
}
if a.Method != "GET" && b.Method == "GET" {
return false
}
ac := countPathParams(a.Path)
bc := countPathParams(b.Path)
if ac != bc {
return ac < bc
}
return len(a.Path) < len(b.Path)
}
func countPathParams(path string) int {
count := 0
for _, seg := range strings.Split(path, "/") {
if strings.HasPrefix(seg, "{") || strings.HasPrefix(seg, ":") {
count++
}
}
return count
}
func normalizePath(p string) string {
parts := strings.Split(p, "/")
for i, part := range parts {
if strings.HasPrefix(part, "{") && strings.HasSuffix(part, "}") {
name := strings.TrimSuffix(strings.TrimPrefix(part, "{"), "}")
parts[i] = "{" + name + "}"
}
}
return strings.Join(parts, "/")
}
func hasTag(tags []string, want string) bool {
for _, t := range tags {
if strings.EqualFold(t, want) {
return true
}
}
return false
}
func writeGenerated(outPath string, eps []endpoint) error {
var b bytes.Buffer
b.WriteString("// Code generated by cmd/cli/gen. DO NOT EDIT.\n")
b.WriteString("package main\n\n")
b.WriteString("var generatedEndpoints = []Endpoint{\n")
for _, ep := range eps {
b.WriteString("\t{\n")
fmt.Fprintf(&b, "\t\tCommandPath: %#v,\n", ep.CommandPath)
fmt.Fprintf(&b, "\t\tMethod: %q,\n", ep.Method)
fmt.Fprintf(&b, "\t\tPath: %q,\n", ep.Path)
fmt.Fprintf(&b, "\t\tSummary: %q,\n", ep.Summary)
fmt.Fprintf(&b, "\t\tIsWebSocket: %t,\n", ep.IsWebSocket)
b.WriteString("\t\tParams: []Param{\n")
for _, p := range ep.Params {
b.WriteString("\t\t\t{\n")
fmt.Fprintf(&b, "\t\t\t\tFlagName: %q,\n", p.FlagName)
fmt.Fprintf(&b, "\t\t\t\tName: %q,\n", p.Name)
fmt.Fprintf(&b, "\t\t\t\tIn: %q,\n", p.In)
fmt.Fprintf(&b, "\t\t\t\tType: %q,\n", p.Type)
fmt.Fprintf(&b, "\t\t\t\tRequired: %t,\n", p.Required)
fmt.Fprintf(&b, "\t\t\t\tDescription: %q,\n", p.Description)
b.WriteString("\t\t\t},\n")
}
b.WriteString("\t\t},\n")
b.WriteString("\t},\n")
}
b.WriteString("}\n")
formatted, err := format.Source(b.Bytes())
if err != nil {
return fmt.Errorf("format generated source: %w", err)
}
return os.WriteFile(outPath, formatted, 0o644)
}
func ensureSlash(s string) string {
if strings.HasPrefix(s, "/") {
return s
}
return "/" + s
}
func defaultType(t string) string {
switch t {
case "integer", "number", "boolean", "array", "object", "string":
return t
default:
return "string"
}
}
func toKebab(s string) string {
if s == "" {
return s
}
s = strings.ReplaceAll(s, "_", "-")
s = strings.ReplaceAll(s, ".", "-")
var out []rune
for i, r := range s {
if unicode.IsUpper(r) {
if i > 0 && out[len(out)-1] != '-' {
out = append(out, '-')
}
out = append(out, unicode.ToLower(r))
continue
}
out = append(out, unicode.ToLower(r))
}
res := strings.Trim(string(out), "-")
for strings.Contains(res, "--") {
res = strings.ReplaceAll(res, "--", "-")
}
return res
}
func must(err error) {
if err != nil {
panic(err)
}
}

View File

@@ -1,10 +0,0 @@
module github.com/yusing/godoxy/cli
go 1.26.0
require (
github.com/gorilla/websocket v1.5.3
github.com/yusing/goutils v0.7.0
)
replace github.com/yusing/goutils => ../../goutils

View File

@@ -1,10 +0,0 @@
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/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg=
github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=

View File

@@ -1,13 +0,0 @@
package main
import (
"fmt"
"os"
)
func main() {
if err := run(os.Args[1:]); err != nil {
fmt.Fprintln(os.Stderr, "error:", err)
os.Exit(1)
}
}

View File

@@ -1,19 +0,0 @@
package main
type Param struct {
FlagName string
Name string
In string
Type string
Required bool
Description string
}
type Endpoint struct {
CommandPath []string
Method string
Path string
Summary string
IsWebSocket bool
Params []Param
}

View File

@@ -7,7 +7,6 @@ import (
"net/http"
"github.com/gin-gonic/gin"
"github.com/rs/zerolog/log"
"github.com/yusing/godoxy/internal/api"
apiV1 "github.com/yusing/godoxy/internal/api/v1"
agentApi "github.com/yusing/godoxy/internal/api/v1/agent"
@@ -129,31 +128,25 @@ func listenDebugServer() {
mux.mux.HandleFunc("/favicon.ico", func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "image/svg+xml")
w.WriteHeader(http.StatusOK)
fmt.Fprint(w, `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100"><text x="50" y="50" text-anchor="middle" dominant-baseline="middle">🐙</text></svg>`)
w.Write([]byte(`<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100"><text x="50" y="50" text-anchor="middle" dominant-baseline="middle">🐙</text></svg>`))
})
mux.HandleFunc("Auth block page", "GET", "/auth/block", AuthBlockPageHandler)
mux.HandleFunc("Idlewatcher loading page", "GET", idlewatcherTypes.PathPrefix, idlewatcher.DebugHandler)
apiHandler := newAPIHandler(mux)
apiHandler := newApiHandler(mux)
mux.mux.HandleFunc("/api/v1/", apiHandler.ServeHTTP)
mux.Finalize()
go func() {
//nolint:gosec
err := http.ListenAndServe(":7777", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Pragma", "no-cache")
w.Header().Set("Cache-Control", "no-cache, no-store, must-revalidate")
w.Header().Set("Expires", "0")
mux.mux.ServeHTTP(w, r)
}))
if err != nil {
log.Err(err).Msg("Error starting debug server")
}
}()
go http.ListenAndServe(":7777", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Pragma", "no-cache")
w.Header().Set("Cache-Control", "no-cache, no-store, must-revalidate")
w.Header().Set("Expires", "0")
mux.mux.ServeHTTP(w, r)
}))
}
func newAPIHandler(debugMux *debugMux) *gin.Engine {
func newApiHandler(debugMux *debugMux) *gin.Engine {
r := gin.New()
r.Use(api.ErrorHandler())
r.Use(api.ErrorLoggingMiddleware())

55
go.mod
View File

@@ -2,11 +2,6 @@ module github.com/yusing/godoxy
go 1.26.0
exclude (
github.com/moby/moby/api v1.53.0 // allow older daemon versions
github.com/moby/moby/client v0.2.2 // allow older daemon versions
)
replace (
github.com/coreos/go-oidc/v3 => ./internal/go-oidc
github.com/luthermonson/go-proxmox => ./internal/go-proxmox
@@ -19,13 +14,15 @@ replace (
github.com/yusing/goutils/server => ./goutils/server
)
exclude github.com/luthermonson/go-proxmox v0.3.0
require (
github.com/PuerkitoBio/goquery v1.11.0 // parsing HTML for extract fav icon; modify_html middleware
github.com/cenkalti/backoff/v5 v5.0.3 // backoff for retrying operations
github.com/coreos/go-oidc/v3 v3.17.0 // oidc authentication
github.com/docker/docker v28.5.2+incompatible // docker daemon
github.com/fsnotify/fsnotify v1.9.0 // file watcher
github.com/gin-gonic/gin v1.11.0 // api server
github.com/go-acme/lego/v4 v4.32.0 // acme client
github.com/go-acme/lego/v4 v4.31.0 // acme client
github.com/go-playground/validator/v10 v10.30.1 // validator
github.com/gobwas/glob v0.2.3 // glob matcher for route rules
github.com/gorilla/websocket v1.5.3 // websocket for API and agent
@@ -44,27 +41,25 @@ require (
require (
github.com/bytedance/gopkg v0.1.3 // xxhash64 for fast hash
github.com/bytedance/sonic v1.15.0 // fast json parsing
github.com/bytedance/sonic v1.15.0 // indirect; fast json parsing
github.com/docker/cli v29.2.1+incompatible // needs docker/cli/cli/connhelper connection helper for docker client
github.com/goccy/go-yaml v1.19.2 // yaml parsing for different config files
github.com/golang-jwt/jwt/v5 v5.3.1 // jwt authentication
github.com/luthermonson/go-proxmox v0.4.0 // proxmox API client
github.com/moby/moby/api v1.52.0 // docker API
github.com/moby/moby/client v0.2.1 // docker client
github.com/oschwald/maxminddb-golang v1.13.1 // maxminddb for geoip database
github.com/golang-jwt/jwt/v5 v5.3.1
github.com/luthermonson/go-proxmox v0.3.2
github.com/oschwald/maxminddb-golang v1.13.1
github.com/quic-go/quic-go v0.59.0 // http3 support
github.com/shirou/gopsutil/v4 v4.26.1 // system information
github.com/spf13/afero v1.15.0 // afero for file system operations
github.com/stretchr/testify v1.11.1 // testing framework
github.com/valyala/fasthttp v1.69.0 // fast http for health check
github.com/yusing/ds v0.4.1 // data structures and algorithms
github.com/yusing/godoxy/agent v0.0.0-20260218101334-add7884a365e
github.com/yusing/godoxy/internal/dnsproviders v0.0.0-20260218101334-add7884a365e
github.com/yusing/godoxy/agent v0.0.0-20260216003355-b4a9f44f4ee9
github.com/yusing/godoxy/internal/dnsproviders v0.0.0-20260216003355-b4a9f44f4ee9
github.com/yusing/gointernals v0.2.0
github.com/yusing/goutils v0.7.0
github.com/yusing/goutils/http/reverseproxy v0.0.0-20260218062549-0b0fa3a059ec
github.com/yusing/goutils/http/websocket v0.0.0-20260218062549-0b0fa3a059ec
github.com/yusing/goutils/server v0.0.0-20260218062549-0b0fa3a059ec
github.com/yusing/goutils/http/reverseproxy v0.0.0-20260215081811-494ab85a33ae
github.com/yusing/goutils/http/websocket v0.0.0-20260215081811-494ab85a33ae
github.com/yusing/goutils/server v0.0.0-20260215081811-494ab85a33ae
)
require (
@@ -122,11 +117,11 @@ require (
github.com/ovh/go-ovh v1.9.0 // indirect
github.com/pelletier/go-toml/v2 v2.2.4 // indirect
github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c // indirect
github.com/pkg/errors v0.9.1 // indirect
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
github.com/quic-go/qpack v0.6.0 // indirect
github.com/samber/lo v1.52.0 // indirect
github.com/samber/slog-common v0.20.0 // indirect
github.com/samber/slog-zerolog/v2 v2.9.1 // indirect
github.com/scaleway/scaleway-sdk-go v1.0.0-beta.36 // indirect
github.com/sirupsen/logrus v1.9.4 // indirect
github.com/sony/gobreaker v1.0.0 // indirect
@@ -142,8 +137,8 @@ require (
golang.org/x/sys v0.41.0 // indirect
golang.org/x/text v0.34.0 // indirect
golang.org/x/tools v0.42.0 // indirect
google.golang.org/api v0.267.0 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20260217215200-42d3e9bedb6d // indirect
google.golang.org/api v0.266.0 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20260209200024-4cfbd4190f57 // indirect
google.golang.org/grpc v1.79.1 // indirect
google.golang.org/protobuf v1.36.11 // indirect
gopkg.in/ini.v1 v1.67.1 // indirect
@@ -151,15 +146,20 @@ require (
gopkg.in/yaml.v3 v3.0.1 // indirect
)
require github.com/moby/moby/api v1.53.0
require (
github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c // indirect
github.com/akamai/AkamaiOPEN-edgegrid-golang/v11 v11.1.0 // indirect
github.com/andybalholm/brotli v1.2.0 // indirect
github.com/boombuler/barcode v1.1.0 // indirect
github.com/bytedance/sonic/loader v0.5.0 // indirect
github.com/cenkalti/backoff/v5 v5.0.3 // indirect
github.com/cespare/xxhash/v2 v2.3.0 // indirect
github.com/cloudwego/base64x v0.1.6 // indirect
github.com/containerd/errdefs v1.0.0 // indirect
github.com/containerd/errdefs/pkg v0.3.0 // indirect
github.com/containerd/log v0.1.0 // indirect
github.com/fatih/color v1.18.0 // indirect
github.com/fatih/structs v1.1.0 // indirect
github.com/gin-contrib/sse v1.1.0 // indirect
@@ -169,28 +169,33 @@ require (
github.com/go-viper/mapstructure/v2 v2.5.0 // indirect
github.com/goccy/go-json v0.10.5 // indirect
github.com/google/go-querystring v1.2.0 // indirect
github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.3 // indirect
github.com/klauspost/compress v1.18.4 // indirect
github.com/klauspost/cpuid/v2 v2.3.0 // indirect
github.com/kolo/xmlrpc v0.0.0-20220921171641-a4b6fa1dd06b // indirect
github.com/linode/linodego v1.65.0 // indirect
github.com/lufia/plan9stats v0.0.0-20260216142805-b3301c5f2a88 // indirect
github.com/lufia/plan9stats v0.0.0-20251013123823-9fd1530e3ec3 // indirect
github.com/moby/sys/atomicwriter v0.1.0 // indirect
github.com/morikuni/aec v1.0.0 // indirect
github.com/nrdcg/goinwx v0.12.0 // indirect
github.com/nrdcg/oci-go-sdk/common/v1065 v1065.108.2 // indirect
github.com/nrdcg/oci-go-sdk/dns/v1065 v1065.108.2 // indirect
github.com/nrdcg/oci-go-sdk/common/v1065 v1065.108.1 // indirect
github.com/nrdcg/oci-go-sdk/dns/v1065 v1065.108.1 // indirect
github.com/pierrec/lz4/v4 v4.1.21 // indirect
github.com/pion/dtls/v3 v3.1.2 // indirect
github.com/pion/logging v0.2.4 // indirect
github.com/pion/transport/v4 v4.0.1 // indirect
github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55 // indirect
github.com/pquerna/otp v1.5.0 // indirect
github.com/samber/slog-zerolog/v2 v2.9.1 // indirect
github.com/stretchr/objx v0.5.3 // indirect
github.com/tklauser/go-sysconf v0.3.16 // indirect
github.com/tklauser/numcpus v0.11.0 // indirect
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
github.com/ugorji/go/codec v1.3.1 // indirect
github.com/ulikunitz/xz v0.5.15 // indirect
github.com/valyala/bytebufferpool v1.0.0 // indirect
github.com/vultr/govultr/v3 v3.27.0 // indirect
github.com/yusufpapurcu/wmi v1.2.4 // indirect
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.38.0 // indirect
go.opentelemetry.io/proto/otlp v1.9.0 // indirect
golang.org/x/arch v0.24.0 // indirect
)

60
go.sum
View File

@@ -23,6 +23,8 @@ github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/resourcegraph/armresourceg
github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/resourcegraph/armresourcegraph v0.9.0/go.mod h1:wVEOJfGTj0oPAUGA1JuRAvz/lxXQsWW16axmHPP47Bk=
github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/resources/armresources v1.2.0 h1:Dd+RhdJn0OTtVGaeDLZpcumkIVCtA/3/Fo42+eoYvVM=
github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/resources/armresources v1.2.0/go.mod h1:5kakwfW5CjC9KK+Q4wjXAg+ShuIm2mBMua0ZFj2C8PE=
github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c h1:udKWzYgxTojEKWjV8V+WSxDXJ4NFATAsZjh8iIbsQIg=
github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E=
github.com/AzureAD/microsoft-authentication-extensions-for-go/cache v0.1.1 h1:WJTmL004Abzc5wDB5VtZG2PJk5ndYDgVacGqfirKxjM=
github.com/AzureAD/microsoft-authentication-extensions-for-go/cache v0.1.1/go.mod h1:tCcJZ0uHAmvjsVYzEFivsRTN00oz5BEsRgQHu5JZ9WE=
github.com/AzureAD/microsoft-authentication-library-for-go v1.6.0 h1:XRzhVemXdgvJqCH0sFfrBUTnUJSBrBf7++ypk+twtRs=
@@ -65,6 +67,8 @@ github.com/containerd/errdefs v1.0.0 h1:tg5yIfIlQIrxYtu9ajqY42W3lpS19XqdxRQeEwYG
github.com/containerd/errdefs v1.0.0/go.mod h1:+YBYIdtsnF4Iw6nWZhJcqGSg/dwvV7tyJ/kCkyJ2k+M=
github.com/containerd/errdefs/pkg v0.3.0 h1:9IKJ06FvyNlexW690DXuQNx2KA2cUJXx151Xdx3ZPPE=
github.com/containerd/errdefs/pkg v0.3.0/go.mod h1:NJw6s9HwNuRhnjJhM7pylWwMyAkmCQvQ4GpJHEqRLVk=
github.com/containerd/log v0.1.0 h1:TCJt7ioM2cr/tfR8GPbGf9/VRAX8D2B4PjzCpfX540I=
github.com/containerd/log v0.1.0/go.mod h1:VRRf09a7mHDIRezVKTRCrOq78v577GXq3bSa3EhrzVo=
github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
@@ -78,6 +82,8 @@ github.com/djherbis/times v1.6.0 h1:w2ctJ92J8fBvWPxugmXIv7Nz7Q3iDMKNx9v5ocVH20c=
github.com/djherbis/times v1.6.0/go.mod h1:gOHeRAz2h+VJNZ5Gmc/o7iD9k4wW7NMVqieYCY99oc0=
github.com/docker/cli v29.2.1+incompatible h1:n3Jt0QVCN65eiVBoUTZQM9mcQICCJt3akW4pKAbKdJg=
github.com/docker/cli v29.2.1+incompatible/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8=
github.com/docker/docker v28.5.2+incompatible h1:DBX0Y0zAjZbSrm1uzOkdr1onVghKaftjlSWt4AFexzM=
github.com/docker/docker v28.5.2+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk=
github.com/docker/go-connections v0.6.0 h1:LlMG9azAe1TqfR7sO+NJttz1gy6KO7VJBh+pMmjSD94=
github.com/docker/go-connections v0.6.0/go.mod h1:AahvXYshr6JgfUJGdDCs2b5EZG/vmaMAntpSFH5BFKE=
github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4=
@@ -100,8 +106,8 @@ github.com/gin-contrib/sse v1.1.0 h1:n0w2GMuUpWDVp7qSpvze6fAu9iRxJY4Hmj6AmBOU05w
github.com/gin-contrib/sse v1.1.0/go.mod h1:hxRZ5gVpWMT7Z0B0gSNYqqsSCNIJMjzvm6fqCz9vjwM=
github.com/gin-gonic/gin v1.11.0 h1:OW/6PLjyusp2PPXtyxKHU0RbX6I/l28FTdDlae5ueWk=
github.com/gin-gonic/gin v1.11.0/go.mod h1:+iq/FyxlGzII0KHiBGjuNn4UNENUlKbGlNmc+W50Dls=
github.com/go-acme/lego/v4 v4.32.0 h1:z7Ss7aa1noabhKj+DBzhNCO2SM96xhE3b0ucVW3x8Tc=
github.com/go-acme/lego/v4 v4.32.0/go.mod h1:lI2fZNdgeM/ymf9xQ9YKbgZm6MeDuf91UrohMQE4DhI=
github.com/go-acme/lego/v4 v4.31.0 h1:gd4oUYdfs83PR1/SflkNdit9xY1iul2I4EystnU8NXM=
github.com/go-acme/lego/v4 v4.31.0/go.mod h1:m6zcfX/zcbMYDa8s6AnCMnoORWNP8Epnei+6NBCTUGs=
github.com/go-jose/go-jose/v4 v4.1.3 h1:CVLmWDhDVRa6Mi/IgCgaopNosCaHz7zrMeF9MlZRkrs=
github.com/go-jose/go-jose/v4 v4.1.3/go.mod h1:x4oUasVrzR7071A4TnHLGSPpNOm2a21K9Kf04k1rs08=
github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
@@ -159,6 +165,8 @@ github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aN
github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
github.com/gotify/server/v2 v2.9.0 h1:2zRCl28wkq0oc6YNbyJS2n0dDOOVvOS3Oez5AG2ij54=
github.com/gotify/server/v2 v2.9.0/go.mod h1:249wwlUqHTr0QsiKARGtFVqds0pNLIMjYLinHyMACdQ=
github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.3 h1:NmZ1PKzSTQbuGHw9DGPFomqkkLWMC+vZCkfs+FHv1Vg=
github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.3/go.mod h1:zQrxl1YP88HQlA6i9c63DSVPFklWpGX4OWAc9bFuaH4=
github.com/h2non/gock v1.2.0 h1:K6ol8rfrRkUOefooBC8elXoaNGYkpp7y2qcxGG6BzUE=
github.com/h2non/gock v1.2.0/go.mod h1:tNhoxHYW2W42cYkYb1WqzdbYIieALC99kpYr7rH/BQk=
github.com/h2non/parth v0.0.0-20190131123155-b4df798d6542 h1:2VTzZjLZBgl62/EtslCrtky5vbi9dd7HrQPQIx6wqiw=
@@ -195,8 +203,8 @@ github.com/linode/linodego v1.65.0 h1:SdsuGD8VSsPWeShXpE7ihl5vec+fD3MgwhnfYC/rj7
github.com/linode/linodego v1.65.0/go.mod h1:tOFiTErdjkbVnV+4S0+NmIE9dqqZUEM2HsJaGu8wMh8=
github.com/lithammer/fuzzysearch v1.1.8 h1:/HIuJnjHuXS8bKaiTMeeDlW2/AyIWk2brx1V8LFgLN4=
github.com/lithammer/fuzzysearch v1.1.8/go.mod h1:IdqeyBClc3FFqSzYq/MXESsS4S0FsZ5ajtkr5xPLts4=
github.com/lufia/plan9stats v0.0.0-20260216142805-b3301c5f2a88 h1:PTw+yKnXcOFCR6+8hHTyWBeQ/P4Nb7dd4/0ohEcWQuM=
github.com/lufia/plan9stats v0.0.0-20260216142805-b3301c5f2a88/go.mod h1:autxFIvghDt3jPTLoqZ9OZ7s9qTGNAWmYCjVFWPX/zg=
github.com/lufia/plan9stats v0.0.0-20251013123823-9fd1530e3ec3 h1:PwQumkgq4/acIiZhtifTV5OUqqiP82UAl0h87xj/l9k=
github.com/lufia/plan9stats v0.0.0-20251013123823-9fd1530e3ec3/go.mod h1:autxFIvghDt3jPTLoqZ9OZ7s9qTGNAWmYCjVFWPX/zg=
github.com/magefile/mage v1.15.0 h1:BvGheCMAsG3bWUDbZ8AyXXpCNwU9u5CB6sM+HNb9HYg=
github.com/magefile/mage v1.15.0/go.mod h1:z5UZb/iS3GoOSn0JgWuiw7dxlurVYTu+/jHXqQg881A=
github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
@@ -214,23 +222,29 @@ github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG
github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
github.com/moby/docker-image-spec v1.3.1 h1:jMKff3w6PgbfSa69GfNg+zN/XLhfXJGnEx3Nl2EsFP0=
github.com/moby/docker-image-spec v1.3.1/go.mod h1:eKmb5VW8vQEh/BAr2yvVNvuiJuY6UIocYsFu/DxxRpo=
github.com/moby/moby/api v1.52.0 h1:00BtlJY4MXkkt84WhUZPRqt5TvPbgig2FZvTbe3igYg=
github.com/moby/moby/api v1.52.0/go.mod h1:8mb+ReTlisw4pS6BRzCMts5M49W5M7bKt1cJy/YbAqc=
github.com/moby/moby/client v0.2.1 h1:1Grh1552mvv6i+sYOdY+xKKVTvzJegcVMhuXocyDz/k=
github.com/moby/moby/client v0.2.1/go.mod h1:O+/tw5d4a1Ha/ZA/tPxIZJapJRUS6LNZ1wiVRxYHyUE=
github.com/moby/moby/api v1.53.0 h1:PihqG1ncw4W+8mZs69jlwGXdaYBeb5brF6BL7mPIS/w=
github.com/moby/moby/api v1.53.0/go.mod h1:8mb+ReTlisw4pS6BRzCMts5M49W5M7bKt1cJy/YbAqc=
github.com/moby/sys/atomicwriter v0.1.0 h1:kw5D/EqkBwsBFi0ss9v1VG3wIkVhzGvLklJ+w3A14Sw=
github.com/moby/sys/atomicwriter v0.1.0/go.mod h1:Ul8oqv2ZMNHOceF643P6FKPXeCmYtlQMvpizfsSoaWs=
github.com/moby/sys/sequential v0.6.0 h1:qrx7XFUd/5DxtqcoH1h438hF5TmOvzC/lspjy7zgvCU=
github.com/moby/sys/sequential v0.6.0/go.mod h1:uyv8EUTrca5PnDsdMGXhZe6CCe8U/UiTWd+lL+7b/Ko=
github.com/moby/term v0.5.2 h1:6qk3FJAFDs6i/q3W/pQ97SX192qKfZgGjCQqfCJkgzQ=
github.com/moby/term v0.5.2/go.mod h1:d3djjFCrjnB+fl8NJux+EJzu0msscUP+f8it8hPkFLc=
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M=
github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
github.com/morikuni/aec v1.0.0 h1:nP9CBfwrvYnBRgY6qfDQkygYDmYwOilePFkwzv4dU8A=
github.com/morikuni/aec v1.0.0/go.mod h1:BbKIizmSmc5MMPqRYbxO4ZU0S0+P200+tUnFx7PXmsc=
github.com/nrdcg/goacmedns v0.2.0 h1:ADMbThobzEMnr6kg2ohs4KGa3LFqmgiBA22/6jUWJR0=
github.com/nrdcg/goacmedns v0.2.0/go.mod h1:T5o6+xvSLrQpugmwHvrSNkzWht0UGAwj2ACBMhh73Cg=
github.com/nrdcg/goinwx v0.12.0 h1:ujdUqDBnaRSFwzVnImvPHYw3w3m9XgmGImNUw1GyMb4=
github.com/nrdcg/goinwx v0.12.0/go.mod h1:IrVKd3ZDbFiMjdPgML4CSxZAY9wOoqLvH44zv3NodJ0=
github.com/nrdcg/oci-go-sdk/common/v1065 v1065.108.2 h1:OWijzl3nHUApvTivl+3+78dbBwmyEHOnb+W9m6ixGbk=
github.com/nrdcg/oci-go-sdk/common/v1065 v1065.108.2/go.mod h1:Gcs8GCaZXL3FdiDWgdnMxlOLEdRprJJnPYB22TX1jw8=
github.com/nrdcg/oci-go-sdk/dns/v1065 v1065.108.2 h1:9LsjN/zaIN7H8JE61NHpbWhxF0UGY96+kMlk3g8OvGU=
github.com/nrdcg/oci-go-sdk/dns/v1065 v1065.108.2/go.mod h1:32vZH06TuwZSn+IDMO1qcDvC2vHVlzUALCwXGWPA+dc=
github.com/nrdcg/oci-go-sdk/common/v1065 v1065.108.1 h1:3oOIAQ9Fd2qTKTS/VlWmvKyBPKKhXBcCXjRZqOUypI4=
github.com/nrdcg/oci-go-sdk/common/v1065 v1065.108.1/go.mod h1:Gcs8GCaZXL3FdiDWgdnMxlOLEdRprJJnPYB22TX1jw8=
github.com/nrdcg/oci-go-sdk/dns/v1065 v1065.108.1 h1:2H75475moAv1hVVYlOk815KfqeiFCiQ7ovqn3OnN6FY=
github.com/nrdcg/oci-go-sdk/dns/v1065 v1065.108.1/go.mod h1:9HGOXiiQxcsG+4amgdr4xBIMq6IchdLW/nQDyZz07IE=
github.com/nrdcg/porkbun v0.4.0 h1:rWweKlwo1PToQ3H+tEO9gPRW0wzzgmI/Ob3n2Guticw=
github.com/nrdcg/porkbun v0.4.0/go.mod h1:/QMskrHEIM0IhC/wY7iTCUgINsxdT2WcOphktJ9+Q54=
github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U=
@@ -255,6 +269,7 @@ github.com/pires/go-proxyproto v0.11.0 h1:gUQpS85X/VJMdUsYyEgyn59uLJvGqPhJV5YvG6
github.com/pires/go-proxyproto v0.11.0/go.mod h1:ZKAAyp3cgy5Y5Mo4n9AlScrkCZwUy0g3Jf+slqQVcuU=
github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c h1:+mdjkGKdHQG3305AYmdv1U2eRNDiU2ErMBj1gwrq8eQ=
github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c/go.mod h1:7rwL4CYBLnjLxUqIJNnCWiEdr3bn6IUYi15bNlnbCCU=
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pkg/xattr v0.4.9 h1:5883YPCtkSd8LFbs13nXplj9g9tlrwoJRjgpgMu1/fE=
github.com/pkg/xattr v0.4.9/go.mod h1:di8WF84zAKk8jzR1UBTEWh9AUlIZZ7M/JNt8e9B6ktU=
@@ -312,8 +327,8 @@ github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS
github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08=
github.com/ugorji/go/codec v1.3.1 h1:waO7eEiFDwidsBN6agj1vJQ4AG7lh2yqXyOXqhgQuyY=
github.com/ugorji/go/codec v1.3.1/go.mod h1:pRBVtBSKl77K30Bv8R2P+cLSGaTtex6fsA2Wjqmfxj4=
github.com/ulikunitz/xz v0.5.15 h1:9DNdB5s+SgV3bQ2ApL10xRc35ck0DuIX/isZvIk+ubY=
github.com/ulikunitz/xz v0.5.15/go.mod h1:nbz6k7qbPmH4IRqmfOplQw/tblSgqTqBwxkY0oWt/14=
github.com/ulikunitz/xz v0.5.11 h1:kpFauv27b6ynzBNT/Xy+1k+fK4WswhN/6PN5WhFAGw8=
github.com/ulikunitz/xz v0.5.11/go.mod h1:nbz6k7qbPmH4IRqmfOplQw/tblSgqTqBwxkY0oWt/14=
github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw=
github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc=
github.com/valyala/fasthttp v1.69.0 h1:fNLLESD2SooWeh2cidsuFtOcrEi4uB4m1mPrkJMZyVI=
@@ -341,6 +356,10 @@ go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.65.0 h1:7iP2uCb
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.65.0/go.mod h1:c7hN3ddxs/z6q9xwvfLPk+UHlWRQyaeR1LdgfL/66l0=
go.opentelemetry.io/otel v1.40.0 h1:oA5YeOcpRTXq6NN7frwmwFR0Cn3RhTVZvXsP4duvCms=
go.opentelemetry.io/otel v1.40.0/go.mod h1:IMb+uXZUKkMXdPddhwAHm6UfOwJyh4ct1ybIlV14J0g=
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.38.0 h1:GqRJVj7UmLjCVyVJ3ZFLdPRmhDUp2zFmQe3RHIOsw24=
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.38.0/go.mod h1:ri3aaHSmCTVYu2AWv44YMauwAQc0aqI9gHKIcSbI1pU=
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.38.0 h1:aTL7F04bJHUlztTsNGJ2l+6he8c+y/b//eR0jjjemT4=
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.38.0/go.mod h1:kldtb7jDTeol0l3ewcmd8SDvx3EmIE7lyvqbasU3QC4=
go.opentelemetry.io/otel/metric v1.40.0 h1:rcZe317KPftE2rstWIBitCdVp89A2HqjkxR3c11+p9g=
go.opentelemetry.io/otel/metric v1.40.0/go.mod h1:ib/crwQH7N3r5kfiBZQbwrTge743UDc7DTFVZrrXnqc=
go.opentelemetry.io/otel/sdk v1.40.0 h1:KHW/jUzgo6wsPh9At46+h4upjtccTmuZCFAc9OJ71f8=
@@ -349,6 +368,8 @@ go.opentelemetry.io/otel/sdk/metric v1.40.0 h1:mtmdVqgQkeRxHgRv4qhyJduP3fYJRMX4A
go.opentelemetry.io/otel/sdk/metric v1.40.0/go.mod h1:4Z2bGMf0KSK3uRjlczMOeMhKU2rhUqdWNoKcYrtcBPg=
go.opentelemetry.io/otel/trace v1.40.0 h1:WA4etStDttCSYuhwvEa8OP8I5EWu24lkOzp+ZYblVjw=
go.opentelemetry.io/otel/trace v1.40.0/go.mod h1:zeAhriXecNGP/s2SEG3+Y8X9ujcJOTqQ5RgdEJcawiA=
go.opentelemetry.io/proto/otlp v1.9.0 h1:l706jCMITVouPOqEnii2fIAuO3IVGBRPV5ICjceRb/A=
go.opentelemetry.io/proto/otlp v1.9.0/go.mod h1:xE+Cx5E/eEHw+ISFkwPLwCZefwVjY+pqKg1qcK03+/4=
go.uber.org/atomic v1.11.0 h1:ZvwS0R+56ePWxUNi+Atn9dWONBPp/AUETXlHW0DxSjE=
go.uber.org/atomic v1.11.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0=
go.uber.org/mock v0.5.2 h1:LbtPTcP8A5k9WPXj54PPPbjcI4Y6lhyOZXn+VS7wNko=
@@ -400,6 +421,7 @@ golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7w
golang.org/x/sys v0.0.0-20201204225414-ed752295db88/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210331175145-43e1dd70ce54/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-20210616094352-59db8d763f22/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-20220615213510-4f61da869c0c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
@@ -447,14 +469,14 @@ golang.org/x/tools v0.42.0/go.mod h1:Ma6lCIwGZvHK6XtgbswSoWroEkhugApmsXyrUmBhfr0
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
gonum.org/v1/gonum v0.16.0 h1:5+ul4Swaf3ESvrOnidPp4GZbzf0mxVQpDCYUQE7OJfk=
gonum.org/v1/gonum v0.16.0/go.mod h1:fef3am4MQ93R2HHpKnLk4/Tbh/s0+wqD5nfa6Pnwy4E=
google.golang.org/api v0.267.0 h1:w+vfWPMPYeRs8qH1aYYsFX68jMls5acWl/jocfLomwE=
google.golang.org/api v0.267.0/go.mod h1:Jzc0+ZfLnyvXma3UtaTl023TdhZu6OMBP9tJ+0EmFD0=
google.golang.org/api v0.266.0 h1:hco+oNCf9y7DmLeAtHJi/uBAY7n/7XC9mZPxu1ROiyk=
google.golang.org/api v0.266.0/go.mod h1:Jzc0+ZfLnyvXma3UtaTl023TdhZu6OMBP9tJ+0EmFD0=
google.golang.org/genproto v0.0.0-20260128011058-8636f8732409 h1:VQZ/yAbAtjkHgH80teYd2em3xtIkkHd7ZhqfH2N9CsM=
google.golang.org/genproto v0.0.0-20260128011058-8636f8732409/go.mod h1:rxKD3IEILWEu3P44seeNOAwZN4SaoKaQ/2eTg4mM6EM=
google.golang.org/genproto/googleapis/api v0.0.0-20260128011058-8636f8732409 h1:merA0rdPeUV3YIIfHHcH4qBkiQAc1nfCKSI7lB4cV2M=
google.golang.org/genproto/googleapis/api v0.0.0-20260128011058-8636f8732409/go.mod h1:fl8J1IvUjCilwZzQowmw2b7HQB2eAuYBabMXzWurF+I=
google.golang.org/genproto/googleapis/rpc v0.0.0-20260217215200-42d3e9bedb6d h1:t/LOSXPJ9R0B6fnZNyALBRfZBH0Uy0gT+uR+SJ6syqQ=
google.golang.org/genproto/googleapis/rpc v0.0.0-20260217215200-42d3e9bedb6d/go.mod h1:4Hqkh8ycfw05ld/3BWL7rJOSfebL2Q+DVDeRgYgxUU8=
google.golang.org/genproto/googleapis/rpc v0.0.0-20260209200024-4cfbd4190f57 h1:mWPCjDEyshlQYzBpMNHaEof6UX1PmHcaUODUywQ0uac=
google.golang.org/genproto/googleapis/rpc v0.0.0-20260209200024-4cfbd4190f57/go.mod h1:j9x/tPzZkyxcgEFkiKEEGxfvyumM01BEtsW8xzOahRQ=
google.golang.org/grpc v1.79.1 h1:zGhSi45ODB9/p3VAawt9a+O/MULLl9dpizzNNpq7flY=
google.golang.org/grpc v1.79.1/go.mod h1:KmT0Kjez+0dde/v2j9vzwoAScgEPx/Bw1CYChhHLrHQ=
google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE=
@@ -472,5 +494,3 @@ gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gotest.tools/v3 v3.5.2 h1:7koQfIKdy+I8UTetycgUqXWSDwpgv193Ka+qRsmBY8Q=
gotest.tools/v3 v3.5.2/go.mod h1:LtdLGcnqToBH83WByAAi/wiwSFCArdFIUV/xxN4pcjA=
pgregory.net/rapid v1.2.0 h1:keKAYRcjm+e1F0oAuU5F5+YPAWcyxNNRK2wud503Gnk=
pgregory.net/rapid v1.2.0/go.mod h1:PY5XlDGj0+V1FCq0o192FdRhpKHGTRIWBgqjDBTrq04=

Submodule goutils updated: 3be815cb6e...494ab85a33

View File

@@ -1,4 +1,4 @@
# internal/acl
# ACL (Access Control List)
Access control at the TCP connection level with IP/CIDR, timezone, and country-based filtering.

View File

@@ -4,7 +4,7 @@ import "context"
type ContextKey struct{}
func SetCtx(ctx interface{ SetValue(key any, value any) }, acl ACL) {
func SetCtx(ctx interface{ SetValue(any, any) }, acl ACL) {
ctx.SetValue(ContextKey{}, acl)
}

View File

@@ -1,4 +1,4 @@
# internal/agentpool
# Agent Pool
Thread-safe pool for managing remote Docker agent connections.

View File

@@ -34,10 +34,7 @@ func newAgent(cfg *agent.AgentConfig) *Agent {
if addr != agent.AgentHost+":443" {
return nil, &net.AddrError{Err: "invalid address", Addr: addr}
}
dialer := &net.Dialer{
Timeout: timeout,
}
return dialer.Dial("tcp", cfg.Addr)
return net.DialTimeout("tcp", cfg.Addr, timeout)
},
TLSConfig: cfg.TLSConfig(),
ReadTimeout: 5 * time.Second,

View File

@@ -2,32 +2,32 @@ package agentpool
import (
"context"
"encoding/json"
"fmt"
"io"
"net/http"
"time"
"github.com/bytedance/sonic"
"github.com/gorilla/websocket"
"github.com/valyala/fasthttp"
agentPkg "github.com/yusing/godoxy/agent/pkg/agent"
"github.com/yusing/godoxy/agent/pkg/agent"
"github.com/yusing/goutils/http/reverseproxy"
)
func (agent *Agent) Do(ctx context.Context, method, endpoint string, body io.Reader) (*http.Response, error) {
req, err := http.NewRequestWithContext(ctx, method, agentPkg.APIBaseURL+endpoint, body)
func (cfg *Agent) Do(ctx context.Context, method, endpoint string, body io.Reader) (*http.Response, error) {
req, err := http.NewRequestWithContext(ctx, method, agent.APIBaseURL+endpoint, body)
if err != nil {
return nil, err
}
return agent.httpClient.Do(req)
return cfg.httpClient.Do(req)
}
func (agent *Agent) Forward(req *http.Request, endpoint string) (*http.Response, error) {
req.URL.Host = agentPkg.AgentHost
func (cfg *Agent) Forward(req *http.Request, endpoint string) (*http.Response, error) {
req.URL.Host = agent.AgentHost
req.URL.Scheme = "https"
req.URL.Path = agentPkg.APIEndpointBase + endpoint
req.URL.Path = agent.APIEndpointBase + endpoint
req.RequestURI = ""
resp, err := agent.httpClient.Do(req)
resp, err := cfg.httpClient.Do(req)
if err != nil {
return nil, err
}
@@ -40,20 +40,20 @@ type HealthCheckResponse struct {
Latency time.Duration `json:"latency"`
}
func (agent *Agent) DoHealthCheck(timeout time.Duration, query string) (ret HealthCheckResponse, err error) {
func (cfg *Agent) DoHealthCheck(timeout time.Duration, query string) (ret HealthCheckResponse, err error) {
req := fasthttp.AcquireRequest()
defer fasthttp.ReleaseRequest(req)
resp := fasthttp.AcquireResponse()
defer fasthttp.ReleaseResponse(resp)
req.SetRequestURI(agentPkg.APIBaseURL + agentPkg.EndpointHealth + "?" + query)
req.SetRequestURI(agent.APIBaseURL + agent.EndpointHealth + "?" + query)
req.Header.SetMethod(fasthttp.MethodGet)
req.Header.Set("Accept-Encoding", "identity")
req.SetConnectionClose()
start := time.Now()
err = agent.fasthttpHcClient.DoTimeout(req, resp, timeout)
err = cfg.fasthttpHcClient.DoTimeout(req, resp, timeout)
ret.Latency = time.Since(start)
if err != nil {
return ret, err
@@ -63,7 +63,7 @@ func (agent *Agent) DoHealthCheck(timeout time.Duration, query string) (ret Heal
ret.Detail = fmt.Sprintf("HTTP %d %s", status, resp.Body())
return ret, nil
} else {
err = sonic.Unmarshal(resp.Body(), &ret)
err = json.Unmarshal(resp.Body(), &ret)
if err != nil {
return ret, err
}
@@ -71,14 +71,14 @@ func (agent *Agent) DoHealthCheck(timeout time.Duration, query string) (ret Heal
return ret, nil
}
func (agent *Agent) Websocket(ctx context.Context, endpoint string) (*websocket.Conn, *http.Response, error) {
transport := agent.Transport()
func (cfg *Agent) Websocket(ctx context.Context, endpoint string) (*websocket.Conn, *http.Response, error) {
transport := cfg.Transport()
dialer := websocket.Dialer{
NetDialContext: transport.DialContext,
NetDialTLSContext: transport.DialTLSContext,
}
return dialer.DialContext(ctx, agentPkg.APIBaseURL+endpoint, http.Header{
"Host": {agentPkg.AgentHost},
return dialer.DialContext(ctx, agent.APIBaseURL+endpoint, http.Header{
"Host": {agent.AgentHost},
})
}
@@ -86,9 +86,9 @@ func (agent *Agent) Websocket(ctx context.Context, endpoint string) (*websocket.
//
// It will create a new request with the same context, method, and body, but with the agent host and scheme, and the endpoint
// If the request has a query, it will be added to the proxy request's URL
func (agent *Agent) ReverseProxy(w http.ResponseWriter, req *http.Request, endpoint string) {
rp := reverseproxy.NewReverseProxy("agent", agentPkg.AgentURL, agent.Transport())
req.URL.Host = agentPkg.AgentHost
func (cfg *Agent) ReverseProxy(w http.ResponseWriter, req *http.Request, endpoint string) {
rp := reverseproxy.NewReverseProxy("agent", agent.AgentURL, cfg.Transport())
req.URL.Host = agent.AgentHost
req.URL.Scheme = "https"
req.URL.Path = endpoint
req.RequestURI = ""

View File

@@ -2,10 +2,8 @@ package api
import (
"net/http"
"reflect"
"github.com/gin-gonic/gin"
"github.com/gin-gonic/gin/codec/json"
"github.com/gorilla/websocket"
"github.com/rs/zerolog/log"
apiV1 "github.com/yusing/godoxy/internal/api/v1"
@@ -49,8 +47,6 @@ func NewHandler(requireAuth bool) *gin.Engine {
r.Use(ErrorLoggingMiddleware())
r.Use(NoCache())
log.Debug().Msg("gin codec json.API: " + reflect.TypeOf(json.API).Name())
r.GET("/api/v1/version", apiV1.Version)
if auth.IsEnabled() && requireAuth {

View File

@@ -1,4 +1,4 @@
# internal/api/v1
# API v1 Package
Implements the v1 REST API handlers for GoDoxy, exposing endpoints for managing routes, Docker containers, certificates, metrics, and system configuration.

View File

@@ -4,7 +4,6 @@ import (
"net/http"
"github.com/gin-gonic/gin"
"github.com/moby/moby/client"
"github.com/yusing/godoxy/internal/docker"
apitypes "github.com/yusing/goutils/apitypes"
)
@@ -43,22 +42,22 @@ func GetContainer(c *gin.Context) {
defer dockerClient.Close()
cont, err := dockerClient.ContainerInspect(c.Request.Context(), id, client.ContainerInspectOptions{})
cont, err := dockerClient.ContainerInspect(c.Request.Context(), id)
if err != nil {
c.Error(apitypes.InternalServerError(err, "failed to inspect container"))
return
}
var state ContainerState
if cont.Container.State != nil {
state = cont.Container.State.Status
if cont.State != nil {
state = cont.State.Status
}
c.JSON(http.StatusOK, &Container{
Server: dockerCfg.URL,
Name: cont.Container.Name,
ID: cont.Container.ID,
Image: cont.Container.Image,
Name: cont.Name,
ID: cont.ID,
Image: cont.Image,
State: state,
})
}

View File

@@ -4,9 +4,8 @@ import (
"context"
"sort"
"github.com/docker/docker/api/types/container"
"github.com/gin-gonic/gin"
"github.com/moby/moby/api/types/container"
"github.com/moby/moby/client"
"github.com/rs/zerolog/log"
gperr "github.com/yusing/goutils/errs"
@@ -40,15 +39,15 @@ func Containers(c *gin.Context) {
func GetContainers(ctx context.Context, dockerClients DockerClients) ([]Container, error) {
errs := gperr.NewBuilder("failed to get containers")
containers := make([]Container, 0)
for name, dockerClient := range dockerClients {
conts, err := dockerClient.ContainerList(ctx, client.ContainerListOptions{All: true})
for server, dockerClient := range dockerClients {
conts, err := dockerClient.ContainerList(ctx, container.ListOptions{All: true})
if err != nil {
errs.AddSubject(err, name)
errs.AddSubject(err, server)
continue
}
for _, cont := range conts.Items {
for _, cont := range conts {
containers = append(containers, Container{
Server: name,
Server: server,
Name: cont.Names[0],
ID: cont.ID,
Image: cont.Image,

View File

@@ -4,9 +4,8 @@ import (
"context"
"sort"
dockerSystem "github.com/docker/docker/api/types/system"
"github.com/gin-gonic/gin"
dockerSystem "github.com/moby/moby/api/types/system"
"github.com/moby/moby/client"
gperr "github.com/yusing/goutils/errs"
strutils "github.com/yusing/goutils/strings"
@@ -65,13 +64,13 @@ func GetDockerInfo(ctx context.Context, dockerClients DockerClients) ([]dockerIn
i := 0
for name, dockerClient := range dockerClients {
info, err := dockerClient.Info(ctx, client.InfoOptions{})
info, err := dockerClient.Info(ctx)
if err != nil {
errs.AddSubject(err, name)
continue
}
info.Info.Name = name
dockerInfos[i] = toDockerInfo(info.Info)
info.Name = name
dockerInfos[i] = toDockerInfo(info)
i++
}

View File

@@ -7,9 +7,9 @@ import (
"net/http"
"strconv"
"github.com/docker/docker/api/types/container"
"github.com/docker/docker/pkg/stdcopy"
"github.com/gin-gonic/gin"
"github.com/moby/moby/api/pkg/stdcopy"
"github.com/moby/moby/client"
"github.com/rs/zerolog/log"
"github.com/yusing/godoxy/internal/docker"
apitypes "github.com/yusing/goutils/apitypes"
@@ -73,7 +73,7 @@ func Logs(c *gin.Context) {
}
defer dockerClient.Close()
opts := client.ContainerLogsOptions{
opts := container.LogsOptions{
ShowStdout: queryParams.Stdout,
ShowStderr: queryParams.Stderr,
Since: queryParams.Since,

View File

@@ -4,23 +4,17 @@ import (
"net/http"
"github.com/gin-gonic/gin"
"github.com/moby/moby/client"
"github.com/yusing/godoxy/internal/docker"
apitypes "github.com/yusing/goutils/apitypes"
)
type RestartRequest struct {
ID string `json:"id" binding:"required"`
client.ContainerRestartOptions
}
// @x-id "restart"
// @BasePath /api/v1
// @Summary Restart container
// @Description Restart container by container id
// @Tags docker
// @Produce json
// @Param request body RestartRequest true "Request"
// @Param request body StopRequest true "Request"
// @Success 200 {object} apitypes.SuccessResponse
// @Failure 400 {object} apitypes.ErrorResponse "Invalid request"
// @Failure 403 {object} apitypes.ErrorResponse
@@ -28,7 +22,7 @@ type RestartRequest struct {
// @Failure 500 {object} apitypes.ErrorResponse
// @Router /docker/restart [post]
func Restart(c *gin.Context) {
var req RestartRequest
var req StopRequest
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, apitypes.Error("invalid request", err))
return
@@ -48,7 +42,7 @@ func Restart(c *gin.Context) {
defer client.Close()
_, err = client.ContainerRestart(c.Request.Context(), req.ID, req.ContainerRestartOptions)
err = client.ContainerRestart(c.Request.Context(), req.ID, req.StopOptions)
if err != nil {
c.Error(apitypes.InternalServerError(err, "failed to restart container"))
return

View File

@@ -3,15 +3,15 @@ package dockerapi
import (
"net/http"
"github.com/docker/docker/api/types/container"
"github.com/gin-gonic/gin"
"github.com/moby/moby/client"
"github.com/yusing/godoxy/internal/docker"
apitypes "github.com/yusing/goutils/apitypes"
)
type StartRequest struct {
ID string `json:"id" binding:"required"`
client.ContainerStartOptions
container.StartOptions
}
// @x-id "start"
@@ -48,7 +48,7 @@ func Start(c *gin.Context) {
defer client.Close()
_, err = client.ContainerStart(c.Request.Context(), req.ID, req.ContainerStartOptions)
err = client.ContainerStart(c.Request.Context(), req.ID, req.StartOptions)
if err != nil {
c.Error(apitypes.InternalServerError(err, "failed to start container"))
return

View File

@@ -8,7 +8,6 @@ import (
"github.com/gin-gonic/gin"
"github.com/moby/moby/api/types/container"
"github.com/moby/moby/client"
"github.com/yusing/godoxy/internal/docker"
entrypoint "github.com/yusing/godoxy/internal/entrypoint/types"
"github.com/yusing/godoxy/internal/types"
@@ -68,7 +67,7 @@ func Stats(c *gin.Context) {
defer dockerClient.Close()
if httpheaders.IsWebsocket(c.Request.Header) {
stats, err := dockerClient.ContainerStats(c.Request.Context(), id, client.ContainerStatsOptions{Stream: true})
stats, err := dockerClient.ContainerStats(c.Request.Context(), id, true)
if err != nil {
c.Error(apitypes.InternalServerError(err, "failed to get container stats"))
return
@@ -102,7 +101,7 @@ func Stats(c *gin.Context) {
}
}
stats, err := dockerClient.ContainerStats(c.Request.Context(), id, client.ContainerStatsOptions{Stream: false})
stats, err := dockerClient.ContainerStats(c.Request.Context(), id, false)
if err != nil {
c.Error(apitypes.InternalServerError(err, "failed to get container stats"))
return

View File

@@ -3,15 +3,15 @@ package dockerapi
import (
"net/http"
"github.com/docker/docker/api/types/container"
"github.com/gin-gonic/gin"
"github.com/moby/moby/client"
"github.com/yusing/godoxy/internal/docker"
apitypes "github.com/yusing/goutils/apitypes"
)
type StopRequest struct {
ID string `json:"id" binding:"required"`
client.ContainerStopOptions
container.StopOptions
}
// @x-id "stop"
@@ -48,7 +48,7 @@ func Stop(c *gin.Context) {
defer client.Close()
_, err = client.ContainerStop(c.Request.Context(), req.ID, req.ContainerStopOptions)
err = client.ContainerStop(c.Request.Context(), req.ID, req.StopOptions)
if err != nil {
c.Error(apitypes.InternalServerError(err, "failed to stop container"))
return

View File

@@ -952,6 +952,7 @@
"application/json"
],
"produces": [
"application/json",
"application/godoxy+yaml"
],
"tags": [
@@ -2946,8 +2947,8 @@
}
}
},
"x-id": "list",
"operationId": "list"
"x-id": "routes",
"operationId": "routes"
}
},
"/route/playground": {
@@ -4844,19 +4845,16 @@
"properties": {
"days": {
"type": "integer",
"minimum": 0,
"x-nullable": false,
"x-omitempty": false
},
"keep_size": {
"type": "integer",
"minimum": 0,
"x-nullable": false,
"x-omitempty": false
},
"last": {
"type": "integer",
"minimum": 0,
"x-nullable": false,
"x-omitempty": false
}
@@ -5093,6 +5091,11 @@
"x-nullable": false,
"x-omitempty": false
},
"isResponseRule": {
"type": "boolean",
"x-nullable": false,
"x-omitempty": false
},
"name": {
"type": "string",
"x-nullable": false,
@@ -6642,13 +6645,11 @@
"x-omitempty": false
},
"fstype": {
"description": "interned",
"type": "string",
"x-nullable": false,
"x-omitempty": false
},
"path": {
"description": "interned",
"type": "string",
"x-nullable": false,
"x-omitempty": false
@@ -6995,7 +6996,6 @@
"x-omitempty": false
},
"name": {
"description": "interned",
"type": "string",
"x-nullable": false,
"x-omitempty": false

View File

@@ -767,13 +767,10 @@ definitions:
LogRetention:
properties:
days:
minimum: 0
type: integer
keep_size:
minimum: 0
type: integer
last:
minimum: 0
type: integer
type: object
MetricsPeriod:
@@ -891,6 +888,8 @@ definitions:
properties:
do:
type: string
isResponseRule:
type: boolean
name:
type: string
"on":
@@ -1691,10 +1690,8 @@ definitions:
free:
type: integer
fstype:
description: interned
type: string
path:
description: interned
type: string
total:
type: number
@@ -1889,7 +1886,6 @@ definitions:
high:
type: number
name:
description: interned
type: string
temperature:
type: number
@@ -2576,6 +2572,7 @@ paths:
- FileTypeProvider
- FileTypeMiddleware
produces:
- application/json
- application/godoxy+yaml
responses:
"200":
@@ -3936,7 +3933,7 @@ paths:
tags:
- route
- websocket
x-id: list
x-id: routes
/route/playground:
post:
consumes:

View File

@@ -30,7 +30,7 @@ type GetFileContentRequest struct {
// @Description Get file content
// @Tags file
// @Accept json
// @Produce application/godoxy+yaml
// @Produce json,application/godoxy+yaml
// @Param query query GetFileContentRequest true "Request"
// @Success 200 {string} application/godoxy+yaml "File content"
// @Failure 400 {object} apitypes.ErrorResponse

View File

@@ -4,12 +4,9 @@ import (
"context"
"encoding/json"
"net/http"
"net/url"
"sync/atomic"
"time"
"github.com/bytedance/sonic"
"github.com/cenkalti/backoff/v5"
"github.com/gin-gonic/gin"
"github.com/rs/zerolog/log"
"github.com/yusing/godoxy/agent/pkg/agent"
@@ -37,11 +34,6 @@ type bytesFromPool struct {
release func([]byte)
}
type systemInfoData struct {
agentName string
systemInfo any
}
// @x-id "all_system_info"
// @BasePath /api/v1
// @Summary Get system info
@@ -79,19 +71,91 @@ func AllSystemInfo(c *gin.Context) {
defer manager.Close()
query := c.Request.URL.Query()
queryEncoded := query.Encode()
queryEncoded := c.Request.URL.Query().Encode()
type SystemInfoData struct {
AgentName string
SystemInfo any
}
// leave 5 extra slots for buffering in case new agents are added.
dataCh := make(chan systemInfoData, 1+agentpool.Num()+5)
dataCh := make(chan SystemInfoData, 1+agentpool.Num()+5)
defer close(dataCh)
ticker := time.NewTicker(req.Interval)
defer ticker.Stop()
go streamSystemInfo(manager, dataCh)
go func() {
for {
select {
case <-manager.Done():
return
case data := <-dataCh:
err := marshalSystemInfo(manager, data.AgentName, data.SystemInfo)
if err != nil {
manager.Close()
return
}
}
}
}()
// processing function for one round.
doRound := func() (bool, error) {
var numErrs atomic.Int32
totalAgents := int32(1) // myself
var errs gperr.Group
// get system info for me and all agents in parallel.
errs.Go(func() error {
data, err := systeminfo.Poller.GetRespData(req.Period, query)
if err != nil {
numErrs.Add(1)
return gperr.PrependSubject(err, "Main server")
}
select {
case <-manager.Done():
return nil
case dataCh <- SystemInfoData{
AgentName: "GoDoxy",
SystemInfo: data,
}:
}
return nil
})
for _, a := range agentpool.Iter() {
totalAgents++
errs.Go(func() error {
data, err := getAgentSystemInfoWithRetry(manager.Context(), a, queryEncoded)
if err != nil {
numErrs.Add(1)
return gperr.PrependSubject(err, "Agent "+a.Name)
}
select {
case <-manager.Done():
return nil
case dataCh <- SystemInfoData{
AgentName: a.Name,
SystemInfo: data,
}:
}
return nil
})
}
err := errs.Wait().Error()
return numErrs.Load() == totalAgents, err
}
// write system info immediately once.
if hasSuccess, err := collectSystemInfoRound(manager, req, query, queryEncoded, dataCh); handleRoundResult(c, hasSuccess, err, false) {
return
if shouldContinue, err := doRound(); err != nil {
if !shouldContinue {
c.Error(apitypes.InternalServerError(err, "failed to get all system info"))
return
}
}
// then continue on the ticker.
@@ -100,95 +164,17 @@ func AllSystemInfo(c *gin.Context) {
case <-manager.Done():
return
case <-ticker.C:
if hasSuccess, err := collectSystemInfoRound(manager, req, query, queryEncoded, dataCh); handleRoundResult(c, hasSuccess, err, true) {
return
if shouldContinue, err := doRound(); err != nil {
if !shouldContinue {
c.Error(apitypes.InternalServerError(err, "failed to get all system info"))
return
}
log.Warn().Err(err).Msg("failed to get some system info")
}
}
}
}
func streamSystemInfo(manager *websocket.Manager, dataCh <-chan systemInfoData) {
for {
select {
case <-manager.Done():
return
case data := <-dataCh:
err := marshalSystemInfo(manager, data.agentName, data.systemInfo)
if err != nil {
manager.Close()
return
}
}
}
}
func queueSystemInfo(manager *websocket.Manager, dataCh chan<- systemInfoData, data systemInfoData) {
select {
case <-manager.Done():
case dataCh <- data:
}
}
func collectSystemInfoRound(
manager *websocket.Manager,
req AllSystemInfoRequest,
query url.Values,
queryEncoded string,
dataCh chan<- systemInfoData,
) (hasSuccess bool, err error) {
var numErrs atomic.Int32
totalAgents := int32(1) // myself
var errs gperr.Group
// get system info for me and all agents in parallel.
errs.Go(func() error {
data, err := systeminfo.Poller.GetRespData(req.Period, query)
if err != nil {
numErrs.Add(1)
return gperr.PrependSubject(err, "Main server")
}
queueSystemInfo(manager, dataCh, systemInfoData{
agentName: "GoDoxy",
systemInfo: data,
})
return nil
})
for _, a := range agentpool.Iter() {
totalAgents++
errs.Go(func() error {
data, err := getAgentSystemInfoWithRetry(manager.Context(), a, queryEncoded)
if err != nil {
numErrs.Add(1)
return gperr.PrependSubject(err, "Agent "+a.Name)
}
queueSystemInfo(manager, dataCh, systemInfoData{
agentName: a.Name,
systemInfo: data,
})
return nil
})
}
err = errs.Wait().Error()
return numErrs.Load() < totalAgents, err
}
func handleRoundResult(c *gin.Context, hasSuccess bool, err error, logPartial bool) (stop bool) {
if err == nil {
return false
}
if !hasSuccess {
c.Error(apitypes.InternalServerError(err, "failed to get all system info"))
return true
}
if logPartial {
log.Warn().Err(err).Msg("failed to get some system info")
}
return false
}
func getAgentSystemInfo(ctx context.Context, a *agentpool.Agent, query string) (bytesFromPool, error) {
ctx, cancel := context.WithTimeout(ctx, 5*time.Second)
defer cancel()
@@ -210,26 +196,35 @@ func getAgentSystemInfo(ctx context.Context, a *agentpool.Agent, query string) (
func getAgentSystemInfoWithRetry(ctx context.Context, a *agentpool.Agent, query string) (bytesFromPool, error) {
const maxRetries = 3
const retryDelay = 5 * time.Second
var attempt int
data, err := backoff.Retry(ctx, func() (bytesFromPool, error) {
attempt++
var lastErr error
for attempt := range maxRetries {
// Apply backoff delay for retries (not for first attempt)
if attempt > 0 {
delay := max((1<<attempt)*time.Second, 5*time.Second)
select {
case <-ctx.Done():
return bytesFromPool{}, ctx.Err()
case <-time.After(delay):
}
}
data, err := getAgentSystemInfo(ctx, a, query)
if err == nil {
return data, nil
}
log.Err(err).Str("agent", a.Name).Int("attempt", attempt).Msg("Agent request attempt failed")
return bytesFromPool{}, err
},
backoff.WithBackOff(backoff.NewConstantBackOff(retryDelay)),
backoff.WithMaxTries(maxRetries),
)
if err != nil {
return bytesFromPool{}, err
lastErr = err
log.Debug().Str("agent", a.Name).Int("attempt", attempt+1).Str("error", err.Error()).Msg("Agent request attempt failed")
// Don't retry on context cancellation
if ctx.Err() != nil {
return bytesFromPool{}, ctx.Err()
}
}
return data, nil
return bytesFromPool{}, lastErr
}
func marshalSystemInfo(ws *websocket.Manager, agentName string, systemInfo any) error {
@@ -241,7 +236,7 @@ func marshalSystemInfo(ws *websocket.Manager, agentName string, systemInfo any)
defer bufFromPool.release(bufFromPool.RawMessage)
}
err := sonic.ConfigDefault.NewEncoder(buf).Encode(map[string]any{
err := json.NewEncoder(buf).Encode(map[string]any{
agentName: systemInfo,
})
if err != nil {

View File

@@ -8,7 +8,6 @@ import (
"github.com/gin-gonic/gin"
"github.com/yusing/godoxy/internal/proxmox"
"github.com/yusing/goutils/apitypes"
"github.com/yusing/goutils/http/httpheaders"
"github.com/yusing/goutils/http/websocket"
)
@@ -16,10 +15,10 @@ import (
// e.g. ws://localhost:8889/api/v1/proxmox/journalctl/pve/127?service=pveproxy&service=pvedaemon&limit=10
type JournalctlRequest struct {
Node string `form:"node" uri:"node" binding:"required"` // Node name
VMID *int `form:"vmid" uri:"vmid"` // Container VMID (optional - if not provided, streams node journalctl)
Services []string `form:"service" uri:"service"` // Service names
Limit *int `form:"limit" uri:"limit" default:"100" binding:"omitempty,min=1,max=1000"` // Limit output lines (1-1000)
Node string `form:"node" uri:"node" binding:"required"` // Node name
VMID *int `form:"vmid" uri:"vmid"` // Container VMID (optional - if not provided, streams node journalctl)
Services []string `form:"service" uri:"service"` // Service names
Limit *int `form:"limit" uri:"limit" default:"100" binding:"min=1,max=1000"` // Limit output lines (1-1000)
} // @name ProxmoxJournalctlRequest
// @x-id "journalctl"
@@ -41,11 +40,6 @@ type JournalctlRequest struct {
// @Router /proxmox/journalctl/{node}/{vmid} [get]
// @Router /proxmox/journalctl/{node}/{vmid}/{service} [get]
func Journalctl(c *gin.Context) {
if !httpheaders.IsWebsocket(c.Request.Header) {
c.JSON(http.StatusBadRequest, apitypes.Error("websocket required"))
return
}
var request JournalctlRequest
uriErr := c.ShouldBindUri(&request)
queryErr := c.ShouldBindQuery(&request)
@@ -54,10 +48,6 @@ func Journalctl(c *gin.Context) {
return
}
if request.Limit == nil {
request.Limit = new(100)
}
node, ok := proxmox.Nodes.Get(request.Node)
if !ok {
c.JSON(http.StatusNotFound, apitypes.Error("node not found"))

View File

@@ -7,7 +7,6 @@ import (
"github.com/gin-gonic/gin"
"github.com/yusing/godoxy/internal/proxmox"
"github.com/yusing/goutils/apitypes"
"github.com/yusing/goutils/http/httpheaders"
"github.com/yusing/goutils/http/websocket"
)
@@ -35,11 +34,6 @@ type TailRequest struct {
// @Failure 500 {object} apitypes.ErrorResponse "Internal server error"
// @Router /proxmox/tail [get]
func Tail(c *gin.Context) {
if !httpheaders.IsWebsocket(c.Request.Header) {
c.JSON(http.StatusBadRequest, apitypes.Error("websocket required"))
return
}
var request TailRequest
if err := c.ShouldBindQuery(&request); err != nil {
c.JSON(http.StatusBadRequest, apitypes.Error("invalid request", err))

View File

@@ -64,6 +64,7 @@ type ParsedRule struct {
On string `json:"on"`
Do string `json:"do"`
ValidationError error `json:"validationError,omitempty"` // we need the structured error, not the plain string
IsResponseRule bool `json:"isResponseRule"`
} // @name ParsedRule
type FinalRequest struct {
@@ -297,6 +298,7 @@ func parseRules(rawRules []RawRule) ([]ParsedRule, rules.Rules, error) {
On: onStr,
Do: doStr,
ValidationError: validationErr,
IsResponseRule: rule.IsResponseRule(),
})
// Only add valid rules to execution list

View File

@@ -15,7 +15,7 @@ import (
type RouteType route.Route // @name Route
// @x-id "list"
// @x-id "routes"
// @BasePath /api/v1
// @Summary List routes
// @Description List routes

View File

@@ -1,4 +1,4 @@
# internal/auth
# Authentication
Authentication providers supporting OIDC and username/password authentication with JWT-based sessions.

View File

@@ -1,12 +1,12 @@
package auth
import (
"encoding/json"
"errors"
"fmt"
"net/http"
"time"
"github.com/bytedance/sonic"
"github.com/golang-jwt/jwt/v5"
"github.com/yusing/godoxy/internal/common"
httputils "github.com/yusing/goutils/http"
@@ -107,7 +107,7 @@ type UserPassAuthCallbackRequest struct {
func (auth *UserPassAuth) PostAuthCallbackHandler(w http.ResponseWriter, r *http.Request) {
var creds UserPassAuthCallbackRequest
err := sonic.ConfigDefault.NewDecoder(r.Body).Decode(&creds)
err := json.NewDecoder(r.Body).Decode(&creds)
if err != nil {
http.Error(w, "invalid request", http.StatusBadRequest)
return

View File

@@ -8,6 +8,7 @@ import (
"time"
"github.com/yusing/godoxy/internal/common"
strutils "github.com/yusing/goutils/strings"
)
var (
@@ -69,12 +70,12 @@ func cookieDomain(r *http.Request) string {
}
}
parts := strings.Split(reqHost, ".")
parts := strutils.SplitRune(reqHost, '.')
if len(parts) < 2 {
return ""
}
parts[0] = ""
return strings.Join(parts, ".")
return strutils.JoinRune(parts, '.')
}
func SetTokenCookie(w http.ResponseWriter, r *http.Request, name, value string, ttl time.Duration) {

View File

@@ -1,4 +1,4 @@
# internal/autocert
# Autocert Package
Automated SSL certificate management using the ACME protocol (Let's Encrypt and compatible CAs).

View File

@@ -23,35 +23,33 @@ import (
strutils "github.com/yusing/goutils/strings"
)
type (
ConfigExtra Config
Config struct {
Email string `json:"email,omitempty"`
Domains []string `json:"domains,omitempty"`
CertPath string `json:"cert_path,omitempty"`
KeyPath string `json:"key_path,omitempty"`
Extra []ConfigExtra `json:"extra,omitempty"`
ACMEKeyPath string `json:"acme_key_path,omitempty"` // shared by all extra providers with the same CA directory URL
Provider string `json:"provider,omitempty"`
Options map[string]strutils.Redacted `json:"options,omitempty"`
type ConfigExtra Config
type Config struct {
Email string `json:"email,omitempty"`
Domains []string `json:"domains,omitempty"`
CertPath string `json:"cert_path,omitempty"`
KeyPath string `json:"key_path,omitempty"`
Extra []ConfigExtra `json:"extra,omitempty"`
ACMEKeyPath string `json:"acme_key_path,omitempty"` // shared by all extra providers with the same CA directory URL
Provider string `json:"provider,omitempty"`
Options map[string]strutils.Redacted `json:"options,omitempty"`
Resolvers []string `json:"resolvers,omitempty"`
Resolvers []string `json:"resolvers,omitempty"`
// Custom ACME CA
CADirURL string `json:"ca_dir_url,omitempty"`
CACerts []string `json:"ca_certs,omitempty"`
// Custom ACME CA
CADirURL string `json:"ca_dir_url,omitempty"`
CACerts []string `json:"ca_certs,omitempty"`
// EAB
EABKid string `json:"eab_kid,omitempty" validate:"required_with=EABHmac"`
EABHmac string `json:"eab_hmac,omitempty" validate:"required_with=EABKid"` // base64 encoded
// EAB
EABKid string `json:"eab_kid,omitempty" validate:"required_with=EABHmac"`
EABHmac string `json:"eab_hmac,omitempty" validate:"required_with=EABKid"` // base64 encoded
HTTPClient *http.Client `json:"-"` // for tests only
HTTPClient *http.Client `json:"-"` // for tests only
challengeProvider challenge.Provider
challengeProvider challenge.Provider
idx int // 0: main, 1+: extra[i]
}
)
idx int // 0: main, 1+: extra[i]
}
var (
ErrMissingField = gperr.New("missing field")

View File

@@ -68,7 +68,7 @@ func TestMultipleCertificatesLifecycle(t *testing.T) {
require.Equal(t, "custom", cfg.Extra[1].Provider)
/* track cert requests for all configs */
os.MkdirAll("certs", 0o755)
os.MkdirAll("certs", 0755)
defer os.RemoveAll("certs")
err = provider.ObtainCertIfNotExistsAll()

View File

@@ -1,4 +1,4 @@
# internal/config
# Configuration Management
Centralized YAML configuration management with thread-safe state access and provider initialization.

View File

@@ -1,4 +1,4 @@
# internal/config/query
# Configuration Query
Read-only access to the active configuration state, including route providers and system statistics.
@@ -149,7 +149,7 @@ No metrics are currently exposed.
## Performance Characteristics
- O(n) where n is number of providers for provider queries
- O(n \* m) where m is routes per provider for route search
- O(n * m) where m is routes per provider for route search
- O(n) for statistics aggregation
- No locking required (uses atomic load)

View File

@@ -1,4 +1,4 @@
# internal/dnsproviders
# DNS Providers
DNS provider integrations for Let's Encrypt certificate management via the lego library.

View File

@@ -5,7 +5,7 @@ go 1.26.0
replace github.com/yusing/godoxy => ../..
require (
github.com/go-acme/lego/v4 v4.32.0
github.com/go-acme/lego/v4 v4.31.0
github.com/yusing/godoxy v0.26.0
)
@@ -23,12 +23,8 @@ require (
github.com/akamai/AkamaiOPEN-edgegrid-golang/v11 v11.1.0 // indirect
github.com/benbjohnson/clock v1.3.5 // indirect
github.com/boombuler/barcode v1.1.0 // indirect
github.com/bytedance/gopkg v0.1.3 // indirect
github.com/bytedance/sonic v1.15.0 // indirect
github.com/bytedance/sonic/loader v0.5.0 // indirect
github.com/cenkalti/backoff/v5 v5.0.3 // indirect
github.com/cespare/xxhash/v2 v2.3.0 // indirect
github.com/cloudwego/base64x v0.1.6 // indirect
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
github.com/fatih/structs v1.1.0 // indirect
github.com/felixge/httpsnoop v1.0.4 // indirect
@@ -53,7 +49,6 @@ require (
github.com/gotify/server/v2 v2.9.0 // indirect
github.com/hashicorp/go-cleanhttp v0.5.2 // indirect
github.com/hashicorp/go-retryablehttp v0.7.8 // indirect
github.com/klauspost/cpuid/v2 v2.3.0 // indirect
github.com/kolo/xmlrpc v0.0.0-20220921171641-a4b6fa1dd06b // indirect
github.com/kylelemons/godebug v1.1.0 // indirect
github.com/leodido/go-urn v1.4.0 // indirect
@@ -65,8 +60,8 @@ require (
github.com/mitchellh/go-homedir v1.1.0 // indirect
github.com/nrdcg/goacmedns v0.2.0 // indirect
github.com/nrdcg/goinwx v0.12.0 // indirect
github.com/nrdcg/oci-go-sdk/common/v1065 v1065.108.2 // indirect
github.com/nrdcg/oci-go-sdk/dns/v1065 v1065.108.2 // indirect
github.com/nrdcg/oci-go-sdk/common/v1065 v1065.108.1 // indirect
github.com/nrdcg/oci-go-sdk/dns/v1065 v1065.108.1 // indirect
github.com/nrdcg/porkbun v0.4.0 // indirect
github.com/ovh/go-ovh v1.9.0 // indirect
github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c // indirect
@@ -78,7 +73,6 @@ require (
github.com/sony/gobreaker v1.0.0 // indirect
github.com/stretchr/objx v0.5.3 // indirect
github.com/stretchr/testify v1.11.1 // indirect
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
github.com/vultr/govultr/v3 v3.27.0 // indirect
github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78 // indirect
github.com/yusing/gointernals v0.2.0 // indirect
@@ -89,7 +83,6 @@ require (
go.opentelemetry.io/otel/metric v1.40.0 // indirect
go.opentelemetry.io/otel/trace v1.40.0 // indirect
go.uber.org/ratelimit v0.3.1 // indirect
golang.org/x/arch v0.24.0 // indirect
golang.org/x/crypto v0.48.0 // indirect
golang.org/x/mod v0.33.0 // indirect
golang.org/x/net v0.50.0 // indirect
@@ -98,8 +91,8 @@ require (
golang.org/x/sys v0.41.0 // indirect
golang.org/x/text v0.34.0 // indirect
golang.org/x/tools v0.42.0 // indirect
google.golang.org/api v0.267.0 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20260217215200-42d3e9bedb6d // indirect
google.golang.org/api v0.266.0 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20260209200024-4cfbd4190f57 // indirect
google.golang.org/grpc v1.79.1 // indirect
google.golang.org/protobuf v1.36.11 // indirect
gopkg.in/ini.v1 v1.67.1 // indirect

View File

@@ -37,18 +37,10 @@ github.com/benbjohnson/clock v1.3.5/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZx
github.com/boombuler/barcode v1.0.1-0.20190219062509-6c824513bacc/go.mod h1:paBWMcWSl3LHKBqUq+rly7CNSldXjb2rDl3JlRe0mD8=
github.com/boombuler/barcode v1.1.0 h1:ChaYjBR63fr4LFyGn8E8nt7dBSt3MiU3zMOZqFvVkHo=
github.com/boombuler/barcode v1.1.0/go.mod h1:paBWMcWSl3LHKBqUq+rly7CNSldXjb2rDl3JlRe0mD8=
github.com/bytedance/gopkg v0.1.3 h1:TPBSwH8RsouGCBcMBktLt1AymVo2TVsBVCY4b6TnZ/M=
github.com/bytedance/gopkg v0.1.3/go.mod h1:576VvJ+eJgyCzdjS+c4+77QF3p7ubbtiKARP3TxducM=
github.com/bytedance/sonic v1.15.0 h1:/PXeWFaR5ElNcVE84U0dOHjiMHQOwNIx3K4ymzh/uSE=
github.com/bytedance/sonic v1.15.0/go.mod h1:tFkWrPz0/CUCLEF4ri4UkHekCIcdnkqXw9VduqpJh0k=
github.com/bytedance/sonic/loader v0.5.0 h1:gXH3KVnatgY7loH5/TkeVyXPfESoqSBSBEiDd5VjlgE=
github.com/bytedance/sonic/loader v0.5.0/go.mod h1:AR4NYCk5DdzZizZ5djGqQ92eEhCCcdf5x77udYiSJRo=
github.com/cenkalti/backoff/v5 v5.0.3 h1:ZN+IMa753KfX5hd8vVaMixjnqRZ3y8CuJKRKj1xcsSM=
github.com/cenkalti/backoff/v5 v5.0.3/go.mod h1:rkhZdG3JZukswDf7f0cwqPNk4K0sa+F97BxZthm/crw=
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
github.com/cloudwego/base64x v0.1.6 h1:t11wG9AECkCDk5fMSoxmufanudBtJ+/HemLstXDLI2M=
github.com/cloudwego/base64x v0.1.6/go.mod h1:OFcloc187FXDaYHvrNIjxSe8ncn0OOM8gEHfghB2IPU=
github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
@@ -62,8 +54,8 @@ github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2
github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U=
github.com/gabriel-vasile/mimetype v1.4.13 h1:46nXokslUBsAJE/wMsp5gtO500a4F3Nkz9Ufpk2AcUM=
github.com/gabriel-vasile/mimetype v1.4.13/go.mod h1:d+9Oxyo1wTzWdyVUPMmXFvp4F9tea18J8ufA774AB3s=
github.com/go-acme/lego/v4 v4.32.0 h1:z7Ss7aa1noabhKj+DBzhNCO2SM96xhE3b0ucVW3x8Tc=
github.com/go-acme/lego/v4 v4.32.0/go.mod h1:lI2fZNdgeM/ymf9xQ9YKbgZm6MeDuf91UrohMQE4DhI=
github.com/go-acme/lego/v4 v4.31.0 h1:gd4oUYdfs83PR1/SflkNdit9xY1iul2I4EystnU8NXM=
github.com/go-acme/lego/v4 v4.31.0/go.mod h1:m6zcfX/zcbMYDa8s6AnCMnoORWNP8Epnei+6NBCTUGs=
github.com/go-jose/go-jose/v4 v4.1.3 h1:CVLmWDhDVRa6Mi/IgCgaopNosCaHz7zrMeF9MlZRkrs=
github.com/go-jose/go-jose/v4 v4.1.3/go.mod h1:x4oUasVrzR7071A4TnHLGSPpNOm2a21K9Kf04k1rs08=
github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
@@ -119,8 +111,6 @@ github.com/jarcoal/httpmock v1.4.1 h1:0Ju+VCFuARfFlhVXFc2HxlcQkfB+Xq12/EotHko+x2
github.com/jarcoal/httpmock v1.4.1/go.mod h1:ftW1xULwo+j0R0JJkJIIi7UKigZUXCLLanykgjwBXL0=
github.com/keybase/go-keychain v0.0.1 h1:way+bWYa6lDppZoZcgMbYsvC7GxljxrskdNInRtuthU=
github.com/keybase/go-keychain v0.0.1/go.mod h1:PdEILRW3i9D8JcdM+FmY6RwkHGnhHxXwkPPMeUgOK1k=
github.com/klauspost/cpuid/v2 v2.3.0 h1:S4CRMLnYUhGeDFDqkGriYKdfoFlDnMtqTiI/sFzhA9Y=
github.com/klauspost/cpuid/v2 v2.3.0/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0=
github.com/kolo/xmlrpc v0.0.0-20220921171641-a4b6fa1dd06b h1:udzkj9S/zlT5X367kqJis0QP7YMxobob6zhzq6Yre00=
github.com/kolo/xmlrpc v0.0.0-20220921171641-a4b6fa1dd06b/go.mod h1:pcaDhQK0/NJZEvtCO0qQPPropqV0sJOJ6YW7X+9kRwM=
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
@@ -150,10 +140,10 @@ github.com/nrdcg/goacmedns v0.2.0 h1:ADMbThobzEMnr6kg2ohs4KGa3LFqmgiBA22/6jUWJR0
github.com/nrdcg/goacmedns v0.2.0/go.mod h1:T5o6+xvSLrQpugmwHvrSNkzWht0UGAwj2ACBMhh73Cg=
github.com/nrdcg/goinwx v0.12.0 h1:ujdUqDBnaRSFwzVnImvPHYw3w3m9XgmGImNUw1GyMb4=
github.com/nrdcg/goinwx v0.12.0/go.mod h1:IrVKd3ZDbFiMjdPgML4CSxZAY9wOoqLvH44zv3NodJ0=
github.com/nrdcg/oci-go-sdk/common/v1065 v1065.108.2 h1:OWijzl3nHUApvTivl+3+78dbBwmyEHOnb+W9m6ixGbk=
github.com/nrdcg/oci-go-sdk/common/v1065 v1065.108.2/go.mod h1:Gcs8GCaZXL3FdiDWgdnMxlOLEdRprJJnPYB22TX1jw8=
github.com/nrdcg/oci-go-sdk/dns/v1065 v1065.108.2 h1:9LsjN/zaIN7H8JE61NHpbWhxF0UGY96+kMlk3g8OvGU=
github.com/nrdcg/oci-go-sdk/dns/v1065 v1065.108.2/go.mod h1:32vZH06TuwZSn+IDMO1qcDvC2vHVlzUALCwXGWPA+dc=
github.com/nrdcg/oci-go-sdk/common/v1065 v1065.108.1 h1:3oOIAQ9Fd2qTKTS/VlWmvKyBPKKhXBcCXjRZqOUypI4=
github.com/nrdcg/oci-go-sdk/common/v1065 v1065.108.1/go.mod h1:Gcs8GCaZXL3FdiDWgdnMxlOLEdRprJJnPYB22TX1jw8=
github.com/nrdcg/oci-go-sdk/dns/v1065 v1065.108.1 h1:2H75475moAv1hVVYlOk815KfqeiFCiQ7ovqn3OnN6FY=
github.com/nrdcg/oci-go-sdk/dns/v1065 v1065.108.1/go.mod h1:9HGOXiiQxcsG+4amgdr4xBIMq6IchdLW/nQDyZz07IE=
github.com/nrdcg/porkbun v0.4.0 h1:rWweKlwo1PToQ3H+tEO9gPRW0wzzgmI/Ob3n2Guticw=
github.com/nrdcg/porkbun v0.4.0/go.mod h1:/QMskrHEIM0IhC/wY7iTCUgINsxdT2WcOphktJ9+Q54=
github.com/ovh/go-ovh v1.9.0 h1:6K8VoL3BYjVV3In9tPJUdT7qMx9h0GExN9EXx1r2kKE=
@@ -188,11 +178,8 @@ github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81P
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI=
github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08=
github.com/vultr/govultr/v3 v3.27.0 h1:J8etMyu/Jh5+idMsu2YZpOWmDXXHeW4VZnkYXmJYHx8=
github.com/vultr/govultr/v3 v3.27.0/go.mod h1:9WwnWGCKnwDlNjHjtt+j+nP+0QWq6hQXzaHgddqrLWY=
github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78 h1:ilQV1hzziu+LLM3zUTJ0trRztfwgjqKnBWNtSRkbmwM=
@@ -221,8 +208,6 @@ go.uber.org/atomic v1.11.0 h1:ZvwS0R+56ePWxUNi+Atn9dWONBPp/AUETXlHW0DxSjE=
go.uber.org/atomic v1.11.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0=
go.uber.org/ratelimit v0.3.1 h1:K4qVE+byfv/B3tC+4nYWP7v/6SimcO7HzHekoMNBma0=
go.uber.org/ratelimit v0.3.1/go.mod h1:6euWsTB6U/Nb3X++xEUXA8ciPJvr19Q/0h1+oDcJhRk=
golang.org/x/arch v0.24.0 h1:qlJ3M9upxvFfwRM51tTg3Yl+8CP9vCC1E7vlFpgv99Y=
golang.org/x/arch v0.24.0/go.mod h1:dNHoOeKiyja7GTvF9NJS1l3Z2yntpQNzgrjh1cU103A=
golang.org/x/crypto v0.48.0 h1:/VRzVqiRSggnhY7gNRxPauEQ5Drw9haKdM0jqfcCFts=
golang.org/x/crypto v0.48.0/go.mod h1:r0kV5h3qnFPlQnBSrULhlsRfryS2pmewsg+XfMgkVos=
golang.org/x/mod v0.33.0 h1:tHFzIWbBifEmbwtGz65eaWyGiGZatSrT9prnU8DbVL8=
@@ -249,14 +234,14 @@ golang.org/x/tools v0.42.0 h1:uNgphsn75Tdz5Ji2q36v/nsFSfR/9BRFvqhGBaJGd5k=
golang.org/x/tools v0.42.0/go.mod h1:Ma6lCIwGZvHK6XtgbswSoWroEkhugApmsXyrUmBhfr0=
gonum.org/v1/gonum v0.16.0 h1:5+ul4Swaf3ESvrOnidPp4GZbzf0mxVQpDCYUQE7OJfk=
gonum.org/v1/gonum v0.16.0/go.mod h1:fef3am4MQ93R2HHpKnLk4/Tbh/s0+wqD5nfa6Pnwy4E=
google.golang.org/api v0.267.0 h1:w+vfWPMPYeRs8qH1aYYsFX68jMls5acWl/jocfLomwE=
google.golang.org/api v0.267.0/go.mod h1:Jzc0+ZfLnyvXma3UtaTl023TdhZu6OMBP9tJ+0EmFD0=
google.golang.org/api v0.266.0 h1:hco+oNCf9y7DmLeAtHJi/uBAY7n/7XC9mZPxu1ROiyk=
google.golang.org/api v0.266.0/go.mod h1:Jzc0+ZfLnyvXma3UtaTl023TdhZu6OMBP9tJ+0EmFD0=
google.golang.org/genproto v0.0.0-20260128011058-8636f8732409 h1:VQZ/yAbAtjkHgH80teYd2em3xtIkkHd7ZhqfH2N9CsM=
google.golang.org/genproto v0.0.0-20260128011058-8636f8732409/go.mod h1:rxKD3IEILWEu3P44seeNOAwZN4SaoKaQ/2eTg4mM6EM=
google.golang.org/genproto/googleapis/api v0.0.0-20260128011058-8636f8732409 h1:merA0rdPeUV3YIIfHHcH4qBkiQAc1nfCKSI7lB4cV2M=
google.golang.org/genproto/googleapis/api v0.0.0-20260128011058-8636f8732409/go.mod h1:fl8J1IvUjCilwZzQowmw2b7HQB2eAuYBabMXzWurF+I=
google.golang.org/genproto/googleapis/rpc v0.0.0-20260217215200-42d3e9bedb6d h1:t/LOSXPJ9R0B6fnZNyALBRfZBH0Uy0gT+uR+SJ6syqQ=
google.golang.org/genproto/googleapis/rpc v0.0.0-20260217215200-42d3e9bedb6d/go.mod h1:4Hqkh8ycfw05ld/3BWL7rJOSfebL2Q+DVDeRgYgxUU8=
google.golang.org/genproto/googleapis/rpc v0.0.0-20260209200024-4cfbd4190f57 h1:mWPCjDEyshlQYzBpMNHaEof6UX1PmHcaUODUywQ0uac=
google.golang.org/genproto/googleapis/rpc v0.0.0-20260209200024-4cfbd4190f57/go.mod h1:j9x/tPzZkyxcgEFkiKEEGxfvyumM01BEtsW8xzOahRQ=
google.golang.org/grpc v1.79.1 h1:zGhSi45ODB9/p3VAawt9a+O/MULLl9dpizzNNpq7flY=
google.golang.org/grpc v1.79.1/go.mod h1:KmT0Kjez+0dde/v2j9vzwoAScgEPx/Bw1CYChhHLrHQ=
google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE=

View File

@@ -1,4 +1,4 @@
# internal/docker
# Docker Integration
Docker container discovery, connection management, and label-based route configuration.

View File

@@ -14,7 +14,7 @@ import (
"unsafe"
"github.com/docker/cli/cli/connhelper"
"github.com/moby/moby/client"
"github.com/docker/docker/client"
"github.com/rs/zerolog/log"
"github.com/yusing/godoxy/agent/pkg/agent"
"github.com/yusing/godoxy/internal/agentpool"
@@ -198,7 +198,9 @@ func NewClient(cfg types.DockerProviderConfig, unique ...bool) (*SharedClient, e
opt = append(opt, client.WithTLSClientConfig(cfg.TLS.CAFile, cfg.TLS.CertFile, cfg.TLS.KeyFile))
}
client, err := client.New(opt...)
opt = append(opt, client.WithAPIVersionNegotiation())
client, err := client.NewClientWithOpts(opt...)
if err != nil {
return nil, err
}

View File

@@ -11,9 +11,8 @@ import (
"strconv"
"strings"
"github.com/docker/docker/api/types/container"
"github.com/docker/go-connections/nat"
"github.com/moby/moby/api/types/container"
"github.com/moby/moby/client"
"github.com/yusing/godoxy/agent/pkg/agent"
"github.com/yusing/godoxy/internal/agentpool"
"github.com/yusing/godoxy/internal/serialization"
@@ -99,18 +98,18 @@ func UpdatePorts(ctx context.Context, c *types.Container) error {
}
defer dockerClient.Close()
inspect, err := dockerClient.ContainerInspect(ctx, c.ContainerID, client.ContainerInspectOptions{})
inspect, err := dockerClient.ContainerInspect(ctx, c.ContainerID)
if err != nil {
return err
}
for port := range inspect.Container.Config.ExposedPorts {
proto, portStr := nat.SplitProtoPort(port.String())
for port := range inspect.Config.ExposedPorts {
proto, portStr := nat.SplitProtoPort(string(port))
portInt, _ := nat.ParsePort(portStr)
if portInt == 0 {
continue
}
c.PublicPortMapping[portInt] = container.PortSummary{
c.PublicPortMapping[portInt] = container.Port{
PublicPort: uint16(portInt), //nolint:gosec
PrivatePort: uint16(portInt), //nolint:gosec
Type: proto,
@@ -211,8 +210,8 @@ func setPrivateHostname(c *types.Container, helper containerHelper) {
}
if c.Network != "" {
v, hasNetwork := helper.NetworkSettings.Networks[c.Network]
if hasNetwork && v.IPAddress.IsValid() {
c.PrivateHostname = v.IPAddress.String()
if hasNetwork && v.IPAddress != "" {
c.PrivateHostname = v.IPAddress
return
}
var hasComposeNetwork bool
@@ -220,9 +219,9 @@ func setPrivateHostname(c *types.Container, helper containerHelper) {
if proj := DockerComposeProject(c); proj != "" {
newNetwork := fmt.Sprintf("%s_%s", proj, c.Network)
v, hasComposeNetwork = helper.NetworkSettings.Networks[newNetwork]
if hasComposeNetwork && v.IPAddress.IsValid() {
if hasComposeNetwork && v.IPAddress != "" {
c.Network = newNetwork // update network to the new one
c.PrivateHostname = v.IPAddress.String()
c.PrivateHostname = v.IPAddress
return
}
}
@@ -235,9 +234,9 @@ func setPrivateHostname(c *types.Container, helper containerHelper) {
}
// fallback to first network if no network is specified
for k, v := range helper.NetworkSettings.Networks {
if v.IPAddress.IsValid() {
if v.IPAddress != "" {
c.Network = k // update network to the first network
c.PrivateHostname = v.IPAddress.String()
c.PrivateHostname = v.IPAddress
return
}
}

View File

@@ -3,7 +3,7 @@ package docker
import (
"strings"
"github.com/moby/moby/api/types/container"
"github.com/docker/docker/api/types/container"
"github.com/yusing/ds/ordered"
"github.com/yusing/godoxy/internal/types"
strutils "github.com/yusing/goutils/strings"
@@ -46,8 +46,8 @@ func (c containerHelper) getMounts() *ordered.Map[string, string] {
}
func (c containerHelper) parseImage() *types.ContainerImage {
colonSep := strings.Split(c.Image, ":")
slashSep := strings.Split(colonSep[0], "/")
colonSep := strutils.SplitRune(c.Image, ':')
slashSep := strutils.SplitRune(colonSep[0], '/')
_, sha256, _ := strings.Cut(c.ImageID, ":")
im := &types.ContainerImage{
SHA256: sha256,

View File

@@ -3,7 +3,7 @@ package docker
import (
"testing"
"github.com/moby/moby/api/types/container"
"github.com/docker/docker/api/types/container"
"github.com/yusing/godoxy/internal/types"
expect "github.com/yusing/goutils/testing"
)

View File

@@ -9,6 +9,7 @@ import (
"github.com/goccy/go-yaml"
"github.com/yusing/godoxy/internal/types"
gperr "github.com/yusing/goutils/errs"
strutils "github.com/yusing/goutils/strings"
)
var ErrInvalidLabel = errors.New("invalid label")
@@ -30,7 +31,7 @@ func ParseLabels(labels map[string]string, aliases ...string) (types.LabelMap, e
ExpandWildcard(labels, aliases...)
for lbl, value := range labels {
parts := strings.Split(lbl, ".")
parts := strutils.SplitRune(lbl, '.')
if parts[0] != NSProxy {
continue
}

View File

@@ -3,12 +3,12 @@ package docker
import (
"context"
"github.com/moby/moby/api/types/container"
"github.com/moby/moby/client"
"github.com/docker/docker/api/types/container"
"github.com/docker/docker/client"
"github.com/yusing/godoxy/internal/types"
)
var listOptions = client.ContainerListOptions{
var listOptions = container.ListOptions{
// created|restarting|running|removing|paused|exited|dead
// Filters: filters.NewArgs(
// filters.Arg("status", "created"),
@@ -31,7 +31,7 @@ func ListContainers(ctx context.Context, dockerCfg types.DockerProviderConfig) (
if err != nil {
return nil, err
}
return containers.Items, nil
return containers, nil
}
func IsErrConnectionFailed(err error) bool {

View File

@@ -1,4 +1,4 @@
# internal/entrypoint
# Entrypoint
The entrypoint package provides the main HTTP entry point for GoDoxy, handling domain-based routing, middleware application, short link matching, access logging, and HTTP server lifecycle management.

View File

@@ -162,9 +162,10 @@ func (ep *Entrypoint) SetAccessLogger(parent task.Parent, cfg *accesslog.Request
}
func findRouteAnyDomain(routes HTTPRoutes, host string) types.HTTPRoute {
before, _, ok := strings.Cut(host, ".")
if ok {
target := before
//nolint:modernize
idx := strings.IndexByte(host, '.')
if idx != -1 {
target := host[:idx]
if r, ok := routes.Get(target); ok {
return r
}

View File

@@ -6,7 +6,7 @@ import (
type ContextKey struct{}
func SetCtx(ctx interface{ SetValue(key any, value any) }, ep Entrypoint) {
func SetCtx(ctx interface{ SetValue(any, any) }, ep Entrypoint) {
ctx.SetValue(ContextKey{}, ep)
}

View File

@@ -1,4 +1,4 @@
# internal/health/check
# Health Check Package
Low-level health check implementations for different protocols and services in GoDoxy.

View File

@@ -2,13 +2,12 @@ package healthcheck
import (
"context"
"encoding/json"
"errors"
"net/http"
"time"
"github.com/bytedance/sonic"
"github.com/moby/moby/api/types/container"
"github.com/moby/moby/client"
"github.com/yusing/godoxy/internal/docker"
"github.com/yusing/godoxy/internal/types"
httputils "github.com/yusing/goutils/http"
@@ -46,7 +45,7 @@ func Docker(ctx context.Context, state *DockerHealthcheckState, timeout time.Dur
defer cancel()
// the actual inspect response is intercepted and returned as RequestInterceptedError
_, err := state.client.ContainerInspect(ctx, state.containerID, client.ContainerInspectOptions{})
_, err := state.client.ContainerInspect(ctx, state.containerID)
var interceptedErr *httputils.RequestInterceptedError
if !httputils.AsRequestInterceptedError(err, &interceptedErr) {
@@ -108,7 +107,7 @@ func interceptDockerInspectResponse(resp *http.Response) (intercepted bool, err
}
var state container.State
err = sonic.Unmarshal(body, &state)
err = json.Unmarshal(body, &state)
release(body)
if err != nil {
return false, err

View File

@@ -24,13 +24,12 @@ var h2cClient = &http.Client{
},
},
}
var pinger = &fasthttp.Client{
MaxConnDuration: 0,
DisableHeaderNamesNormalizing: true,
DisablePathNormalizing: true,
TLSConfig: &tls.Config{
InsecureSkipVerify: true, //nolint:gosec
InsecureSkipVerify: true,
},
MaxConnsPerHost: 1000,
NoDefaultUserAgentHeader: true,
@@ -52,7 +51,7 @@ func HTTP(url *url.URL, method, path string, timeout time.Duration) (types.Healt
respErr := pinger.DoTimeout(req, resp, timeout)
lat := time.Since(start)
return processHealthResponse(lat, respErr, resp.StatusCode), nil
return processHealthResponse(lat, respErr, resp.StatusCode)
}
func H2C(ctx context.Context, url *url.URL, method, path string, timeout time.Duration) (types.HealthCheckResult, error) {
@@ -88,7 +87,7 @@ func H2C(ctx context.Context, url *url.URL, method, path string, timeout time.Du
defer resp.Body.Close()
}
return processHealthResponse(lat, err, func() int { return resp.StatusCode }), nil
return processHealthResponse(lat, err, func() int { return resp.StatusCode })
}
var userAgent = "GoDoxy/" + version.Get().String()
@@ -101,20 +100,20 @@ func setCommonHeaders(setHeader func(key, value string)) {
setHeader("Pragma", "no-cache")
}
func processHealthResponse(lat time.Duration, err error, getStatusCode func() int) types.HealthCheckResult {
func processHealthResponse(lat time.Duration, err error, getStatusCode func() int) (types.HealthCheckResult, error) {
if err != nil {
var tlsErr *tls.CertificateVerificationError
if ok := errors.As(err, &tlsErr); !ok {
return types.HealthCheckResult{
Latency: lat,
Detail: err.Error(),
}
}, nil
}
return types.HealthCheckResult{
Latency: lat,
Healthy: true,
Detail: tlsErr.Error(),
}
}, nil
}
statusCode := getStatusCode()
@@ -122,11 +121,11 @@ func processHealthResponse(lat time.Duration, err error, getStatusCode func() in
return types.HealthCheckResult{
Latency: lat,
Detail: http.StatusText(statusCode),
}
}, nil
}
return types.HealthCheckResult{
Latency: lat,
Healthy: true,
}
}, nil
}

View File

@@ -1,4 +1,4 @@
# internal/health/monitor
# Health Monitor Package
Route health monitoring with configurable check intervals, retry policies, and notification integration.

View File

@@ -6,7 +6,6 @@ import (
"fmt"
"math/rand"
"net/url"
"strings"
"sync/atomic"
"time"
@@ -200,7 +199,7 @@ func (mon *monitor) Detail() string {
// Name implements HealthMonitor.
func (mon *monitor) Name() string {
parts := strings.Split(mon.service, "/")
parts := strutils.SplitRune(mon.service, '/')
return parts[len(parts)-1]
}
@@ -245,11 +244,9 @@ func (mon *monitor) checkUpdateHealth() error {
// change of status
if result.Healthy != (lastStatus == types.StatusHealthy) {
if result.Healthy {
mon.notifyServiceUp(&logger, &result)
mon.numConsecFailures.Store(0)
if mon.downNotificationSent.Load() { // Only notify if the service down has been notified
mon.notifyServiceUp(&logger, &result)
mon.downNotificationSent.Store(false) // Reset notification state when service comes back up
}
mon.downNotificationSent.Store(false) // Reset notification state when service comes back up
} else if mon.config.Retries < 0 {
// immediate notification when retries < 0
mon.notifyServiceDown(&logger, &result)

View File

@@ -171,12 +171,12 @@ func TestNotification_ServiceRecoversBeforeThreshold(t *testing.T) {
err = mon.checkUpdateHealth()
require.NoError(t, err)
// Should have no notifications because threshold was never met.
// Recovery notification is only sent after a down notification was sent.
// Should have 1 up notification, but no down notification
// because threshold was never met
up, down, last := tracker.getStats()
require.Equal(t, 0, down)
require.Equal(t, 0, up)
require.Empty(t, last)
require.Equal(t, 1, up)
require.Equal(t, "up", last)
}
func TestNotification_ConsecutiveFailureReset(t *testing.T) {
@@ -210,11 +210,10 @@ func TestNotification_ConsecutiveFailureReset(t *testing.T) {
err = mon.checkUpdateHealth()
require.NoError(t, err)
// Should have no notifications, consecutive failures should reset.
// Recovery notification is only sent after a down notification was sent.
// Should have 1 up notification, consecutive failures should reset
up, down, _ := tracker.getStats()
require.Equal(t, 0, down)
require.Equal(t, 0, up)
require.Equal(t, 1, up)
// Go down again - consecutive counter should start from 0
mon.checkHealth = func(u *url.URL) (types.HealthCheckResult, error) {
@@ -228,7 +227,7 @@ func TestNotification_ConsecutiveFailureReset(t *testing.T) {
// Should still have no down notifications (need 2 consecutive)
up, down, _ = tracker.getStats()
require.Equal(t, 0, down)
require.Equal(t, 0, up)
require.Equal(t, 1, up)
// Second consecutive failure - should trigger notification
err = mon.checkUpdateHealth()
@@ -237,7 +236,7 @@ func TestNotification_ConsecutiveFailureReset(t *testing.T) {
// Now should have down notification
up, down, last := tracker.getStats()
require.Equal(t, 1, down)
require.Equal(t, 0, up)
require.Equal(t, 1, up)
require.Equal(t, "down", last)
}
@@ -280,11 +279,11 @@ func TestNotification_ContextCancellation(t *testing.T) {
require.Equal(t, 0, up)
}
func TestImmediateUpNotificationAfterDownNotification(t *testing.T) {
func TestImmediateUpNotification(t *testing.T) {
config := types.HealthCheckConfig{
Interval: 100 * time.Millisecond,
Timeout: 50 * time.Millisecond,
Retries: 2,
Retries: 2, // NotifyAfter should not affect up notifications
}
mon, tracker := createTestMonitor(config, func(u *url.URL) (types.HealthCheckResult, error) {
@@ -293,7 +292,6 @@ func TestImmediateUpNotificationAfterDownNotification(t *testing.T) {
// Start unhealthy
mon.status.Store(types.StatusUnhealthy)
mon.downNotificationSent.Store(true)
// Set to healthy
mon.checkHealth = func(u *url.URL) (types.HealthCheckResult, error) {
@@ -304,7 +302,7 @@ func TestImmediateUpNotificationAfterDownNotification(t *testing.T) {
err := mon.checkUpdateHealth()
require.NoError(t, err)
// Up notification should happen immediately once a prior down notification exists.
// Up notification should happen immediately regardless of NotifyAfter setting
require.Equal(t, types.StatusHealthy, mon.Status())
// Should have exactly 1 up notification immediately

View File

@@ -1,4 +1,4 @@
# internal/homepage
# Homepage
The homepage package provides the GoDoxy WebUI dashboard with support for categories, favorites, widgets, dynamic item configuration, and icon management.

View File

@@ -9,6 +9,10 @@ import (
expect "github.com/yusing/goutils/testing"
)
func strPtr(s string) *string {
return &s
}
func TestOverrideItem(t *testing.T) {
a := &Item{
Alias: "foo",
@@ -16,7 +20,7 @@ func TestOverrideItem(t *testing.T) {
Show: false,
Name: "Foo",
Icon: &icons.URL{
FullURL: new("/favicon.ico"),
FullURL: strPtr("/favicon.ico"),
Source: icons.SourceRelative,
},
Category: "App",
@@ -27,7 +31,7 @@ func TestOverrideItem(t *testing.T) {
Name: "Bar",
Category: "Test",
Icon: &icons.URL{
FullURL: new("@walkxcode/example.png"),
FullURL: strPtr("@walkxcode/example.png"),
Source: icons.SourceWalkXCode,
},
}

View File

@@ -1,4 +1,4 @@
# internal/homepage/icons
# Icons Package
Icon URL parsing, fetching, and listing for the homepage dashboard.

View File

@@ -3,12 +3,10 @@ package iconfetch
import (
"bytes"
"context"
"crypto/tls"
"errors"
"fmt"
"io"
"math"
"mime"
"net/http"
"net/url"
"slices"
@@ -19,6 +17,7 @@ import (
"github.com/gin-gonic/gin"
"github.com/vincent-petithory/dataurl"
"github.com/yusing/godoxy/internal/homepage/icons"
gphttp "github.com/yusing/godoxy/internal/net/gphttp"
apitypes "github.com/yusing/goutils/apitypes"
"github.com/yusing/goutils/cache"
httputils "github.com/yusing/goutils/http"
@@ -77,42 +76,26 @@ func FetchFavIconFromURL(ctx context.Context, iconURL *icons.URL) (Result, error
return FetchResultWithErrorf(http.StatusBadRequest, "invalid icon source")
}
var fetchIconClient = http.Client{
Timeout: faviconFetchTimeout,
Transport: &http.Transport{
TLSClientConfig: &tls.Config{
InsecureSkipVerify: true, //nolint:gosec
},
},
CheckRedirect: func(req *http.Request, via []*http.Request) error {
return http.ErrUseLastResponse
},
}
var FetchIconAbsolute = cache.NewKeyFunc(func(ctx context.Context, url string) (Result, error) {
req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)
if err != nil {
return FetchResultWithErrorf(http.StatusInternalServerError, "cannot create request: %w", err)
}
resp, err := fetchIconClient.Do(req)
if err != nil {
resp, err := gphttp.Do(req)
if err == nil {
defer resp.Body.Close()
} else {
if errors.Is(err, context.DeadlineExceeded) || errors.Is(err, context.Canceled) {
return FetchResultWithErrorf(http.StatusBadGateway, "request timeout")
}
return FetchResultWithErrorf(http.StatusBadGateway, "connection error: %w", err)
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
return FetchResultWithErrorf(resp.StatusCode, "upstream error: http %d", resp.StatusCode)
}
ct, _, _ := mime.ParseMediaType(resp.Header.Get("Content-Type"))
if ct != "" && !strings.HasPrefix(ct, "image/") {
return FetchResultWithErrorf(http.StatusNotFound, "not an image")
}
icon, err := io.ReadAll(resp.Body)
if err != nil {
return FetchResultWithErrorf(http.StatusInternalServerError, "failed to read response body: %w", err)
@@ -122,15 +105,10 @@ var FetchIconAbsolute = cache.NewKeyFunc(func(ctx context.Context, url string) (
return FetchResultWithErrorf(http.StatusNotFound, "empty icon")
}
if ct == "" {
ct = http.DetectContentType(icon)
if !strings.HasPrefix(ct, "image/") {
return FetchResultWithErrorf(http.StatusNotFound, "not an image")
}
}
res := Result{Icon: icon}
res.contentType = ct
if contentType := resp.Header.Get("Content-Type"); contentType != "" {
res.contentType = contentType
}
// else leave it empty
return res, nil
}).WithMaxEntries(200).WithRetriesExponentialBackoff(3).WithTTL(4 * time.Hour).Build()

View File

@@ -2,12 +2,12 @@ package iconlist
import (
"context"
"encoding/json"
"net/http"
"slices"
"strings"
"time"
"github.com/bytedance/sonic"
"github.com/lithammer/fuzzysearch/fuzzy"
"github.com/rs/zerolog/log"
"github.com/yusing/godoxy/internal/common"
@@ -55,27 +55,26 @@ func init() {
func InitCache() {
m := make(IconMap)
err := serialization.LoadFileIfExist(common.IconListCachePath, &m, sonic.Unmarshal)
switch {
case err != nil:
err := serialization.LoadFileIfExist(common.IconListCachePath, &m, json.Unmarshal)
if err != nil {
// backward compatible
oldFormat := struct {
Icons IconMap
LastUpdate time.Time
}{}
err = serialization.LoadFileIfExist(common.IconListCachePath, &oldFormat, sonic.Unmarshal)
err = serialization.LoadFileIfExist(common.IconListCachePath, &oldFormat, json.Unmarshal)
if err != nil {
log.Error().Err(err).Msg("failed to load icons")
} else {
m = oldFormat.Icons
// store it to disk immediately
_ = serialization.SaveFile(common.IconListCachePath, &m, 0o644, sonic.Marshal)
_ = serialization.SaveFile(common.IconListCachePath, &m, 0o644, json.Marshal)
}
case len(m) > 0:
} else if len(m) > 0 {
log.Info().
Int("icons", len(m)).
Msg("icons loaded")
default:
} else {
if err := updateIcons(m); err != nil {
log.Error().Err(err).Msg("failed to update icons")
}
@@ -85,7 +84,7 @@ func InitCache() {
task.OnProgramExit("save_icons_cache", func() {
icons := iconsCache.Load()
_ = serialization.SaveFile(common.IconListCachePath, &icons, 0o644, sonic.Marshal)
_ = serialization.SaveFile(common.IconListCachePath, &icons, 0o644, json.Marshal)
})
go backgroundUpdateIcons()
@@ -106,7 +105,7 @@ func backgroundUpdateIcons() {
// swap old cache with new cache
iconsCache.Store(newCache)
// save it to disk
err := serialization.SaveFile(common.IconListCachePath, &newCache, 0o644, sonic.Marshal)
err := serialization.SaveFile(common.IconListCachePath, &newCache, 0o644, json.Marshal)
if err != nil {
log.Warn().Err(err).Msg("failed to save icons")
}
@@ -143,46 +142,33 @@ func SearchIcons(keyword string, limit int) []*IconMetaSearch {
return a.rank - b.rank
}
dashedKeyword := strings.ReplaceAll(keyword, " ", "-")
whitespacedKeyword := strings.ReplaceAll(keyword, "-", " ")
var rank int
icons := ListAvailableIcons()
for k, icon := range icons {
source, ref := k.SourceRef()
var rank int
switch {
case strings.EqualFold(ref, dashedKeyword):
// exact match: best rank, use source as tiebreaker (lower index = higher priority)
if strutils.ContainsFold(string(k), keyword) || strutils.ContainsFold(icon.DisplayName, keyword) {
rank = 0
case strutils.HasPrefixFold(ref, dashedKeyword):
// prefix match: rank by how much extra the name has (shorter = better)
rank = 100 + len(ref) - len(dashedKeyword)
case strutils.ContainsFold(ref, dashedKeyword) || strutils.ContainsFold(icon.DisplayName, whitespacedKeyword):
// contains match
rank = 500 + len(ref) - len(dashedKeyword)
default:
rank = fuzzy.RankMatchFold(keyword, ref)
} else {
rank = fuzzy.RankMatchFold(keyword, string(k))
if rank == -1 || rank > 3 {
continue
}
rank += 1000
}
source, ref := k.SourceRef()
ranked := &IconMetaSearch{
Source: source,
Ref: ref,
Meta: icon,
rank: rank,
}
results = append(results, ranked)
// Sorted insert based on rank (lower rank = better match)
insertPos, _ := slices.BinarySearchFunc(results, ranked, sortByRank)
results = slices.Insert(results, insertPos, ranked)
if len(results) == searchLimit {
break
}
}
slices.SortStableFunc(results, sortByRank)
// Extract results and limit to the requested count
return results[:min(len(results), limit)]
}
@@ -263,8 +249,6 @@ func httpGetImpl(url string) ([]byte, func([]byte), error) {
}
/*
UpdateWalkxCodeIcons updates the icon map with the icons from walkxcode.
format:
{
@@ -286,7 +270,7 @@ func UpdateWalkxCodeIcons(m IconMap) error {
}
data := make(map[string][]string)
err = sonic.Unmarshal(body, &data)
err = json.Unmarshal(body, &data)
release(body)
if err != nil {
return err
@@ -365,7 +349,7 @@ func UpdateSelfhstIcons(m IconMap) error {
}
data := make([]SelfhStIcon, 0)
err = sonic.Unmarshal(body, &data) //nolint:musttag
err = json.Unmarshal(body, &data) //nolint:musttag
release(body)
if err != nil {
return err

View File

@@ -106,21 +106,21 @@ func (u *URL) Parse(v string) error {
func (u *URL) parse(v string, checkExists bool) error {
if v == "" {
return gperr.PrependSubject(ErrInvalidIconURL, "empty url")
return ErrInvalidIconURL
}
slashIndex := strings.Index(v, "/")
if slashIndex == -1 {
return gperr.PrependSubject(ErrInvalidIconURL, v)
return ErrInvalidIconURL
}
beforeSlash := v[:slashIndex]
switch beforeSlash {
case "http:", "https:":
u.FullURL = &v
u.Source = SourceAbsolute
case "@target", "": // @target/favicon.ico, /favicon.ico
case "@target", "": // @target/favicon.ico, /favicon.ico
url := v[slashIndex:]
if url == "/" {
return gperr.PrependSubject(ErrInvalidIconURL, v).Withf("%s", "empty path")
return fmt.Errorf("%w: empty path", ErrInvalidIconURL)
}
u.FullURL = &url
u.Source = SourceRelative
@@ -132,16 +132,16 @@ func (u *URL) parse(v string, checkExists bool) error {
}
parts := strings.Split(v[slashIndex+1:], ".")
if len(parts) != 2 {
return gperr.PrependSubject(ErrInvalidIconURL, v).Withf("expect %s/<reference>.<format>, e.g. %s/adguard-home.webp", beforeSlash, beforeSlash)
return fmt.Errorf("%w: expect %s/<reference>.<format>, e.g. %s/adguard-home.webp", ErrInvalidIconURL, beforeSlash, beforeSlash)
}
reference, format := parts[0], strings.ToLower(parts[1])
if reference == "" || format == "" {
return gperr.PrependSubject(ErrInvalidIconURL, v).Withf("empty reference or format")
return ErrInvalidIconURL
}
switch format {
case "svg", "png", "webp":
default:
return gperr.PrependSubject(ErrInvalidIconURL, v).Withf("invalid image format, expect svg/png/webp")
return fmt.Errorf("%w: invalid image format, expect svg/png/webp", ErrInvalidIconURL)
}
isLight, isDark := false, false
if strings.HasSuffix(reference, "-light") {
@@ -159,7 +159,7 @@ func (u *URL) parse(v string, checkExists bool) error {
IsDark: isDark,
}
if checkExists && !u.HasIcon() {
return gperr.PrependSubject(ErrInvalidIconURL, v).Withf("no such icon from %s", u.Source)
return fmt.Errorf("%w: no such icon %s.%s from %s", ErrInvalidIconURL, reference, format, u.Source)
}
default:
return gperr.PrependSubject(ErrInvalidIconURL, v)

View File

@@ -7,6 +7,10 @@ import (
expect "github.com/yusing/goutils/testing"
)
func strPtr(s string) *string {
return &s
}
func TestIconURL(t *testing.T) {
tests := []struct {
name string
@@ -18,7 +22,7 @@ func TestIconURL(t *testing.T) {
name: "absolute",
input: "http://example.com/icon.png",
wantValue: &URL{
FullURL: new("http://example.com/icon.png"),
FullURL: strPtr("http://example.com/icon.png"),
Source: SourceAbsolute,
},
},
@@ -26,7 +30,7 @@ func TestIconURL(t *testing.T) {
name: "relative",
input: "@target/icon.png",
wantValue: &URL{
FullURL: new("/icon.png"),
FullURL: strPtr("/icon.png"),
Source: SourceRelative,
},
},
@@ -34,7 +38,7 @@ func TestIconURL(t *testing.T) {
name: "relative2",
input: "/icon.png",
wantValue: &URL{
FullURL: new("/icon.png"),
FullURL: strPtr("/icon.png"),
Source: SourceRelative,
},
},

View File

@@ -1,4 +1,4 @@
# internal/homepage/integrations/qbittorrent
# qBittorrent Integration Package
This package provides a qBittorrent widget for the GoDoxy homepage dashboard, enabling real-time monitoring of torrent status and transfer statistics.

View File

@@ -2,37 +2,25 @@ package qbittorrent
import (
"context"
"errors"
"encoding/json"
"fmt"
"io"
"net/http"
"net/url"
"github.com/bytedance/sonic"
"github.com/yusing/godoxy/internal/homepage/widgets"
strutils "github.com/yusing/goutils/strings"
)
type Client struct {
URL string
Username string
Password strutils.Redacted
Password string
}
func (c *Client) Initialize(ctx context.Context, url string, cfg map[string]any) error {
c.URL = url
username, ok := cfg["username"].(string)
if !ok {
return errors.New("username is not a string")
}
c.Username = username
password, ok := cfg["password"].(string)
if !ok {
return errors.New("password is not a string")
}
c.Password = strutils.Redacted(password)
c.Username = cfg["username"].(string)
c.Password = cfg["password"].(string)
_, err := c.Version(ctx)
if err != nil {
@@ -49,7 +37,7 @@ func (c *Client) doRequest(ctx context.Context, method, endpoint string, query u
}
if c.Username != "" && c.Password != "" {
req.SetBasicAuth(c.Username, c.Password.String())
req.SetBasicAuth(c.Username, c.Password)
}
resp, err := widgets.HTTPClient.Do(req)
@@ -71,7 +59,7 @@ func jsonRequest[T any](ctx context.Context, client *Client, endpoint string, qu
}
defer resp.Body.Close()
err = sonic.ConfigDefault.NewDecoder(resp.Body).Decode(&result)
err = json.NewDecoder(resp.Body).Decode(&result)
if err != nil {
return result, err
}

View File

@@ -2,11 +2,10 @@ package qbittorrent
import (
"context"
"encoding/json"
"net/url"
"strconv"
"time"
"github.com/bytedance/sonic"
)
const endpointLogs = "/api/v2/log/main"
@@ -45,7 +44,7 @@ func (l *LogEntry) Level() string {
}
func (l *LogEntry) MarshalJSON() ([]byte, error) {
return sonic.Marshal(map[string]any{
return json.Marshal(map[string]any{
"id": l.ID,
"timestamp": l.Timestamp,
"level": l.Level(),

View File

@@ -0,0 +1,13 @@
# Types Package
Configuration types for the homepage package.
## Config
```go
type Config struct {
UseDefaultCategories bool `json:"use_default_categories"`
}
var ActiveConfig atomic.Pointer[Config]
```

View File

@@ -1,4 +1,4 @@
# internal/homepage/widgets
# Homepage Widgets Package
> [!WARNING]
>

View File

@@ -1,4 +1,4 @@
# internal/idlewatcher
# Idlewatcher
Manages container lifecycle based on idle timeout, automatically stopping/pausing containers and waking them on request.

View File

@@ -1,10 +1,10 @@
package idlewatcher
import (
"encoding/json"
"iter"
"strconv"
"github.com/bytedance/sonic"
strutils "github.com/yusing/goutils/strings"
)
@@ -14,7 +14,7 @@ type watcherDebug struct {
func (w watcherDebug) MarshalJSON() ([]byte, error) {
state := w.state.Load()
return sonic.Marshal(map[string]any{
return json.Marshal(map[string]any{
"name": w.Name(),
"state": map[string]string{
"status": string(state.status),

View File

@@ -1,11 +1,11 @@
package idlewatcher
import (
"encoding/json"
"fmt"
"io"
"github.com/bytedance/sonic"
gevents "github.com/yusing/goutils/events"
"github.com/yusing/goutils/events"
)
type WakeEvent struct {
@@ -26,7 +26,7 @@ const (
)
func writeSSE(w io.Writer, v any) error {
data, err := sonic.Marshal(v)
data, err := json.Marshal(v)
if err != nil {
return err
}
@@ -58,12 +58,12 @@ func (w *Watcher) sendEvent(eventType WakeEventType, message string, err error)
w.l.Debug().Str("event", string(eventType)).Str("message", message).Err(err).Msg("sending event")
level := gevents.LevelInfo
level := events.LevelInfo
if eventType == WakeEventError {
level = gevents.LevelError
level = events.LevelError
}
w.events.Add(gevents.NewEvent(
w.events.Add(events.NewEvent(
level,
w.cfg.ContainerName(),
string(eventType),

View File

@@ -62,6 +62,6 @@ func DebugHandler(rw http.ResponseWriter, r *http.Request) {
}
}
default:
_ = w.writeLoadingPage(rw)
w.writeLoadingPage(rw)
}
}

Some files were not shown because too many files have changed in this diff Show More