Compare commits

...

408 Commits

Author SHA1 Message Date
yusing
8d4618cedf chore(deps): go mod tidy 2025-12-10 17:37:46 +08:00
yusing
2ba758939b chore(deps): upgrade dependencies 2025-12-10 17:27:00 +08:00
yusing
fdd37b777a fix(http): 'runtime error: comparing uncomparable type httputils.UnwrittenBody' 2025-12-10 17:23:13 +08:00
yusing
bc19a54976 chore(deps): upgrade dependencies in submodules 2025-12-08 14:17:14 +08:00
yusing
12d999809f fix(http): correct Unwrap method and enhance error handling in Hijack method
- Updated the Hijack method in LazyResponseModifier and ResponseModifier to return a wrapped error for unsupported hijacking.
- Added a nil check in LazyResponseModifier's Unwrap method to ensure safe access to the underlying ResponseWriter.
2025-12-08 14:06:58 +08:00
yusing
6771293336 fix(middleware): enhance response modification handling in ServeHTTP
- Replaced ResponseModifier with new LazyResponseModifier.
- Added logic to skip modification for non-HTML content.
2025-12-08 13:45:53 +08:00
yusing
d240c9dfee fix(io): limit buffer size to 16KB to avoid high memory usage and improve context propagation 2025-12-08 10:46:00 +08:00
yusing
c7eda38933 refactor(route): simplify context handling in RouteContext
- Removed unnecessary requestInternal struct and directly accessed the context field of http.Request.
- Simplified the initialization of ctxFieldOffset.
2025-12-05 18:26:34 +08:00
yusing
09caa888ad refactor(config): update config structures to use strutils.Redacted for sensitive fields
- Modified Config structs in various packages to replace string fields with strutils.Redacted to prevent logging sensitive information.
- Updated serialization methods to accommodate new data types.
- Adjusted API token handling in Proxmox configuration.
2025-12-05 18:26:16 +08:00
yusing
e41a487371 chore: remove go.work 2025-12-05 17:51:22 +08:00
yusing
7c08a8da2e Revert "ci: Add workflow to automatically merge main into compat on tag push"
This reverts commit 9930f3fa2e.
2025-12-05 16:29:45 +08:00
yusing
82df824490 chore: go mod tidy 2025-12-05 16:20:29 +08:00
yusing
2f341001c1 chore(Makefile): add socket-proxy to docker build test and update build command syntax 2025-12-05 16:10:14 +08:00
yusing
25ee8041da refactor(http,rules): move SharedData and ResponseModifier to httputils
- implemented dependency injection for rule auth handler
2025-12-05 16:06:36 +08:00
yusing
8687a57b6c fix(Dockerfile): exclude goutils in mod caching stage 2025-12-05 01:29:30 +08:00
yusing
3f4ed31e46 fix(middleware): skip modify response for websocket and event-stream requests in ServeHTTP 2025-12-05 01:18:27 +08:00
yusing
9930f3fa2e ci: Add workflow to automatically merge main into compat on tag push 2025-12-05 01:11:50 +08:00
yusing
2157545e17 fix(route): nil panic when used as an idlewatcher dependency 2025-12-05 01:10:48 +08:00
yusing
f721395ff0 refactor(healthcheck): agent health check 2025-12-05 00:45:24 +08:00
yusing
0dc7c59af1 refactor(deps): upgrade go to 1.25.5; isolate dependencies for reverseproxy, websocket and server modules 2025-12-05 00:36:16 +08:00
yusing
e3fe126a5c chore(example): introduce health check configuration defaults in example config 2025-12-04 18:08:26 +08:00
yusing
aa2575696d fix(http): handle 0 content length properly in some cases 2025-12-04 17:33:01 +08:00
yusing
c1f9c2c957 fix(middleware): skip modification for HEAD requests in ModifyHTML middleware 2025-12-04 17:27:26 +08:00
yusing
c098fef615 fix(http): enhance Content-Length handling in ResponseModifier
- Introduced origContentLength and bodyModified fields to track original content length and body modification status.
- Updated ContentLength and ContentLengthStr methods to return accurate content length based on body modification state.
- Adjusted Write and FlushRelease methods to ensure proper handling of Content-Length header.
- Modified middleware to use the new ContentLengthStr method.
2025-12-04 17:26:15 +08:00
yusing
9cdc985fb0 fix(tests): correct test expectations for middleware bypass and rules 2025-12-04 16:18:14 +08:00
yusing
2034738422 refactor(labels): refine wildcard expansion logic and tests
- Added multiple test cases for the ExpandWildcard function to cover various scenarios including basic wildcards, no wildcards, empty labels, and YAML configurations.
- Improved handling of nested maps and invalid YAML inputs.
- Ensured that explicit labels and reference aliases are correctly processed and expanded.
2025-12-04 16:16:43 +08:00
yusing
55a42b81de refactor(healthcheck): streamline health check configuration and defaults
- Moved health check constants from common package alongside type definition.
- Updated health check configuration to use struct directly instead of pointers.
- Introduced global default health check config
2025-12-04 15:19:10 +08:00
yusing
48627753d6 refactor(routes): simplify route exclusion check and health check defaults 2025-12-04 15:12:35 +08:00
yusing
09b514393d chore(deps): upgrade go version and dependencies 2025-12-04 12:18:12 +08:00
yusing
3b2ae5dbd6 refactor: move some utility functions to goutils and update references 2025-12-04 12:17:33 +08:00
yusing
fac3d67a51 chore; update goutils 2025-11-23 11:46:04 +08:00
yusing
cb642d7b32 fix(middleware): correct body mutation behavior in ServeHTTP
Refactor ServeHTTP to properly handle response body mutations by:
- Using ResponseModifier to capture response before modification
- Reading body content and allowing middleware to modify it
- Writing modified body back if changed during modification
- Ensuring proper order: RequestModifier before, ResponseModifier after next()

Previously, httputils.NewModifyResponseWriter did not correctly handle
body mutations. The new implementation captures the full response,
allows modification via modifyResponse, and properly writes back any
changes to the body.

Add BodyReader() and SetBody() methods to ResponseModifier to support
reading and replacing response body content.
2025-11-17 16:32:58 +08:00
yusing
9285977495 chore(deps): upgrade dependencies 2025-11-17 15:15:06 +08:00
yusing
e00cd8a35b fix(http): add Body field to response ModifyResponseWriter to avoid nil dereference panic 2025-11-17 15:12:13 +08:00
yusing
8ac459c038 fix(rules): ignore unsupported flush errors in ResponseModifier 2025-11-17 10:59:20 +08:00
yusing
1bcaf0dab5 fix(docker): revert to API version negotiation for Docker client 2025-11-15 10:52:03 +08:00
yusing
a291a49a0e fix(swagger): update netip.Addr definition type to string 2025-11-14 22:28:29 +08:00
yusing
28fdf3d2f4 fix(types/route): update HealthCheck JSON tag to be nullable for load-balancer routes 2025-11-14 22:16:30 +08:00
yusing
84b17baf46 refactor(rules): correct json format; remove MarshalJSON from []Rules 2025-11-14 20:53:35 +08:00
yusing
06ddb178f8 fix(proxmox): handle case when no nodes are available in AvailableNodeNames function 2025-11-14 10:59:58 +08:00
yusing
61fa7d2665 chore(debug): add debug logging for bypass rules and remove for route validation 2025-11-14 10:58:55 +08:00
yusing
615521ee1c chore: update submodule goutils 2025-11-14 10:33:11 +08:00
yusing
bbe308e821 ci: add Jenkinsfile for CI pipeline and configure SonarQube analysis 2025-11-13 23:39:23 +08:00
yusing
c156173757 refactor(docker): migrate from github.com/docker/docker to github.com/moby/moby 2025-11-13 23:03:27 +08:00
yusing
b1aae1cacf fix(rule): allow manifest.json in webui rule to make PWA work properly 2025-11-13 20:51:20 +08:00
yusing
f46552b477 fix(log): fix scrambled config log output 2025-11-13 20:49:38 +08:00
yusing
efe1350ffd chore: upgrade dependencies 2025-11-13 15:30:15 +08:00
yusing
219eedf3c5 fix(oidc): correct behavior when working with bypass rules
- Introduced a new handler for unknown paths in the OIDCProvider to prevent fallback to the default login page.
- Forced OIDC middleware to treat unknown path as logic path to redirect to login property when bypass rules is declared.
- Refactored OIDC path constants.
- Updated checkBypass middleware to enforce path prefixes for bypass rules, ensuring proper request handling.
2025-11-13 15:13:20 +08:00
yusing
f6dcc8f118 fix(idlewatcher): correct variable declaration for event data parsing in loading.js 2025-11-07 17:08:58 +08:00
yusing
4d6541c851 chore(deps): update Go version in Dockerfile and go mod tidy 2025-11-07 16:35:35 +08:00
yusing
c9db350cbc fix(route): add workaround for WebUI rule preset loading in validateRules function
- Introduced a temporary fix for loading the "webui.yml" rule preset when the container image is "godoxy-frontend".
- Added error handling for cases where the rule preset is not found.
- Marked the change with a FIXME comment to investigate the underlying issue in the future.
2025-11-07 16:16:58 +08:00
yusing
56374d595a fix(idlewatcher): correct target URL validation logic in NewWatcher function 2025-11-07 15:55:56 +08:00
yusing
d81521f293 refactor: improve HTTPS detection logic by using case-insensitive comparison for X-Forwarded-Proto header 2025-11-07 15:49:51 +08:00
yusing
e9ac3cd1a9 refactor: fix incorrect logic introduced in previous commits and improve error handling 2025-11-07 15:48:38 +08:00
yusing
d33ff2192a refactor(loadbalancer): implement sticky sessions and improve algorithm separation
- Refactor load balancer interface to separate server selection (ChooseServer) from request handling
- Add cookie-based sticky session support with configurable max-age and secure cookie handling
- Integrate idlewatcher requests with automatic sticky session assignment
- Improve algorithm implementations:
  * Replace fnv with xxhash3 for better performance in IP hash and server keys
  * Add proper bounds checking and error handling in all algorithms
  * Separate concerns between server selection and request processing
- Add Sticky and StickyMaxAge fields to LoadBalancerConfig
- Create dedicated sticky.go for session management utilities
2025-11-07 15:24:57 +08:00
yusing
910ef639a4 feat(idlewatcher): implement real-time SSE-based loading page with enhanced UX
This major overhaul of the idlewatcher system introduces a modern, real-time loading experience with Server-Sent Events (SSE) streaming and improved error handling.

- **Real-time Event Streaming**: New SSE endpoint (`/$godoxy/wake-events`) provides live updates during container wake process
- **Enhanced Loading Page**: Modern console-style interface with timestamped events and color-coded status messages
- **Improved Static Asset Management**: Dedicated paths for CSS, JS, and favicon to avoid conflicting with upstream assets
- **Event History Buffer**: Stores wake events for reconnecting clients and debugging

- Refactored HTTP request handling with cleaner static asset routing
- Added `WakeEvent` system with structured event types (starting, waking_dep, dep_ready, container_woke, waiting_ready, ready, error)
- Implemented thread-safe event broadcasting using xsync.Map for concurrent SSE connections
- Enhanced error handling with detailed logging and user-friendly error messages
- Simplified loading page template system with better asset path management
- Fixed race conditions in dependency waking and state management

- Removed `common.go` functions (canceled, waitStarted) - moved inline for better context
- Updated Waker interface to accept context parameter in Wake() method
- New static asset paths use `/$godoxy/` prefix to avoid conflicts

- Console-style output with Fira Code font for better readability
- Color-coded event types (yellow for starting, blue for dependencies, green for success, red for errors)
- Automatic page refresh when container becomes ready
- Improved visual design with better glassmorphism effects and responsive layout
- Real-time progress feedback during dependency wake and container startup

This change transforms the static loading page into a dynamic, informative experience that keeps users informed during the wake process while maintaining backward compatibility with existing routing behavior.
2025-11-07 14:58:33 +08:00
yusing
3cbd70f73a fix(health_check): correct agent routes health check logic 2025-11-07 10:21:15 +08:00
yusing
83d70d3bb2 fix(health_monitor): fix UpdateURL behavior in Docker and AgentProxied health monitor 2025-11-07 00:53:34 +08:00
yusing
bbb1b8497f fix(health_monitor): treat some errors as unhealth instead of actual error in RawHealthMonitor 2025-11-06 23:37:34 +08:00
yusing
d57d76dc65 feat(loading_page): move loading page css to style.css and serve as static asset 2025-11-06 20:32:40 +08:00
yusing
ef893974ea feat(route): implement PreferOver method for deterministic route replacement 2025-11-06 20:19:49 +08:00
yusing
b90f2409ab refactor(docker): set alias initially to have better debuggability 2025-11-06 20:15:03 +08:00
yusing
36e9b0d416 chore: upgrade go to 1.25.4 and dependencies 2025-11-06 20:13:01 +08:00
yusing
306cb7a20e fix(access_logger): fix stdout and path not working at the same time 2025-11-01 12:07:22 +08:00
yusing
e3915210aa fix(time): data race in DefaultTimeNow 2025-11-01 02:18:24 +08:00
yusing
e8fb202ea9 fix(docker): fix wildcard not working correctly with #N ref aliases 2025-11-01 02:10:09 +08:00
yusing
082b2f5da2 refactor(websocket): use only the first error and fix race condition 2025-11-01 01:28:12 +08:00
yusing
e670acb4b8 fix(access_logger): nil panic when stdout only, improve concurrency safety 2025-11-01 01:17:55 +08:00
yusing
77e486f4fe refactor(route): ensure validation and start only starts once, and lock error before finishing 2025-10-31 18:10:09 +08:00
yusing
3ccaba3163 fix(validation): prioritize pointer method for custom validation in serialization 2025-10-31 18:06:41 +08:00
yusing
705923960c feat(fileserver): add rules support for fileservers 2025-10-31 17:32:37 +08:00
yusing
ca737c8979 fix(modify-html): re-enable modifying HTML with chunked encoding 2025-10-31 17:30:23 +08:00
yusing
b6b5d4dbd7 fix(auth): handle nil defaultAuth to prevent nil panic before auth intializes 2025-10-31 17:15:03 +08:00
yusing
b2919fbaf6 feat(rules): supress some errors in rule execution 2025-10-31 17:13:09 +08:00
yusing
722c40d103 chore(examples): update example configurations with comments for certificate paths and lite variant 2025-10-30 11:45:06 +08:00
yusing
860d9c71b6 fix(pool,io): overlap memory on buffer splitting; hook in HookReadCloser should run after Close 2025-10-29 22:48:28 +08:00
yusing
e354d901c4 fix(monitor): safer approach to avoid nil panic in edge cases 2025-10-29 00:19:17 +08:00
yusing
921a8fb935 fix(monitor): handle missing container state in Docker health check 2025-10-28 23:49:59 +08:00
yusing
975354cdc1 chore(compose): comment out user for lite variant in example configuration 2025-10-28 23:15:54 +08:00
yusing
7d38bfd2d2 build: drop old image name support 2025-10-28 22:05:35 +08:00
yusing
5506cafa26 fix(rules): pages not loading correct for lite webui variant 2025-10-28 22:00:54 +08:00
yusing
9fd5bff81a fix(oidc): fix Webui OIDC loop 2025-10-28 21:54:46 +08:00
yusing
38041ca5b8 fix(pool): handle buffer capacity check in GetSized
- Added a check to ensure the buffer's capacity is sufficient before reusing it.
- Included a FIXME comment to address an unexpected condition in buffer allocation.
2025-10-28 21:48:27 +08:00
yusing
61be88c1d3 chore: upgrade dependencies 2025-10-28 21:40:23 +08:00
yusing
cb4dcb962e fix(http): nil panic in goutils http/intercept.go 2025-10-28 21:37:19 +08:00
yusing
1797a222cd fix(middlewares): correctly bypass middlewares with response rules 2025-10-28 20:44:46 +08:00
yusing
098fb7e62d fix(compose): update rootless compose example 2025-10-28 17:03:20 +08:00
yusing
d4dfec8293 refactor(http): proper ResponseWriter and headers handling across files 2025-10-28 14:43:10 +08:00
yusing
f29b69ff3b refactor(rules): remove Flush method and replace with http.NewResponseController in ResponseModifier 2025-10-27 17:46:23 +08:00
yusing
5e00e1c437 fix(middleware): correct and simplify HTML modification / buffer management logic, correct Accept-Encoding header 2025-10-27 15:08:29 +08:00
yusing
39c8cc2820 fix(auth): nil panic by handling in TryRefreshToken 2025-10-27 14:25:05 +08:00
yusing
56232dbd0e fix(monitor): nil panic in DockerHealthMonitor 2025-10-27 12:46:22 +08:00
yusing
baf774f927 fix(middleware): properly release buffer on error and not to reuse content for bytes.Buffer 2025-10-26 23:16:38 +08:00
yusing
a3c82209c6 refactor(api): disable caching completely 2025-10-26 21:33:58 +08:00
yusing
386d946bd2 feat(rules): support variables for error comand 2025-10-26 20:25:46 +08:00
yusing
ee9bf31d30 chore(compose): add comments for lite variant uid/gid configuration in example 2025-10-26 19:46:59 +08:00
yusing
2c87eebee3 chore(compose): remove host network_mode from example 2025-10-26 19:27:56 +08:00
yusing
5be784d567 chore(env): remove frontend port configuration from example files 2025-10-26 19:26:53 +08:00
yusing
a999c51bf8 fix(metrics): json marshaling 2025-10-26 16:57:16 +08:00
yusing
7ca722b256 fix(metrics): correct network data aggregation logic in system_info.go 2025-10-26 16:46:34 +08:00
yusing
51295be463 fix(json): ensure valid json 2025-10-26 16:38:08 +08:00
yusing
51fc5f017a feat(api): add sonic build tag in Makefile to let gin use sonic for json handling 2025-10-26 16:36:41 +08:00
yusing
e4996733fc fix(types): add placeholder field in VirtualMemoryStat for swagger 2025-10-26 16:04:28 +08:00
yusing
f76d86dfa2 feat(api): rules playground API
- updated swagger
2025-10-26 15:56:18 +08:00
yusing
8778f4ea73 fix(json): unmarshal error introduced in previous commit 2025-10-26 01:29:39 +08:00
yusing
6f75bb7593 refactor(api): replace apitypes module and fix swagger generation 2025-10-26 01:05:18 +08:00
yusing
964ba1eac1 chore: update dev environment configuration and base images
- Changed API_SECRET to API_JWT_SECRET in dev.compose.yml
- Updated base image from alpine to debian in dev.Dockerfile
- Upgraded golang version from 1.25.2 to 1.25.3 in Dockerfile
2025-10-25 23:31:53 +08:00
yusing
6e7b571946 feat(rules): add regex for image and font file paths in webui presets 2025-10-25 23:31:22 +08:00
yusing
fc7a81faf5 chore: upgrade dependencies 2025-10-25 23:27:35 +08:00
yusing
488ad160e7 fix(rules): ensure postform and form initialized, fix tests 2025-10-25 23:07:18 +08:00
yusing
1ec2872f3d feat(rules): replace go templates with custom variable expansion
- Replace template syntax ({{ .Request.Method }}) with $-prefixed variables ($req_method)
- Implement custom variable parser with static ($req_method, $status_code) and dynamic ($header(), $arg(), $form()) variables
- Replace templateOrStr interface with templateString struct and ExpandVars methods
- Add parser improvements for reliable quote handling
- Add new error types: ErrUnterminatedParenthesis, ErrUnexpectedVar, ErrExpectOneOrTwoArgs
- Update all tests and help text to use new variable syntax
- Add comprehensive unit and benchmark tests for variable expansion
2025-10-25 22:43:47 +08:00
yusing
9c3346dd9d fix(agent): correct usage of task in StartAgentServer and updated test expectations 2025-10-22 00:02:13 +08:00
yusing
203faa8e7e chore: update goutils 2025-10-22 00:01:27 +08:00
yusing
fbc853fa6a fix(script): incorrectly set DOCKER_SOCKET as rootless 2025-10-20 20:48:38 +08:00
yusing
3fefbdfded chore: update goutils 2025-10-20 20:46:31 +08:00
yusing
48be6def12 feat(autocert): add hostinger autocert provider
- upgraded dependencies and submodules
2025-10-19 10:48:00 +08:00
yusing
94d6b7a168 fix(pool): missing storeFullCap when allocating new buffer 2025-10-18 19:59:14 +08:00
yusing
1ca4b4939e perf(healthcheck): stop docker client from hogging resources in health checks 2025-10-18 19:35:32 +08:00
yusing
f8716d990e perf(pool): split bytes pool into tiered sized and unsized pools
- Remove BytesPoolWithMemory; split into UnsizedBytesPool and 11-tier SizedBytesPool
- Track buffer capacities with xsync Map to prevent capacity leaks
- Improve buffer reuse: split large buffers and put remainders back in pool
- Optimize small buffers to use unsized pool
- Expand test coverage and benchmarks for various allocation sizes
2025-10-18 17:38:01 +08:00
yusing
5a91db8d10 perf: use fasthttp for health checks; upgrade go to 1.25.3 2025-10-17 22:50:13 +08:00
yusing
3e73be60a1 fix(gotify): error if token not present 2025-10-16 10:25:38 +08:00
yusing
af9363209b fix(systeminfo): correct system info JSON format 2025-10-16 10:09:51 +08:00
yusing
ccc35b2a00 refactor: remove functional.Set wrapper 2025-10-16 10:08:25 +08:00
yusing
44536139c1 refactor: refine byte pools usage and fix memory leak in rules 2025-10-15 23:53:26 +08:00
yusing
2b4c39a79e perf(mem): reduced memory usage in metrics and task by string interning and deduplicating fields 2025-10-15 23:51:47 +08:00
yusing
ddf78aacba perf(logging): optimize multi-line message formatting
- Refactors the fmtMessage function to use strings.Builder
  - Simplifies multi-writer creation with a helper function
  - Updates the new console writer initialization pattern
  - Moves InitLogger function to the top
  - Fixed NewLoggerWithFixedLevel
2025-10-15 21:18:25 +08:00
yusing
f5a006ce81 refactor(task): fix onFinish not being called and simplify by replacing semaphore with channel 2025-10-15 15:07:19 +08:00
yusing
290af4e311 perf(mem): replace Scheme and ExcludedReason string with uint8 type to reduce mem usage 2025-10-15 14:35:44 +08:00
yusing
feafdf05f2 fix(validation): correct CustomValidator and strutils.Parser handling, add tests 2025-10-15 14:20:47 +08:00
yusing
b09bfd6c1e fix(serialization): use replace os.LookupEnv with env.LookupEnv 2025-10-15 00:12:17 +08:00
yusing
e13b18621d fix(favicon): add status code in error message 2025-10-15 00:11:38 +08:00
Yuzerion
53f3397b7a feat(rules): add post-request rules system with response manipulation (#160)
* Add comprehensive post-request rules support for response phase
* Enable response body, status, and header manipulation via set commands
* Refactor command handlers to support both request and response phases
* Implement response modifier system for post-request template execution
* Support response-based rule matching with status and header checks
* Add comprehensive benchmarks for matcher performance
* Refactor authentication and proxying commands for unified error handling
* Support negated conditions with !
* Enhance error handling, error formatting and validation
* Routes: add `rule_file` field with rule preset support
* Environment variable substitution: now supports variables without `GODOXY_` prefix

* new conditions:
  * `on resp_header <key> [<value>]`
  * `on status <status>`
* new commands:
  * `require_auth`
  * `set resp_header <key> <template>`
  * `set resp_body <template>`
  * `set status <code>`
  * `log <level> <path> <template>`
  * `notify <level> <provider> <title_template> <body_template>`
2025-10-14 23:53:06 +08:00
yusing
19968834d2 fix(autocert): added back timewebcloud provider 2025-10-13 07:12:38 +08:00
yusing
d41c6f8d77 fix(cookie): net/http: invalid Cookie.Domain .0.0.1:3000 2025-10-13 07:09:17 +08:00
yusing
dcc5ab8952 fix(entrypoint): 404 everything with match_domains 2025-10-13 06:45:29 +08:00
yusing
cc8858332d fix(agent): remove leftover pointer in tls.Config 2025-10-12 22:23:14 +08:00
yusing
82f02ea2bf fix(rules): nil panic when only having default rule 2025-10-12 22:21:01 +08:00
yusing
046ff8a020 chore: update config example about new not_found rule 2025-10-12 22:02:52 +08:00
yusing
dc9ae32e8f feat(entrypoint): add not found rule to customize 404 behavior 2025-10-12 21:04:49 +08:00
yusing
5640d5d454 fix(serialization): correctly handle json tag 2025-10-12 20:59:12 +08:00
yusing
c66de99fcb perf: further optimize http and body buffer handling 2025-10-12 20:57:51 +08:00
yusing
eef994082c fix(panic): nil panic in IterRoutes 2025-10-12 16:51:52 +08:00
yusing
8c670ab92e chore: update README.md and config.example.yml for new changes 2025-10-12 14:25:55 +08:00
yusing
d11ddb7c91 fix(ci): checkout submodules 2025-10-12 14:23:00 +08:00
yusing
78aea4b4d2 fix: gopsutil 2025-10-12 13:04:28 +08:00
yusing
80dd142861 refactor(rules): rename Static and Returning commands into Terminating and NonTerminating commands 2025-10-12 09:38:06 +08:00
yusing
92aa61e732 refactor(log): simplify access logger and disable stdout buffering
- Remove MultiWriter complexity and use single writer interface
  - Disable buffering for stdout logging to ensure immediate output
  - Replace slice-based closer/rotate support with type assertions
  - Simplify rotation result handling by passing result pointer
  - Update buffer size constants and improve memory management
  - Remove redundant stdout_logger.go and multi_writer.go files
  - Fix test cases to match new rotation API signature
2025-10-11 19:14:59 +08:00
yusing
848f26aa86 test(list-icon): fix tests regarding previous changes 2025-10-11 19:05:49 +08:00
yusing
81e500fcfc fix(log): stdout logging format 2025-10-11 18:30:38 +08:00
yusing
f417e0fa25 fix(notif): remove test logging 2025-10-11 18:13:19 +08:00
yusing
cb5a8e7b9d fix(acl): correct acl log handling and add country to summary 2025-10-11 17:03:08 +08:00
yusing
16cad11e89 fix(notif): format not being applied correctly 2025-10-11 16:54:52 +08:00
yusing
2bfbdbf519 refactor(acl): adjust summary format and add total count 2025-10-11 16:13:52 +08:00
yusing
d5e9a7b3b6 fix(notif): respect Method and empty content-type 2025-10-11 16:02:18 +08:00
yusing
7ea415078f fix(config): fix error and logging handling 2025-10-11 14:00:04 +08:00
yusing
e67704695b fix(acl): complete tcp and udp wrapper interface 2025-10-11 13:47:13 +08:00
yusing
804c7eec60 fix: env parsing 2025-10-11 13:37:18 +08:00
yusing
ea8be56bf8 fix(server): race condition on server stop 2025-10-11 13:26:10 +08:00
yusing
20c77edce5 chore: go mod tidy 2025-10-11 13:20:19 +08:00
yusing
4f2f0f58e2 fix(autocert): nil dereference 2025-10-11 13:12:45 +08:00
yusing
ac8ad149b8 fix(icons): list icon logic 2025-10-11 13:07:50 +08:00
yusing
14ec80c883 fix: Dockerfile mod caching 2025-10-11 13:01:52 +08:00
yusing
5de5f854ce fix: dockerfile 2025-10-11 12:56:55 +08:00
yusing
3d8994b42e chore: enhance example config 2025-10-11 12:46:54 +08:00
yusing
66043e4a26 refactor!: simplify lego generate script; add and drop some dns provider 2025-10-11 12:44:16 +08:00
yusing
d1e403e16f fix(docker): correct image in rootless docker compose 2025-10-11 11:27:14 +08:00
yusing
e72e20af69 fix(script): add missing domain input in setup.sh 2025-10-11 11:16:39 +08:00
yusing
ad6201c27a feat(dev): add parca in dev docker for profiling 2025-10-10 23:24:39 +08:00
yusing
c4c9e9300c chore: simplify dev docker and update Makefile accordingly 2025-10-10 23:24:14 +08:00
yusing
b23c3f1c3b refactor(icons): replace mutex-based cache with atomic synk.Value
- Remove sync.RWMutex and Cache struct in favor of atomic Value
  - Implement background goroutine for periodic icon updates
  - Add backward compatibility for old cache format
  - Improve concurrent access to icon cache
  - Simplify ListAvailableIcons()
2025-10-10 23:21:30 +08:00
yusing
38c0419483 feat(config): add temporary logging for failed reloads
- Add tmpLogBuf and tmpLog fields to capture config loading logs
  - Flush temporary logs only when reload succeeds
  - Extract NewLogger function for creating custom loggers
  - Update State interface to include FlushTmpLog method
2025-10-10 22:20:12 +08:00
yusing
357ce38b18 fix(idlewatcher): correctly restart on config reload 2025-10-10 21:57:36 +08:00
yusing
ef34c3ffdd fix(server): should wait for server to stop 2025-10-10 21:47:03 +08:00
yusing
2e411373a2 fix(config): failed reload should not start providers in new state 2025-10-10 21:46:02 +08:00
yusing
3dedd66ad1 test(rules): add tests for glob and regex, remove old path glob test 2025-10-10 21:39:21 +08:00
yusing
98f047d88a fix(rules): correct dollar sign handling 2025-10-10 21:37:54 +08:00
yusing
973a58e982 fix(import): remove unused import in rules/validate.go 2025-10-10 20:49:12 +08:00
yusing
4b55d1c607 fix: missing bracket in main.go 2025-10-10 20:46:47 +08:00
yusing
63eff4707c fix: submodules url 2025-10-10 20:40:33 +08:00
yusing
55a74c36b0 refactor(acl): optimize slice allocation in logNotifyLoop 2025-10-10 20:21:53 +08:00
yusing
fbabb7b7fb refactor(acl): default not to notify allowed and skip when total is 0 2025-10-10 20:20:09 +08:00
yusing
7a1841e9a5 fix(acl): add json tag for notify 2025-10-10 15:26:08 +08:00
yusing
d82bfd0ebd feat(acl): add periodic notification system for access summaries
- Add Notify configuration with To field and interval
  - Track allowed/blocked IP counts per address
  - Send periodic summary notifications with access statistics
  - Optimize logging with channel-based processing for concurrent safety
2025-10-10 15:24:48 +08:00
yusing
1f41c035ea feat(notification): add To field to LogMessage 2025-10-10 14:47:20 +08:00
yusing
c2c9f42fb3 feat(rules): glob and regex support, env var substitution
- optimized `remote` rule for ip matching
- updated descriptions
2025-10-10 14:43:48 +08:00
yusing
60cfff3435 refactor(idlewatcher): streamline loading screen favicon handling 2025-10-10 12:55:06 +08:00
yusing
c93a460043 refactor(config): add omitempty on some fields 2025-10-10 10:34:49 +08:00
yusing
9bf7a0beef revert(config): added back pointer for agent and notification config for correct unmarshaling 2025-10-10 10:07:49 +08:00
yusing
c89c737ecd fix(pool): variable shadowing 2025-10-10 10:01:42 +08:00
yusing
382fc61a9c chore: update submodules 2025-10-10 09:57:08 +08:00
yusing
b2de33e835 fix: Makefile 2025-10-10 09:55:21 +08:00
yusing
86644054e6 refactor: use goutils/env in socket-proxy 2025-10-10 09:54:16 +08:00
yusing
c2dcabe144 refactor(rules): remove 'caller' parameter in BuildHandler 2025-10-10 09:53:44 +08:00
yusing
c59ddc1df6 refactor: add ShouldExclude() bool to Route interface 2025-10-10 09:53:08 +08:00
yusing
f4db874fd6 refactor(proxmox): rename checkIPPrivate to privateIPOrNil 2025-10-10 09:52:38 +08:00
yusing
f334f5c13c feat(config): pretty print active config and routes by provider on load / reload 2025-10-10 09:51:32 +08:00
yusing
5acc4c3894 fix(homepage): logic error in ListAvailableIcons, ensure the cache is ready in GetHomepageMeta 2025-10-10 09:27:23 +08:00
yusing
a8aa82f687 chore: upgrade dependencies 2025-10-10 09:11:29 +08:00
yusing
0f3a1ac6e6 refactor: improved task lifecycle management 2025-10-10 09:07:47 +08:00
yusing
9fceda6729 fix: data race in strings.Title 2025-10-09 23:08:30 +08:00
yusing
becb49e864 fix(uptime): set to 0 instead of returning error on overflow check 2025-10-09 22:53:38 +08:00
yusing
3aed41e078 refactor: move version.go to goutils 2025-10-09 01:14:43 +08:00
yusing
8047067b2b refactor(utils): move utils/atomic to goutils 2025-10-09 01:07:47 +08:00
yusing
c3fa7c66a7 feat(entrypoint): added CatchAll and NotFound rules and handler 2025-10-09 01:03:16 +08:00
yusing
cab68807ee refactor(config): restructured with better concurrency and error handling, reduced cross referencing 2025-10-09 01:02:24 +08:00
yusing
d08be872a0 refactor(errors): simplify gperr.Builder usage 2025-10-09 00:28:22 +08:00
yusing
bb5f0cdf09 chore(go): upgrade to go1.25.2 2025-10-08 23:38:33 +08:00
yusing
a150f1a628 refactor(config): reduce references to config.GetInstance() 2025-10-07 21:49:00 +08:00
yusing
584db2efce refactor(docker): use atomic.Int instead of plain integer 2025-10-07 21:30:12 +08:00
yusing
c27bc0e129 refactor(docker): simplify docker client initialization in api 2025-10-07 21:26:52 +08:00
yusing
b46b464e65 refactor: add goutils as submodule, remove go.mod from internal/utils 2025-10-07 20:43:41 +08:00
yusing
52ec309f6b chore: go mod tidy 2025-10-05 20:41:37 +08:00
yusing
6051f75145 refactor(favicon): improve cache and error handling 2025-10-05 20:37:27 +08:00
yusing
f4f104d206 refactor: add go-oidc as submodule 2025-10-05 12:38:40 +08:00
yusing
448a2fbd6f chore: update gopsutil 2025-10-05 12:21:05 +08:00
yusing
74224c8e87 refactor(metrics): optimize and simplify system info; add gopsutil as submodule 2025-10-05 12:05:58 +08:00
yusing
ae57edfcb0 refactor(routes): remove unnecessary indirection 2025-10-03 23:28:03 +08:00
yusing
fc23e262d7 chore: update dependencies 2025-10-03 23:26:27 +08:00
yusing
11a3935e0c refactor(serialization): streamline custom validation logic in ValidateWithCustomValidator function 2025-10-03 23:20:14 +08:00
yusing
42e7adbf86 refactor(serialization): small optimization 2025-10-03 23:19:13 +08:00
yusing
1e0c7a15d8 refactor(metrics): optimize memory allocation in period entries
- Replace heap allocation with stack-allocated array in Entries.Get() method.
- Also refactor uptime module to use value types instead of pointer types.
2025-10-03 23:19:12 +08:00
yusing
ba8edb160f refactor(metrics): replace hardcoded time with contants, merge three tickers into one 2025-10-03 23:19:12 +08:00
Yuzerion
4852efcf9c feat: faster serialization (#157)
* refactor: improve deserialization performance

* refactor(serialization): simplify string conversion logic in Convert function

* fix(serialization): default value lookup

* refactor: add comment about concurrency in RegisterDefaultValueFactory

---------

Co-authored-by: yusing <yusing@6uo.me>
2025-10-02 20:30:31 +08:00
yusing
ef40793301 fix: Dockerfile 2025-10-01 19:56:38 +08:00
yusing
80862bcd2e chore(go.mod): mod tidy and exclude problematic v0.4.2 of goutils 2025-09-29 17:58:44 +08:00
yusing
45b16abd68 refactor(health): improve health status JSON unmarshalling 2025-09-29 17:52:03 +08:00
yusing
f411e17d80 feat(json): improve JSON performance with bytedance/sonic 2025-09-29 17:43:34 +08:00
yusing
024100aa8c fix(agent): failed to parse agent proxy config: unexpected end of JSON input 2025-09-28 20:30:02 +08:00
yusing
9d508c5950 feat(health): add random delay to health monitor to prevent thundering herd problem 2025-09-28 02:35:44 +08:00
yusing
2ff5e5c0b6 chore(deps): upgrade dependencies 2025-09-27 14:19:30 +08:00
yusing
2a05c6a630 refactor: move websocket package and some http utils to seperate repo 2025-09-27 14:16:42 +08:00
yusing
6776f20332 refactor: move task, error and testing utils to separte repo; apply gofumpt 2025-09-27 13:41:50 +08:00
yusing
5043ef778f refactor: remove gphttp.ServerError method 2025-09-27 12:47:51 +08:00
yusing
22bcf1201b refactor: move some io, http and string utils to separate repo 2025-09-27 12:46:41 +08:00
yusing
acecd827d6 refactor(synk): consolidate pool statistics tracking and replace GC tracking with dropped tracking 2025-09-27 11:35:38 +08:00
yusing
b2713a4b83 refactor(health): optimize health checking 2025-09-27 11:32:18 +08:00
yusing
e2aeef3a86 refactor(synk): replace runtime weak pointer functions with weak package and simplify buffer handling 2025-09-27 11:24:50 +08:00
yusing
9545482a44 refactor(pprof): remove memory profiling settings and enhance GC logging 2025-09-27 11:24:10 +08:00
yusing
d406b940d9 style: fix some golangci-lint warnings 2025-09-26 23:45:59 +08:00
yusing
dc1175ad69 refactor(http): remove and replace error helpers with standard http.Error 2025-09-26 23:39:00 +08:00
yusing
1409a4e8b9 chore(lint): update linting tool versions in trunk.yaml and enable fieldalignment check 2025-09-26 23:32:48 +08:00
yusing
8ec9752656 refactor(env): move env parsing to separate repo (cont. f7149453d6) 2025-09-26 21:41:57 +08:00
yusing
a932688ca3 chore(deps): upgrade gopsutils 2025-09-26 21:40:28 +08:00
yusing
55c1c918ba refactor: remove / throttle some debug logging 2025-09-26 21:00:35 +08:00
yusing
14e243d245 chore(deps): upgrade dependencies 2025-09-26 20:47:21 +08:00
yusing
f7149453d6 refactor(env): move env parsing to separate repo 2025-09-26 20:41:10 +08:00
yusing
00d137d05c refactor: remove obsolete args.go 2025-09-22 17:18:53 +08:00
yusing
f9affba9fc refactor(modules): replace github.com/yusing/go-proxy with github.com/yusing/godoxy 2025-09-22 16:44:59 +08:00
yusing
6b3bf84148 fix(stream): nil panic when logging error 2025-09-22 10:27:09 +08:00
yusing
62a667758d docs(README): add section for updating and uninstalling system agent 2025-09-21 13:19:27 +08:00
yusing
ddd27156fc refactor(Dockerfile): simplify development Dockerfile 2025-09-21 13:00:43 +08:00
yusing
af8e2d56b2 fix(agent): respect response header timeout and compression settings 2025-09-21 11:58:31 +08:00
yusing
74a215b894 feat(agentproxy): simplify configuration handling and related header management 2025-09-21 11:52:42 +08:00
yusing
ccdc0046fd refactor(agent): update version handling in AgentConfig to use pkg.Version type 2025-09-21 11:51:17 +08:00
yusing
2f7fdc4c51 feat(version): add comparison methods 2025-09-21 11:47:50 +08:00
yusing
de1f4da126 feat(ReverseProxy): add SSL/TLS configuration options and build TLS config method 2025-09-21 10:47:37 +08:00
yusing
a48ccb4423 refactor(server): improve proxy protocol handling 2025-09-19 11:59:34 +08:00
yusing
193fd9a249 docs(config): update config.example.yml with access control and proxy protocol comments 2025-09-19 10:47:35 +08:00
yusing
0bc4c4af77 fix(vscode): update schema URLs in settings.example.json 2025-09-19 10:41:27 +08:00
yusing
5fa1417add fix(server): set default logger in server start options if not provided 2025-09-19 10:31:00 +08:00
yusing
b763c92645 refactor(stream): update TCP and UDP stream listeners to support proxy protocol and ACL wrapping 2025-09-19 10:23:47 +08:00
yusing
09b14a47e9 refactor(config): add SupportProxyProtocol to Entrypoint config 2025-09-18 17:36:19 +08:00
yusing
83a69322fa refactor(server): enhance server start options and support for proxy protocol 2025-09-18 17:34:02 +08:00
yusing
3aba5a1911 refactor(agent): simplify ReverseProxy method by directly modifying request URL 2025-09-17 14:07:06 +08:00
yusing
ca805edfe0 fix(agent): incorrect uri in reverse proxy 2025-09-17 14:03:35 +08:00
yusing
7205bf47de feat(autocert): add DNS resolver options to Config and update provider initialization 2025-09-16 15:43:49 +08:00
yusing
b12999210f feat(docker): add tmpfs caching for Next.js in compose files 2025-09-14 21:24:01 +08:00
yusing
8b8969f033 fix(auth): change userpass to redirect to login and update documentation 2025-09-14 21:11:20 +08:00
yusing
025ebab1ce refactor(api): remove unused ErrorCode type 2025-09-14 20:50:07 +08:00
yusing
ea7bd0d19a fix(docker): update dev docker compose 2025-09-14 18:39:40 +08:00
yusing
f889f5c08d fix(oidc): simplify LoginHandler to always redirect to IdP 2025-09-14 14:33:28 +08:00
yusing
932c20f32d chore(docker): update .gitignore to exclude all .env files and modify dev.compose.yml to include env_file for development 2025-09-14 13:47:02 +08:00
yusing
2a08c55e39 feat(auth): add GET endpoint for logout and update documentation 2025-09-14 13:07:24 +08:00
yusing
93e1d17090 fix(auth): revert userpass PostAuthCallback to respond http 200 2025-09-14 11:19:37 +08:00
yusing
d72d403e2c docs(README): update README files to include new Star History section and replace outdated screenshots
- Added "Star History" section with a chart link.
- Replaced outdated screenshots with new "Routes" and "Servers" images.
- Removed references to deleted screenshots for better clarity.
2025-09-14 01:30:37 +08:00
yusing
b5d70a0592 docs(README): remove WebUI announcement from README 2025-09-14 01:15:36 +08:00
yusing
da71dcf058 fix(docker): simplify and fix logs api 2025-09-14 00:32:47 +08:00
yusing
6b17272347 chore(deps): upgraded dependencies 2025-09-14 00:19:53 +08:00
yusing
98afb02e7f fix(makefile): add dev-logs target and fix frontend lib path 2025-09-14 00:18:24 +08:00
yusing
103fd3b904 docs(swagger): updated swagger json and yaml 2025-09-14 00:17:43 +08:00
yusing
59917f52d7 feat(agent): add runtime configuration to agent env and script 2025-09-14 00:16:47 +08:00
yusing
24fb2e07e6 refactor(api: added all new endpoints and optionally set gin mode 2025-09-14 00:14:28 +08:00
yusing
8f1c02ca72 docs(README): update README files to include container runtime and ForwardAuth support 2025-09-14 00:13:39 +08:00
yusing
e359bc8fd9 fix(swagger): improve non-nullable property handling in Swagger JSON
- Updated set_non_nullable function to ensure required properties are processed correctly.
- Added logic to handle cases where 'required' is not present, maintaining existing functionality for non-nullable properties.
2025-09-14 00:12:35 +08:00
yusing
7b028adaa9 feat(api): add GetContainer endpoint for Docker container retrieval
- Implemented GetContainer function to retrieve container details by ID.
- Added error handling for missing ID, container not found, and client creation failures.
- Enhanced Container struct to support omitempty for state field in JSON responses.
- Updated API documentation with Swagger annotations for the new endpoint.
2025-09-14 00:12:23 +08:00
yusing
f3913e1f6f feat(api): add Docker container management endpoints
- Implemented Restart, Start, and Stop endpoints for managing Docker containers.
- Each endpoint includes request validation, error handling, and appropriate responses.
- Enhanced API documentation with Swagger annotations for all new routes.
2025-09-14 00:11:51 +08:00
yusing
b72f3bde53 refactor(routes): remove old HomepageCategories method 2025-09-14 00:11:32 +08:00
yusing
6077a1d70b feat(metrics): add AllSystemInfo endpoint for real-time system metrics
- Implemented AllSystemInfo function to retrieve and stream system information from agents via WebSocket.
- Introduced AllSystemInfoRequest struct for query parameter binding and validation.
- Enhanced error handling for invalid requests and WebSocket upgrades.
- Utilized goroutines for concurrent data fetching from multiple agents, with retry logic for robustness.
2025-09-14 00:10:55 +08:00
yusing
59cae0967a feat(api): updated docker logs api
- Refactored docker logs endpoint to use container ID directly.
2025-09-14 00:10:37 +08:00
yusing
1e1999b0af feat(agent): add ReverseProxy method and enhance Forward method
- Introduced ReverseProxy method to handle requests to the agent with context, method, and body.
- Updated Forward method to return *http.Response instead of byte data.
- Enhanced SystemInfo function to support querying by agent name in addition to agent address.
2025-09-14 00:09:07 +08:00
yusing
b64725f2f8 refactor(stats): change uptime type from string to int64 2025-09-14 00:07:56 +08:00
yusing
124069aaa4 refactor(metrics): optimize JSON marshaling and aggregation logic
- Updated JSON marshaling in SystemInfo to use quoted keys.
- Refactored aggregation logic to dynamically append entries.
- Adjusted test cases to reflect changes in data structure and ensure accurate serialization.
2025-09-14 00:07:34 +08:00
yusing
d56663d3f9 feat(metrics): enhance metrics handling with interval validation and historical data reconstruction
- Introduced addWithTime method for adding entries with specific timestamps.
- Added validateInterval and fixInterval methods to ensure correct interval settings.
- Updated JSON unmarshalling to respect entry timestamps and validate intervals post-load.
- Refactored poller to use a constant PollInterval for consistency across the codebase.
2025-09-14 00:06:30 +08:00
yusing
d1476edf91 test(middleware): update bypass and rule tests 2025-09-14 00:05:05 +08:00
yusing
4ed6c7c74d fix(rules): add swaggertype annotations for Rule fields 2025-09-14 00:04:14 +08:00
yusing
f31b1b5ed3 refactor(misc): enhance performance on bytes pool, entrypoint, access log and route context handling
- Introduced benchmark tests for Entrypoint and ReverseProxy to evaluate performance.
- Updated Entrypoint's ServeHTTP method to improve route context management.
- Added new test file for entrypoint benchmarks and refined existing tests for route handling.
2025-09-14 00:03:27 +08:00
yusing
e0d25e475c feat(docker): implement container ID to Docker host mapping 2025-09-14 00:01:00 +08:00
yusing
ef65481394 feat(routes): enhance route retrieval with search functionality
- Added SearchRoute method to Config for searching routes by alias.
- Updated Route function to check for excluded routes if the initial lookup fails, returning the found route or a 404 status accordingly.
2025-09-13 23:58:38 +08:00
yusing
1e9303b1ef refactor(docker): update ListContainers function to accept context and improve timeout handling 2025-09-13 23:55:47 +08:00
yusing
2c290a3916 feat(homepage): enhance homepage functionality with new item click tracking, sort methods and category management
- Added ItemClick endpoint to increment item click counts.
- Refactored Categories function to dynamically generate categories based on available items.
- Introduced sorting methods for homepage items and categories.
- Updated item configuration to include visibility, favorite status, and sort orders.
- Improved handling of item URLs and added support for websocket connections in item retrieval.
2025-09-13 23:52:54 +08:00
yusing
58a2dc73dd refactor(docker_watcher): rename docker_events to dockerEvents 2025-09-13 23:50:13 +08:00
yusing
1c080e067d refactor(routes): centralize route existence checking
- Removed All routes pool
2025-09-13 23:49:45 +08:00
yusing
2717dc963a feat(agent): add container runtime support and enhance agent configuration
- Introduced ContainerRuntime field in AgentConfig and AgentEnvConfig.
- Added IterAgents and NumAgents functions for agent pool management.
- Updated agent creation and verification endpoints to handle container runtime.
- Enhanced Docker Compose template to support different container runtimes.
- Added runtime endpoint to retrieve agent runtime information.
2025-09-13 23:44:03 +08:00
yusing
4509622dde fix(reverseproxy): properly suppress http2.errStreamClosed 2025-09-13 23:26:10 +08:00
yusing
60c13a797b refactor(config): parallelize route provider initialization 2025-09-13 23:25:29 +08:00
yusing
5e1da915dc refactor(agents): enhance VerifyNewAgent 2025-09-13 23:24:43 +08:00
yusing
3288624cf2 refactor(auth): change PostAuthCallbackHandler to redirect to home page instead of sending OK status 2025-09-13 23:21:58 +08:00
yusing
190d5e1ece fix(serialization): improve nil handling in mapUnmarshalValidate 2025-09-13 23:20:10 +08:00
yusing
0d2229cca0 refactor(xsync): replace functional map with xsync.Map, remove functional/map 2025-09-13 23:19:20 +08:00
yusing
493c0afdfa feat(websocket): implement Reader for reading binary data from the manager
- Removed Close method from Writer
2025-09-13 22:38:24 +08:00
yusing
99c1922342 feat(websocket): add deduplication support to PeriodicWrite function and introduce DeepEqual utility 2025-09-13 22:37:51 +08:00
yusing
a483e15a20 refactor(middlewares): remove xsync wrapper and replace strutils.SplitLine with bytes.Line 2025-09-13 22:33:21 +08:00
yusing
fbe82c3082 refactor(metrics): optimize JSON marshaling in SystemInfo and Aggregated structures for improved performance and memory management 2025-09-13 22:30:10 +08:00
yusing
24bcc2d2d2 fix(api): correct error formatting 2025-09-13 22:29:48 +08:00
yusing
d8c8cff8b7 fix(metrics): non ws response being encoded twice; simplified response handling 2025-09-13 22:29:17 +08:00
yusing
ef54d336a2 refactor(auth): remove GET method from /auth/callback endpoint and update Swagger documentation 2025-09-13 22:29:08 +08:00
yusing
0a5df1bd7f refactor(metrics): remove pointers from type parameter T to avoid unnecessary indirection 2025-09-13 22:28:57 +08:00
yusing
205928a741 refactor(real_ip): move header check before everything else 2025-09-13 22:23:00 +08:00
yusing
11d18091fd feat(route): add ExcludedReason field 2025-09-13 22:22:50 +08:00
yusing
3be72e5c68 fix(api): conditionally enable auth APIs based on auth configuration 2025-09-13 22:22:37 +08:00
yusing
a9847b6f81 refactor(homepage): improve icon search functionality and add case-insensitive string matching 2025-09-13 22:22:23 +08:00
yusing
04d823d616 feat(serialization): add 'd', 'w',' 'M' units support for time duration
- Updated Makefile to include `-checklinkname=0` in LDFLAGS
2025-09-12 11:41:59 +08:00
yusing
1be2ea44a2 cont: f7de703c15 2025-09-11 22:38:29 +08:00
yusing
978407ae7e chore(agent): upgrade dependencies 2025-09-11 22:19:22 +08:00
yusing
81f8bad77d breaking(dns_providers): drop support for serveral dns providers
- Dropped `namesilo`, `binarylane`,`edgeone`,`baiducloud`,`huaweicloud`,`tencentcloud`,`alidns`

- Introduce support for azion, conohav3, dyndnsfree, nicru, zoneedit
- dns providers dependencies upgrade
2025-09-11 22:14:30 +08:00
yusing
f7de703c15 feat(yaml): extend environment variable substitution to all YAML files
- returns error for unset environment variables
2025-09-11 22:04:13 +08:00
yusing
acf7490991 chore(deps): upgrade go dependencies 2025-09-10 23:46:17 +08:00
yusing
7770ce7025 fix(reverseproxy): improve error handling for HTTP proxy errors and add suppress some HTTP2 and HTTP/3 error codes 2025-09-10 23:20:23 +08:00
yusing
c9c5677b35 fix(notif): use markdown format if invalid 2025-09-10 22:59:11 +08:00
yusing
226ee2e5e5 fix(docker): correct environment variables in rootless setup 2025-09-10 10:01:05 +08:00
yusing
aec937a114 fix(makefile): remove GOARCH 2025-09-10 09:01:54 +08:00
yusing
bab9471bde feat(config): implement environment variable substitution in configuration file reading 2025-09-09 23:33:05 +08:00
yusing
4ebd1dbf32 feat(setup): enhance setup script for rootless Docker support and network configuration 2025-09-09 23:13:38 +08:00
yusing
82a4a61df0 feat(docker): add example configuration files for rootless Docker setup 2025-09-09 22:48:26 +08:00
yusing
9e56ea5db1 fix(docker): add healthcheck label to Dockerfile to prevent self checking 2025-09-09 22:36:26 +08:00
yusing
719682c99f refactor(websocket): enhance connection management by ensuring resources are released on context cancellation 2025-09-09 22:25:02 +08:00
yusing
f81a2b6607 fix(docker): treat containers from $DOCKER_HOST as local 2025-09-09 22:23:50 +08:00
yusing
f47ba0a9b5 feat(docs): update README files to include logo and improve table of contents formatting 2025-09-09 14:40:09 +08:00
yusing
52e949de85 feat: Add development environment configuration with Docker Compose and Dockerfile 2025-09-08 09:15:24 +08:00
yusing
abeb26b556 fix(monitor): prevent nil pointer dereference in Finish method 2025-09-08 09:02:19 +08:00
yusing
23d392d88b fix(route): improve error handling in route.Start method 2025-09-08 09:02:19 +08:00
yusing
d588664bfa fix: prevent panicking on misconfigurations 2025-09-08 09:02:19 +08:00
DeAndre Harris
41ce784a7f feat: Add per-route OIDC client ID and secret support (#145) 2025-09-08 08:16:30 +08:00
yusing
577169d03c refactor(idlewatcher): improve container readiness handling and health check logic
- Simplified the wakeFromHTTP and wakeFromStream methods by removing unnecessary loops and integrating direct checks for container readiness.
- Introduced a waitForReady method to streamline the waiting process for container readiness notifications.
- Enhanced the checkUpdateState method to include timeout detection for container startup.
- Added health check retries and logging for better monitoring of container state transitions.
2025-09-06 07:51:28 +08:00
yusing
b43274e9e6 refactor(idlewatcher): replace map with ordered.Map for deduplicating dependencies 2025-09-06 07:49:50 +08:00
yusing
d83c367e7f chore: update Go version to 1.25.1 in Dockerfile and module files 2025-09-06 07:48:57 +08:00
yusing
d9fbd53870 refactor(api): remove unused Swagger docs.go and clean up dependencies; Makefile update 2025-09-06 07:48:23 +08:00
yusing
7f54f50af8 docs(README): add announcement for new WebUI availability in nightly tag 2025-09-06 07:46:09 +08:00
yusing
8339c42470 refactor(middleware): simplify buffer allocation in themed middleware 2025-09-02 23:28:47 +08:00
yusing
ed39942d65 feat(api): implement caching middleware and allow favicons to be cached 2025-09-02 23:00:22 +08:00
yusing
998488f285 chore(trunk): update CLI and plugin versions, and bump linter dependencies 2025-09-02 22:59:00 +08:00
yusing
aac5016b78 refactor(httpheaders): replace strutils.SplitComma with strings.SplitSeq 2025-09-02 22:58:46 +08:00
yusing
d2b4d3e6e3 feat(auth): enhance cookieDomain function to support additional local domains 2025-09-02 22:58:24 +08:00
yusing
a2d4c468cd refactor(forwardauth): finalize middleware implementation with better headers handling 2025-09-02 22:58:13 +08:00
yusing
c550255458 feat(middledware): middleware-specific logging methods 2025-09-02 22:56:30 +08:00
yusing
6a3e28dfd7 fix(config): handle missing config file and middleware directory gracefully and log a warning 2025-09-02 22:55:43 +08:00
yusing
4513c221d5 refactor(modifyhtml): improved memory manangement and response body handling 2025-09-02 22:55:24 +08:00
yusing
245dba034e feat(io): introduce ReadAllBody and HookCloser for enhanced response handling and resource management 2025-09-02 22:53:54 +08:00
yusing
f39896fe30 refactor(handler): move version API out of auth and remove Swagger routes 2025-09-02 22:50:57 +08:00
yusing
b051987a1c refactor: apply renamed NewBytesPool with GetBytesPool 2025-09-02 22:50:57 +08:00
yusing
c128557c81 chore: update dependencies 2025-09-02 22:50:57 +08:00
yusing
6405325e56 Refactor(websocket): remove unused code 2025-09-02 22:50:57 +08:00
yusing
c3d2a90501 fix(websocket): ensure resources are properly released by closing the manager in PeriodicWrite function 2025-09-02 22:50:57 +08:00
yusing
31d49453a7 feat(pool): introduce BytesPoolWithMemory for optimized memory management and add benchmark for memory usage 2025-09-02 22:50:57 +08:00
yusing
04657420b8 refactor(websocket): enable compression for WebSocket connections to improve performance, removed buffer size to use HTTP buffer 2025-09-02 22:50:57 +08:00
FrozenFrog
2f0b8b6c09 Add TinyAuth forward-auth middleware implementation (#143)
* feat: add tinyauth middleware

---------

Co-authored-by: yusing <yusing@6uo.me>
2025-09-02 17:43:34 +08:00
yusing
5e15fd4bbe fix(fileserver): correct middleware handler to avoid self recursion 2025-08-19 22:26:38 +08:00
yusing
a5022e31a2 fix(auth,oidc): added GET method /auth/callback endpoint to fix OIDC 404 and update documentation accordingly 2025-08-19 22:26:30 +08:00
yusing
a057f0e956 fix(homepage): incorrect url
- fixed url being overridden
- fixed sub-subdomain being stripped
- fixed empty url for routes with FQDN aliases
2025-08-19 21:01:04 +08:00
yusing
dfe0014609 fix(route): handle subdomain removal in URL construction for homepage items 2025-08-18 20:46:59 +08:00
yusing
dfc2d5e35c feat(handler): add SkipOriginCheckMiddleware and configuration option to bypass origin checks 2025-08-17 20:46:26 +08:00
yusing
d3bfb2488b refactor(handler): simplifying code applying auth middleware 2025-08-17 20:41:46 +08:00
yusing
baf5b5eff1 fix(handler): fixed health check import and corrected code for new metrics api handler 2025-08-17 20:24:42 +08:00
yusing
1c7e3e42f8 refactor(websocket): simplify NewManagerWithUpgrade function to use context for custom upgrader 2025-08-17 20:23:57 +08:00
yusing
beb1913285 fix(agent): remove unused dependencies 2025-08-17 20:08:04 +08:00
yusing
e14d6baedb fix(agent): change request binding from query to JSON and update hostport formatting 2025-08-17 19:45:08 +08:00
yusing
cfb37d5bd0 refactor(agent): update AgentConfig struct to use exported fields and improve JSON serialization 2025-08-17 19:08:23 +08:00
yusing
f53d384533 refactor(agent): rename requests.go to http_requests.go 2025-08-17 19:07:50 +08:00
yusing
8360aa59d1 fix(websocket): allow localhost origin in WebSocket manager 2025-08-17 17:36:07 +08:00
yusing
6ec1016f29 feat(websocket): implement CORS support in WebSocket manager for origin validation 2025-08-17 17:14:10 +08:00
yusing
35b0dcb418 chore(todo): remove completed TODOs 2025-08-17 17:10:21 +08:00
yusing
353f818b41 fix(icons): update selfhstIcons URL to point to the raw GitHub content 2025-08-17 16:39:11 +08:00
yusing
b58cabf998 feat(makefile): add support for generating Swagger markdown and update output paths for API types 2025-08-17 16:31:18 +08:00
yusing
231c0c7665 feat(api): enhance Swagger documentation with detailed API info and update request parameters for uptime 2025-08-17 16:31:08 +08:00
yusing
9931c10fa6 chore(trunk): update dependencies and versions for CLI, plugins, runtimes, and linters 2025-08-17 15:50:27 +08:00
yusing
d56a6bc19d chore(golangci): disabled new linters godot, noinlineerr, tagalign, and wsl_v5 2025-08-17 15:50:16 +08:00
yusing
e0a110cad3 fix(websocket): ensure proper connection closure and stop ping check ticker in Close method 2025-08-17 14:53:25 +08:00
yusing
d1eb3470b5 fix(api): renew endpoint uses get instead of post, correct @Accept docs 2025-08-17 14:52:56 +08:00
yusing
e52c86e0b7 fix(api): missing health field for route in docs and generated api code 2025-08-17 14:33:53 +08:00
yusing
c19d82c876 feat(autocert): add EAB configuration support and corresponding tests 2025-08-17 11:45:26 +08:00
yusing
d2f317b44d docs(api): update swagger docs 2025-08-17 01:50:38 +08:00
yusing
ba9cb083cf fix(api): ErrorResponse no longer includes ANSI escape sequeneces 2025-08-17 01:50:26 +08:00
yusing
06669534cd fix(api): validate API response 2025-08-17 01:49:49 +08:00
yusing
07d6f36159 feat(docker): include full labels, mountpoints and image details 2025-08-17 01:47:58 +08:00
yusing
55018c8ab6 fix(route): update HealthJSON field to exclude from JSON serialization 2025-08-17 00:40:57 +08:00
yusing
0862920324 refactor(concurrency): simplify some waitgroup usage, remove extra wg.Done in wg.Go left in 11af9d1 in io.go 2025-08-17 00:23:54 +08:00
yusing
b32750d545 fix(websocket): websocket concurrent write during ping pong 2025-08-17 00:17:14 +08:00
yusing
a836920eca refactor(notif): replace RangeAllParalel with for loop and WaitGroup in dispatch 2025-08-16 23:58:59 +08:00
yusing
6b89cd9106 fix(api): added missing "x-id"s 2025-08-16 23:24:03 +08:00
yusing
11af9d107a refactor(concurrency): replaced manual WaitGroup management with new wg.Go() and removed redundant code. 2025-08-16 23:14:40 +08:00
yusing
7a9b8b3fb9 fix(api): corrected json marshaling for HealthInfo 2025-08-16 23:05:29 +08:00
yusing
90efa36193 refactor(api): better type safety, split homepage override apis, fixed favicon api docs 2025-08-16 19:57:14 +08:00
yusing
1e78a0a0a0 fix(api): wrong "accept" type in file set and validate API 2025-08-16 17:31:11 +08:00
yusing
52324fbef2 fix(tests): updated docker types in tests 2025-08-16 13:09:08 +08:00
yusing
8b40baa49f chore(dependencies): upgrade Go version and update related dependencies
- Updated Go version from 1.24.5 to 1.25.0 across multiple modules.
- Incremented versions for go-acme/lego from v4.25.1 to v4.25.2 and yusing/go-proxy from v0.16.1 to v0.16.2.
- Updated indirect dependencies including cloud.google.com/go/auth, golang.org/x/net, and others to their latest versions.
- Cleaned up and organized go.mod and go.sum files.
2025-08-16 13:08:39 +08:00
yusing
35a3e3fef6 refactor(api): restructured API for type safety, maintainability and docs generation
- These changes makes the API incombatible with previous versions
- Added new types for error handling, success responses, and health checks.
- Updated health check logic to utilize the new types for better clarity and structure.
- Refactored existing handlers to improve response consistency and error handling.
- Updated Makefile to include a new target for generating API types from Swagger.
- Updated "new agent" API to respond an encrypted cert pair
2025-08-16 13:04:05 +08:00
yusing
fce9ce21c9 chore(dependencies): update go-acme/lego, quic-go, and other dependencies
- Upgraded go-acme/lego from v4.24.0 to v4.25.1.
- Updated quic-go from v0.53.0 to v0.54.0.
- Incremented versions for yusing/go-proxy and related dependencies.
- Updated aws/smithy-go from v1.22.4 to v1.22.5 and baidubce/bce-sdk-go from v0.9.235 to v0.9.236.
- Updated barcode library from v1.0.2 to v1.1.0.
- Updated google.golang.org/api from v0.242.0 to v0.243.0 and grpc from v1.73.0 to v1.74.2.
- Cleaned up unused dependencies and updated indirect dependencies.
2025-07-27 21:49:11 +08:00
yusing
475e697490 fix(healthcheck): fix spamming notifications for unhealthy services
- Added a new `downNotificationSent` flag to track if a service down notification has been sent.
- Reset the notification state when a service comes back up.
- Updated logic to ensure notifications are sent only once after reaching the configured retry threshold for consecutive failures.
2025-07-27 21:47:29 +08:00
517 changed files with 29581 additions and 20273 deletions

View File

@@ -63,9 +63,6 @@ GODOXY_METRICS_DISABLE_DISK=false
GODOXY_METRICS_DISABLE_NETWORK=false
GODOXY_METRICS_DISABLE_SENSORS=false
# Frontend listening port
GODOXY_FRONTEND_PORT=3000
# Frontend aliases (subdomains / FQDNs, e.g. godoxy, godoxy.domain.com)
GODOXY_FRONTEND_ALIASES=godoxy

View File

@@ -25,6 +25,8 @@ jobs:
id-token: write
steps:
- uses: actions/checkout@v4
with:
submodules: 'recursive'
- uses: actions/setup-go@v5
with:
go-version-file: go.mod

View File

@@ -10,7 +10,6 @@ jobs:
uses: ./.github/workflows/docker-image.yml
with:
image_name: ${{ github.repository_owner }}/godoxy
old_image_name: ${{ github.repository_owner }}/go-proxy
tag: latest
target: main
build-prod-agent:

View File

@@ -9,9 +9,6 @@ on:
image_name:
required: true
type: string
old_image_name:
required: false
type: string
target:
required: true
type: string
@@ -156,17 +153,6 @@ jobs:
docker buildx imagetools create $(jq -cr '.tags | map("-t " + .) | join(" ")' <<< "$DOCKER_METADATA_OUTPUT_JSON") \
$(printf '${{ env.REGISTRY }}/${{ inputs.image_name }}@sha256:%s ' *)
- name: Old image name
if: inputs.old_image_name != ''
run: |
docker buildx imagetools create -t ${{ env.REGISTRY }}/${{ inputs.old_image_name }}:${{ steps.meta.outputs.version }}\
${{ env.REGISTRY }}/${{ inputs.image_name }}:${{ steps.meta.outputs.version }}
- name: Inspect image
run: |
docker buildx imagetools inspect ${{ env.REGISTRY }}/${{ inputs.image_name }}:${{ steps.meta.outputs.version }}
- name: Inspect image (old)
if: inputs.old_image_name != ''
run: |
docker buildx imagetools inspect ${{ env.REGISTRY }}/${{ inputs.old_image_name }}:${{ steps.meta.outputs.version }}

1
.gitignore vendored
View File

@@ -29,6 +29,7 @@ todo.md
.aider*
mtrace.json
.env
*.env
.cursorrules
.cursor/
.windsurfrules

9
.gitmodules vendored Normal file
View File

@@ -0,0 +1,9 @@
[submodule "internal/gopsutil"]
path = internal/gopsutil
url = https://github.com/godoxy-app/gopsutil.git
[submodule "internal/go-oidc"]
path = internal/go-oidc
url = https://github.com/godoxy-app/go-oidc.git
[submodule "goutils"]
path = goutils
url = https://github.com/yusing/goutils.git

View File

@@ -18,6 +18,7 @@ linters:
- gocognit
- goconst
- gocyclo
- godot
- gomoddirectives
- gosmopolitan
- ireturn
@@ -29,16 +30,19 @@ linters:
- nestif
- nlreturn
- nonamedreturns
- noinlineerr
- paralleltest
- revive
- rowserrcheck
- sqlclosecheck
- tagalign
- tagliatelle
- testpackage
- tparallel
- varnamelen
- wrapcheck
- wsl
- wsl_v5
settings:
errcheck:
exclude-functions:
@@ -66,7 +70,6 @@ linters:
govet:
disable:
- shadow
- fieldalignment
enable-all: true
misspell:
locale: US
@@ -104,8 +107,7 @@ linters:
- all
- -SA1019
dot-import-whitelist:
- github.com/yusing/go-proxy/internal/utils/testing
- github.com/yusing/go-proxy/internal/api/v1/utils
- github.com/yusing/godoxy/internal/utils/testing
tagalign:
align: false
sort: true

View File

@@ -2,17 +2,17 @@
# To learn more about the format of this file, see https://docs.trunk.io/reference/trunk-yaml
version: 0.1
cli:
version: 1.22.15
version: 1.25.0
# Trunk provides extensibility via plugins. (https://docs.trunk.io/plugins)
plugins:
sources:
- id: trunk
ref: v1.6.8
ref: v1.7.2
uri: https://github.com/trunk-io/plugins
# Many linters and tools depend on runtimes - configure them here. (https://docs.trunk.io/runtimes)
runtimes:
enabled:
- node@18.20.5
- node@22.16.0
- python@3.10.8
- go@1.24.3
# This is the section where you manage your linters. (https://docs.trunk.io/check/configuration)
@@ -21,18 +21,18 @@ lint:
- markdownlint
- yamllint
enabled:
- checkov@3.2.432
- golangci-lint2@2.1.6
- hadolint@2.12.1-beta
- checkov@3.2.471
- golangci-lint2@2.5.0
- hadolint@2.14.0
- actionlint@1.7.7
- git-diff-check
- gofmt@1.20.4
- osv-scanner@2.0.2
- osv-scanner@2.2.2
- oxipng@9.1.5
- prettier@3.5.3
- shellcheck@0.10.0
- prettier@3.6.2
- shellcheck@0.11.0
- shfmt@3.6.0
- trufflehog@3.88.33
- trufflehog@3.90.8
actions:
disabled:
- trunk-announce

View File

@@ -1,10 +1,10 @@
{
"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,5 +1,5 @@
# Stage 1: deps
FROM golang:1.24.5-alpine AS deps
FROM golang:1.25.5-alpine AS deps
HEALTHCHECK NONE
# package version does not matter
@@ -7,13 +7,20 @@ HEALTHCHECK NONE
RUN apk add --no-cache tzdata make libcap-setcap
ENV GOPATH=/root/go
ENV GOCACHE=/root/.cache/go-build
WORKDIR /src
COPY goutils/go.mod goutils/go.sum ./goutils/
COPY internal/go-oidc/go.mod internal/go-oidc/go.sum ./internal/go-oidc/
COPY internal/gopsutil/go.mod internal/gopsutil/go.sum ./internal/gopsutil/
COPY go.mod go.sum ./
# remove godoxy stuff from go.mod first
RUN sed -i '/^module github\.com\/yusing\/go-proxy/!{/github\.com\/yusing\/go-proxy/d}' go.mod && \
RUN --mount=type=cache,target=/root/.cache/go-build \
--mount=type=cache,target=/root/go/pkg/mod \
sed -i '/^module github\.com\/yusing\/godoxy/!{/github\.com\/yusing\/godoxy/d}' go.mod && \
sed -i '/^module github\.com\/yusing\/goutils/!{/github\.com\/yusing\/goutils/d}' go.mod && \
go mod download -x
# Stage 2: builder
@@ -28,6 +35,7 @@ COPY internal ./internal
COPY pkg ./pkg
COPY agent ./agent
COPY socket-proxy ./socket-proxy
COPY goutils ./goutils
ARG VERSION
ENV VERSION=${VERSION}
@@ -35,9 +43,6 @@ ENV VERSION=${VERSION}
ARG MAKE_ARGS
ENV MAKE_ARGS=${MAKE_ARGS}
ENV GOCACHE=/root/.cache/go-build
ENV GOPATH=/root/go
RUN --mount=type=cache,target=/root/.cache/go-build \
--mount=type=cache,target=/root/go/pkg/mod \
make ${MAKE_ARGS} docker=1 build
@@ -47,6 +52,7 @@ FROM scratch
LABEL maintainer="yusing@6uo.me"
LABEL proxy.exclude=1
LABEL proxy.#1.healthcheck.disable=true
# copy timezone data
COPY --from=builder /usr/share/zoneinfo /usr/share/zoneinfo

11
Jenkinsfile vendored Normal file
View File

@@ -0,0 +1,11 @@
node {
stage('SCM') {
checkout scm
}
stage('SonarQube Analysis') {
def scannerHome = tool 'SonarScanner';
withSonarQubeEnv() {
sh "${scannerHome}/bin/sonar-scanner"
}
}
}

View File

@@ -3,8 +3,11 @@ export VERSION ?= $(shell git describe --tags --abbrev=0)
export BUILD_DATE ?= $(shell date -u +'%Y%m%d-%H%M')
export GOOS = linux
LDFLAGS = -X github.com/yusing/go-proxy/pkg.version=${VERSION}
WEBUI_DIR ?= ../godoxy-webui
DOCS_DIR ?= ${WEBUI_DIR}/wiki
GO_TAGS = sonic
LDFLAGS = -X github.com/yusing/goutils/version.version=${VERSION} -checklinkname=0
ifeq ($(agent), 1)
NAME = godoxy-agent
@@ -24,26 +27,28 @@ ifeq ($(trace), 1)
endif
ifeq ($(race), 1)
debug = 1
BUILD_FLAGS += -race
endif
ifeq ($(debug), 1)
CGO_ENABLED = 1
GODOXY_DEBUG = 1
BUILD_FLAGS += -gcflags=all='-N -l' -tags debug -asan
else ifeq ($(pprof), 1)
GO_TAGS += debug
BUILD_FLAGS += -race
else ifeq ($(debug), 1)
CGO_ENABLED = 1
GODOXY_DEBUG = 1
GO_TAGS += debug
BUILD_FLAGS += -asan # FIXME: -gcflags=all='-N -l'
else ifeq ($(pprof), 1)
CGO_ENABLED = 0
GORACE = log_path=logs/pprof strip_path_prefix=$(shell pwd)/ halt_on_error=1
BUILD_FLAGS += -tags pprof
GO_TAGS += pprof
VERSION := ${VERSION}-pprof
else
CGO_ENABLED = 0
LDFLAGS += -s -w
BUILD_FLAGS += -pgo=auto -tags production
GO_TAGS += production
BUILD_FLAGS += -pgo=auto
endif
BUILD_FLAGS += -ldflags='$(LDFLAGS)'
BUILD_FLAGS += -tags '$(GO_TAGS)' -ldflags='$(LDFLAGS)'
BIN_PATH := $(shell pwd)/bin/${NAME}
export NAME
@@ -70,11 +75,12 @@ endif
.PHONY: debug
test:
GODOXY_TEST=1 go test ./internal/...
go test -v -race ./internal/...
docker-build-test:
docker build -t godoxy .
docker build --build-arg=MAKE_ARGS=agent=1 -t godoxy-agent .
docker build --build-arg=MAKE_ARGS=socket-proxy=1 -t godoxy-socket-proxy .
go_ver := $(shell go version | cut -d' ' -f3 | cut -d'o' -f2)
files := $(shell find . -name go.mod -type f -or -name Dockerfile -type f)
@@ -105,15 +111,20 @@ mod-tidy:
build:
mkdir -p $(shell dirname ${BIN_PATH})
cd ${PWD} && go build ${BUILD_FLAGS} -o ${BIN_PATH} ./cmd
go build -C ${PWD} ${BUILD_FLAGS} -o ${BIN_PATH} ./cmd
${POST_BUILD}
run:
cd ${PWD} && [ -f .env ] && godotenv -f .env go run ${BUILD_FLAGS} ./cmd
debug:
make NAME="godoxy-test" debug=1 build
sh -c 'HTTP_ADDR=:81 HTTPS_ADDR=:8443 API_ADDR=:8899 DEBUG=1 bin/godoxy-test'
dev:
docker compose -f dev.compose.yml $(args)
dev-build: build
docker compose -f dev.compose.yml up -t 0 -d app --force-recreate
dev-run: build
cd dev-data && ${BIN_PATH}
mtrace:
${BIN_PATH} debug-ls-mtrace > mtrace.json
@@ -134,4 +145,20 @@ cloc:
cloc --include-lang=Go --not-match-f '_test.go$$' .
push-github:
git push origin $(shell git rev-parse --abbrev-ref HEAD)
git push origin $(shell git rev-parse --abbrev-ref HEAD)
gen-swagger:
swag init --parseDependency --parseInternal --parseFuncBody -g handler.go -d internal/api -o internal/api/v1/docs
python3 scripts/fix-swagger-json.py
# we don't need this
rm internal/api/v1/docs/docs.go
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}/lib -n api.ts -p internal/api/v1/docs/swagger.json
bunx --bun prettier --config ${WEBUI_DIR}/.prettierrc --write ${WEBUI_DIR}/lib/api.ts

View File

@@ -1,10 +1,11 @@
<div align="center">
# GoDoxy
<img src="assets/godoxy.png" width="200">
[![Quality Gate Status](https://sonarcloud.io/api/project_badges/measure?project=yusing_go-proxy&metric=alert_status)](https://sonarcloud.io/summary/new_code?id=yusing_go-proxy)
![GitHub last commit](https://img.shields.io/github/last-commit/yusing/godoxy)
[![Lines of Code](https://sonarcloud.io/api/project_badges/measure?project=yusing_go-proxy&metric=ncloc)](https://sonarcloud.io/summary/new_code?id=go-proxy)
![Demo](https://img.shields.io/website?url=https%3A%2F%2Fdemo.godoxy.dev&label=Demo&link=https%3A%2F%2Fdemo.godoxy.dev)
[![Discord](https://dcbadge.limes.pink/api/server/umReR62nRd?style=flat)](https://discord.gg/umReR62nRd)
@@ -16,29 +17,30 @@ A lightweight, simple, and performant reverse proxy with WebUI.
<h5>EN | <a href="README_CHT.md">中文</a></h5>
Have questions? Ask [ChatGPT](https://chatgpt.com/g/g-6825390374b481919ad482f2e48936a1-godoxy-assistant)! (Thanks to [@ismesid](https://github.com/arevindh))
<img src="screenshots/webui.jpg" style="max-width: 650">
Have questions? Ask [ChatGPT](https://chatgpt.com/g/g-6825390374b481919ad482f2e48936a1-godoxy-assistant)! (Thanks to [@ismesid](https://github.com/arevindh))
</div>
## Table of content
<!-- TOC -->
- [GoDoxy](#godoxy)
- [Table of content](#table-of-content)
- [Running demo](#running-demo)
- [Key Features](#key-features)
- [Prerequisites](#prerequisites)
- [Setup](#setup)
- [How does GoDoxy work](#how-does-godoxy-work)
- [Screenshots](#screenshots)
- [idlesleeper](#idlesleeper)
- [Metrics and Logs](#metrics-and-logs)
- [Manual Setup](#manual-setup)
- [Folder structrue](#folder-structrue)
- [Build it yourself](#build-it-yourself)
- [Table of content](#table-of-content)
- [Running demo](#running-demo)
- [Key Features](#key-features)
- [Prerequisites](#prerequisites)
- [Setup](#setup)
- [How does GoDoxy work](#how-does-godoxy-work)
- [Update / Uninstall system agent](#update--uninstall-system-agent)
- [Screenshots](#screenshots)
- [idlesleeper](#idlesleeper)
- [Metrics and Logs](#metrics-and-logs)
- [Manual Setup](#manual-setup)
- [Folder structrue](#folder-structrue)
- [Build it yourself](#build-it-yourself)
- [Star History](#star-history)
## Running demo
@@ -57,10 +59,14 @@ Have questions? Ask [ChatGPT](https://chatgpt.com/g/g-6825390374b481919ad482f2e4
- Country **(Maxmind account required)**
- Timezone **(Maxmind account required)**
- **Access logging**
- Periodic notification of access summaries for number of allowed and blocked connections
- **Advanced Automation**
- Automatic SSL certificate management with Let's Encrypt ([using DNS-01 Challenge](https://docs.godoxy.dev/DNS-01-Providers))
- Auto-configuration for Docker containers
- Hot-reloading of configurations and container state changes
- **Container Runtime Support**
- Docker
- Podman
- **Idle-sleep**: stop and wake containers based on traffic _(see [screenshots](#idlesleeper))_
- Docker containers
- Proxmox LXCs
@@ -68,6 +74,7 @@ Have questions? Ask [ChatGPT](https://chatgpt.com/g/g-6825390374b481919ad482f2e4
- HTTP reserve proxy
- TCP/UDP port forwarding
- **OpenID Connect support**: SSO and secure your apps easily
- **ForwardAuth support**: integrate with any auth provider (e.g. TinyAuth)
- **Customization**
- [HTTP middlewares](https://docs.godoxy.dev/Middlewares)
- [Custom error pages support](https://docs.godoxy.dev/Custom-Error-Pages)
@@ -123,6 +130,20 @@ Configure Wildcard DNS Record(s) to point to machine running `GoDoxy`, e.g.
>
> For example, with the label `proxy.aliases: qbt` you can access your app via `qbt.domain.com`.
## Update / Uninstall system agent
Update:
```bash
bash -c "$(curl -fsSL https://github.com/yusing/godoxy/raw/refs/heads/main/scripts/install-agent.sh)" -- update
```
Uninstall:
```bash
bash -c "$(curl -fsSL https://github.com/yusing/godoxy/raw/refs/heads/main/scripts/install-agent.sh)" -- uninstall
```
## Screenshots
### idlesleeper
@@ -134,22 +155,12 @@ Configure Wildcard DNS Record(s) to point to machine running `GoDoxy`, e.g.
<div align="center">
<table>
<tr>
<td align="center"><img src="screenshots/uptime.png" alt="Uptime Monitor" width="250"/></td>
<td align="center"><img src="screenshots/docker-logs.jpg" alt="Docker Logs" width="250"/></td>
<td align="center"><img src="screenshots/docker.jpg" alt="Server Overview" width="250"/></td>
<td align="center"><img src="screenshots/routes.jpg" alt="Routes" width="350"/></td>
<td align="center"><img src="screenshots/servers.jpg" alt="Servers" width="350"/></td>
</tr>
<tr>
<td align="center"><b>Uptime Monitor</b></td>
<td align="center"><b>Docker Logs</b></td>
<td align="center"><b>Server Overview</b></td>
</tr>
<tr>
<td align="center"><img src="screenshots/system-monitor.jpg" alt="System Monitor" width="250"/></td>
<td align="center"><img src="screenshots/system-info-graphs.jpg" alt="Graphs" width="250"/></td>
</tr>
<tr>
<td align="center"><b>System Monitor</b></td>
<td align="center"><b>Graphs</b></td>
<td align="center"><b>Routes</b></td>
<td align="center"><b>Servers</b></td>
</tr>
</table>
</div>
@@ -201,4 +212,8 @@ Configure Wildcard DNS Record(s) to point to machine running `GoDoxy`, e.g.
5. build binary with `make build`
## Star History
[![Star History Chart](https://api.star-history.com/svg?repos=yusing/godoxy&type=Date)](https://www.star-history.com/#yusing/godoxy&Date)
[🔼Back to top](#table-of-content)

View File

@@ -1,10 +1,11 @@
<div align="center">
# GoDoxy
<img src="assets/godoxy.png" width="200">
[![Quality Gate Status](https://sonarcloud.io/api/project_badges/measure?project=yusing_go-proxy&metric=alert_status)](https://sonarcloud.io/summary/new_code?id=yusing_go-proxy)
![GitHub last commit](https://img.shields.io/github/last-commit/yusing/godoxy)
[![Lines of Code](https://sonarcloud.io/api/project_badges/measure?project=yusing_go-proxy&metric=ncloc)](https://sonarcloud.io/summary/new_code?id=yusing_go-proxy)
![Demo](https://img.shields.io/website?url=https%3A%2F%2Fdemo.godoxy.dev&label=Demo&link=https%3A%2F%2Fdemo.godoxy.dev)
[![Discord](https://dcbadge.limes.pink/api/server/umReR62nRd?style=flat)](https://discord.gg/umReR62nRd)
@@ -16,28 +17,29 @@
<h5><a href="README.md">EN</a> | 中文</h5>
有疑問? 問 [ChatGPT](https://chatgpt.com/g/g-6825390374b481919ad482f2e48936a1-godoxy-assistant)!(鳴謝 [@ismesid](https://github.com/arevindh)
<img src="https://github.com/user-attachments/assets/4bb371f4-6e4c-425c-89b2-b9e962bdd46f" style="max-width: 650">
有疑問? 問 [ChatGPT](https://chatgpt.com/g/g-6825390374b481919ad482f2e48936a1-godoxy-assistant)!(鳴謝 [@ismesid](https://github.com/arevindh)
</div>
## 目錄
<!-- TOC -->
- [GoDoxy](#godoxy)
- [目錄](#目錄)
- [運行示例](#運行示例)
- [主要特點](#主要特點)
- [前置需求](#前置需求)
- [安裝](#安裝)
- [手動安裝](#手動安裝)
- [資料夾結構](#資料夾結構)
- [截圖](#截圖)
- [閒置休眠](#閒置休眠)
- [監控](#監控)
- [自行編譯](#自行編譯)
- [目錄](#目錄)
- [運行示例](#運行示例)
- [主要特點](#主要特點)
- [前置需求](#前置需求)
- [安裝](#安裝)
- [手動安裝](#手動安裝)
- [資料夾結構](#資料夾結構)
- [更新 / 卸載系統代理 (System Agent)](#更新--卸載系統代理-system-agent)
- [截圖](#截圖)
- [閒置休眠](#閒置休眠)
- [監控](#監控)
- [自行編譯](#自行編譯)
- [Star History](#star-history)
## 運行示例
@@ -56,10 +58,14 @@
- 國家 **(需要 Maxmind 帳戶)**
- 時區 **(需要 Maxmind 帳戶)**
- **存取日誌記錄**
- 定時發送摘要 (允許和拒絕的連線次數)
- **自動化**
- 使用 Let's Encrypt 自動管理 SSL 憑證 ([使用 DNS-01 驗證](https://docs.godoxy.dev/DNS-01-Providers))
- Docker 容器自動配置
- 設定檔與容器狀態變更時自動熱重載
- **容器運行時支援**
- Docker
- Podman
- **閒置休眠**:根據流量停止和喚醒容器 _(參見[截圖](#閒置休眠))_
- Docker 容器
- Proxmox LXC 容器
@@ -67,6 +73,7 @@
- HTTP 反向代理
- TCP/UDP 連接埠轉送
- **OpenID Connect 支援**:輕鬆實現單點登入 (SSO) 並保護您的應用程式
- **ForwardAuth 支援**:整合任何 auth provider (例如 TinyAuth)
- **客製化**
- [HTTP 中介軟體](https://docs.godoxy.dev/Middlewares)
- [支援自訂錯誤頁面](https://docs.godoxy.dev/Custom-Error-Pages)
@@ -80,8 +87,6 @@
- **高效能**
-**[Go](https://go.dev)** 語言編寫
[🔼 回到頂部](#目錄)
## 前置需求
設置 DNS 記錄指向運行 `GoDoxy` 的機器,例如:
@@ -106,8 +111,6 @@
3. 現在可以在 WebUI `https://godoxy.yourdomain.com` 進行額外配置
[🔼 回到頂部](#目錄)
### 手動安裝
1. 建立 `config` 目錄,然後將 `config.example.yml` 下載到 `config/config.yml`
@@ -143,35 +146,37 @@
└── .env
```
## 更新 / 卸載系統代理 (System Agent)
更新:
```bash
sudo /bin/bash -c "$(curl -fsSL https://github.com/yusing/godoxy/raw/refs/heads/main/scripts/install-agent.sh)" -- update
```
卸載:
```bash
sudo /bin/bash -c "$(curl -fsSL https://github.com/yusing/godoxy/raw/refs/heads/main/scripts/install-agent.sh)" -- uninstall
```
## 截圖
### 閒置休眠
![閒置休眠](screenshots/idlesleeper.webp)
[🔼 回到頂部](#目錄)
### 監控
<div align="center">
<table>
<tr>
<td align="center"><img src="screenshots/uptime.png" alt="Uptime Monitor" width="250"/></td>
<td align="center"><img src="screenshots/docker-logs.jpg" alt="Docker Logs" width="250"/></td>
<td align="center"><img src="screenshots/docker.jpg" alt="Server Overview" width="250"/></td>
<td align="center"><img src="screenshots/routes.jpg" alt="Routes" width="350"/></td>
<td align="center"><img src="screenshots/servers.jpg" alt="Servers" width="350"/></td>
</tr>
<tr>
<td align="center"><b>運行時間監控</b></td>
<td align="center"><b>Docker 日誌</b></td>
<td align="center"><b>伺服器概覽</b></td>
</tr>
<tr>
<td align="center"><img src="screenshots/system-monitor.jpg" alt="System Monitor" width="250"/></td>
<td align="center"><img src="screenshots/system-info-graphs.jpg" alt="Graphs" width="250"/></td>
</tr>
<tr>
<td align="center"><b>系統監控</b></td>
<td align="center"><b>圖表</b></td>
<td align="center"><b>路由</b></td>
<td align="center"><b>伺服器</b></td>
</tr>
</table>
</div>
@@ -188,4 +193,8 @@
5. 使用 `make build` 編譯二進制檔案
## Star History
[![Star History Chart](https://api.star-history.com/svg?repos=yusing/godoxy&type=Date)](https://www.star-history.com/#yusing/godoxy&Date)
[🔼 回到頂部](#目錄)

View File

@@ -5,15 +5,15 @@ import (
"github.com/rs/zerolog"
"github.com/rs/zerolog/log"
"github.com/yusing/go-proxy/agent/pkg/agent"
"github.com/yusing/go-proxy/agent/pkg/env"
"github.com/yusing/go-proxy/agent/pkg/server"
"github.com/yusing/go-proxy/internal/gperr"
"github.com/yusing/go-proxy/internal/metrics/systeminfo"
httpServer "github.com/yusing/go-proxy/internal/net/gphttp/server"
"github.com/yusing/go-proxy/internal/task"
"github.com/yusing/go-proxy/pkg"
socketproxy "github.com/yusing/go-proxy/socketproxy/pkg"
"github.com/yusing/godoxy/agent/pkg/agent"
"github.com/yusing/godoxy/agent/pkg/env"
"github.com/yusing/godoxy/agent/pkg/server"
"github.com/yusing/godoxy/internal/metrics/systeminfo"
socketproxy "github.com/yusing/godoxy/socketproxy/pkg"
httpServer "github.com/yusing/goutils/server"
strutils "github.com/yusing/goutils/strings"
"github.com/yusing/goutils/task"
"github.com/yusing/goutils/version"
)
func main() {
@@ -26,26 +26,27 @@ func main() {
ca := &agent.PEMPair{}
err := ca.Load(env.AgentCACert)
if err != nil {
gperr.LogFatal("init CA error", err)
log.Fatal().Err(err).Msg("init CA error")
}
caCert, err := ca.ToTLSCert()
if err != nil {
gperr.LogFatal("init CA error", err)
log.Fatal().Err(err).Msg("init CA error")
}
srv := &agent.PEMPair{}
srv.Load(env.AgentSSLCert)
if err != nil {
gperr.LogFatal("init SSL error", err)
log.Fatal().Err(err).Msg("init SSL error")
}
srvCert, err := srv.ToTLSCert()
if err != nil {
gperr.LogFatal("init SSL error", err)
log.Fatal().Err(err).Msg("init SSL error")
}
log.Info().Msgf("GoDoxy Agent version %s", pkg.GetVersion())
log.Info().Msgf("GoDoxy Agent version %s", version.Get())
log.Info().Msgf("Agent name: %s", env.AgentName)
log.Info().Msgf("Agent port: %d", env.AgentPort)
log.Info().Msgf("Agent runtime: %s", env.Runtime)
log.Info().Msg(`
Tips:
@@ -63,9 +64,11 @@ Tips:
server.StartAgentServer(t, opts)
if socketproxy.ListenAddr != "" {
log.Info().Msgf("Docker socket listening on: %s", socketproxy.ListenAddr)
runtime := strutils.Title(string(env.Runtime))
log.Info().Msgf("%s socket listening on: %s", runtime, socketproxy.ListenAddr)
opts := httpServer.Options{
Name: "docker",
Name: runtime,
HTTPAddr: socketproxy.ListenAddr,
Handler: socketproxy.NewHandler(),
}

View File

@@ -1,87 +1,127 @@
module github.com/yusing/go-proxy/agent
module github.com/yusing/godoxy/agent
go 1.24.5
go 1.25.5
replace github.com/yusing/go-proxy => ..
replace (
github.com/shirou/gopsutil/v4 => ../internal/gopsutil
github.com/yusing/godoxy => ../
github.com/yusing/godoxy/socketproxy => ../socket-proxy
github.com/yusing/goutils => ../goutils
github.com/yusing/goutils/http/reverseproxy => ../goutils/http/reverseproxy
github.com/yusing/goutils/http/websocket => ../goutils/http/websocket
github.com/yusing/goutils/server => ../goutils/server
)
replace github.com/yusing/go-proxy/socketproxy => ../socket-proxy
replace github.com/yusing/go-proxy/internal/utils => ../internal/utils
replace github.com/docker/docker => github.com/godoxy-app/docker v0.0.0-20250523125835-a2474a6ebe30
replace github.com/shirou/gopsutil/v4 => github.com/godoxy-app/gopsutil/v4 v4.0.0-20250607110153-34d627ba1b5d
exclude github.com/containerd/nerdctl/mod/tigron v0.0.0
require (
github.com/bytedance/sonic v1.14.2
github.com/gin-gonic/gin v1.11.0
github.com/gorilla/websocket v1.5.3
github.com/pion/dtls/v3 v3.0.6
github.com/puzpuzpuz/xsync/v4 v4.1.0
github.com/puzpuzpuz/xsync/v4 v4.2.0
github.com/rs/zerolog v1.34.0
github.com/stretchr/testify v1.10.0
github.com/yusing/go-proxy v0.16.0
github.com/yusing/go-proxy/internal/utils v0.0.0
github.com/yusing/go-proxy/socketproxy v0.0.0-00010101000000-000000000000
github.com/stretchr/testify v1.11.1
github.com/valyala/fasthttp v1.68.0
github.com/yusing/godoxy v0.20.10
github.com/yusing/godoxy/socketproxy v0.0.0-00010101000000-000000000000
github.com/yusing/goutils v0.7.0
github.com/yusing/goutils/http/reverseproxy v0.0.0-20251210092226-e7258c13e982
github.com/yusing/goutils/server v0.0.0-20251210092226-e7258c13e982
)
require (
github.com/Microsoft/go-winio v0.6.2 // indirect
github.com/PuerkitoBio/goquery v1.10.3 // indirect
github.com/PuerkitoBio/goquery v1.11.0 // indirect
github.com/andybalholm/brotli v1.2.0 // indirect
github.com/andybalholm/cascadia v1.3.3 // indirect
github.com/buger/goterm v1.0.4 // indirect
github.com/bytedance/gopkg v0.1.3 // indirect
github.com/bytedance/sonic/loader v0.4.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/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
github.com/diskfs/go-diskfs v1.7.0 // indirect
github.com/distribution/reference v0.6.0 // indirect
github.com/docker/cli v28.3.2+incompatible // indirect
github.com/docker/docker v28.3.2+incompatible // indirect
github.com/docker/go-connections v0.5.0 // indirect
github.com/djherbis/times v1.6.0 // indirect
github.com/docker/cli v29.1.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.8.4 // indirect
github.com/gabriel-vasile/mimetype v1.4.9 // indirect
github.com/ebitengine/purego v0.9.1 // indirect
github.com/felixge/httpsnoop v1.0.4 // indirect
github.com/gabriel-vasile/mimetype v1.4.11 // indirect
github.com/gin-contrib/sse v1.1.0 // indirect
github.com/go-acme/lego/v4 v4.29.0 // indirect
github.com/go-jose/go-jose/v4 v4.1.3 // indirect
github.com/go-logr/logr v1.4.3 // indirect
github.com/go-logr/stdr v1.2.2 // indirect
github.com/go-ole/go-ole v1.3.0 // indirect
github.com/go-playground/locales v0.14.1 // indirect
github.com/go-playground/universal-translator v0.18.1 // indirect
github.com/go-playground/validator/v10 v10.27.0 // indirect
github.com/goccy/go-yaml v1.18.0 // indirect
github.com/gogo/protobuf v1.3.2 // indirect
github.com/go-playground/validator/v10 v10.28.0 // indirect
github.com/gobwas/glob v0.2.3 // indirect
github.com/goccy/go-json v0.10.5 // indirect
github.com/goccy/go-yaml v1.19.0 // indirect
github.com/gorilla/mux v1.8.1 // indirect
github.com/gotify/server/v2 v2.6.3 // indirect
github.com/kr/pretty v0.1.0 // indirect
github.com/gotify/server/v2 v2.7.3 // indirect
github.com/jinzhu/copier v0.4.0 // indirect
github.com/json-iterator/go v1.1.13-0.20220915233716-71ac16282d12 // indirect
github.com/klauspost/compress v1.18.2 // indirect
github.com/klauspost/cpuid/v2 v2.3.0 // indirect
github.com/leodido/go-urn v1.4.0 // indirect
github.com/lithammer/fuzzysearch v1.1.8 // indirect
github.com/lufia/plan9stats v0.0.0-20250317134145-8bc96cf8fc35 // indirect
github.com/lufia/plan9stats v0.0.0-20251013123823-9fd1530e3ec3 // indirect
github.com/luthermonson/go-proxmox v0.2.3 // indirect
github.com/magefile/mage v1.15.0 // indirect
github.com/mattn/go-colorable v0.1.14 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect
github.com/miekg/dns v1.1.68 // 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/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
github.com/opencontainers/image-spec v1.1.1 // indirect
github.com/oschwald/maxminddb-golang v1.13.1 // indirect
github.com/pion/logging v0.2.4 // indirect
github.com/pion/transport/v3 v3.0.7 // indirect
github.com/pkg/errors v0.9.1 // indirect
github.com/pelletier/go-toml/v2 v2.2.4 // indirect
github.com/pires/go-proxyproto v0.8.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/quic-go/qpack v0.5.1 // indirect
github.com/quic-go/quic-go v0.53.0 // indirect
github.com/samber/lo v1.51.0 // indirect
github.com/quic-go/qpack v0.6.0 // indirect
github.com/quic-go/quic-go v0.57.1 // indirect
github.com/samber/lo v1.52.0 // indirect
github.com/samber/slog-common v0.19.0 // indirect
github.com/samber/slog-zerolog/v2 v2.7.3 // indirect
github.com/shirou/gopsutil/v4 v4.25.6 // indirect
github.com/samber/slog-zerolog/v2 v2.9.0 // indirect
github.com/shirou/gopsutil/v4 v4.25.11 // indirect
github.com/sirupsen/logrus v1.9.4-0.20230606125235-dd1b4c2e81af // indirect
github.com/spf13/afero v1.14.0 // indirect
github.com/tklauser/go-sysconf v0.3.15 // indirect
github.com/tklauser/numcpus v0.10.0 // indirect
github.com/spf13/afero v1.15.0 // 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/valyala/bytebufferpool v1.0.0 // indirect
github.com/vincent-petithory/dataurl v1.0.0 // indirect
github.com/yusing/ds v0.3.1 // indirect
github.com/yusing/gointernals v0.1.16 // indirect
github.com/yusing/goutils/http/websocket v0.0.0-20251210092226-e7258c13e982 // indirect
github.com/yusufpapurcu/wmi v1.2.4 // indirect
go.uber.org/atomic v1.11.0 // indirect
go.uber.org/mock v0.5.2 // indirect
golang.org/x/crypto v0.40.0 // indirect
golang.org/x/mod v0.26.0 // indirect
golang.org/x/net v0.42.0 // indirect
golang.org/x/sync v0.16.0 // indirect
golang.org/x/sys v0.34.0 // indirect
golang.org/x/text v0.27.0 // indirect
golang.org/x/time v0.12.0 // indirect
golang.org/x/tools v0.35.0 // indirect
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 // indirect
go.opentelemetry.io/auto/sdk v1.2.1 // indirect
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.64.0 // indirect
go.opentelemetry.io/otel v1.39.0 // indirect
go.opentelemetry.io/otel/metric v1.39.0 // indirect
go.opentelemetry.io/otel/trace v1.39.0 // indirect
golang.org/x/arch v0.23.0 // indirect
golang.org/x/crypto v0.46.0 // indirect
golang.org/x/mod v0.31.0 // indirect
golang.org/x/net v0.48.0 // indirect
golang.org/x/sync v0.19.0 // indirect
golang.org/x/sys v0.39.0 // indirect
golang.org/x/text v0.32.0 // indirect
golang.org/x/time v0.14.0 // indirect
golang.org/x/tools v0.40.0 // indirect
google.golang.org/protobuf v1.36.10 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
)

View File

@@ -1,36 +1,69 @@
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.10.3 h1:pFYcNSqHxBD06Fpj/KsbStFRsgRATgnf3LeXiUkhzPo=
github.com/PuerkitoBio/goquery v1.10.3/go.mod h1:tMUX0zDMHXYlAQk6p35XxQMqMweEKB7iK7iLNd4RH4Y=
github.com/PuerkitoBio/goquery v1.11.0 h1:jZ7pwMQXIITcUXNH83LLk+txlaEy6NVOfTuP43xxfqw=
github.com/PuerkitoBio/goquery v1.11.0/go.mod h1:wQHgxUOU3JGuj3oD/QFfxUdlzW6xPHfqyHre6VMY4DQ=
github.com/anchore/go-lzo v0.1.0 h1:NgAacnzqPeGH49Ky19QKLBZEuFRqtTG9cdaucc3Vncs=
github.com/anchore/go-lzo v0.1.0/go.mod h1:3kLx0bve2oN1iDwgM1U5zGku1Tfbdb0No5qp1eL1fIk=
github.com/andybalholm/brotli v1.2.0 h1:ukwgCxwYrmACq68yiUqwIWnGY0cTPox/M94sVwToPjQ=
github.com/andybalholm/brotli v1.2.0/go.mod h1:rzTDkvFWvIrjDXZHkuS16NPggd91W3kUSvPlQ1pLaKY=
github.com/andybalholm/cascadia v1.3.3 h1:AG2YHrzJIm4BZ19iwJ/DAua6Btl3IwJX+VI4kktS1LM=
github.com/andybalholm/cascadia v1.3.3/go.mod h1:xNd9bqTn98Ln4DwST8/nG+H0yuB8Hmgu1YHNnWw0GeA=
github.com/cenkalti/backoff/v5 v5.0.2 h1:rIfFVxEf1QsI7E1ZHfp/B4DF/6QBAUhmgkxc0H7Zss8=
github.com/cenkalti/backoff/v5 v5.0.2/go.mod h1:rkhZdG3JZukswDf7f0cwqPNk4K0sa+F97BxZthm/crw=
github.com/buger/goterm v1.0.4 h1:Z9YvGmOih81P0FbVtEYTFF6YsSgxSUKEhf/f9bTMXbY=
github.com/buger/goterm v1.0.4/go.mod h1:HiFWV3xnkolgrBV3mY8m0X0Pumt4zg4QhbdOzQtB8tE=
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.14.2 h1:k1twIoe97C1DtYUo+fZQy865IuHia4PR5RPiuGPPIIE=
github.com/bytedance/sonic v1.14.2/go.mod h1:T80iDELeHiHKSc0C9tubFygiuXoGzrkjKzX2quAx980=
github.com/bytedance/sonic/loader v0.4.0 h1:olZ7lEqcxtZygCK9EKYKADnpQoYkRQxaeY2NYzevs+o=
github.com/bytedance/sonic/loader v0.4.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/containerd/errdefs v1.0.0 h1:tg5yIfIlQIrxYtu9ajqY42W3lpS19XqdxRQeEwYG8PI=
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=
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=
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM=
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/diskfs/go-diskfs v1.7.0 h1:vonWmt5CMowXwUc79jWyGrf2DIMeoOjkLlMnQYGVOs8=
github.com/diskfs/go-diskfs v1.7.0/go.mod h1:LhQyXqOugWFRahYUSw47NyZJPezFzB9UELwhpszLP/k=
github.com/distribution/reference v0.6.0 h1:0IXCQ5g4/QMHHkarYzh5l+u8T3t73zM5QvfrDyIgxBk=
github.com/distribution/reference v0.6.0/go.mod h1:BbU0aIcezP1/5jX/8MP0YiH4SdvB5Y4f/wlDRiLyi3E=
github.com/docker/cli v28.3.2+incompatible h1:mOt9fcLE7zaACbxW1GeS65RI67wIJrTnqS3hP2huFsY=
github.com/docker/cli v28.3.2+incompatible/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8=
github.com/docker/go-connections v0.5.0 h1:USnMq7hx7gwdVZq1L49hLXaFtUdTADjXGp+uj1Br63c=
github.com/docker/go-connections v0.5.0/go.mod h1:ov60Kzw0kKElRwhNs9UlUHAE/F9Fe6GLaXnqyDdmEXc=
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.1.2+incompatible h1:s4QI7drXpIo78OM+CwuthPsO5kCf8cpNsck5PsLVTH8=
github.com/docker/cli v29.1.2+incompatible/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8=
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=
github.com/docker/go-units v0.5.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk=
github.com/ebitengine/purego v0.8.4 h1:CF7LEKg5FFOsASUj0+QwaXf8Ht6TlFxg09+S9wz0omw=
github.com/ebitengine/purego v0.8.4/go.mod h1:iIjxzd6CiRiOG0UyXP+V1+jWqUXVjPKLAI0mRfJZTmQ=
github.com/gabriel-vasile/mimetype v1.4.9 h1:5k+WDwEsD9eTLL8Tz3L0VnmVh9QxGjRmjBvAG7U/oYY=
github.com/gabriel-vasile/mimetype v1.4.9/go.mod h1:WnSQhFKJuBlRyLiKohA/2DtIlPFAbguNaG7QCHcyGok=
github.com/ebitengine/purego v0.9.1 h1:a/k2f2HQU3Pi399RPW1MOaZyhKJL9w/xFpKAg4q1s0A=
github.com/ebitengine/purego v0.9.1/go.mod h1:iIjxzd6CiRiOG0UyXP+V1+jWqUXVjPKLAI0mRfJZTmQ=
github.com/elliotwutingfeng/asciiset v0.0.0-20230602022725-51bbb787efab h1:h1UgjJdAAhj+uPL68n7XASS6bU+07ZX1WJvVS2eyoeY=
github.com/elliotwutingfeng/asciiset v0.0.0-20230602022725-51bbb787efab/go.mod h1:GLo/8fDswSAniFG+BFIaiSPcK610jyzgEhWYPQwuQdw=
github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg=
github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U=
github.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S9k=
github.com/fsnotify/fsnotify v1.9.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0=
github.com/gabriel-vasile/mimetype v1.4.11 h1:AQvxbp830wPhHTqc1u7nzoLT+ZFxGY7emj5DR5DYFik=
github.com/gabriel-vasile/mimetype v1.4.11/go.mod h1:d+9Oxyo1wTzWdyVUPMmXFvp4F9tea18J8ufA774AB3s=
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.29.0 h1:vKMEtvoKb0gOO9rWO9zMBwE4CgI5A5CWDsK4QEeBqzo=
github.com/go-acme/lego/v4 v4.29.0/go.mod h1:rnYyDj1NdDd9y1dHkVuUS97j7bfe9I61+oY9odKaHM8=
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=
github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI=
github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
@@ -44,44 +77,57 @@ github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/o
github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY=
github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY=
github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY=
github.com/go-playground/validator/v10 v10.27.0 h1:w8+XrWVMhGkxOaaowyKH35gFydVHOvC0/uWoy2Fzwn4=
github.com/go-playground/validator/v10 v10.27.0/go.mod h1:I5QpIEbmr8On7W0TktmJAumgzX4CA1XNl4ZmDuVHKKo=
github.com/goccy/go-yaml v1.18.0 h1:8W7wMFS12Pcas7KU+VVkaiCng+kG8QiFeFwzFb+rwuw=
github.com/goccy/go-yaml v1.18.0/go.mod h1:XBurs7gK8ATbW4ZPGKgcbrY1Br56PdM69F7LkFRi1kA=
github.com/go-playground/validator/v10 v10.28.0 h1:Q7ibns33JjyW48gHkuFT91qX48KG0ktULL6FgHdG688=
github.com/go-playground/validator/v10 v10.28.0/go.mod h1:GoI6I1SjPBh9p7ykNE/yj3fFYbyDOpwMn5KXd+m2hUU=
github.com/go-test/deep v1.0.8 h1:TDsG77qcSprGbC6vTN8OuXp5g+J+b5Pcguhf7Zt61VM=
github.com/go-test/deep v1.0.8/go.mod h1:5C2ZWiW0ErCdrYzpqxLbTX7MG14M9iiw8DgHncVwcsE=
github.com/gobwas/glob v0.2.3 h1:A4xDbljILXROh+kObIiy5kIaPYD8e96x1tgBhUI5J+Y=
github.com/gobwas/glob v0.2.3/go.mod h1:d3Ez4x06l9bZtSvzIay5+Yzi0fmZzPgnTbPcKjJAkT8=
github.com/goccy/go-json v0.10.5 h1:Fq85nIqj+gXn/S5ahsiTlK3TmC85qgirsdTP/+DeaC4=
github.com/goccy/go-json v0.10.5/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M=
github.com/goccy/go-yaml v1.19.0 h1:EmkZ9RIsX+Uq4DYFowegAuJo8+xdX3T/2dwNPXbxEYE=
github.com/goccy/go-yaml v1.19.0/go.mod h1:XBurs7gK8ATbW4ZPGKgcbrY1Br56PdM69F7LkFRi1kA=
github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
github.com/godoxy-app/docker v0.0.0-20250523125835-a2474a6ebe30 h1:+5pYG8clUrZbUDP+x149jkRfYAGaNpAXOwut0jluoYA=
github.com/godoxy-app/docker v0.0.0-20250523125835-a2474a6ebe30/go.mod h1:7VkicOZ3VrlxOe/EP/8uwsWLGKI2wt2MV7CgxTDIYgA=
github.com/godoxy-app/gopsutil/v4 v4.0.0-20250607110153-34d627ba1b5d h1:kih+Q38BQKBsbQmv7mZb7OP3YuSPN78ENTZUxCu4T6M=
github.com/godoxy-app/gopsutil/v4 v4.0.0-20250607110153-34d627ba1b5d/go.mod h1:2nclxpbWQUvbTR33HI8Z/RXUG4SaF67X/pMaI/fUMa8=
github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q=
github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q=
github.com/golang-jwt/jwt/v5 v5.3.0 h1:pv4AsKCKKZuqlgs5sUmn4x8UlGa0kEVt/puTpKx9vvo=
github.com/golang-jwt/jwt/v5 v5.3.0/go.mod h1:fxCRLWMO43lRc8nhHWY6LGqRcf+1gQWArsqaEUEa5bE=
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/gorilla/mux v1.8.1 h1:TuBL49tXwgrFYWhqrNgrUNEY92u81SPhu7sTdzQEiWY=
github.com/gorilla/mux v1.8.1/go.mod h1:AKf9I4AEqPTmMytcMc0KkNouC66V3BtZ4qD5fmWSiMQ=
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/gotify/server/v2 v2.6.3 h1:2sLDRsQ/No1+hcFwFDvjNtwKepfCSIR8L3BkXl/Vz1I=
github.com/gotify/server/v2 v2.6.3/go.mod h1:IyeQ/iL3vetcuqUAzkCMVObIMGGJx4zb13/mVatIwE8=
github.com/grpc-ecosystem/grpc-gateway/v2 v2.26.3 h1:5ZPtiqj0JL5oKWmcsq4VMaAW5ukBEgSGXEN89zeH1Jo=
github.com/grpc-ecosystem/grpc-gateway/v2 v2.26.3/go.mod h1:ndYquD05frm2vACXE1nsccT4oJzjhw2arTS2cpUD1PI=
github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8=
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI=
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/gotify/server/v2 v2.7.3 h1:nro/ZnxdlZFvxFcw9LREGA8zdk6CK744azwhuhX/A4g=
github.com/gotify/server/v2 v2.7.3/go.mod h1:VAtE1RIc/2j886PYs9WPQbMjqbFsoyQ0G8IdFtnAxU0=
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=
github.com/h2non/parth v0.0.0-20190131123155-b4df798d6542/go.mod h1:Ow0tF8D4Kplbc8s8sSb3V2oUCygFHVp8gC3Dn6U4MNI=
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=
github.com/json-iterator/go v1.1.13-0.20220915233716-71ac16282d12/go.mod h1:TBzl5BIHNXfS9+C35ZyJaklL7mLDbgUkcgXzSLa8Tk0=
github.com/klauspost/compress v1.18.2 h1:iiPHWW0YrcFgpBYhsA6D1+fqHssJscY/Tm/y2Uqnapk=
github.com/klauspost/compress v1.18.2/go.mod h1:R0h/fSBs8DE4ENlcrlib3PsXS61voFxhIs2DeRhCvJ4=
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/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
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-20250317134145-8bc96cf8fc35 h1:PpXWgLPs+Fqr325bN2FD2ISlRRztXibcX6e8f5FR5Dc=
github.com/lufia/plan9stats v0.0.0-20250317134145-8bc96cf8fc35/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/luthermonson/go-proxmox v0.2.3 h1:NAjUJ5Jd1ynIK6UHMGd/VLGgNZWpGXhfL+DBmAVSEaA=
github.com/luthermonson/go-proxmox v0.2.3/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=
github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE=
github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8=
@@ -89,112 +135,132 @@ github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/
github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/miekg/dns v1.1.68 h1:jsSRkNozw7G/mnmXULynzMNIsgY2dHC8LO6U6Ij2JEA=
github.com/miekg/dns v1.1.68/go.mod h1:fujopn7TB3Pu3JM69XaawiU0wqjpL9/8xGop5UrTPps=
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/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/morikuni/aec v1.0.0 h1:nP9CBfwrvYnBRgY6qfDQkygYDmYwOilePFkwzv4dU8A=
github.com/morikuni/aec v1.0.0/go.mod h1:BbKIizmSmc5MMPqRYbxO4ZU0S0+P200+tUnFx7PXmsc=
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/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/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=
github.com/opencontainers/image-spec v1.1.1/go.mod h1:qpqAh3Dmcf36wStyyWU+kCeDgrGnAve2nCC8+7h8Q0M=
github.com/oschwald/maxminddb-golang v1.13.1 h1:G3wwjdN9JmIK2o/ermkHM+98oX5fS+k5MbwsmL4MRQE=
github.com/oschwald/maxminddb-golang v1.13.1/go.mod h1:K4pgV9N/GcK694KSTmVSDTODk4IsCNThNdTmnaBZ/F8=
github.com/pion/dtls/v3 v3.0.6 h1:7Hkd8WhAJNbRgq9RgdNh1aaWlZlGpYTzdqjy9x9sK2E=
github.com/pion/dtls/v3 v3.0.6/go.mod h1:iJxNQ3Uhn1NZWOMWlLxEEHAN5yX7GyPvvKw04v9bzYU=
github.com/pion/logging v0.2.4 h1:tTew+7cmQ+Mc1pTBLKH2puKsOvhm32dROumOZ655zB8=
github.com/pion/logging v0.2.4/go.mod h1:DffhXTKYdNZU+KtJ5pyQDjvOAh/GsNSyv1lbkFbe3so=
github.com/pion/transport/v3 v3.0.7 h1:iRbMH05BzSNwhILHoBoAPxoB9xQgOaJk+591KC9P1o0=
github.com/pion/transport/v3 v3.0.7/go.mod h1:YleKiTZ4vqNxVwh77Z0zytYi7rXHl7j6uPLGhhz9rwo=
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
github.com/pelletier/go-toml/v2 v2.2.4 h1:mye9XuhQ6gvn5h28+VilKrrPoQVanw5PMw/TB0t5Ec4=
github.com/pelletier/go-toml/v2 v2.2.4/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY=
github.com/pierrec/lz4/v4 v4.1.21 h1:yOVMLb6qSIDP67pl/5F7RepeKYu/VmTyEXvuMI5d9mQ=
github.com/pierrec/lz4/v4 v4.1.21/go.mod h1:gZWDp/Ze/IJXGXf23ltt2EXimqmTUXEy0GFuRQyBid4=
github.com/pires/go-proxyproto v0.8.1 h1:9KEixbdJfhrbtjpz/ZwCdWDD2Xem0NZ38qMYaASJgp0=
github.com/pires/go-proxyproto v0.8.1/go.mod h1:ZKAAyp3cgy5Y5Mo4n9AlScrkCZwUy0g3Jf+slqQVcuU=
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=
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=
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55 h1:o4JXh1EVt9k/+g42oCprj/FisM4qX9L3sZB3upGN2ZU=
github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55/go.mod h1:OmDBASR4679mdNQnz2pUhc2G8CO2JrUAVFDRBDP/hJE=
github.com/puzpuzpuz/xsync/v4 v4.1.0 h1:x9eHRl4QhZFIPJ17yl4KKW9xLyVWbb3/Yq4SXpjF71U=
github.com/puzpuzpuz/xsync/v4 v4.1.0/go.mod h1:VJDmTCJMBt8igNxnkQd86r+8KUeN1quSfNKu5bLYFQo=
github.com/quic-go/qpack v0.5.1 h1:giqksBPnT/HDtZ6VhtFKgoLOWmlyo9Ei6u9PqzIMbhI=
github.com/quic-go/qpack v0.5.1/go.mod h1:+PC4XFrEskIVkcLzpEkbLqq1uCoxPhQuvK5rH1ZgaEg=
github.com/quic-go/quic-go v0.53.0 h1:QHX46sISpG2S03dPeZBgVIZp8dGagIaiu2FiVYvpCZI=
github.com/quic-go/quic-go v0.53.0/go.mod h1:e68ZEaCdyviluZmy44P6Iey98v/Wfz6HCjQEm+l8zTY=
github.com/puzpuzpuz/xsync/v4 v4.2.0 h1:dlxm77dZj2c3rxq0/XNvvUKISAmovoXF4a4qM6Wvkr0=
github.com/puzpuzpuz/xsync/v4 v4.2.0/go.mod h1:VJDmTCJMBt8igNxnkQd86r+8KUeN1quSfNKu5bLYFQo=
github.com/quic-go/qpack v0.6.0 h1:g7W+BMYynC1LbYLSqRt8PBg5Tgwxn214ZZR34VIOjz8=
github.com/quic-go/qpack v0.6.0/go.mod h1:lUpLKChi8njB4ty2bFLX2x4gzDqXwUpaO1DP9qMDZII=
github.com/quic-go/quic-go v0.57.1 h1:25KAAR9QR8KZrCZRThWMKVAwGoiHIrNbT72ULHTuI10=
github.com/quic-go/quic-go v0.57.1/go.mod h1:ly4QBAjHA2VhdnxhojRsCUOeJwKYg+taDlos92xb1+s=
github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ=
github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc=
github.com/rs/xid v1.6.0/go.mod h1:7XoLgs4eV+QndskICGsho+ADou8ySMSjJKDIan90Nz0=
github.com/rs/zerolog v1.34.0 h1:k43nTLIwcTVQAncfCw4KZ2VY6ukYoZaBPNOE8txlOeY=
github.com/rs/zerolog v1.34.0/go.mod h1:bJsvje4Z08ROH4Nhs5iH600c3IkWhwp44iRc54W6wYQ=
github.com/samber/lo v1.51.0 h1:kysRYLbHy/MB7kQZf5DSN50JHmMsNEdeY24VzJFu7wI=
github.com/samber/lo v1.51.0/go.mod h1:4+MXEGsJzbKGaUEQFKBq2xtfuznW9oz/WrgyzMzRoM0=
github.com/samber/lo v1.52.0 h1:Rvi+3BFHES3A8meP33VPAxiBZX/Aws5RxrschYGjomw=
github.com/samber/lo v1.52.0/go.mod h1:4+MXEGsJzbKGaUEQFKBq2xtfuznW9oz/WrgyzMzRoM0=
github.com/samber/slog-common v0.19.0 h1:fNcZb8B2uOLooeYwFpAlKjkQTUafdjfqKcwcC89G9YI=
github.com/samber/slog-common v0.19.0/go.mod h1:dTz+YOU76aH007YUU0DffsXNsGFQRQllPQh9XyNoA3M=
github.com/samber/slog-zerolog/v2 v2.7.3 h1:/MkPDl/tJhijN2GvB1MWwBn2FU8RiL3rQ8gpXkQm2EY=
github.com/samber/slog-zerolog/v2 v2.7.3/go.mod h1:oWU7WHof4Xp8VguiNO02r1a4VzkgoOyOZhY5CuRke60=
github.com/samber/slog-zerolog/v2 v2.9.0 h1:6LkOabJmZdNLaUWkTC3IVVA+dq7b/V0FM6lz6/7+THI=
github.com/samber/slog-zerolog/v2 v2.9.0/go.mod h1:gnQW9VnCfM34v2pRMUIGMsZOVbYLqY/v0Wxu6atSVGc=
github.com/sirupsen/logrus v1.9.4-0.20230606125235-dd1b4c2e81af h1:Sp5TG9f7K39yfB+If0vjp97vuT74F72r8hfRpP8jLU0=
github.com/sirupsen/logrus v1.9.4-0.20230606125235-dd1b4c2e81af/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
github.com/spf13/afero v1.14.0 h1:9tH6MapGnn/j0eb0yIXiLjERO8RB6xIVZRDCX7PtqWA=
github.com/spf13/afero v1.14.0/go.mod h1:acJQ8t0ohCGuMN3O+Pv0V0hgMxNYDlvdk+VTfyZmbYo=
github.com/spf13/afero v1.15.0 h1:b/YBCLWAJdFWJTN9cLhiXXcD7mzKn9Dm86dNnfyQw1I=
github.com/spf13/afero v1.15.0/go.mod h1:NC2ByUVxtQs4b3sIUphxK0NioZnmxgyCrfzeuq8lxMg=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
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/tklauser/go-sysconf v0.3.15 h1:VE89k0criAymJ/Os65CSn1IXaol+1wrsFHEB8Ol49K4=
github.com/tklauser/go-sysconf v0.3.15/go.mod h1:Dmjwr6tYFIseJw7a3dRLJfsHAMXZ3nEnL/aZY+0IuI4=
github.com/tklauser/numcpus v0.10.0 h1:18njr6LDBk1zuna922MgdjQuJFjrdppsZG60sHGfjso=
github.com/tklauser/numcpus v0.10.0/go.mod h1:BiTKazU708GQTYF4mB+cmlpT2Is1gLk7XVuEeem8LsQ=
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/tklauser/go-sysconf v0.3.16 h1:frioLaCQSsF5Cy1jgRBrzr6t502KIIwQ0MArYICU0nA=
github.com/tklauser/go-sysconf v0.3.16/go.mod h1:/qNL9xxDhc7tx3HSRsLWNnuzbVfh3e7gh/BmM179nYI=
github.com/tklauser/numcpus v0.11.0 h1:nSTwhKH5e1dMNsCdVBukSZrURJRoHbSEQjdEbY+9RXw=
github.com/tklauser/numcpus v0.11.0/go.mod h1:z+LwcLq54uWZTX0u/bGobaV34u6V7KNlTZejzM6/3MQ=
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/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/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.68.0 h1:v12Nx16iepr8r9ySOwqI+5RBJ/DqTxhOy1HrHoDFnok=
github.com/valyala/fasthttp v1.68.0/go.mod h1:5EXiRfYQAoiO/khu4oU9VISC/eVY6JqmSpPJoHCKsz4=
github.com/vincent-petithory/dataurl v1.0.0 h1:cXw+kPto8NLuJtlMsI152irrVw9fRDX8AbShPRpg2CI=
github.com/vincent-petithory/dataurl v1.0.0/go.mod h1:FHafX5vmDzyP+1CQATJn7WFKc9CvnvxyvZy6I1MrG/U=
github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/xyproto/randomstring v1.0.5 h1:YtlWPoRdgMu3NZtP45drfy1GKoojuR7hmRcnhZqKjWU=
github.com/xyproto/randomstring v1.0.5/go.mod h1:rgmS5DeNXLivK7YprL0pY+lTuhNQW3iGxZ18UQApw/E=
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
github.com/yusing/ds v0.3.1 h1:mCqTgTQD8RhiBpcysvii5kZ7ZBmqcknVsFubNALGLbY=
github.com/yusing/ds v0.3.1/go.mod h1:XhKV4l7cZwBbbl7lRzNC9zX27zvCM0frIwiuD40ULRk=
github.com/yusing/gointernals v0.1.16 h1:GrhZZdxzA+jojLEqankctJrOuAYDb7kY1C93S1pVR34=
github.com/yusing/gointernals v0.1.16/go.mod h1:B/0FVXt4WPmgzVy3ynzkqKi+BSGaJVmwCJBRXYapo34=
github.com/yusufpapurcu/wmi v1.2.4 h1:zFUKzehAFReQwLys1b/iSMl+JQGSCSjtVqQn9bBrPo0=
github.com/yusufpapurcu/wmi v1.2.4/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0=
go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA=
go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A=
go.opentelemetry.io/otel v1.37.0 h1:9zhNfelUvx0KBfu/gb+ZgeAfAgtWrfHJZcAqFC228wQ=
go.opentelemetry.io/otel v1.37.0/go.mod h1:ehE/umFRLnuLa/vSccNq9oS1ErUlkkK71gMcN34UG8I=
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.36.0 h1:dNzwXjZKpMpE2JhmO+9HsPl42NIXFIFSUSSs0fiqra0=
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.36.0/go.mod h1:90PoxvaEB5n6AOdZvi+yWJQoE95U8Dhhw2bSyRqnTD0=
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.36.0 h1:nRVXXvf78e00EwY6Wp0YII8ww2JVWshZ20HfTlE11AM=
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.36.0/go.mod h1:r49hO7CgrxY9Voaj3Xe8pANWtr0Oq916d0XAmOoCZAQ=
go.opentelemetry.io/otel/metric v1.37.0 h1:mvwbQS5m0tbmqML4NqK+e3aDiO02vsf/WgbsdpcPoZE=
go.opentelemetry.io/otel/metric v1.37.0/go.mod h1:04wGrZurHYKOc+RKeye86GwKiTb9FKm1WHtO+4EVr2E=
go.opentelemetry.io/otel/sdk v1.36.0 h1:b6SYIuLRs88ztox4EyrvRti80uXIFy+Sqzoh9kFULbs=
go.opentelemetry.io/otel/sdk v1.36.0/go.mod h1:+lC+mTgD+MUWfjJubi2vvXWcVxyr9rmlshZni72pXeY=
go.opentelemetry.io/otel/trace v1.37.0 h1:HLdcFNbRQBE2imdSEgm/kwqmQj1Or1l/7bW6mxVK7z4=
go.opentelemetry.io/otel/trace v1.37.0/go.mod h1:TlgrlQ+PtQO5XFerSPUYG0JSgGyryXewPGyayAWSBS0=
go.opentelemetry.io/proto/otlp v1.6.0 h1:jQjP+AQyTf+Fe7OKj/MfkDrmK4MNVtw2NpXsf9fefDI=
go.opentelemetry.io/proto/otlp v1.6.0/go.mod h1:cicgGehlFuNdgZkcALOCh3VE6K/u2tAjzlRhDwmVpZc=
go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64=
go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y=
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.64.0 h1:ssfIgGNANqpVFCndZvcuyKbl0g+UAVcbBcqGkG28H0Y=
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.64.0/go.mod h1:GQ/474YrbE4Jx8gZ4q5I4hrhUzM6UPzyrqJYV2AqPoQ=
go.opentelemetry.io/otel v1.39.0 h1:8yPrr/S0ND9QEfTfdP9V+SiwT4E0G7Y5MO7p85nis48=
go.opentelemetry.io/otel v1.39.0/go.mod h1:kLlFTywNWrFyEdH0oj2xK0bFYZtHRYUdv1NklR/tgc8=
go.opentelemetry.io/otel/metric v1.39.0 h1:d1UzonvEZriVfpNKEVmHXbdf909uGTOQjA0HF0Ls5Q0=
go.opentelemetry.io/otel/metric v1.39.0/go.mod h1:jrZSWL33sD7bBxg1xjrqyDjnuzTUB0x1nBERXd7Ftcs=
go.opentelemetry.io/otel/sdk v1.39.0 h1:nMLYcjVsvdui1B/4FRkwjzoRVsMK8uL/cj0OyhKzt18=
go.opentelemetry.io/otel/sdk v1.39.0/go.mod h1:vDojkC4/jsTJsE+kh+LXYQlbL8CgrEcwmt1ENZszdJE=
go.opentelemetry.io/otel/sdk/metric v1.39.0 h1:cXMVVFVgsIf2YL6QkRF4Urbr/aMInf+2WKg+sEJTtB8=
go.opentelemetry.io/otel/sdk/metric v1.39.0/go.mod h1:xq9HEVH7qeX69/JnwEfp6fVq5wosJsY1mt4lLfYdVew=
go.opentelemetry.io/otel/trace v1.39.0 h1:2d2vfpEDmCJ5zVYz7ijaJdOF59xLomrvj7bjt6/qCJI=
go.opentelemetry.io/otel/trace v1.39.0/go.mod h1:88w4/PnZSazkGzz/w84VHpQafiU4EtqqlVdxWy+rNOA=
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=
go.uber.org/mock v0.5.2/go.mod h1:wLlUxC2vVTPTaE3UD51E0BGOAElKrILxhVSDYQLld5o=
golang.org/x/arch v0.23.0 h1:lKF64A2jF6Zd8L0knGltUnegD62JMFBiCPBmQpToHhg=
golang.org/x/arch v0.23.0/go.mod h1:dNHoOeKiyja7GTvF9NJS1l3Z2yntpQNzgrjh1cU103A=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/crypto v0.13.0/go.mod h1:y6Z2r+Rw4iayiXXAIxJIDAJ1zMW4yaTpebo8fPOliYc=
golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU=
golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8=
golang.org/x/crypto v0.31.0/go.mod h1:kDsLvtWBEx7MV9tJOj9bnXsPbxwJQ6csT/x4KIN4Ssk=
golang.org/x/crypto v0.40.0 h1:r4x+VvoG5Fm+eJcxMaY8CQM7Lb0l1lsmjGBQ6s8BfKM=
golang.org/x/crypto v0.40.0/go.mod h1:Qr1vMER5WyS2dfPHAlsOj01wgLbsyWtFn/aY+5+ZdxY=
golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/crypto v0.46.0 h1:cKRW/pmt1pKAfetfu+RCEvjvZkA9RimPbh7bhFjGVBU=
golang.org/x/crypto v0.46.0/go.mod h1:Evb/oLKmMraqjZ2iQTwDwvCtJkczlDuTmdJXoZVzqU0=
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
golang.org/x/mod v0.12.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
golang.org/x/mod v0.15.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
golang.org/x/mod v0.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
golang.org/x/mod v0.26.0 h1:EGMPT//Ezu+ylkCijjPc+f4Aih7sZvaAr+O3EHBxvZg=
golang.org/x/mod v0.26.0/go.mod h1:/j6NAhSk8iQ723BGAUyoAcn7SlD7s15Dp9Nd/SfeaFQ=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/mod v0.31.0 h1:HaW9xtz0+kOcWKwli0ZXy79Ix+UW/vOfmWI5QVd2tgI=
golang.org/x/mod v0.31.0/go.mod h1:43JraMp9cGx1Rx3AqioxrbrhNsLl2l/iNAvuBkrezpg=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
@@ -203,27 +269,27 @@ golang.org/x/net v0.15.0/go.mod h1:idbUs1IY1+zTqbi8yxTbhexhEEk5ur9LInksu6HrEpk=
golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44=
golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM=
golang.org/x/net v0.33.0/go.mod h1:HXLR5J+9DxmrqMwG9qjGCxZ+zKXxBru04zlTvWlWuN4=
golang.org/x/net v0.42.0 h1:jzkYrhi3YQWD6MLBJcsklgQsoAcw89EcZbJw8Z614hs=
golang.org/x/net v0.42.0/go.mod h1:FF1RA5d3u7nAYA4z2TkclSCKh68eSXtiFwcWQpPXdt8=
golang.org/x/net v0.48.0 h1:zyQRTTrjc33Lhh0fBgT/H3oZq9WuvRR5gPC70xpDiQU=
golang.org/x/net v0.48.0/go.mod h1:+ndRgGjkh8FGtu1w1FGbEC31if4VrNVMuKTgcAAnQRY=
golang.org/x/oauth2 v0.34.0 h1:hqK/t4AKgbqWkdkcAeI8XLmbK+4m4G5YeQRrmiotGlw=
golang.org/x/oauth2 v0.34.0/go.mod h1:lzm5WQJQwKZ3nwavOZ3IS5Aulzxi68dUSgRHujetwEA=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.3.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y=
golang.org/x/sync v0.6.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/sync v0.10.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/sync v0.16.0 h1:ycBJEhp9p4vXvUZNszeOq0kGTPghopOL8q0fq3vstxw=
golang.org/x/sync v0.16.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
golang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4=
golang.org/x/sync v0.19.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
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-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-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
@@ -235,8 +301,8 @@ golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.34.0 h1:H5Y5sJ2L2JRdyv7ROF1he/lPdvFsd0mJHFw2ThKHxLA=
golang.org/x/sys v0.34.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
golang.org/x/sys v0.39.0 h1:CvCKL8MeisomCi6qNZ+wbb0DN9E5AATixKsvNtMoMFk=
golang.org/x/sys v0.39.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
golang.org/x/telemetry v0.0.0-20240228155512-f48c80bd79b2/go.mod h1:TeRTkGYfJXctD9OcfyVLyj2J3IxLnKwHJR8f4D8a3YE=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
@@ -255,38 +321,28 @@ golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ=
golang.org/x/text v0.27.0 h1:4fGWRpyh641NLlecmyl4LOe6yDdfaYNrGb2zdfo4JV4=
golang.org/x/text v0.27.0/go.mod h1:1D28KMCvyooCX9hBiosv5Tz/+YLxj0j7XhWjpSUF7CU=
golang.org/x/time v0.12.0 h1:ScB/8o8olJvc+CQPWrK3fPZNfh7qgwCrY0zJmoEQLSE=
golang.org/x/time v0.12.0/go.mod h1:CDIdPxbZBQxdj6cxyCIdrNogrJKMJ7pr37NYpMcMDSg=
golang.org/x/text v0.32.0 h1:ZD01bjUt1FQ9WJ0ClOL5vxgxOI/sVCNgX1YtKwcY0mU=
golang.org/x/text v0.32.0/go.mod h1:o/rUWzghvpD5TXrTIBuJU77MTaN0ljMWE47kxGJQ7jY=
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.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
golang.org/x/tools v0.13.0/go.mod h1:HvlwmtVNQAhOuCjW7xxvovg8wbNq7LwfXh/k7wXUl58=
golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk=
golang.org/x/tools v0.35.0 h1:mBffYraMEf7aa0sB+NuKnuCy8qI/9Bughn8dC2Gu5r0=
golang.org/x/tools v0.35.0/go.mod h1:NKdj5HkL/73byiZSJjqJgKn3ep7KjFkBOkR/Hps3VPw=
golang.org/x/tools v0.40.0 h1:yLkxfA+Qnul4cs9QA3KnlFu0lVmd8JJfoq+E41uSutA=
golang.org/x/tools v0.40.0/go.mod h1:Ik/tzLRlbscWpqqMRjyWYDisX8bG13FrdXp3o4Sr9lc=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
google.golang.org/genproto v0.0.0-20250505200425-f936aa4a68b2 h1:1tXaIXCracvtsRxSBsYDiSBN0cuJvM7QYW+MrpIRY78=
google.golang.org/genproto/googleapis/api v0.0.0-20250519155744-55703ea1f237 h1:Kog3KlB4xevJlAcbbbzPfRG0+X9fdoGM+UBRKVz6Wr0=
google.golang.org/genproto/googleapis/api v0.0.0-20250519155744-55703ea1f237/go.mod h1:ezi0AVyMKDWy5xAncvjLWH7UcLBB5n7y2fQ8MzjJcto=
google.golang.org/genproto/googleapis/rpc v0.0.0-20250715232539-7130f93afb79 h1:1ZwqphdOdWYXsUHgMpU/101nCtf/kSp9hOrcvFsnl10=
google.golang.org/genproto/googleapis/rpc v0.0.0-20250715232539-7130f93afb79/go.mod h1:qQ0YXyHHx3XkvlzUtpXDkS29lDSafHMZBAZDc03LQ3A=
google.golang.org/grpc v1.73.0 h1:VIWSmpI2MegBtTuFt5/JWy2oXxtjJ/e89Z70ImfD2ok=
google.golang.org/grpc v1.73.0/go.mod h1:50sbHOUqWoCQGI8V2HQLJM0B+LMlIUjNSZmow7EVBQc=
google.golang.org/protobuf v1.36.6 h1:z1NpPI8ku2WgiWnf+t9wTPsn6eP1L7ksHUlkfLvd9xY=
google.golang.org/protobuf v1.36.6/go.mod h1:jduwjTPXsFjZGTmRluh+L6NjiWu7pchiJ2/5YcXBHnY=
google.golang.org/protobuf v1.36.10 h1:AYd7cD/uASjIL6Q9LiTjz8JLcrh/88q5UObnmY3aOOE=
google.golang.org/protobuf v1.36.10/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY=
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
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,14 +1,17 @@
package agent
import (
"github.com/yusing/go-proxy/internal/common"
"github.com/yusing/go-proxy/internal/utils/functional"
"iter"
"os"
"strings"
"github.com/puzpuzpuz/xsync/v4"
)
var agentPool = functional.NewMapOf[string, *AgentConfig]()
var agentPool = xsync.NewMap[string, *AgentConfig](xsync.WithPresize(10))
func init() {
if common.IsTest {
if strings.HasSuffix(os.Args[0], ".test") {
agentPool.Store("test-agent", &AgentConfig{
Addr: "test-agent",
})
@@ -24,7 +27,7 @@ func GetAgent(agentAddrOrDockerHost string) (*AgentConfig, bool) {
func GetAgentByName(name string) (*AgentConfig, bool) {
for _, agent := range agentPool.Range {
if agent.Name() == name {
if agent.Name == name {
return agent, true
}
}
@@ -51,7 +54,15 @@ func ListAgents() []*AgentConfig {
return agents
}
func IterAgents() iter.Seq2[string, *AgentConfig] {
return agentPool.Range
}
func NumAgents() int {
return agentPool.Size()
}
func getAgentByAddr(addr string) (agent *AgentConfig, ok bool) {
agent, ok = agentPool.Load(addr)
return
return agent, ok
}

View File

@@ -10,6 +10,16 @@ var (
AGENT_PORT="{{.Port}}" \
AGENT_CA_CERT="{{.CACert}}" \
AGENT_SSL_CERT="{{.SSLCert}}" \
{{ if eq .ContainerRuntime "nerdctl" -}}
DOCKER_SOCKET="/var/run/containerd/containerd.sock" \
RUNTIME="nerdctl" \
{{ else if eq .ContainerRuntime "podman" -}}
DOCKER_SOCKET="/var/run/podman/podman.sock" \
RUNTIME="podman" \
{{ else -}}
DOCKER_SOCKET="/var/run/docker.sock" \
RUNTIME="docker" \
{{ end -}}
bash -c "$(curl -fsSL https://raw.githubusercontent.com/yusing/godoxy/main/scripts/install-agent.sh)"`
installScriptTemplate = template.Must(template.New("install.sh").Parse(installScript))
)

View File

@@ -4,7 +4,6 @@ import (
"context"
"crypto/tls"
"crypto/x509"
"encoding/json"
"errors"
"fmt"
"net"
@@ -16,23 +15,27 @@ import (
"github.com/rs/zerolog"
"github.com/rs/zerolog/log"
"github.com/yusing/go-proxy/agent/pkg/certs"
"github.com/yusing/go-proxy/pkg"
"github.com/valyala/fasthttp"
"github.com/yusing/godoxy/agent/pkg/certs"
"github.com/yusing/goutils/version"
)
type AgentConfig struct {
Addr string
Addr string `json:"addr"`
Name string `json:"name"`
Version version.Version `json:"version" swaggertype:"string"`
Runtime ContainerRuntime `json:"runtime"`
httpClient *http.Client
tlsConfig *tls.Config
name string
version string
l zerolog.Logger
}
httpClient *http.Client
fasthttpClientHealthCheck *fasthttp.Client
tlsConfig tls.Config
l zerolog.Logger
} // @name Agent
const (
EndpointVersion = "/version"
EndpointName = "/name"
EndpointRuntime = "/runtime"
EndpointProxyHTTP = "/proxy/http"
EndpointHealth = "/health"
EndpointLogs = "/logs"
@@ -80,7 +83,7 @@ func (cfg *AgentConfig) Parse(addr string) error {
return nil
}
var serverVersion = pkg.GetVersion()
var serverVersion = version.Get()
func (cfg *AgentConfig) StartWithCerts(ctx context.Context, ca, crt, key []byte) error {
clientCert, err := tls.X509KeyPair(crt, key)
@@ -95,7 +98,7 @@ func (cfg *AgentConfig) StartWithCerts(ctx context.Context, ca, crt, key []byte)
return errors.New("invalid ca certificate")
}
cfg.tlsConfig = &tls.Config{
cfg.tlsConfig = tls.Config{
Certificates: []tls.Certificate{clientCert},
RootCAs: caCertPool,
ServerName: CertsDNSName,
@@ -103,34 +106,60 @@ func (cfg *AgentConfig) StartWithCerts(ctx context.Context, ca, crt, key []byte)
// create transport and http client
cfg.httpClient = cfg.NewHTTPClient()
applyNormalTransportConfig(cfg.httpClient)
cfg.fasthttpClientHealthCheck = cfg.NewFastHTTPHealthCheckClient()
ctx, cancel := context.WithTimeout(ctx, 5*time.Second)
defer cancel()
// get agent name
name, _, err := cfg.Fetch(ctx, EndpointName)
name, _, err := cfg.fetchString(ctx, EndpointName)
if err != nil {
return err
}
cfg.name = string(name)
cfg.Name = name
cfg.l = log.With().Str("agent", cfg.name).Logger()
cfg.l = log.With().Str("agent", cfg.Name).Logger()
// check agent version
agentVersionBytes, _, err := cfg.Fetch(ctx, EndpointVersion)
agentVersion, _, err := cfg.fetchString(ctx, EndpointVersion)
if err != nil {
return err
}
cfg.version = string(agentVersionBytes)
agentVersion := pkg.ParseVersion(cfg.version)
if serverVersion.IsNewerMajorThan(agentVersion) {
log.Warn().Msgf("agent %s major version mismatch: server: %s, agent: %s", cfg.name, serverVersion, agentVersion)
// check agent runtime
runtime, status, err := cfg.fetchString(ctx, EndpointRuntime)
if err != nil {
return err
}
switch status {
case http.StatusOK:
switch runtime {
case "docker":
cfg.Runtime = ContainerRuntimeDocker
// case "nerdctl":
// cfg.Runtime = ContainerRuntimeNerdctl
case "podman":
cfg.Runtime = ContainerRuntimePodman
default:
return fmt.Errorf("invalid agent runtime: %s", runtime)
}
case http.StatusNotFound:
// backward compatibility, old agent does not have runtime endpoint
cfg.Runtime = ContainerRuntimeDocker
default:
return fmt.Errorf("failed to get agent runtime: HTTP %d %s", status, runtime)
}
log.Info().Msgf("agent %q initialized", cfg.name)
cfg.Version = version.Parse(agentVersion)
if serverVersion.IsNewerThanMajor(cfg.Version) {
log.Warn().Msgf("agent %s major version mismatch: server: %s, agent: %s", cfg.Name, serverVersion, cfg.Version)
}
log.Info().Msgf("agent %q initialized", cfg.Name)
return nil
}
@@ -159,6 +188,25 @@ func (cfg *AgentConfig) NewHTTPClient() *http.Client {
}
}
func (cfg *AgentConfig) NewFastHTTPHealthCheckClient() *fasthttp.Client {
return &fasthttp.Client{
Dial: func(addr string) (net.Conn, error) {
if addr != AgentHost+":443" {
return nil, &net.AddrError{Err: "invalid address", Addr: addr}
}
return net.Dial("tcp", cfg.Addr)
},
TLSConfig: &cfg.tlsConfig,
ReadTimeout: 5 * time.Second,
WriteTimeout: 3 * time.Second,
DisableHeaderNamesNormalizing: true,
DisablePathNormalizing: true,
NoDefaultUserAgentHeader: true,
ReadBufferSize: 1024,
WriteBufferSize: 1024,
}
}
func (cfg *AgentConfig) Transport() *http.Transport {
return &http.Transport{
DialContext: func(ctx context.Context, network, addr string) (net.Conn, error) {
@@ -170,7 +218,7 @@ func (cfg *AgentConfig) Transport() *http.Transport {
}
return cfg.DialContext(ctx)
},
TLSClientConfig: cfg.tlsConfig,
TLSClientConfig: &cfg.tlsConfig,
}
}
@@ -180,18 +228,14 @@ func (cfg *AgentConfig) DialContext(ctx context.Context) (net.Conn, error) {
return dialer.DialContext(ctx, "tcp", cfg.Addr)
}
func (cfg *AgentConfig) Name() string {
return cfg.name
}
func (cfg *AgentConfig) String() string {
return cfg.name + "@" + cfg.Addr
return cfg.Name + "@" + cfg.Addr
}
func (cfg *AgentConfig) MarshalJSON() ([]byte, error) {
return json.Marshal(map[string]string{
"name": cfg.Name(),
"addr": cfg.Addr,
"version": cfg.version,
})
func applyNormalTransportConfig(client *http.Client) {
transport := client.Transport.(*http.Transport)
transport.MaxIdleConns = 100
transport.MaxIdleConnsPerHost = 100
transport.ReadBufferSize = 16384
transport.WriteBufferSize = 16384
}

View File

@@ -8,9 +8,9 @@ import (
)
var (
//go:embed templates/agent.compose.yml
//go:embed templates/agent.compose.yml.tmpl
agentComposeYAML string
agentComposeYAMLTemplate = template.Must(template.New("agent.compose.yml").Parse(agentComposeYAML))
agentComposeYAMLTemplate = template.Must(template.New("agent.compose.yml.tmpl").Parse(agentComposeYAML))
)
const (
@@ -20,7 +20,8 @@ const (
func (c *AgentComposeConfig) Generate() (string, error) {
buf := bytes.NewBuffer(make([]byte, 0, 1024))
if err := agentComposeYAMLTemplate.Execute(buf, c); err != nil {
err := agentComposeYAMLTemplate.Execute(buf, c)
if err != nil {
return "", err
}
return buf.String(), nil

View File

@@ -1,11 +1,13 @@
package agent
type (
AgentEnvConfig struct {
Name string
Port int
CACert string
SSLCert string
ContainerRuntime string
AgentEnvConfig struct {
Name string
Port int
CACert string
SSLCert string
ContainerRuntime ContainerRuntime
}
AgentComposeConfig struct {
Image string
@@ -15,3 +17,9 @@ type (
Generate() (string, error)
}
)
const (
ContainerRuntimeDocker ContainerRuntime = "docker"
ContainerRuntimePodman ContainerRuntime = "podman"
// ContainerRuntimeNerdctl ContainerRuntime = "nerdctl"
)

View File

@@ -0,0 +1,112 @@
package agent
import (
"context"
"fmt"
"io"
"net/http"
"time"
"github.com/bytedance/sonic"
"github.com/gorilla/websocket"
"github.com/valyala/fasthttp"
httputils "github.com/yusing/goutils/http"
"github.com/yusing/goutils/http/reverseproxy"
)
func (cfg *AgentConfig) Do(ctx context.Context, method, endpoint string, body io.Reader) (*http.Response, error) {
req, err := http.NewRequestWithContext(ctx, method, APIBaseURL+endpoint, body)
if err != nil {
return nil, err
}
return cfg.httpClient.Do(req)
}
func (cfg *AgentConfig) Forward(req *http.Request, endpoint string) (*http.Response, error) {
req.URL.Host = AgentHost
req.URL.Scheme = "https"
req.URL.Path = APIEndpointBase + endpoint
req.RequestURI = ""
resp, err := cfg.httpClient.Do(req)
if err != nil {
return nil, err
}
return resp, nil
}
type HealthCheckResponse struct {
Healthy bool `json:"healthy"`
Detail string `json:"detail"`
Latency time.Duration `json:"latency"`
}
func (cfg *AgentConfig) 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(APIBaseURL + EndpointHealth + "?" + query)
req.Header.SetMethod(fasthttp.MethodGet)
req.Header.Set("Accept-Encoding", "identity")
req.SetConnectionClose()
start := time.Now()
err = cfg.fasthttpClientHealthCheck.DoTimeout(req, resp, timeout)
ret.Latency = time.Since(start)
if err != nil {
return ret, err
}
if status := resp.StatusCode(); status != http.StatusOK {
ret.Detail = fmt.Sprintf("HTTP %d %s", status, resp.Body())
return ret, nil
} else {
err = sonic.Unmarshal(resp.Body(), &ret)
if err != nil {
return ret, err
}
}
return ret, nil
}
func (cfg *AgentConfig) fetchString(ctx context.Context, endpoint string) (string, int, error) {
resp, err := cfg.Do(ctx, "GET", endpoint, nil)
if err != nil {
return "", 0, err
}
defer resp.Body.Close()
data, release, err := httputils.ReadAllBody(resp)
if err != nil {
return "", 0, err
}
ret := string(data)
release(data)
return ret, resp.StatusCode, nil
}
func (cfg *AgentConfig) 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, APIBaseURL+endpoint, http.Header{
"Host": {AgentHost},
})
}
// ReverseProxy reverse proxies the request to the agent
//
// 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 (cfg *AgentConfig) ReverseProxy(w http.ResponseWriter, req *http.Request, endpoint string) {
rp := reverseproxy.NewReverseProxy("agent", AgentURL, cfg.Transport())
req.URL.Host = AgentHost
req.URL.Scheme = "https"
req.URL.Path = endpoint
req.RequestURI = ""
rp.ServeHTTP(w, req)
}

View File

@@ -1,6 +1,10 @@
package agent
import (
"crypto/aes"
"crypto/cipher"
"crypto/ecdsa"
"crypto/elliptic"
"crypto/rand"
"crypto/tls"
"crypto/x509"
@@ -8,13 +12,11 @@ import (
"encoding/base64"
"encoding/pem"
"errors"
"fmt"
"io"
"math/big"
"strings"
"time"
"crypto/ecdsa"
"crypto/elliptic"
"fmt"
)
const (
@@ -74,6 +76,62 @@ func (p *PEMPair) Load(data string) (err error) {
return nil
}
func (p *PEMPair) Encrypt(encKey []byte) (PEMPair, error) {
cert, err := encrypt(p.Cert, encKey)
if err != nil {
return PEMPair{}, err
}
key, err := encrypt(p.Key, encKey)
if err != nil {
return PEMPair{}, err
}
return PEMPair{Cert: cert, Key: key}, nil
}
func (p *PEMPair) Decrypt(encKey []byte) (PEMPair, error) {
cert, err := decrypt(p.Cert, encKey)
if err != nil {
return PEMPair{}, err
}
key, err := decrypt(p.Key, encKey)
if err != nil {
return PEMPair{}, err
}
return PEMPair{Cert: cert, Key: key}, nil
}
func encrypt(data []byte, key []byte) ([]byte, error) {
block, err := aes.NewCipher(key)
if err != nil {
return nil, err
}
gcm, err := cipher.NewGCM(block)
if err != nil {
return nil, err
}
nonce := make([]byte, gcm.NonceSize())
if _, err := io.ReadFull(rand.Reader, nonce); err != nil {
return nil, err
}
return gcm.Seal(nonce, nonce, data, nil), nil
}
func decrypt(data []byte, key []byte) ([]byte, error) {
block, err := aes.NewCipher(key)
if err != nil {
return nil, err
}
gcm, err := cipher.NewGCM(block)
if err != nil {
return nil, err
}
nonce := data[:gcm.NonceSize()]
ciphertext := data[gcm.NonceSize():]
return gcm.Open(nil, nonce, ciphertext, nil)
}
func (p *PEMPair) ToTLSCert() (*tls.Certificate, error) {
cert, err := tls.X509KeyPair(p.Cert, p.Key)
return &cert, err
@@ -185,5 +243,5 @@ func NewAgent() (ca, srv, client *PEMPair, err error) {
}
client = toPEMPair(clientCertDER, clientKey)
return
return ca, srv, client, err
}

View File

@@ -1,6 +1,7 @@
package agent
import (
"crypto/rand"
"crypto/tls"
"crypto/x509"
"fmt"
@@ -89,3 +90,23 @@ func TestServerClient(t *testing.T) {
require.NoError(t, err)
require.Equal(t, resp.StatusCode, http.StatusOK)
}
func TestPEMPairEncryptDecrypt(t *testing.T) {
encKey := make([]byte, 32)
_, err := rand.Read(encKey)
require.NoError(t, err)
ca, _, _, err := NewAgent()
require.NoError(t, err)
encCA, err := ca.Encrypt(encKey)
require.NoError(t, err)
require.NotNil(t, encCA)
decCA, err := encCA.Decrypt(encKey)
require.NoError(t, err)
require.NotNil(t, decCA)
require.Equal(t, string(ca.Cert), string(decCA.Cert))
require.Equal(t, string(ca.Key), string(decCA.Key))
}

View File

@@ -1,53 +0,0 @@
package agent
import (
"context"
"io"
"net/http"
"github.com/gorilla/websocket"
)
func (cfg *AgentConfig) Do(ctx context.Context, method, endpoint string, body io.Reader) (*http.Response, error) {
req, err := http.NewRequestWithContext(ctx, method, APIBaseURL+endpoint, body)
if err != nil {
return nil, err
}
return cfg.httpClient.Do(req)
}
func (cfg *AgentConfig) Forward(req *http.Request, endpoint string) ([]byte, int, error) {
req = req.WithContext(req.Context())
req.URL.Host = AgentHost
req.URL.Scheme = "https"
req.URL.Path = APIEndpointBase + endpoint
req.RequestURI = ""
resp, err := cfg.httpClient.Do(req)
if err != nil {
return nil, 0, err
}
defer resp.Body.Close()
data, _ := io.ReadAll(resp.Body)
return data, resp.StatusCode, nil
}
func (cfg *AgentConfig) Fetch(ctx context.Context, endpoint string) ([]byte, int, error) {
resp, err := cfg.Do(ctx, "GET", endpoint, nil)
if err != nil {
return nil, 0, err
}
defer resp.Body.Close()
data, _ := io.ReadAll(resp.Body)
return data, resp.StatusCode, nil
}
func (cfg *AgentConfig) 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, APIBaseURL+endpoint, http.Header{
"Host": {AgentHost},
})
}

View File

@@ -0,0 +1,66 @@
services:
agent:
image: "{{.Image}}"
container_name: godoxy-agent
restart: always
{{ if eq .ContainerRuntime "podman" -}}
ports:
- "{{.Port}}:{{.Port}}"
{{ else -}}
network_mode: host # do not change this
{{ end -}}
environment:
{{ if eq .ContainerRuntime "nerdctl" -}}
DOCKER_SOCKET: "/var/run/containerd/containerd.sock"
RUNTIME: "nerdctl"
{{ else if eq .ContainerRuntime "podman" -}}
DOCKER_SOCKET: "/var/run/podman/podman.sock"
RUNTIME: "podman"
{{ else -}}
DOCKER_SOCKET: "/var/run/docker.sock"
RUNTIME: "docker"
{{ end -}}
AGENT_NAME: "{{.Name}}"
AGENT_PORT: "{{.Port}}"
AGENT_CA_CERT: "{{.CACert}}"
AGENT_SSL_CERT: "{{.SSLCert}}"
# use agent as a docker socket proxy: [host]:port
# set LISTEN_ADDR to enable (e.g. 127.0.0.1:2375)
LISTEN_ADDR:
POST: false
ALLOW_RESTARTS: false
ALLOW_START: false
ALLOW_STOP: false
AUTH: false
BUILD: false
COMMIT: false
CONFIGS: false
CONTAINERS: false
DISTRIBUTION: false
EVENTS: true
EXEC: false
GRPC: false
IMAGES: false
INFO: false
NETWORKS: false
NODES: false
PING: true
PLUGINS: false
SECRETS: false
SERVICES: false
SESSION: false
SWARM: false
SYSTEM: false
TASKS: false
VERSION: true
VOLUMES: false
volumes:
{{ if eq .ContainerRuntime "podman" -}}
- /var/run/podman/podman.sock:/var/run/podman/podman.sock
{{ else if eq .ContainerRuntime "nerdctl" -}}
- /var/run/containerd/containerd.sock:/var/run/containerd/containerd.sock
- /var/lib/nerdctl:/var/lib/nerdctl:ro # required to read metadata like network info
{{ else -}}
- /var/run/docker.sock:/var/run/docker.sock
{{ end -}}
- ./data:/app/data

View File

@@ -0,0 +1,73 @@
package agentproxy
import (
"encoding/base64"
"net/http"
"strconv"
"time"
"github.com/bytedance/sonic"
route "github.com/yusing/godoxy/internal/route/types"
)
type Config struct {
Scheme string `json:"scheme,omitempty"`
Host string `json:"host,omitempty"` // host or host:port
route.HTTPConfig
}
func ConfigFromHeaders(h http.Header) (Config, error) {
cfg, err := proxyConfigFromHeaders(h)
if cfg.Host == "" || err != nil {
cfg = proxyConfigFromHeadersLegacy(h)
}
return cfg, nil
}
func proxyConfigFromHeadersLegacy(h http.Header) (cfg Config) {
cfg.Host = h.Get(HeaderXProxyHost)
isHTTPS, _ := strconv.ParseBool(h.Get(HeaderXProxyHTTPS))
cfg.NoTLSVerify, _ = strconv.ParseBool(h.Get(HeaderXProxySkipTLSVerify))
responseHeaderTimeout, err := strconv.Atoi(h.Get(HeaderXProxyResponseHeaderTimeout))
if err != nil {
responseHeaderTimeout = 0
}
cfg.ResponseHeaderTimeout = time.Duration(responseHeaderTimeout) * time.Second
cfg.Scheme = "http"
if isHTTPS {
cfg.Scheme = "https"
}
return cfg
}
func proxyConfigFromHeaders(h http.Header) (cfg Config, err error) {
cfg.Scheme = h.Get(HeaderXProxyScheme)
cfg.Host = h.Get(HeaderXProxyHost)
cfgBase64 := h.Get(HeaderXProxyConfig)
cfgJSON, err := base64.StdEncoding.DecodeString(cfgBase64)
if err != nil {
return cfg, err
}
err = sonic.Unmarshal(cfgJSON, &cfg)
return cfg, err
}
func (cfg *Config) SetAgentProxyConfigHeadersLegacy(h http.Header) {
h.Set(HeaderXProxyHost, cfg.Host)
h.Set(HeaderXProxyHTTPS, strconv.FormatBool(cfg.Scheme == "https"))
h.Set(HeaderXProxySkipTLSVerify, strconv.FormatBool(cfg.NoTLSVerify))
h.Set(HeaderXProxyResponseHeaderTimeout, strconv.Itoa(int(cfg.ResponseHeaderTimeout.Round(time.Second).Seconds())))
}
func (cfg *Config) SetAgentProxyConfigHeaders(h http.Header) {
h.Set(HeaderXProxyHost, cfg.Host)
h.Set(HeaderXProxyScheme, string(cfg.Scheme))
cfgJSON, _ := sonic.Marshal(cfg.HTTPConfig)
cfgBase64 := base64.StdEncoding.EncodeToString(cfgJSON)
h.Set(HeaderXProxyConfig, cfgBase64)
}

View File

@@ -1,27 +1,14 @@
package agentproxy
import (
"net/http"
"strconv"
const (
HeaderXProxyScheme = "X-Proxy-Scheme"
HeaderXProxyHost = "X-Proxy-Host"
HeaderXProxyConfig = "X-Proxy-Config"
)
// deprecated
const (
HeaderXProxyHost = "X-Proxy-Host"
HeaderXProxyHTTPS = "X-Proxy-Https"
HeaderXProxySkipTLSVerify = "X-Proxy-Skip-Tls-Verify"
HeaderXProxyResponseHeaderTimeout = "X-Proxy-Response-Header-Timeout"
)
type AgentProxyHeaders struct {
Host string
IsHTTPS bool
SkipTLSVerify bool
ResponseHeaderTimeout int
}
func SetAgentProxyHeaders(r *http.Request, headers *AgentProxyHeaders) {
r.Header.Set(HeaderXProxyHost, headers.Host)
r.Header.Set(HeaderXProxyHTTPS, strconv.FormatBool(headers.IsHTTPS))
r.Header.Set(HeaderXProxySkipTLSVerify, strconv.FormatBool(headers.SkipTLSVerify))
r.Header.Set(HeaderXProxyResponseHeaderTimeout, strconv.Itoa(headers.ResponseHeaderTimeout))
}

View File

@@ -6,7 +6,7 @@ import (
"io"
"path/filepath"
"github.com/yusing/go-proxy/internal/utils/strutils"
strutils "github.com/yusing/goutils/strings"
)
const AgentCertsBasePath = "certs"

View File

@@ -4,7 +4,7 @@ import (
"testing"
"github.com/stretchr/testify/require"
"github.com/yusing/go-proxy/agent/pkg/certs"
"github.com/yusing/godoxy/agent/pkg/certs"
)
func TestZipCert(t *testing.T) {

25
agent/pkg/env/env.go vendored
View File

@@ -3,7 +3,10 @@ package env
import (
"os"
"github.com/yusing/go-proxy/internal/common"
"github.com/yusing/godoxy/agent/pkg/agent"
"github.com/yusing/goutils/env"
"github.com/rs/zerolog/log"
)
func DefaultAgentName() string {
@@ -21,6 +24,7 @@ var (
AgentCACert string
AgentSSLCert string
DockerSocket string
Runtime agent.ContainerRuntime
)
func init() {
@@ -28,11 +32,18 @@ func init() {
}
func Load() {
DockerSocket = common.GetEnvString("DOCKER_SOCKET", "/var/run/docker.sock")
AgentName = common.GetEnvString("AGENT_NAME", DefaultAgentName())
AgentPort = common.GetEnvInt("AGENT_PORT", 8890)
AgentSkipClientCertCheck = common.GetEnvBool("AGENT_SKIP_CLIENT_CERT_CHECK", false)
DockerSocket = env.GetEnvString("DOCKER_SOCKET", "/var/run/docker.sock")
AgentName = env.GetEnvString("AGENT_NAME", DefaultAgentName())
AgentPort = env.GetEnvInt("AGENT_PORT", 8890)
AgentSkipClientCertCheck = env.GetEnvBool("AGENT_SKIP_CLIENT_CERT_CHECK", false)
AgentCACert = common.GetEnvString("AGENT_CA_CERT", "")
AgentSSLCert = common.GetEnvString("AGENT_SSL_CERT", "")
AgentCACert = env.GetEnvString("AGENT_CA_CERT", "")
AgentSSLCert = env.GetEnvString("AGENT_SSL_CERT", "")
Runtime = agent.ContainerRuntime(env.GetEnvString("RUNTIME", "docker"))
switch Runtime {
case agent.ContainerRuntimeDocker, agent.ContainerRuntimePodman: //, agent.ContainerRuntimeNerdctl:
default:
log.Fatal().Str("runtime", string(Runtime)).Msg("invalid runtime")
}
}

View File

@@ -1,19 +1,18 @@
package handler
import (
"encoding/json"
"context"
"fmt"
"net/http"
"net/url"
"os"
"strings"
"github.com/yusing/go-proxy/internal/watcher/health"
"github.com/yusing/go-proxy/internal/watcher/health/monitor"
"github.com/bytedance/sonic"
"github.com/yusing/godoxy/internal/types"
"github.com/yusing/godoxy/internal/watcher/health/monitor"
)
var defaultHealthConfig = health.DefaultHealthConfig()
func CheckHealth(w http.ResponseWriter, r *http.Request) {
query := r.URL.Query()
scheme := query.Get("scheme")
@@ -22,8 +21,10 @@ func CheckHealth(w http.ResponseWriter, r *http.Request) {
return
}
var result *health.HealthCheckResult
var err error
var (
result types.HealthCheckResult
err error
)
switch scheme {
case "fileserver":
path := query.Get("path")
@@ -32,7 +33,7 @@ func CheckHealth(w http.ResponseWriter, r *http.Request) {
return
}
_, err := os.Stat(path)
result = &health.HealthCheckResult{Healthy: err == nil}
result = types.HealthCheckResult{Healthy: err == nil}
if err != nil {
result.Detail = err.Error()
}
@@ -47,7 +48,7 @@ func CheckHealth(w http.ResponseWriter, r *http.Request) {
Scheme: scheme,
Host: host,
Path: path,
}, defaultHealthConfig).CheckHealth()
}, healthCheckConfigFromRequest(r)).CheckHealth()
case "tcp", "udp":
host := query.Get("host")
if host == "" {
@@ -66,7 +67,7 @@ func CheckHealth(w http.ResponseWriter, r *http.Request) {
result, err = monitor.NewRawHealthMonitor(&url.URL{
Scheme: scheme,
Host: host,
}, defaultHealthConfig).CheckHealth()
}, healthCheckConfigFromRequest(r)).CheckHealth()
}
if err != nil {
@@ -76,5 +77,15 @@ func CheckHealth(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusOK)
json.NewEncoder(w).Encode(result)
sonic.ConfigDefault.NewEncoder(w).Encode(result)
}
func healthCheckConfigFromRequest(r *http.Request) types.HealthCheckConfig {
// we only need timeout and base context because it's one shot request
return types.HealthCheckConfig{
Timeout: types.HealthCheckTimeoutDefault,
BaseContext: func() context.Context {
return r.Context()
},
}
}

View File

@@ -10,9 +10,9 @@ import (
"testing"
"github.com/stretchr/testify/require"
"github.com/yusing/go-proxy/agent/pkg/agent"
"github.com/yusing/go-proxy/agent/pkg/handler"
"github.com/yusing/go-proxy/internal/watcher/health"
"github.com/yusing/godoxy/agent/pkg/agent"
"github.com/yusing/godoxy/agent/pkg/handler"
"github.com/yusing/godoxy/internal/types"
)
func TestCheckHealthHTTP(t *testing.T) {
@@ -81,7 +81,7 @@ func TestCheckHealthHTTP(t *testing.T) {
require.Equal(t, recorder.Code, tt.expectedStatus)
if tt.expectedStatus == http.StatusOK {
var result health.HealthCheckResult
var result types.HealthCheckResult
require.NoError(t, json.Unmarshal(recorder.Body.Bytes(), &result))
require.Equal(t, result.Healthy, tt.expectedHealthy)
}
@@ -125,7 +125,7 @@ func TestCheckHealthFileServer(t *testing.T) {
require.Equal(t, recorder.Code, tt.expectedStatus)
var result health.HealthCheckResult
var result types.HealthCheckResult
require.NoError(t, json.Unmarshal(recorder.Body.Bytes(), &result))
require.Equal(t, result.Healthy, tt.expectedHealthy)
require.Equal(t, result.Detail, tt.expectedDetail)
@@ -217,7 +217,7 @@ func TestCheckHealthTCPUDP(t *testing.T) {
require.Equal(t, recorder.Code, tt.expectedStatus)
if tt.expectedStatus == http.StatusOK {
var result health.HealthCheckResult
var result types.HealthCheckResult
require.NoError(t, json.Unmarshal(recorder.Body.Bytes(), &result))
require.Equal(t, result.Healthy, tt.expectedHealthy)
}

View File

@@ -4,11 +4,13 @@ import (
"fmt"
"net/http"
"github.com/yusing/go-proxy/agent/pkg/agent"
"github.com/yusing/go-proxy/agent/pkg/env"
"github.com/yusing/go-proxy/internal/metrics/systeminfo"
"github.com/yusing/go-proxy/pkg"
socketproxy "github.com/yusing/go-proxy/socketproxy/pkg"
"github.com/gin-gonic/gin"
"github.com/gorilla/websocket"
"github.com/yusing/godoxy/agent/pkg/agent"
"github.com/yusing/godoxy/agent/pkg/env"
"github.com/yusing/godoxy/internal/metrics/systeminfo"
socketproxy "github.com/yusing/godoxy/socketproxy/pkg"
"github.com/yusing/goutils/version"
)
type ServeMux struct{ *http.ServeMux }
@@ -21,16 +23,38 @@ func (mux ServeMux) HandleFunc(endpoint string, handler http.HandlerFunc) {
mux.ServeMux.HandleFunc(agent.APIEndpointBase+endpoint, handler)
}
var upgrader = &websocket.Upgrader{
// no origin check needed for internal websocket
CheckOrigin: func(r *http.Request) bool {
return true
},
}
func NewAgentHandler() http.Handler {
gin.SetMode(gin.ReleaseMode)
mux := ServeMux{http.NewServeMux()}
metricsHandler := gin.Default()
{
metrics := metricsHandler.Group(agent.APIEndpointBase)
metrics.GET(agent.EndpointSystemInfo, func(c *gin.Context) {
c.Set("upgrader", upgrader)
systeminfo.Poller.ServeHTTP(c)
})
}
mux.HandleFunc(agent.EndpointProxyHTTP+"/{path...}", ProxyHTTP)
mux.HandleEndpoint("GET", agent.EndpointVersion, pkg.GetVersionHTTPHandler())
mux.HandleEndpoint("GET", agent.EndpointVersion, func(w http.ResponseWriter, r *http.Request) {
fmt.Fprint(w, version.Get())
})
mux.HandleEndpoint("GET", agent.EndpointName, func(w http.ResponseWriter, r *http.Request) {
fmt.Fprint(w, env.AgentName)
})
mux.HandleEndpoint("GET", agent.EndpointRuntime, func(w http.ResponseWriter, r *http.Request) {
fmt.Fprint(w, env.Runtime)
})
mux.HandleEndpoint("GET", agent.EndpointHealth, CheckHealth)
mux.HandleEndpoint("GET", agent.EndpointSystemInfo, systeminfo.Poller.ServeHTTP)
mux.HandleEndpoint("GET", agent.EndpointSystemInfo, metricsHandler.ServeHTTP)
mux.ServeMux.HandleFunc("/", socketproxy.DockerSocketHandler(env.DockerSocket))
return mux
}

View File

@@ -1,14 +1,13 @@
package handler
import (
"crypto/tls"
"fmt"
"net/http"
"net/http/httputil"
"strconv"
"time"
"github.com/yusing/go-proxy/agent/pkg/agent"
"github.com/yusing/go-proxy/agent/pkg/agentproxy"
"github.com/yusing/godoxy/agent/pkg/agent"
"github.com/yusing/godoxy/agent/pkg/agentproxy"
)
func NewTransport() *http.Transport {
@@ -24,31 +23,24 @@ func NewTransport() *http.Transport {
}
func ProxyHTTP(w http.ResponseWriter, r *http.Request) {
host := r.Header.Get(agentproxy.HeaderXProxyHost)
isHTTPS, _ := strconv.ParseBool(r.Header.Get(agentproxy.HeaderXProxyHTTPS))
skipTLSVerify, _ := strconv.ParseBool(r.Header.Get(agentproxy.HeaderXProxySkipTLSVerify))
responseHeaderTimeout, err := strconv.Atoi(r.Header.Get(agentproxy.HeaderXProxyResponseHeaderTimeout))
cfg, err := agentproxy.ConfigFromHeaders(r.Header)
if err != nil {
responseHeaderTimeout = 0
}
if host == "" {
http.Error(w, "missing required headers", http.StatusBadRequest)
http.Error(w, fmt.Sprintf("failed to parse agent proxy config: %s", err.Error()), http.StatusBadRequest)
return
}
scheme := "http"
if isHTTPS {
scheme = "https"
}
transport := NewTransport()
if skipTLSVerify {
transport.TLSClientConfig = &tls.Config{InsecureSkipVerify: true}
if cfg.ResponseHeaderTimeout > 0 {
transport.ResponseHeaderTimeout = cfg.ResponseHeaderTimeout
}
if cfg.DisableCompression {
transport.DisableCompression = true
}
if responseHeaderTimeout > 0 {
transport.ResponseHeaderTimeout = time.Duration(responseHeaderTimeout) * time.Second
transport.TLSClientConfig, err = cfg.BuildTLSConfig(r.URL)
if err != nil {
http.Error(w, fmt.Sprintf("failed to build TLS client config: %s", err.Error()), http.StatusInternalServerError)
return
}
r.URL.Scheme = ""
@@ -58,8 +50,8 @@ func ProxyHTTP(w http.ResponseWriter, r *http.Request) {
rp := &httputil.ReverseProxy{
Director: func(r *http.Request) {
r.URL.Scheme = scheme
r.URL.Host = host
r.URL.Scheme = cfg.Scheme
r.URL.Host = cfg.Host
},
Transport: transport,
}

View File

@@ -7,10 +7,10 @@ import (
"net/http"
"github.com/rs/zerolog/log"
"github.com/yusing/go-proxy/agent/pkg/env"
"github.com/yusing/go-proxy/agent/pkg/handler"
"github.com/yusing/go-proxy/internal/net/gphttp/server"
"github.com/yusing/go-proxy/internal/task"
"github.com/yusing/godoxy/agent/pkg/env"
"github.com/yusing/godoxy/agent/pkg/handler"
"github.com/yusing/goutils/server"
"github.com/yusing/goutils/task"
)
type Options struct {
@@ -39,5 +39,5 @@ func StartAgentServer(parent task.Parent, opt Options) {
TLSConfig: tlsConfig,
}
server.Start(parent, agentServer, nil, &log.Logger)
server.Start(parent.Subtask("agent-server", false), agentServer, server.WithLogger(&log.Logger))
}

BIN
assets/godoxy.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 138 KiB

View File

@@ -5,29 +5,28 @@ import (
"sync"
"github.com/rs/zerolog/log"
"github.com/yusing/go-proxy/internal/auth"
"github.com/yusing/go-proxy/internal/common"
"github.com/yusing/go-proxy/internal/config"
"github.com/yusing/go-proxy/internal/dnsproviders"
"github.com/yusing/go-proxy/internal/gperr"
"github.com/yusing/go-proxy/internal/homepage"
"github.com/yusing/go-proxy/internal/logging"
"github.com/yusing/go-proxy/internal/logging/memlogger"
"github.com/yusing/go-proxy/internal/metrics/systeminfo"
"github.com/yusing/go-proxy/internal/metrics/uptime"
"github.com/yusing/go-proxy/internal/net/gphttp/middleware"
"github.com/yusing/go-proxy/internal/task"
"github.com/yusing/go-proxy/pkg"
"github.com/yusing/godoxy/internal/api"
"github.com/yusing/godoxy/internal/auth"
"github.com/yusing/godoxy/internal/common"
"github.com/yusing/godoxy/internal/config"
"github.com/yusing/godoxy/internal/dnsproviders"
"github.com/yusing/godoxy/internal/homepage"
"github.com/yusing/godoxy/internal/logging"
"github.com/yusing/godoxy/internal/logging/memlogger"
"github.com/yusing/godoxy/internal/metrics/systeminfo"
"github.com/yusing/godoxy/internal/metrics/uptime"
"github.com/yusing/godoxy/internal/net/gphttp/middleware"
"github.com/yusing/godoxy/internal/route/rules"
gperr "github.com/yusing/goutils/errs"
"github.com/yusing/goutils/server"
"github.com/yusing/goutils/task"
"github.com/yusing/goutils/version"
)
func parallel(fns ...func()) {
var wg sync.WaitGroup
for _, fn := range fns {
wg.Add(1)
go func() {
defer wg.Done()
fn()
}()
wg.Go(fn)
}
wg.Wait()
}
@@ -36,7 +35,7 @@ func main() {
initProfiling()
logging.InitLogger(os.Stderr, memlogger.GetMemLogger())
log.Info().Msgf("GoDoxy version %s", pkg.GetVersion())
log.Info().Msgf("GoDoxy version %s", version.Get())
log.Trace().Msg("trace enabled")
parallel(
dnsproviders.InitProviders,
@@ -54,26 +53,29 @@ func main() {
prepareDirectory(dir)
}
cfg, err := config.Load()
err := config.Load()
if err != nil {
gperr.LogWarn("errors in config", err)
}
cfg.Start(&config.StartServersOptions{
Proxy: true,
})
config.StartProxyServers()
if err := auth.Initialize(); err != nil {
log.Fatal().Err(err).Msg("failed to initialize authentication")
}
rules.InitAuthHandler(auth.AuthOrProceed)
// API Handler needs to start after auth is initialized.
cfg.StartServers(&config.StartServersOptions{
API: true,
server.StartServer(task.RootTask("api_server", false), server.Options{
Name: "api",
HTTPAddr: common.APIHTTPAddr,
Handler: api.NewHandler(),
})
uptime.Poller.Start()
config.WatchChanges()
task.WaitExit(cfg.Value().TimeoutShutdown)
task.WaitExit(config.Value().TimeoutShutdown)
}
func prepareDirectory(dir string) {

View File

@@ -10,16 +10,10 @@ import (
"time"
"github.com/rs/zerolog/log"
"github.com/yusing/go-proxy/internal/utils/strutils"
strutils "github.com/yusing/goutils/strings"
)
const mb = 1024 * 1024
func initProfiling() {
debug.SetGCPercent(-1)
debug.SetMemoryLimit(50 * mb)
debug.SetMaxStack(4 * mb)
go func() {
log.Info().Msgf("pprof server started at http://localhost:7777/debug/pprof/")
log.Error().Err(http.ListenAndServe(":7777", nil)).Msg("pprof server failed")
@@ -27,9 +21,14 @@ func initProfiling() {
go func() {
ticker := time.NewTicker(time.Second * 10)
defer ticker.Stop()
var m runtime.MemStats
var gcStats debug.GCStats
for range ticker.C {
var m runtime.MemStats
runtime.ReadMemStats(&m)
debug.ReadGCStats(&gcStats)
log.Info().Msgf("-----------------------------------------------------")
log.Info().Msgf("Timestamp: %s", time.Now().Format(time.RFC3339))
log.Info().Msgf(" Go Heap - In Use (Alloc/HeapAlloc): %s", strutils.FormatByteSize(m.Alloc))
@@ -37,8 +36,12 @@ func initProfiling() {
log.Info().Msgf(" Go Stacks - In Use (StackInuse): %s", strutils.FormatByteSize(m.StackInuse))
log.Info().Msgf(" Go Runtime - Other Sys (MSpanInuse, MCacheInuse, BuckHashSys, GCSys, OtherSys): %s", strutils.FormatByteSize(m.MSpanInuse+m.MCacheInuse+m.BuckHashSys+m.GCSys+m.OtherSys))
log.Info().Msgf(" Go Runtime - Total from OS (Sys): %s", strutils.FormatByteSize(m.Sys))
log.Info().Msgf(" Go Runtime - Freed from OS (HeapReleased): %s", strutils.FormatByteSize(m.HeapReleased))
log.Info().Msgf(" Number of Goroutines: %d", runtime.NumGoroutine())
log.Info().Msgf(" Number of GCs: %d", m.NumGC)
log.Info().Msgf(" Number of completed GC cycles: %d", m.NumGC)
log.Info().Msgf(" Number of GCs: %d", gcStats.NumGC)
log.Info().Msgf(" Total GC time: %s", gcStats.PauseTotal)
log.Info().Msgf(" Last GC time: %s", gcStats.LastGC.Format(time.DateTime))
log.Info().Msg("-----------------------------------------------------")
}
}()

View File

@@ -22,24 +22,28 @@ services:
- ${SOCKET_PROXY_LISTEN_ADDR:-127.0.0.1:2375}:2375
frontend:
image: ghcr.io/yusing/godoxy-frontend:${TAG:-latest}
# lite variant
# image: ghcr.io/yusing/godoxy-frontend:${TAG:-latest}-lite
container_name: godoxy-frontend
restart: unless-stopped
network_mode: host # do not change this
env_file: .env
# comment out `user` for lite variant
user: ${GODOXY_UID:-1000}:${GODOXY_GID:-1000}
read_only: true
tmpfs:
- /app/.next/cache # next image caching
# for lite variant, do not change uid/gid
# - /var/cache/nginx:uid=101,gid=101
# - /run:uid=101,gid=101
security_opt:
- no-new-privileges:true
cap_drop:
- all
depends_on:
- app
environment:
HOSTNAME: 127.0.0.1
PORT: ${GODOXY_FRONTEND_PORT:-3000}
labels:
proxy.aliases: ${GODOXY_FRONTEND_ALIASES:-godoxy}
proxy.#1.port: ${GODOXY_FRONTEND_PORT:-3000}
# proxy.#1.middlewares.cidr_whitelist: |
# status: 403
# message: IP not allowed
@@ -72,10 +76,9 @@ services:
- ./error_pages:/app/error_pages:ro
- ./data:/app/data
# To use autocert, certs will be stored in "./certs".
# You can also use a docker volume to store it
# This path stores certs obtained from autocert and agent TLS client certs
- ./certs:/app/certs
# remove "./certs:/app/certs" and uncomment below to use existing certificate
# mount existing certificate
# - /path/to/certs/cert.crt:/app/certs/cert.crt
# - /path/to/certs/priv.key:/app/certs/priv.key

View File

@@ -4,6 +4,8 @@
# autocert:
# provider: local
# cert_path: /path/to/cert.crt # default: /app/certs/cert.crt
# key_path: /path/to/priv.key # default: /app/certs/priv.key
# 2. cloudflare
# autocert:
@@ -17,6 +19,10 @@
# 3. other providers, see https://docs.godoxy.dev/DNS-01-Providers
# Access Control
# When enabled, it will be applied globally at connection level,
# all incoming connections (web, tcp and udp) will be checked against the ACL rules.
# acl:
# default: allow # or deny (default: allow)
# allow_local: true # or false (default: true)
@@ -31,12 +37,21 @@
# - country:US
# - timezone:Asia/Shanghai
# log: # warning: logging ACL can be slow based on the number of incoming connections and configured rules
# buffer_size: 65536 # (default: 64KB)
# path: /app/logs/acl.log # (default: none)
# stdout: false # (default: false)
# keep: last 10 # (default: none)
# keep: 30 days # (default: 30 days)
# log_allowed: false # (default: false)
# notify:
# interval: 1m # (default: 1m)
# to: [gotify, discord] # names under providers.notification
# include_allowed: false # (default: false)
entrypoint:
# Proxy Protocol: https://www.haproxy.com/blog/use-the-proxy-protocol-to-preserve-a-clients-ip-address
# When set to true, web entrypoint and all tcp routes will be wrapped with Proxy Protocol listener in order to preserve the client's IP address.
# Note that HTTP/3 with proxy protocol is not supported yet.
support_proxy_protocol: false
# Below define an example of middleware config
# 1. set security headers
# 2. block non local IP connections
@@ -57,20 +72,27 @@ entrypoint:
X-Frame-Options: SAMEORIGIN
Referrer-Policy: same-origin
Strict-Transport-Security: max-age=63072000; includeSubDomains; preload
# - use: CIDRWhitelist
# allow:
# - "127.0.0.1"
# - "10.0.0.0/8"
# - "172.16.0.0/12"
# - "192.168.0.0/16"
# status: 403
# message: "Forbidden"
# - use: RedirectHTTP
# below enables access log
access_log:
format: combined
path: /app/logs/entrypoint.log
stdout: false # (default: false)
keep: 30 days # (default: 30 days)
# customize behavior for non-existent routes, e.g. pass over to another proxy
#
# rules:
# not_found:
# - name: default
# do: proxy http://other-proxy:8080
defaults:
healthcheck:
interval: 5s
timeout: 15s
retries: 3
providers:
# include files are standalone yaml files under `config/` directory
@@ -95,9 +117,13 @@ providers:
# remote-1: tcp://10.0.2.1:2375
# remote-2: ssh://root:1234@10.0.2.2
# notification providers (notify when service health changes)
# notification providers
#
# notification:
# - name: ntfy
# provider: ntfy
# url: https://ntfy.domain.tld
# topic: godoxy
# - name: gotify
# provider: gotify
# url: https://gotify.domain.tld
@@ -106,6 +132,11 @@ providers:
# provider: webhook
# url: https://discord.com/api/webhooks/...
# template: discord # this means use payload template from internal/notif/templates/discord.json
# - name: pushover
# provider: webhook
# url: https://api.pushover.net/1/messages.json
# mime_type: application/x-www-form-urlencoded
# payload: '{"token": "your-app-token", "user": "your-user-key", "title": $title, "message": $message}'
# Proxmox providers (for idlesleep support for proxmox LXCs)
#
@@ -115,8 +146,8 @@ providers:
# secret: aaaa-bbbb-cccc-dddd
# no_tls_verify: true
# Check https://docs.godoxy.dev/Certificates-and-domain-matching
# for explaination of `match_domains`
# Match domains
# See https://docs.godoxy.dev/Certificates-and-domain-matching
#
# match_domains:
# - my.site

7
dev.Dockerfile Normal file
View File

@@ -0,0 +1,7 @@
FROM debian:bookworm-slim
RUN apt-get update && apt-get install -y ca-certificates
WORKDIR /app
CMD ["/app/run"]

69
dev.compose.yml Normal file
View File

@@ -0,0 +1,69 @@
services:
app:
image: godoxy-dev
build:
context: .
dockerfile: dev.Dockerfile
container_name: godoxy-proxy-dev
restart: unless-stopped
env_file: dev.env
environment:
DOCKER_HOST: unix:///var/run/docker.sock
TZ: Asia/Hong_Kong
API_ADDR: 127.0.0.1:8999
API_USER: dev
API_PASSWORD: 1234
API_SKIP_ORIGIN_CHECK: true
API_JWT_TTL: 24h
DEBUG: true
API_JWT_SECRET: 1234567891234567
labels:
proxy.exclude: true
proxy.#1.healthcheck.disable: true
ipc: host
network_mode: host
volumes:
- ./bin/godoxy:/app/run:ro
- /var/run/docker.sock:/var/run/docker.sock
- ./dev-data/config:/app/config
- ./dev-data/certs:/app/certs
- ./dev-data/error_pages:/app/error_pages:ro
- ./dev-data/data:/app/data
- ./dev-data/logs:/app/logs
- ~/certs/myCA.pem:/etc/ssl/certs/ca.crt:ro
parca:
image: ghcr.io/parca-dev/parca:v0.24.2
container_name: godoxy-parca
restart: unless-stopped
command: [/parca, --config-path, /parca.yaml]
network_mode: host
# ports:
# - 7070:7070
configs:
- source: parca
target: /parca.yaml
labels:
proxy.#1.port: "7070"
tinyauth:
image: ghcr.io/steveiliop56/tinyauth:v3
container_name: tinyauth
restart: unless-stopped
environment:
- SECRET=12345678912345671234567891234567
- APP_URL=https://tinyauth.my.app
- USERS=user:$$2a$$10$$UdLYoJ5lgPsC0RKqYH/jMua7zIn0g9kPqWmhYayJYLaZQ/FTmH2/u # user:password
labels:
proxy.tinyauth.port: "3000"
configs:
parca:
content: |
object_storage:
bucket:
type: "FILESYSTEM"
config:
directory: "./data"
scrape_configs:
- job_name: "parca"
scrape_interval: "1s"
static_configs:
- targets: [ 'localhost:7777' ]

304
go.mod
View File

@@ -1,263 +1,183 @@
module github.com/yusing/go-proxy
module github.com/yusing/godoxy
go 1.24.5
go 1.25.5
replace github.com/yusing/go-proxy/agent => ./agent
replace github.com/yusing/go-proxy/internal/dnsproviders => ./internal/dnsproviders
replace github.com/yusing/go-proxy/internal/utils => ./internal/utils
replace github.com/coreos/go-oidc/v3 => github.com/godoxy-app/go-oidc/v3 v3.0.0-20250523122447-f078841dec22
replace github.com/docker/docker => github.com/godoxy-app/docker v0.0.0-20250523125835-a2474a6ebe30
replace github.com/shirou/gopsutil/v4 => github.com/godoxy-app/gopsutil/v4 v4.0.0-20250607110153-34d627ba1b5d
replace (
github.com/coreos/go-oidc/v3 => ./internal/go-oidc
github.com/shirou/gopsutil/v4 => ./internal/gopsutil
github.com/yusing/godoxy/agent => ./agent
github.com/yusing/godoxy/internal/dnsproviders => ./internal/dnsproviders
github.com/yusing/goutils => ./goutils
github.com/yusing/goutils/http/reverseproxy => ./goutils/http/reverseproxy
github.com/yusing/goutils/http/websocket => ./goutils/http/websocket
github.com/yusing/goutils/server => ./goutils/server
)
require (
github.com/PuerkitoBio/goquery v1.10.3 // parsing HTML for extract fav icon
github.com/coreos/go-oidc/v3 v3.14.1 // oidc authentication
github.com/docker/docker v28.3.2+incompatible // docker daemon
github.com/PuerkitoBio/goquery v1.11.0 // parsing HTML for extract fav icon
github.com/coreos/go-oidc/v3 v3.17.0 // oidc authentication
github.com/fsnotify/fsnotify v1.9.0 // file watcher
github.com/go-acme/lego/v4 v4.24.0 // acme client
github.com/go-playground/validator/v10 v10.27.0 // validator
github.com/gin-gonic/gin v1.11.0 // api server
github.com/go-acme/lego/v4 v4.29.0 // acme client
github.com/go-playground/validator/v10 v10.28.0 // 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
github.com/gotify/server/v2 v2.6.3 // reference the Message struct for json response
github.com/gotify/server/v2 v2.7.3 // reference the Message struct for json response
github.com/lithammer/fuzzysearch v1.1.8 // fuzzy search for searching icons and filtering metrics
github.com/puzpuzpuz/xsync/v4 v4.1.0 // lock free map for concurrent operations
github.com/pires/go-proxyproto v0.8.1 // proxy protocol support
github.com/puzpuzpuz/xsync/v4 v4.2.0 // lock free map for concurrent operations
github.com/rs/zerolog v1.34.0 // logging
github.com/shirou/gopsutil/v4 v4.25.6 // system info metrics
github.com/vincent-petithory/dataurl v1.0.0 // data url for fav icon
golang.org/x/crypto v0.40.0 // encrypting password with bcrypt
golang.org/x/net v0.42.0 // HTTP header utilities
golang.org/x/oauth2 v0.30.0 // oauth2 authentication
golang.org/x/sync v0.16.0
golang.org/x/time v0.12.0 // time utilities
golang.org/x/crypto v0.46.0 // encrypting password with bcrypt
golang.org/x/net v0.48.0 // HTTP header utilities
golang.org/x/oauth2 v0.34.0 // oauth2 authentication
golang.org/x/sync v0.19.0
golang.org/x/time v0.14.0 // time utilities
)
require (
github.com/docker/cli v28.3.2+incompatible
github.com/goccy/go-yaml v1.18.0 // yaml parsing for different config files
github.com/golang-jwt/jwt/v5 v5.2.3
github.com/luthermonson/go-proxmox v0.2.2
github.com/oschwald/maxminddb-golang v1.13.1
github.com/quic-go/quic-go v0.53.0
github.com/samber/slog-zerolog/v2 v2.7.3
github.com/spf13/afero v1.14.0
github.com/stretchr/testify v1.10.0
github.com/yusing/go-proxy/agent v0.0.0-20250720044732-46c7ee4d8443
github.com/yusing/go-proxy/internal/dnsproviders v0.0.0-20250720044732-46c7ee4d8443
github.com/yusing/go-proxy/internal/utils v0.0.0
github.com/bytedance/gopkg v0.1.3 // xxhash64 for fast hash
github.com/bytedance/sonic v1.14.2 // fast json parsing
github.com/docker/cli v29.1.2+incompatible // needs docker/cli/cli/connhelper connection helper for docker client
github.com/goccy/go-yaml v1.19.0 // yaml parsing for different config files
github.com/golang-jwt/jwt/v5 v5.3.0 // jwt authentication
github.com/luthermonson/go-proxmox v0.2.3 // 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/quic-go/quic-go v0.57.1 // http3 support
github.com/shirou/gopsutil/v4 v4.25.11 // 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.68.0 // fast http for health check
github.com/yusing/ds v0.3.1 // data structures and algorithms
github.com/yusing/godoxy/agent v0.0.0-20251210092313-fdd37b777a09
github.com/yusing/godoxy/internal/dnsproviders v0.0.0-20251210092313-fdd37b777a09
github.com/yusing/gointernals v0.1.16
github.com/yusing/goutils v0.7.0
github.com/yusing/goutils/http/reverseproxy v0.0.0-20251210092226-e7258c13e982
github.com/yusing/goutils/http/websocket v0.0.0-20251210092226-e7258c13e982
github.com/yusing/goutils/server v0.0.0-20251210092226-e7258c13e982
)
require (
cloud.google.com/go/auth v0.16.3 // indirect
cloud.google.com/go/auth v0.17.0 // indirect
cloud.google.com/go/auth/oauth2adapt v0.2.8 // indirect
cloud.google.com/go/compute/metadata v0.7.0 // indirect
github.com/AdamSLevy/jsonrpc2/v14 v14.1.0 // indirect
github.com/Azure/azure-sdk-for-go/sdk/azcore v1.18.1 // indirect
github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.10.1 // indirect
github.com/Azure/azure-sdk-for-go/sdk/internal v1.11.1 // indirect
cloud.google.com/go/compute/metadata v0.9.0 // indirect
github.com/Azure/azure-sdk-for-go/sdk/azcore v1.20.0 // indirect
github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.13.1 // indirect
github.com/Azure/azure-sdk-for-go/sdk/internal v1.11.2 // indirect
github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/dns/armdns v1.2.0 // indirect
github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/privatedns/armprivatedns v1.3.0 // indirect
github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/resourcegraph/armresourcegraph v0.9.0 // indirect
github.com/AzureAD/microsoft-authentication-library-for-go v1.4.2 // indirect
github.com/AzureAD/microsoft-authentication-library-for-go v1.6.0 // indirect
github.com/Microsoft/go-winio v0.6.2 // indirect
github.com/OpenDNS/vegadns2client v0.0.0-20180418235048-a3fa4a771d87 // indirect
github.com/akamai/AkamaiOPEN-edgegrid-golang v1.2.2 // indirect
github.com/andybalholm/cascadia v1.3.3 // indirect
github.com/aws/aws-sdk-go-v2 v1.36.6 // indirect
github.com/aws/aws-sdk-go-v2/config v1.29.18 // indirect
github.com/aws/aws-sdk-go-v2/credentials v1.17.71 // indirect
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.33 // indirect
github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.37 // indirect
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.37 // indirect
github.com/aws/aws-sdk-go-v2/internal/ini v1.8.3 // indirect
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.12.4 // indirect
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.12.18 // indirect
github.com/aws/aws-sdk-go-v2/service/lightsail v1.43.5 // indirect
github.com/aws/aws-sdk-go-v2/service/route53 v1.53.1 // indirect
github.com/aws/aws-sdk-go-v2/service/sso v1.25.6 // indirect
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.30.4 // indirect
github.com/aws/aws-sdk-go-v2/service/sts v1.34.1 // indirect
github.com/aws/smithy-go v1.22.4 // indirect
github.com/baidubce/bce-sdk-go v0.9.235 // indirect
github.com/benbjohnson/clock v1.3.5 // indirect
github.com/boombuler/barcode v1.0.2 // indirect
github.com/buger/goterm v1.0.4 // indirect
github.com/cenkalti/backoff/v4 v4.3.0 // indirect
github.com/civo/civogo v0.6.1 // indirect
github.com/cloudflare/cloudflare-go v0.115.0 // indirect
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
github.com/diskfs/go-diskfs v1.6.0 // indirect
github.com/diskfs/go-diskfs v1.7.0 // indirect
github.com/distribution/reference v0.6.0 // indirect
github.com/djherbis/times v1.6.0 // indirect
github.com/docker/go-connections v0.5.0
github.com/docker/go-connections v0.6.0
github.com/docker/go-units v0.5.0 // indirect
github.com/ebitengine/purego v0.8.4 // indirect
github.com/exoscale/egoscale/v3 v3.1.24 // indirect
github.com/fatih/structs v1.1.0 // indirect
github.com/ebitengine/purego v0.9.1 // indirect
github.com/felixge/httpsnoop v1.0.4 // indirect
github.com/fxamacker/cbor/v2 v2.9.0 // indirect
github.com/gabriel-vasile/mimetype v1.4.9 // indirect
github.com/go-errors/errors v1.5.1 // indirect
github.com/go-jose/go-jose/v4 v4.1.1 // indirect
github.com/gabriel-vasile/mimetype v1.4.11 // indirect
github.com/go-jose/go-jose/v4 v4.1.3 // indirect
github.com/go-logr/logr v1.4.3 // indirect
github.com/go-logr/stdr v1.2.2 // indirect
github.com/go-ole/go-ole v1.3.0 // indirect
github.com/go-playground/locales v0.14.1 // indirect
github.com/go-playground/universal-translator v0.18.1 // indirect
github.com/go-resty/resty/v2 v2.16.5 // indirect
github.com/go-viper/mapstructure/v2 v2.4.0 // indirect
github.com/goccy/go-json v0.10.5 // indirect; indirectindirect
github.com/gofrs/flock v0.12.1 // indirect
github.com/gogo/protobuf v1.3.2 // indirect
github.com/google/go-querystring v1.1.0 // indirect
github.com/gofrs/flock v0.13.0 // indirect
github.com/google/s2a-go v0.1.9 // indirect
github.com/google/uuid v1.6.0 // indirect
github.com/googleapis/enterprise-certificate-proxy v0.3.6 // indirect
github.com/googleapis/enterprise-certificate-proxy v0.3.7 // indirect
github.com/googleapis/gax-go/v2 v2.15.0 // indirect
github.com/gophercloud/gophercloud v1.14.1 // indirect
github.com/gophercloud/utils v0.0.0-20231010081019-80377eca5d56 // indirect
github.com/hashicorp/go-cleanhttp v0.5.2 // indirect
github.com/hashicorp/go-retryablehttp v0.7.8 // indirect
github.com/hashicorp/go-uuid v1.0.3 // indirect
github.com/huaweicloud/huaweicloud-sdk-go-v3 v0.1.159 // indirect
github.com/iij/doapi v0.0.0-20190504054126-0bbf12d6d7df // indirect
github.com/infobloxopen/infoblox-go-client/v2 v2.10.0 // indirect
github.com/jinzhu/copier v0.4.0 // indirect
github.com/json-iterator/go v1.1.12 // indirect
github.com/k0kubun/go-ansi v0.0.0-20180517002512-3bf9e2903213 // indirect
github.com/kolo/xmlrpc v0.0.0-20220921171641-a4b6fa1dd06b // indirect
github.com/json-iterator/go v1.1.13-0.20220915233716-71ac16282d12 // indirect
github.com/kylelemons/godebug v1.1.0 // indirect
github.com/labbsr0x/bindman-dns-webhook v1.0.2 // indirect
github.com/labbsr0x/goh v1.0.1 // indirect
github.com/leodido/go-urn v1.4.0 // indirect
github.com/linode/linodego v1.53.0 // indirect
github.com/liquidweb/liquidweb-cli v0.7.0 // indirect
github.com/liquidweb/liquidweb-go v1.6.4 // indirect
github.com/lufia/plan9stats v0.0.0-20250317134145-8bc96cf8fc35 // indirect
github.com/magefile/mage v1.15.0 // indirect
github.com/mattn/go-colorable v0.1.14 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect
github.com/miekg/dns v1.1.67 // indirect
github.com/mimuret/golang-iij-dpf v0.9.1 // indirect
github.com/miekg/dns v1.1.68 // indirect
github.com/mitchellh/go-homedir v1.1.0 // indirect
github.com/mitchellh/mapstructure v1.5.0 // indirect
github.com/moby/docker-image-spec v1.3.1 // indirect
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
github.com/modern-go/reflect2 v1.0.2 // indirect
github.com/nrdcg/auroradns v1.1.0 // indirect
github.com/nrdcg/bunny-go v0.0.0-20250327222614-988a091fc7ea // indirect
github.com/nrdcg/desec v0.11.0 // indirect
github.com/nrdcg/freemyip v0.3.0 // indirect
github.com/nrdcg/goacmedns v0.2.0 // indirect
github.com/nrdcg/goinwx v0.11.0 // indirect
github.com/nrdcg/mailinabox v0.2.0 // indirect
github.com/nrdcg/namesilo v0.2.1 // indirect
github.com/nrdcg/nodion v0.1.0 // indirect
github.com/nrdcg/porkbun v0.4.0 // indirect
github.com/nzdjb/go-metaname v1.0.0 // indirect
github.com/opencontainers/go-digest v1.0.0 // indirect
github.com/opencontainers/image-spec v1.1.1 // indirect
github.com/ovh/go-ovh v1.9.0 // indirect
github.com/patrickmn/go-cache v2.1.0+incompatible // indirect
github.com/pelletier/go-toml/v2 v2.2.4 // indirect
github.com/peterhellberg/link v1.2.0 // 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/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55 // indirect
github.com/pquerna/otp v1.5.0 // indirect
github.com/quic-go/qpack v0.5.1 // indirect
github.com/regfish/regfish-dnsapi-go v0.1.1 // indirect
github.com/rogpeppe/go-internal v1.14.1 // indirect
github.com/sacloud/api-client-go v0.3.2 // indirect
github.com/sacloud/go-http v0.1.9 // indirect
github.com/sacloud/iaas-api-go v1.16.1 // indirect
github.com/sacloud/packages-go v0.0.11 // indirect
github.com/sagikazarmark/locafero v0.9.0 // indirect
github.com/samber/lo v1.51.0 // indirect
github.com/quic-go/qpack v0.6.0 // indirect
github.com/samber/lo v1.52.0 // indirect
github.com/samber/slog-common v0.19.0 // indirect
github.com/scaleway/scaleway-sdk-go v1.0.0-beta.34 // indirect
github.com/selectel/domains-go v1.1.0 // indirect
github.com/shopspring/decimal v1.4.0 // indirect
github.com/samber/slog-zerolog/v2 v2.9.0 // indirect
github.com/scaleway/scaleway-sdk-go v1.0.0-beta.35 // indirect
github.com/sirupsen/logrus v1.9.4-0.20230606125235-dd1b4c2e81af // indirect
github.com/smartystreets/go-aws-auth v0.0.0-20180515143844-0c1422d1fdb9 // indirect
github.com/softlayer/softlayer-go v1.1.7 // indirect
github.com/softlayer/xmlrpc v0.0.0-20200409220501-5f089df7cb7e // indirect
github.com/sony/gobreaker v1.0.0 // indirect
github.com/sourcegraph/conc v0.3.0 // indirect
github.com/spf13/cast v1.9.2 // indirect
github.com/spf13/pflag v1.0.7 // indirect
github.com/spf13/viper v1.20.1 // indirect
github.com/subosito/gotenv v1.6.0 // indirect
github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common v1.0.1209 // indirect
github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/dnspod v1.0.1200 // indirect
github.com/tjfoc/gmsm v1.4.1 // indirect
github.com/tklauser/go-sysconf v0.3.15 // indirect
github.com/tklauser/numcpus v0.10.0 // indirect
github.com/transip/gotransip/v6 v6.26.0 // indirect
github.com/ultradns/ultradns-go-sdk v1.8.0-20241010134910-243eeec // indirect
github.com/vinyldns/go-vinyldns v0.9.16 // indirect
github.com/volcengine/volc-sdk-golang v1.0.216 // indirect
github.com/vultr/govultr/v3 v3.21.1 // indirect
github.com/x448/float16 v0.8.4 // indirect
github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78 // indirect
github.com/yusufpapurcu/wmi v1.2.4 // indirect
go.mongodb.org/mongo-driver v1.17.4 // indirect
go.opentelemetry.io/auto/sdk v1.1.0 // indirect
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.62.0 // indirect
go.opentelemetry.io/otel v1.37.0 // indirect
go.opentelemetry.io/otel/metric v1.37.0 // indirect
go.opentelemetry.io/otel/trace v1.37.0 // indirect
go.opentelemetry.io/auto/sdk v1.2.1 // indirect
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.64.0
go.opentelemetry.io/otel v1.39.0 // indirect
go.opentelemetry.io/otel/metric v1.39.0 // indirect
go.opentelemetry.io/otel/trace v1.39.0 // indirect
go.uber.org/atomic v1.11.0
go.uber.org/mock v0.5.2 // indirect
go.uber.org/multierr v1.11.0 // indirect
go.uber.org/ratelimit v0.3.1 // indirect
golang.org/x/mod v0.26.0 // indirect
golang.org/x/sys v0.34.0 // indirect
golang.org/x/text v0.27.0 // indirect
golang.org/x/tools v0.35.0 // indirect
google.golang.org/api v0.242.0 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20250715232539-7130f93afb79 // indirect
google.golang.org/grpc v1.73.0 // indirect
google.golang.org/protobuf v1.36.6 // indirect
gopkg.in/inf.v0 v0.9.1 // indirect
golang.org/x/mod v0.31.0 // indirect
golang.org/x/sys v0.39.0 // indirect
golang.org/x/text v0.32.0 // indirect
golang.org/x/tools v0.40.0 // indirect
google.golang.org/api v0.257.0 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20251202230838-ff82c1b0f217 // indirect
google.golang.org/grpc v1.77.0 // indirect
google.golang.org/protobuf v1.36.10 // indirect
gopkg.in/ini.v1 v1.67.0 // indirect
gopkg.in/ns1/ns1-go.v2 v2.14.4 // indirect
gopkg.in/yaml.v2 v2.4.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
k8s.io/api v0.33.3 // indirect
k8s.io/apimachinery v0.33.3 // indirect
k8s.io/klog/v2 v2.130.1 // indirect
k8s.io/utils v0.0.0-20250604170112-4c0f3b243397 // indirect
sigs.k8s.io/json v0.0.0-20241014173422-cfa47c3a1cc8 // indirect
sigs.k8s.io/randfill v1.0.0 // indirect
sigs.k8s.io/structured-merge-diff/v4 v4.7.0 // indirect
sigs.k8s.io/yaml v1.5.0 // indirect
)
require (
github.com/alibabacloud-go/alibabacloud-gateway-spi v0.0.5 // indirect
github.com/alibabacloud-go/alidns-20150109/v4 v4.5.10 // indirect
github.com/alibabacloud-go/darabonba-openapi/v2 v2.1.8 // indirect
github.com/alibabacloud-go/debug v1.0.1 // indirect
github.com/alibabacloud-go/endpoint-util v1.1.1 // indirect
github.com/alibabacloud-go/openapi-util v0.1.1 // indirect
github.com/alibabacloud-go/tea v1.3.9 // indirect
github.com/alibabacloud-go/tea-utils/v2 v2.0.7 // indirect
github.com/aliyun/credentials-go v1.4.6 // indirect
github.com/clbanning/mxj/v2 v2.7.0 // indirect
github.com/akamai/AkamaiOPEN-edgegrid-golang/v11 v11.1.0 // indirect
github.com/andybalholm/brotli v1.2.0 // indirect
github.com/bytedance/sonic/loader v0.4.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/dnsimple/dnsimple-go/v4 v4.0.0 // indirect
github.com/namedotcom/go/v4 v4.0.2 // indirect
github.com/nrdcg/oci-go-sdk/common/v1065 v1065.95.2 // indirect
github.com/nrdcg/oci-go-sdk/dns/v1065 v1065.95.2 // indirect
github.com/onsi/ginkgo/v2 v2.23.3 // indirect
github.com/onsi/gomega v1.36.3 // indirect
github.com/selectel/go-selvpcclient/v4 v4.1.0 // indirect
go.yaml.in/yaml/v2 v2.4.2 // indirect
github.com/fatih/color v1.18.0 // indirect
github.com/gin-contrib/sse v1.1.0 // indirect
github.com/go-ole/go-ole v1.3.0 // indirect
github.com/go-ozzo/ozzo-validation/v4 v4.3.0 // indirect
github.com/go-resty/resty/v2 v2.17.0 // indirect
github.com/goccy/go-json v0.10.5 // indirect
github.com/google/go-querystring v1.1.0 // indirect
github.com/klauspost/compress v1.18.2 // indirect
github.com/klauspost/cpuid/v2 v2.3.0 // indirect
github.com/linode/linodego v1.62.0 // indirect
github.com/lufia/plan9stats v0.0.0-20251013123823-9fd1530e3ec3 // indirect
github.com/nrdcg/oci-go-sdk/common/v1065 v1065.105.1 // indirect
github.com/nrdcg/oci-go-sdk/dns/v1065 v1065.105.1 // indirect
github.com/pierrec/lz4/v4 v4.1.21 // indirect
github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55 // 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.25.0 // indirect
github.com/yusufpapurcu/wmi v1.2.4 // indirect
golang.org/x/arch v0.23.0 // indirect
google.golang.org/genproto v0.0.0-20251111163417-95abcf5c77ba // indirect
google.golang.org/genproto/googleapis/api v0.0.0-20251111163417-95abcf5c77ba // indirect
)

2601
go.sum

File diff suppressed because it is too large Load Diff

1
goutils Submodule

Submodule goutils added at 79a4b13087

View File

@@ -1,17 +1,22 @@
package acl
import (
"fmt"
"math"
"net"
"sync/atomic"
"time"
"github.com/puzpuzpuz/xsync/v4"
"github.com/rs/zerolog"
"github.com/rs/zerolog/log"
"github.com/yusing/go-proxy/internal/common"
"github.com/yusing/go-proxy/internal/gperr"
"github.com/yusing/go-proxy/internal/logging/accesslog"
"github.com/yusing/go-proxy/internal/maxmind"
"github.com/yusing/go-proxy/internal/task"
"github.com/yusing/go-proxy/internal/utils"
"github.com/yusing/godoxy/internal/common"
"github.com/yusing/godoxy/internal/logging/accesslog"
"github.com/yusing/godoxy/internal/maxmind"
"github.com/yusing/godoxy/internal/notif"
gperr "github.com/yusing/goutils/errs"
strutils "github.com/yusing/goutils/strings"
"github.com/yusing/goutils/task"
)
type Config struct {
@@ -21,16 +26,42 @@ type Config struct {
Deny Matchers `json:"deny"`
Log *accesslog.ACLLoggerConfig `json:"log"`
Notify struct {
To []string `json:"to"` // list of notification providers
Interval time.Duration `json:"interval"` // interval between notifications
IncludeAllowed *bool `json:"include_allowed"` // default: false
} `json:"notify"`
config
valErr gperr.Error
}
const defaultNotifyInterval = 1 * time.Minute
type config struct {
defaultAllow bool
allowLocal bool
ipCache *xsync.Map[string, *checkCache]
logAllowed bool
logger *accesslog.AccessLogger
// will be nil if Notify.To is empty
// these are per IP, reset every Notify.Interval
allowedCount map[string]uint32
blockedCount map[string]uint32
// these are total, never reset
totalAllowedCount uint64
totalBlockedCount uint64
logAllowed bool
// will be nil if Log is nil
logger accesslog.AccessLogger
// will never tick if Notify.To is empty
notifyTicker *time.Ticker
notifyAllowed bool
// will be nil if both Log and Notify.To are empty
logNotifyCh chan ipLog
}
type checkCache struct {
@@ -39,10 +70,18 @@ type checkCache struct {
created time.Time
}
type ipLog struct {
info *maxmind.IPInfo
allowed bool
}
// could be nil
var ActiveConfig atomic.Pointer[Config]
const cacheTTL = 1 * time.Minute
func (c *checkCache) Expired() bool {
return c.created.Add(cacheTTL).Before(utils.TimeNow())
return c.created.Add(cacheTTL).Before(time.Now())
}
// TODO: add stats
@@ -69,6 +108,10 @@ func (c *Config) Validate() gperr.Error {
c.allowLocal = true
}
if c.Notify.Interval < 0 {
c.Notify.Interval = defaultNotifyInterval
}
if c.Log != nil {
c.logAllowed = c.Log.LogAllowed
}
@@ -79,6 +122,12 @@ func (c *Config) Validate() gperr.Error {
}
c.ipCache = xsync.NewMap[string, *checkCache]()
if c.Notify.IncludeAllowed != nil {
c.notifyAllowed = *c.Notify.IncludeAllowed
} else {
c.notifyAllowed = false
}
return nil
}
@@ -86,7 +135,7 @@ func (c *Config) Valid() bool {
return c != nil && c.valErr == nil
}
func (c *Config) Start(parent *task.Task) gperr.Error {
func (c *Config) Start(parent task.Parent) gperr.Error {
if c.Log != nil {
logger, err := accesslog.NewAccessLogger(parent, c.Log)
if err != nil {
@@ -97,6 +146,23 @@ func (c *Config) Start(parent *task.Task) gperr.Error {
if c.valErr != nil {
return c.valErr
}
if c.needLogOrNotify() {
c.logNotifyCh = make(chan ipLog, 100)
}
if c.needNotify() {
c.allowedCount = make(map[string]uint32)
c.blockedCount = make(map[string]uint32)
c.notifyTicker = time.NewTicker(c.Notify.Interval)
} else {
c.notifyTicker = time.NewTicker(time.Duration(math.MaxInt64)) // never tick
}
if c.needLogOrNotify() {
go c.logNotifyLoop(parent)
}
log.Info().
Str("default", c.Default).
Bool("allow_local", c.allowLocal).
@@ -113,16 +179,93 @@ func (c *Config) cacheRecord(info *maxmind.IPInfo, allow bool) {
c.ipCache.Store(info.Str, &checkCache{
IPInfo: info,
allow: allow,
created: utils.TimeNow(),
created: time.Now(),
})
}
func (c *config) log(info *maxmind.IPInfo, allowed bool) {
if c.logger == nil {
return
func (c *Config) needLogOrNotify() bool {
return c.needLog() || c.needNotify()
}
func (c *Config) needLog() bool {
return c.logger != nil
}
func (c *Config) needNotify() bool {
return len(c.Notify.To) > 0
}
func (c *Config) getCachedCity(ip string) string {
record, ok := c.ipCache.Load(ip)
if ok {
if record.City != nil {
if record.City.Country.IsoCode != "" {
return record.City.Country.IsoCode
}
return record.City.Location.TimeZone
}
}
if !allowed || c.logAllowed {
c.logger.LogACL(info, !allowed)
return "unknown location"
}
func (c *Config) logNotifyLoop(parent task.Parent) {
defer c.notifyTicker.Stop()
for {
select {
case <-parent.Context().Done():
return
case log := <-c.logNotifyCh:
if c.logger != nil {
if !log.allowed || c.logAllowed {
c.logger.LogACL(log.info, !log.allowed)
}
}
if c.needNotify() {
if log.allowed {
if c.notifyAllowed {
c.allowedCount[log.info.Str]++
c.totalAllowedCount++
}
} else {
c.blockedCount[log.info.Str]++
c.totalBlockedCount++
}
}
case <-c.notifyTicker.C: // will never tick when notify is disabled
total := len(c.allowedCount) + len(c.blockedCount)
if total == 0 {
continue
}
total++
fieldsBody := make(notif.ListBody, total)
i := 0
fieldsBody[i] = fmt.Sprintf("Total: allowed %d, blocked %d", c.totalAllowedCount, c.totalBlockedCount)
i++
for ip, count := range c.allowedCount {
fieldsBody[i] = fmt.Sprintf("%s (%s): allowed %d times", ip, c.getCachedCity(ip), count)
i++
}
for ip, count := range c.blockedCount {
fieldsBody[i] = fmt.Sprintf("%s (%s): blocked %d times", ip, c.getCachedCity(ip), count)
i++
}
notif.Notify(&notif.LogMessage{
Level: zerolog.InfoLevel,
Title: "ACL Summary for last " + strutils.FormatDuration(c.Notify.Interval),
Body: fieldsBody,
To: c.Notify.To,
})
clear(c.allowedCount)
clear(c.blockedCount)
}
}
}
// log and notify if needed
func (c *Config) logAndNotify(info *maxmind.IPInfo, allowed bool) {
if c.logNotifyCh != nil {
c.logNotifyCh <- ipLog{info: info, allowed: allowed}
}
}
@@ -137,30 +280,30 @@ func (c *Config) IPAllowed(ip net.IP) bool {
}
if c.allowLocal && ip.IsPrivate() {
c.log(&maxmind.IPInfo{IP: ip, Str: ip.String()}, true)
c.logAndNotify(&maxmind.IPInfo{IP: ip, Str: ip.String()}, true)
return true
}
ipStr := ip.String()
record, ok := c.ipCache.Load(ipStr)
if ok && !record.Expired() {
c.log(record.IPInfo, record.allow)
c.logAndNotify(record.IPInfo, record.allow)
return record.allow
}
ipAndStr := &maxmind.IPInfo{IP: ip, Str: ipStr}
if c.Allow.Match(ipAndStr) {
c.log(ipAndStr, true)
c.logAndNotify(ipAndStr, true)
c.cacheRecord(ipAndStr, true)
return true
}
if c.Deny.Match(ipAndStr) {
c.log(ipAndStr, false)
c.logAndNotify(ipAndStr, false)
c.cacheRecord(ipAndStr, false)
return false
}
c.log(ipAndStr, c.defaultAllow)
c.logAndNotify(ipAndStr, c.defaultAllow)
c.cacheRecord(ipAndStr, c.defaultAllow)
return c.defaultAllow
}

View File

@@ -4,8 +4,8 @@ import (
"net"
"strings"
"github.com/yusing/go-proxy/internal/gperr"
"github.com/yusing/go-proxy/internal/maxmind"
"github.com/yusing/godoxy/internal/maxmind"
gperr "github.com/yusing/goutils/errs"
)
type MatcherFunc func(*maxmind.IPInfo) bool

View File

@@ -5,8 +5,8 @@ import (
"reflect"
"testing"
maxmind "github.com/yusing/go-proxy/internal/maxmind/types"
"github.com/yusing/go-proxy/internal/serialization"
maxmind "github.com/yusing/godoxy/internal/maxmind/types"
"github.com/yusing/godoxy/internal/serialization"
)
func TestMatchers(t *testing.T) {

View File

@@ -1,6 +1,7 @@
package acl
import (
"errors"
"io"
"net"
"time"
@@ -54,6 +55,21 @@ func (s *TCPListener) Accept() (net.Conn, error) {
return c, nil
}
type tcpListener interface {
SetDeadline(t time.Time) error
}
var _ tcpListener = (*net.TCPListener)(nil)
func (s *TCPListener) SetDeadline(t time.Time) error {
switch lis := s.lis.(type) {
case tcpListener:
return lis.SetDeadline(t)
default:
return errors.New("not a TCPListener")
}
}
func (s *TCPListener) Close() error {
return s.lis.Close()
}

View File

@@ -1,6 +1,7 @@
package acl
import (
"errors"
"net"
"time"
)
@@ -74,6 +75,31 @@ func (s *UDPListener) SetWriteDeadline(t time.Time) error {
return s.lis.SetWriteDeadline(t)
}
type udpListener interface {
SetReadBuffer(bytes int) error
SetWriteBuffer(bytes int) error
}
var _ udpListener = (*net.UDPConn)(nil)
func (s *UDPListener) SetReadBuffer(bytes int) error {
switch lis := s.lis.(type) {
case udpListener:
return lis.SetReadBuffer(bytes)
default:
return errors.New("not a UDPConn")
}
}
func (s *UDPListener) SetWriteBuffer(bytes int) error {
switch lis := s.lis.(type) {
case udpListener:
return lis.SetWriteBuffer(bytes)
default:
return errors.New("not a UDPConn")
}
}
func (s *UDPListener) Close() error {
return s.lis.Close()
}

View File

@@ -1,111 +1,207 @@
package api
import (
"fmt"
"net/http"
"reflect"
v1 "github.com/yusing/go-proxy/internal/api/v1"
"github.com/yusing/go-proxy/internal/api/v1/certapi"
"github.com/yusing/go-proxy/internal/api/v1/dockerapi"
"github.com/yusing/go-proxy/internal/api/v1/favicon"
"github.com/yusing/go-proxy/internal/auth"
config "github.com/yusing/go-proxy/internal/config/types"
"github.com/yusing/go-proxy/internal/logging/memlogger"
"github.com/yusing/go-proxy/internal/metrics/uptime"
"github.com/yusing/go-proxy/internal/net/gphttp/gpwebsocket"
"github.com/yusing/go-proxy/internal/net/gphttp/httpheaders"
"github.com/yusing/go-proxy/internal/utils/strutils"
"github.com/yusing/go-proxy/pkg"
"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"
agentApi "github.com/yusing/godoxy/internal/api/v1/agent"
authApi "github.com/yusing/godoxy/internal/api/v1/auth"
certApi "github.com/yusing/godoxy/internal/api/v1/cert"
dockerApi "github.com/yusing/godoxy/internal/api/v1/docker"
fileApi "github.com/yusing/godoxy/internal/api/v1/file"
homepageApi "github.com/yusing/godoxy/internal/api/v1/homepage"
metricsApi "github.com/yusing/godoxy/internal/api/v1/metrics"
routeApi "github.com/yusing/godoxy/internal/api/v1/route"
"github.com/yusing/godoxy/internal/auth"
"github.com/yusing/godoxy/internal/common"
apitypes "github.com/yusing/goutils/apitypes"
gperr "github.com/yusing/goutils/errs"
)
type (
ServeMux struct {
*http.ServeMux
cfg config.ConfigInstance
// @title GoDoxy API
// @version 1.0
// @description GoDoxy API
// @termsOfService https://github.com/yusing/godoxy/blob/main/LICENSE
// @contact.name Yusing
// @contact.url https://github.com/yusing/godoxy/issues
// @license.name MIT
// @license.url https://github.com/yusing/godoxy/blob/main/LICENSE
// @BasePath /api/v1
// @externalDocs.description GoDoxy Docs
// @externalDocs.url https://docs.godoxy.dev
func NewHandler() *gin.Engine {
if !common.IsDebug {
gin.SetMode("release")
}
WithCfgHandler = func(config.ConfigInstance, http.ResponseWriter, *http.Request)
)
r := gin.New()
r.Use(ErrorHandler())
r.Use(ErrorLoggingMiddleware())
r.Use(NoCache())
func (mux ServeMux) HandleFunc(methods, endpoint string, h any, requireAuth ...bool) {
var handler http.HandlerFunc
switch h := h.(type) {
case func(http.ResponseWriter, *http.Request):
handler = h
case http.Handler:
handler = h.ServeHTTP
case WithCfgHandler:
handler = func(w http.ResponseWriter, r *http.Request) {
h(mux.cfg, w, r)
log.Debug().Msg("gin codec json.API: " + reflect.TypeOf(json.API).Name())
r.GET("/api/v1/version", apiV1.Version)
if auth.IsEnabled() {
v1Auth := r.Group("/api/v1/auth")
{
v1Auth.HEAD("/check", authApi.Check)
v1Auth.POST("/login", authApi.Login)
v1Auth.GET("/callback", authApi.Callback)
v1Auth.POST("/callback", authApi.Callback)
v1Auth.POST("/logout", authApi.Logout)
v1Auth.GET("/logout", authApi.Logout)
}
default:
panic(fmt.Errorf("unsupported handler type: %T", h))
}
matchDomains := mux.cfg.Value().MatchDomains
if len(matchDomains) > 0 {
origHandler := handler
handler = func(w http.ResponseWriter, r *http.Request) {
if httpheaders.IsWebsocket(r.Header) {
gpwebsocket.SetWebsocketAllowedDomains(r.Header, matchDomains)
v1 := r.Group("/api/v1")
if auth.IsEnabled() {
v1.Use(AuthMiddleware())
}
if common.APISkipOriginCheck {
v1.Use(SkipOriginCheckMiddleware())
}
{
// enable cache for favicon
v1.GET("/favicon", apiV1.FavIcon)
v1.GET("/health", apiV1.Health)
v1.GET("/icons", apiV1.Icons)
v1.POST("/reload", apiV1.Reload)
v1.GET("/stats", apiV1.Stats)
route := v1.Group("/route")
{
route.GET("/list", routeApi.Routes)
route.GET("/:which", routeApi.Route)
route.GET("/providers", routeApi.Providers)
route.GET("/by_provider", routeApi.ByProvider)
route.POST("/playground", routeApi.Playground)
}
file := v1.Group("/file")
{
file.GET("/list", fileApi.List)
file.GET("/content", fileApi.Get)
file.PUT("/content", fileApi.Set)
file.POST("/content", fileApi.Set)
file.POST("/validate", fileApi.Validate)
}
homepage := v1.Group("/homepage")
{
homepage.GET("/categories", homepageApi.Categories)
homepage.GET("/items", homepageApi.Items)
homepage.POST("/set/item", homepageApi.SetItem)
homepage.POST("/set/items_batch", homepageApi.SetItemsBatch)
homepage.POST("/set/item_visible", homepageApi.SetItemVisible)
homepage.POST("/set/item_favorite", homepageApi.SetItemFavorite)
homepage.POST("/set/item_sort_order", homepageApi.SetItemSortOrder)
homepage.POST("/set/item_all_sort_order", homepageApi.SetItemAllSortOrder)
homepage.POST("/set/item_fav_sort_order", homepageApi.SetItemFavSortOrder)
homepage.POST("/set/category_order", homepageApi.SetCategoryOrder)
homepage.POST("/item_click", homepageApi.ItemClick)
}
cert := v1.Group("/cert")
{
cert.GET("/info", certApi.Info)
cert.GET("/renew", certApi.Renew)
}
agent := v1.Group("/agent")
{
agent.GET("/list", agentApi.List)
agent.POST("/create", agentApi.Create)
agent.POST("/verify", agentApi.Verify)
}
metrics := v1.Group("/metrics")
{
metrics.GET("/system_info", metricsApi.SystemInfo)
metrics.GET("/all_system_info", metricsApi.AllSystemInfo)
metrics.GET("/uptime", metricsApi.Uptime)
}
docker := v1.Group("/docker")
{
docker.GET("/container/:id", dockerApi.GetContainer)
docker.GET("/containers", dockerApi.Containers)
docker.GET("/info", dockerApi.Info)
docker.GET("/logs/:id", dockerApi.Logs)
docker.POST("/start", dockerApi.Start)
docker.POST("/stop", dockerApi.Stop)
docker.POST("/restart", dockerApi.Restart)
}
}
return r
}
func NoCache() gin.HandlerFunc {
return func(c *gin.Context) {
// skip cache if Cache-Control header is set
if c.Writer.Header().Get("Cache-Control") == "" {
c.Header("Cache-Control", "no-cache, no-store, must-revalidate")
c.Header("Pragma", "no-cache")
c.Header("Expires", "0")
}
c.Next()
}
}
func AuthMiddleware() gin.HandlerFunc {
return func(c *gin.Context) {
err := auth.GetDefaultAuth().CheckToken(c.Request)
if err != nil {
c.JSON(http.StatusUnauthorized, apitypes.Error("Unauthorized", err))
c.Abort()
return
}
c.Next()
}
}
func SkipOriginCheckMiddleware() gin.HandlerFunc {
upgrader := &websocket.Upgrader{
CheckOrigin: func(r *http.Request) bool {
return true
},
}
return func(c *gin.Context) {
c.Set("upgrader", upgrader)
c.Next()
}
}
func ErrorHandler() gin.HandlerFunc {
return func(c *gin.Context) {
c.Next()
if len(c.Errors) > 0 {
logger := log.With().Str("uri", c.Request.RequestURI).Logger()
for _, err := range c.Errors {
gperr.LogError("Internal error", err.Err, &logger)
}
if !c.IsWebsocket() {
c.JSON(http.StatusInternalServerError, apitypes.Error("Internal server error"))
}
origHandler(w, r)
}
}
if len(requireAuth) > 0 && requireAuth[0] {
handler = auth.RequireAuth(handler)
}
if methods == "" {
mux.ServeMux.HandleFunc(endpoint, handler)
} else {
for _, m := range strutils.CommaSeperatedList(methods) {
mux.ServeMux.HandleFunc(m+" "+endpoint, handler)
}
}
}
func NewHandler(cfg config.ConfigInstance) http.Handler {
mux := ServeMux{http.NewServeMux(), cfg}
mux.HandleFunc("GET", "/v1", v1.Index)
mux.HandleFunc("GET", "/v1/version", pkg.GetVersionHTTPHandler())
mux.HandleFunc("GET", "/v1/stats", v1.Stats, true)
mux.HandleFunc("POST", "/v1/reload", v1.Reload, true)
mux.HandleFunc("GET", "/v1/list", v1.ListRoutesHandler, true)
mux.HandleFunc("GET", "/v1/list/routes", v1.ListRoutesHandler, true)
mux.HandleFunc("GET", "/v1/list/route/{which}", v1.ListRouteHandler, true)
mux.HandleFunc("GET", "/v1/list/routes_by_provider", v1.ListRoutesByProviderHandler, true)
mux.HandleFunc("GET", "/v1/list/files", v1.ListFilesHandler, true)
mux.HandleFunc("GET", "/v1/list/homepage_config", v1.ListHomepageConfigHandler, true)
mux.HandleFunc("GET", "/v1/list/route_providers", v1.ListRouteProvidersHandler, true)
mux.HandleFunc("GET", "/v1/list/homepage_categories", v1.ListHomepageCategoriesHandler, true)
mux.HandleFunc("GET", "/v1/list/icons", v1.ListIconsHandler, true)
mux.HandleFunc("GET", "/v1/file/{type}/{filename}", v1.GetFileContent, true)
mux.HandleFunc("POST,PUT", "/v1/file/{type}/{filename}", v1.SetFileContent, true)
mux.HandleFunc("POST", "/v1/file/validate/{type}", v1.ValidateFile, true)
mux.HandleFunc("GET", "/v1/health", v1.Health, true)
mux.HandleFunc("GET", "/v1/logs", memlogger.Handler(), true)
mux.HandleFunc("GET", "/v1/favicon", favicon.GetFavIcon, true)
mux.HandleFunc("POST", "/v1/homepage/set", v1.SetHomePageOverrides, true)
mux.HandleFunc("GET", "/v1/agents", v1.ListAgents, true)
mux.HandleFunc("GET", "/v1/agents/new", v1.NewAgent, true)
mux.HandleFunc("POST", "/v1/agents/verify", v1.VerifyNewAgent, true)
mux.HandleFunc("GET", "/v1/metrics/system_info", v1.SystemInfo, true)
mux.HandleFunc("GET", "/v1/metrics/uptime", uptime.Poller.ServeHTTP, true)
mux.HandleFunc("GET", "/v1/cert/info", certapi.GetCertInfo, true)
mux.HandleFunc("", "/v1/cert/renew", certapi.RenewCert, true)
mux.HandleFunc("GET", "/v1/docker/info", dockerapi.DockerInfo, true)
mux.HandleFunc("GET", "/v1/docker/logs/{server}/{container}", dockerapi.Logs, true)
mux.HandleFunc("GET", "/v1/docker/containers", dockerapi.Containers, true)
defaultAuth := auth.GetDefaultAuth()
if defaultAuth == nil {
return mux
}
mux.HandleFunc("GET", "/v1/auth/check", auth.AuthCheckHandler)
mux.HandleFunc("GET,POST", "/v1/auth/redirect", defaultAuth.LoginHandler)
mux.HandleFunc("GET,POST", "/v1/auth/callback", defaultAuth.PostAuthCallbackHandler)
mux.HandleFunc("GET,POST", "/v1/auth/logout", defaultAuth.LogoutHandler)
return mux
func ErrorLoggingMiddleware() gin.HandlerFunc {
return gin.CustomRecoveryWithWriter(nil, func(c *gin.Context, err any) {
log.Error().Any("error", err).Str("uri", c.Request.RequestURI).Msg("Internal error")
if !c.IsWebsocket() {
c.JSON(http.StatusInternalServerError, apitypes.Error("Internal server error"))
}
})
}

View File

@@ -0,0 +1,67 @@
package agentapi
import (
"crypto/rand"
"encoding/base64"
"sync/atomic"
"time"
"github.com/rs/zerolog/log"
"github.com/yusing/godoxy/agent/pkg/agent"
)
type PEMPairResponse struct {
Cert string `json:"cert" format:"base64"`
Key string `json:"key" format:"base64"`
} // @name PEMPairResponse
var encryptionKey atomic.Value
const rotateKeyInterval = 15 * time.Minute
func init() {
if err := rotateKey(); err != nil {
log.Panic().Err(err).Msg("failed to generate encryption key")
}
go func() {
for range time.Tick(rotateKeyInterval) {
if err := rotateKey(); err != nil {
log.Error().Err(err).Msg("failed to rotate encryption key")
}
}
}()
}
func getEncryptionKey() []byte {
return encryptionKey.Load().([]byte)
}
func rotateKey() error {
// generate a random 32 bytes key
key := make([]byte, 32)
if _, err := rand.Read(key); err != nil {
return err
}
encryptionKey.Store(key)
return nil
}
func toPEMPairResponse(encPEMPair agent.PEMPair) PEMPairResponse {
return PEMPairResponse{
Cert: base64.StdEncoding.EncodeToString(encPEMPair.Cert),
Key: base64.StdEncoding.EncodeToString(encPEMPair.Key),
}
}
func fromEncryptedPEMPairResponse(pemPair PEMPairResponse) (agent.PEMPair, error) {
encCert, err := base64.StdEncoding.DecodeString(pemPair.Cert)
if err != nil {
return agent.PEMPair{}, err
}
encKey, err := base64.StdEncoding.DecodeString(pemPair.Key)
if err != nil {
return agent.PEMPair{}, err
}
pair := agent.PEMPair{Cert: encCert, Key: encKey}
return pair.Decrypt(getEncryptionKey())
}

View File

@@ -0,0 +1,107 @@
package agentapi
import (
"net"
"net/http"
"strconv"
_ "embed"
"github.com/gin-gonic/gin"
"github.com/yusing/godoxy/agent/pkg/agent"
apitypes "github.com/yusing/goutils/apitypes"
)
type NewAgentRequest struct {
Name string `json:"name" binding:"required"`
Host string `json:"host" binding:"required"`
Port int `json:"port" binding:"required,min=1,max=65535"`
Type string `json:"type" binding:"required,oneof=docker system"`
Nightly bool `json:"nightly" binding:"omitempty"`
ContainerRuntime agent.ContainerRuntime `json:"container_runtime" binding:"omitempty,oneof=docker podman" default:"docker"`
} // @name NewAgentRequest
type NewAgentResponse struct {
Compose string `json:"compose"`
CA PEMPairResponse `json:"ca"`
Client PEMPairResponse `json:"client"`
} // @name NewAgentResponse
// @x-id "create"
// @BasePath /api/v1
// @Summary Create a new agent
// @Description Create a new agent and return the docker compose file, encrypted CA and client PEMs
// @Description The returned PEMs are encrypted with a random key and will be used for verification when adding a new agent
// @Tags agent
// @Accept json
// @Produce json
// @Param request body NewAgentRequest true "Request"
// @Success 200 {object} NewAgentResponse
// @Failure 400 {object} apitypes.ErrorResponse
// @Failure 403 {object} apitypes.ErrorResponse
// @Failure 409 {object} apitypes.ErrorResponse
// @Failure 500 {object} apitypes.ErrorResponse
// @Router /agent/create [post]
func Create(c *gin.Context) {
var request NewAgentRequest
if err := c.ShouldBindJSON(&request); err != nil {
c.JSON(http.StatusBadRequest, apitypes.Error("invalid request", err))
return
}
hostport := net.JoinHostPort(request.Host, strconv.Itoa(request.Port))
if _, ok := agent.GetAgent(hostport); ok {
c.JSON(http.StatusConflict, apitypes.Error("agent already exists"))
return
}
var image string
if request.Nightly {
image = agent.DockerImageNightly
} else {
image = agent.DockerImageProduction
}
ca, srv, client, err := agent.NewAgent()
if err != nil {
c.Error(apitypes.InternalServerError(err, "failed to create agent"))
return
}
var cfg agent.Generator = &agent.AgentEnvConfig{
Name: request.Name,
Port: request.Port,
CACert: ca.String(),
SSLCert: srv.String(),
ContainerRuntime: request.ContainerRuntime,
}
if request.Type == "docker" {
cfg = &agent.AgentComposeConfig{
Image: image,
AgentEnvConfig: cfg.(*agent.AgentEnvConfig),
}
}
template, err := cfg.Generate()
if err != nil {
c.Error(apitypes.InternalServerError(err, "failed to generate agent config"))
return
}
key := getEncryptionKey()
encCA, err := ca.Encrypt(key)
if err != nil {
c.Error(apitypes.InternalServerError(err, "failed to encrypt CA PEMs"))
return
}
encClient, err := client.Encrypt(key)
if err != nil {
c.Error(apitypes.InternalServerError(err, "failed to encrypt client PEMs"))
return
}
c.JSON(http.StatusOK, NewAgentResponse{
Compose: template,
CA: toPEMPairResponse(encCA),
Client: toPEMPairResponse(encClient),
})
}

View File

@@ -0,0 +1,33 @@
package agentapi
import (
"net/http"
"time"
"github.com/gin-gonic/gin"
"github.com/yusing/godoxy/agent/pkg/agent"
"github.com/yusing/goutils/http/httpheaders"
"github.com/yusing/goutils/http/websocket"
_ "github.com/yusing/goutils/apitypes"
)
// @x-id "list"
// @BasePath /api/v1
// @Summary List agents
// @Description List agents
// @Tags agent,websocket
// @Accept json
// @Produce json
// @Success 200 {array} Agent
// @Failure 403 {object} apitypes.ErrorResponse
// @Router /agent/list [get]
func List(c *gin.Context) {
if httpheaders.IsWebsocket(c.Request.Header) {
websocket.PeriodicWrite(c, 10*time.Second, func() (any, error) {
return agent.ListAgents(), nil
})
} else {
c.JSON(http.StatusOK, agent.ListAgents())
}
}

View File

@@ -0,0 +1,114 @@
package agentapi
import (
"fmt"
"net/http"
"os"
"github.com/gin-gonic/gin"
"github.com/yusing/godoxy/agent/pkg/agent"
"github.com/yusing/godoxy/agent/pkg/certs"
config "github.com/yusing/godoxy/internal/config/types"
"github.com/yusing/godoxy/internal/route/provider"
apitypes "github.com/yusing/goutils/apitypes"
gperr "github.com/yusing/goutils/errs"
)
type VerifyNewAgentRequest struct {
Host string `json:"host"`
CA PEMPairResponse `json:"ca"`
Client PEMPairResponse `json:"client"`
ContainerRuntime agent.ContainerRuntime `json:"container_runtime"`
} // @name VerifyNewAgentRequest
// @x-id "verify"
// @BasePath /api/v1
// @Summary Verify a new agent
// @Description Verify a new agent and return the number of routes added
// @Tags agent
// @Accept json
// @Produce json
// @Param request body VerifyNewAgentRequest true "Request"
// @Success 200 {object} SuccessResponse
// @Failure 400 {object} ErrorResponse
// @Failure 403 {object} ErrorResponse
// @Failure 500 {object} ErrorResponse
// @Router /agent/verify [post]
func Verify(c *gin.Context) {
var request VerifyNewAgentRequest
if err := c.ShouldBindJSON(&request); err != nil {
c.JSON(http.StatusBadRequest, apitypes.Error("invalid request", err))
return
}
filename, ok := certs.AgentCertsFilepath(request.Host)
if !ok {
c.JSON(http.StatusBadRequest, apitypes.Error("invalid host", nil))
return
}
ca, err := fromEncryptedPEMPairResponse(request.CA)
if err != nil {
c.JSON(http.StatusBadRequest, apitypes.Error("invalid CA", err))
return
}
client, err := fromEncryptedPEMPairResponse(request.Client)
if err != nil {
c.JSON(http.StatusBadRequest, apitypes.Error("invalid client", err))
return
}
nRoutesAdded, err := verifyNewAgent(request.Host, ca, client, request.ContainerRuntime)
if err != nil {
c.JSON(http.StatusBadRequest, apitypes.Error("invalid request", err))
return
}
zip, err := certs.ZipCert(ca.Cert, client.Cert, client.Key)
if err != nil {
c.Error(apitypes.InternalServerError(err, "failed to zip certs"))
return
}
if err := os.WriteFile(filename, zip, 0o600); err != nil {
c.Error(apitypes.InternalServerError(err, "failed to write certs"))
return
}
c.JSON(http.StatusOK, apitypes.Success(fmt.Sprintf("Added %d routes", nRoutesAdded)))
}
func verifyNewAgent(host string, ca agent.PEMPair, client agent.PEMPair, containerRuntime agent.ContainerRuntime) (int, gperr.Error) {
cfgState := config.ActiveState.Load()
for _, a := range cfgState.Value().Providers.Agents {
if a.Addr == host {
return 0, gperr.New("agent already exists")
}
}
var agentCfg agent.AgentConfig
agentCfg.Addr = host
agentCfg.Runtime = containerRuntime
err := agentCfg.StartWithCerts(cfgState.Context(), ca.Cert, client.Cert, client.Key)
if err != nil {
return 0, gperr.Wrap(err, "failed to start agent")
}
provider := provider.NewAgentProvider(&agentCfg)
if _, loaded := cfgState.LoadOrStoreProvider(provider.String(), provider); loaded {
return 0, gperr.Errorf("provider %s already exists", provider.String())
}
// agent must be added before loading routes
agent.AddAgent(&agentCfg)
err = provider.LoadRoutes()
if err != nil {
cfgState.DeleteProvider(provider.String())
agent.RemoveAgent(&agentCfg)
return 0, gperr.Wrap(err, "failed to load routes")
}
return provider.NumRoutes(), nil
}

View File

@@ -0,0 +1,24 @@
//nolint:dupword
package auth
import (
"github.com/gin-gonic/gin"
"github.com/yusing/godoxy/internal/auth"
)
// @x-id "callback"
// @Base /api/v1
// @Summary Auth Callback
// @Description Handles the callback from the provider after successful authentication
// @Tags auth
// @Produce plain
// @Param body body auth.UserPassAuthCallbackRequest true "Userpass only"
// @Success 200 {string} string "Userpass: OK"
// @Success 302 {string} string "OIDC: Redirects to home page"
// @Failure 400 {string} string "OIDC: invalid request (missing state cookie or oauth state)"
// @Failure 400 {string} string "Userpass: invalid request / credentials"
// @Failure 500 {string} string "Internal server error"
// @Router /auth/callback [post]
func Callback(c *gin.Context) {
auth.GetDefaultAuth().PostAuthCallbackHandler(c.Writer, c.Request)
}

View File

@@ -0,0 +1,19 @@
package auth
import (
"github.com/gin-gonic/gin"
"github.com/yusing/godoxy/internal/auth"
)
// @x-id "check"
// @Base /api/v1
// @Summary Check authentication status
// @Description Checks if the user is authenticated by validating their token
// @Tags auth
// @Produce plain
// @Success 200 {string} string "OK"
// @Failure 302 {string} string "Redirects to login page or IdP"
// @Router /auth/check [head]
func Check(c *gin.Context) {
auth.AuthCheckHandler(c.Writer, c.Request)
}

View File

@@ -0,0 +1,19 @@
package auth
import (
"github.com/gin-gonic/gin"
"github.com/yusing/godoxy/internal/auth"
)
// @x-id "login"
// @Base /api/v1
// @Summary Login
// @Description Initiates the login process by redirecting the user to the provider's login page
// @Tags auth
// @Produce plain
// @Success 302 {string} string "Redirects to login page or IdP"
// @Failure 429 {string} string "Too Many Requests"
// @Router /auth/login [post]
func Login(c *gin.Context) {
auth.GetDefaultAuth().LoginHandler(c.Writer, c.Request)
}

View File

@@ -0,0 +1,19 @@
package auth
import (
"github.com/gin-gonic/gin"
"github.com/yusing/godoxy/internal/auth"
)
// @x-id "logout"
// @Base /api/v1
// @Summary Logout
// @Description Logs out the user by invalidating the token
// @Tags auth
// @Produce plain
// @Success 302 {string} string "Redirects to home page"
// @Router /auth/logout [post]
// @Router /auth/logout [get]
func Logout(c *gin.Context) {
auth.GetDefaultAuth().LogoutHandler(c.Writer, c.Request)
}

View File

@@ -0,0 +1,53 @@
package certapi
import (
"net/http"
"github.com/gin-gonic/gin"
"github.com/yusing/godoxy/internal/autocert"
apitypes "github.com/yusing/goutils/apitypes"
)
type CertInfo struct {
Subject string `json:"subject"`
Issuer string `json:"issuer"`
NotBefore int64 `json:"not_before"`
NotAfter int64 `json:"not_after"`
DNSNames []string `json:"dns_names"`
EmailAddresses []string `json:"email_addresses"`
} // @name CertInfo
// @x-id "info"
// @BasePath /api/v1
// @Summary Get cert info
// @Description Get cert info
// @Tags cert
// @Produce json
// @Success 200 {object} CertInfo
// @Failure 403 {object} apitypes.ErrorResponse
// @Failure 404 {object} apitypes.ErrorResponse
// @Failure 500 {object} apitypes.ErrorResponse
// @Router /cert/info [get]
func Info(c *gin.Context) {
autocert := autocert.ActiveProvider.Load()
if autocert == nil {
c.JSON(http.StatusNotFound, apitypes.Error("autocert is not enabled"))
return
}
cert, err := autocert.GetCert(nil)
if err != nil {
c.Error(apitypes.InternalServerError(err, "failed to get cert info"))
return
}
certInfo := CertInfo{
Subject: cert.Leaf.Subject.CommonName,
Issuer: cert.Leaf.Issuer.CommonName,
NotBefore: cert.Leaf.NotBefore.Unix(),
NotAfter: cert.Leaf.NotAfter.Unix(),
DNSNames: cert.Leaf.DNSNames,
EmailAddresses: cert.Leaf.EmailAddresses,
}
c.JSON(http.StatusOK, certInfo)
}

View File

@@ -0,0 +1,72 @@
package certapi
import (
"net/http"
"time"
"github.com/gin-gonic/gin"
"github.com/rs/zerolog/log"
"github.com/yusing/godoxy/internal/autocert"
"github.com/yusing/godoxy/internal/logging/memlogger"
apitypes "github.com/yusing/goutils/apitypes"
gperr "github.com/yusing/goutils/errs"
"github.com/yusing/goutils/http/websocket"
)
// @x-id "renew"
// @BasePath /api/v1
// @Summary Renew cert
// @Description Renew cert
// @Tags cert,websocket
// @Produce plain
// @Success 200 {object} apitypes.SuccessResponse
// @Failure 403 {object} apitypes.ErrorResponse
// @Failure 500 {object} apitypes.ErrorResponse
// @Router /cert/renew [get]
func Renew(c *gin.Context) {
autocert := autocert.ActiveProvider.Load()
if autocert == nil {
c.JSON(http.StatusNotFound, apitypes.Error("autocert is not enabled"))
return
}
manager, err := websocket.NewManagerWithUpgrade(c)
if err != nil {
c.Error(apitypes.InternalServerError(err, "failed to create websocket manager"))
return
}
defer manager.Close()
logs, cancel := memlogger.Events()
defer cancel()
done := make(chan struct{})
go func() {
defer close(done)
err = autocert.ObtainCert()
if err != nil {
gperr.LogError("failed to obtain cert", err)
_ = manager.WriteData(websocket.TextMessage, []byte(err.Error()), 10*time.Second)
} else {
log.Info().Msg("cert obtained successfully")
}
}()
for {
select {
case l := <-logs:
if err != nil {
return
}
err = manager.WriteData(websocket.TextMessage, l, 10*time.Second)
if err != nil {
return
}
case <-done:
return
}
}
}

View File

@@ -1,41 +0,0 @@
package certapi
import (
"encoding/json"
"net/http"
config "github.com/yusing/go-proxy/internal/config/types"
)
type CertInfo struct {
Subject string `json:"subject"`
Issuer string `json:"issuer"`
NotBefore int64 `json:"not_before"`
NotAfter int64 `json:"not_after"`
DNSNames []string `json:"dns_names"`
EmailAddresses []string `json:"email_addresses"`
}
func GetCertInfo(w http.ResponseWriter, r *http.Request) {
autocert := config.GetInstance().AutoCertProvider()
if autocert == nil {
http.Error(w, "autocert is not enabled", http.StatusNotFound)
return
}
cert, err := autocert.GetCert(nil)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
certInfo := CertInfo{
Subject: cert.Leaf.Subject.CommonName,
Issuer: cert.Leaf.Issuer.CommonName,
NotBefore: cert.Leaf.NotBefore.Unix(),
NotAfter: cert.Leaf.NotAfter.Unix(),
DNSNames: cert.Leaf.DNSNames,
EmailAddresses: cert.Leaf.EmailAddresses,
}
json.NewEncoder(w).Encode(&certInfo)
}

View File

@@ -1,54 +0,0 @@
package certapi
import (
"net/http"
"github.com/rs/zerolog/log"
config "github.com/yusing/go-proxy/internal/config/types"
"github.com/yusing/go-proxy/internal/gperr"
"github.com/yusing/go-proxy/internal/logging/memlogger"
"github.com/yusing/go-proxy/internal/net/gphttp/gpwebsocket"
)
func RenewCert(w http.ResponseWriter, r *http.Request) {
autocert := config.GetInstance().AutoCertProvider()
if autocert == nil {
http.Error(w, "autocert is not enabled", http.StatusNotFound)
return
}
conn, err := gpwebsocket.Initiate(w, r)
if err != nil {
return
}
defer conn.Close()
logs, cancel := memlogger.Events()
defer cancel()
done := make(chan struct{})
go func() {
defer close(done)
err = autocert.ObtainCert()
if err != nil {
gperr.LogError("failed to obtain cert", err)
_ = gpwebsocket.WriteText(conn, err.Error())
} else {
log.Info().Msg("cert obtained successfully")
}
}()
for {
select {
case l := <-logs:
if err != nil {
return
}
if err := gpwebsocket.WriteText(conn, string(l)); err != nil {
return
}
case <-done:
return
}
}
}

View File

@@ -1,133 +0,0 @@
package v1
import (
"fmt"
"io"
"net/http"
"os"
"path"
"strings"
"github.com/yusing/go-proxy/internal/common"
config "github.com/yusing/go-proxy/internal/config/types"
"github.com/yusing/go-proxy/internal/gperr"
"github.com/yusing/go-proxy/internal/net/gphttp"
"github.com/yusing/go-proxy/internal/net/gphttp/middleware"
"github.com/yusing/go-proxy/internal/route/provider"
)
type FileType string
const (
FileTypeConfig FileType = "config"
FileTypeProvider FileType = "provider"
FileTypeMiddleware FileType = "middleware"
)
func fileType(file string) FileType {
switch {
case strings.HasPrefix(path.Base(file), "config."):
return FileTypeConfig
case strings.HasPrefix(file, common.MiddlewareComposeBasePath):
return FileTypeMiddleware
}
return FileTypeProvider
}
func (t FileType) IsValid() bool {
switch t {
case FileTypeConfig, FileTypeProvider, FileTypeMiddleware:
return true
}
return false
}
func (t FileType) GetPath(filename string) string {
if t == FileTypeMiddleware {
return path.Join(common.MiddlewareComposeBasePath, filename)
}
return path.Join(common.ConfigBasePath, filename)
}
func getArgs(r *http.Request) (fileType FileType, filename string, err error) {
fileType = FileType(r.PathValue("type"))
if !fileType.IsValid() {
err = fmt.Errorf("invalid file type: %s", fileType)
return
}
filename = r.PathValue("filename")
if filename == "" {
err = fmt.Errorf("missing filename")
}
return
}
func GetFileContent(w http.ResponseWriter, r *http.Request) {
fileType, filename, err := getArgs(r)
if err != nil {
gphttp.BadRequest(w, err.Error())
return
}
content, err := os.ReadFile(fileType.GetPath(filename))
if err != nil {
gphttp.ServerError(w, r, err)
return
}
gphttp.WriteBody(w, content)
}
func validateFile(fileType FileType, content []byte) gperr.Error {
switch fileType {
case FileTypeConfig:
return config.Validate(content)
case FileTypeMiddleware:
errs := gperr.NewBuilder("middleware errors")
middleware.BuildMiddlewaresFromYAML("", content, errs)
return errs.Error()
}
return provider.Validate(content)
}
func ValidateFile(w http.ResponseWriter, r *http.Request) {
fileType := FileType(r.PathValue("type"))
if !fileType.IsValid() {
gphttp.BadRequest(w, "invalid file type")
return
}
content, err := io.ReadAll(r.Body)
if err != nil {
gphttp.ServerError(w, r, err)
return
}
r.Body.Close()
if valErr := validateFile(fileType, content); valErr != nil {
gphttp.JSONError(w, valErr, http.StatusBadRequest)
return
}
w.WriteHeader(http.StatusOK)
}
func SetFileContent(w http.ResponseWriter, r *http.Request) {
fileType, filename, err := getArgs(r)
if err != nil {
gphttp.BadRequest(w, err.Error())
return
}
content, err := io.ReadAll(r.Body)
if err != nil {
gphttp.ServerError(w, r, err)
return
}
if valErr := validateFile(fileType, content); valErr != nil {
gphttp.JSONError(w, valErr, http.StatusBadRequest)
return
}
err = os.WriteFile(fileType.GetPath(filename), content, 0o644)
if err != nil {
gphttp.ServerError(w, r, err)
return
}
w.WriteHeader(http.StatusOK)
}

View File

@@ -0,0 +1,64 @@
package dockerapi
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"
)
// @x-id "container"
// @BasePath /api/v1
// @Summary Get container
// @Description Get container by container id
// @Tags docker
// @Produce json
// @Param id path string true "Container ID"
// @Success 200 {object} Container
// @Failure 400 {object} apitypes.ErrorResponse "ID is required"
// @Failure 403 {object} apitypes.ErrorResponse
// @Failure 404 {object} apitypes.ErrorResponse "Container not found"
// @Failure 500 {object} apitypes.ErrorResponse
// @Router /docker/container/{id} [get]
func GetContainer(c *gin.Context) {
id := c.Param("id")
if id == "" {
c.JSON(http.StatusBadRequest, apitypes.Error("id is required"))
return
}
dockerHost, ok := docker.GetDockerHostByContainerID(id)
if !ok {
c.JSON(http.StatusNotFound, apitypes.Error("container not found"))
return
}
dockerClient, err := docker.NewClient(dockerHost)
if err != nil {
c.Error(apitypes.InternalServerError(err, "failed to create docker client"))
return
}
defer dockerClient.Close()
cont, err := dockerClient.ContainerInspect(c.Request.Context(), id, client.ContainerInspectOptions{})
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
}
c.JSON(http.StatusOK, &Container{
Server: dockerHost,
Name: cont.Container.Name,
ID: cont.Container.ID,
Image: cont.Container.Image,
State: state,
})
}

View File

@@ -0,0 +1,69 @@
package dockerapi
import (
"context"
"sort"
"github.com/gin-gonic/gin"
"github.com/moby/moby/api/types/container"
"github.com/moby/moby/client"
gperr "github.com/yusing/goutils/errs"
_ "github.com/yusing/goutils/apitypes"
)
type ContainerState = container.ContainerState // @name ContainerState
type Container struct {
Server string `json:"server"`
Name string `json:"name"`
ID string `json:"id"`
Image string `json:"image"`
State ContainerState `json:"state,omitempty" extensions:"x-nullable"`
} // @name ContainerResponse
// @x-id "containers"
// @BasePath /api/v1
// @Summary Get containers
// @Description Get containers
// @Tags docker
// @Produce json
// @Success 200 {array} Container
// @Failure 403 {object} apitypes.ErrorResponse
// @Failure 500 {object} apitypes.ErrorResponse
// @Router /docker/containers [get]
func Containers(c *gin.Context) {
serveHTTP[Container](c, GetContainers)
}
func GetContainers(ctx context.Context, dockerClients DockerClients) ([]Container, gperr.Error) {
errs := gperr.NewBuilder("failed to get containers")
containers := make([]Container, 0)
for server, dockerClient := range dockerClients {
conts, err := dockerClient.ContainerList(ctx, client.ContainerListOptions{All: true})
if err != nil {
errs.Add(err)
continue
}
for _, cont := range conts.Items {
containers = append(containers, Container{
Server: server,
Name: cont.Names[0],
ID: cont.ID,
Image: cont.Image,
State: cont.State,
})
}
}
sort.Slice(containers, func(i, j int) bool {
return containers[i].Name < containers[j].Name
})
if err := errs.Error(); err != nil {
gperr.LogError("failed to get containers", err)
if len(containers) == 0 {
return nil, err
}
return containers, nil
}
return containers, nil
}

View File

@@ -0,0 +1,82 @@
package dockerapi
import (
"context"
"sort"
"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"
_ "github.com/yusing/goutils/apitypes"
)
type containerStats struct {
Total int `json:"total"`
Running int `json:"running"`
Paused int `json:"paused"`
Stopped int `json:"stopped"`
} // @name ContainerStats
type dockerInfo struct {
Name string `json:"name"`
ServerVersion string `json:"version"`
Containers containerStats `json:"containers"`
Images int `json:"images"`
NCPU int `json:"n_cpu"`
MemTotal string `json:"memory"`
} // @name ServerInfo
func toDockerInfo(info dockerSystem.Info) dockerInfo {
return dockerInfo{
Name: info.Name,
ServerVersion: info.ServerVersion,
Containers: containerStats{
Total: info.ContainersRunning,
Running: info.ContainersRunning,
Paused: info.ContainersPaused,
Stopped: info.ContainersStopped,
},
Images: info.Images,
NCPU: info.NCPU,
MemTotal: strutils.FormatByteSize(info.MemTotal),
}
}
// @x-id "info"
// @BasePath /api/v1
// @Summary Get docker info
// @Description Get docker info
// @Tags docker
// @Produce json
// @Success 200 {object} dockerInfo
// @Failure 403 {object} apitypes.ErrorResponse
// @Failure 500 {object} apitypes.ErrorResponse
// @Router /docker/info [get]
func Info(c *gin.Context) {
serveHTTP[dockerInfo](c, GetDockerInfo)
}
func GetDockerInfo(ctx context.Context, dockerClients DockerClients) ([]dockerInfo, gperr.Error) {
errs := gperr.NewBuilder("failed to get docker info")
dockerInfos := make([]dockerInfo, len(dockerClients))
i := 0
for name, dockerClient := range dockerClients {
info, err := dockerClient.Info(ctx, client.InfoOptions{})
if err != nil {
errs.Add(err)
continue
}
info.Info.Name = name
dockerInfos[i] = toDockerInfo(info.Info)
i++
}
sort.Slice(dockerInfos, func(i, j int) bool {
return dockerInfos[i].Name < dockerInfos[j].Name
})
return dockerInfos, errs.Error()
}

View File

@@ -0,0 +1,112 @@
package dockerapi
import (
"context"
"errors"
"fmt"
"net/http"
"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"
"github.com/yusing/goutils/http/websocket"
"github.com/yusing/goutils/task"
)
type LogsQueryParams struct {
Stdout bool `form:"stdout,default=true"`
Stderr bool `form:"stderr,default=true"`
Since string `form:"from"`
Until string `form:"to"`
Levels string `form:"levels"`
} // @name LogsQueryParams
// @x-id "logs"
// @BasePath /api/v1
// @Summary Get docker container logs
// @Description Get docker container logs by container id
// @Tags docker,websocket
// @Accept json
// @Produce json
// @Param id path string true "container id"
// @Param stdout query bool false "show stdout"
// @Param stderr query bool false "show stderr"
// @Param from query string false "from timestamp"
// @Param to query string false "to timestamp"
// @Param levels query string false "levels"
// @Success 200
// @Failure 400 {object} apitypes.ErrorResponse
// @Failure 403 {object} apitypes.ErrorResponse
// @Failure 404 {object} apitypes.ErrorResponse "server not found or container not found"
// @Failure 500 {object} apitypes.ErrorResponse
// @Router /docker/logs/{id} [get]
func Logs(c *gin.Context) {
id := c.Param("id")
if id == "" {
c.JSON(http.StatusBadRequest, apitypes.Error("container id is required"))
return
}
var queryParams LogsQueryParams
if err := c.ShouldBindQuery(&queryParams); err != nil {
c.JSON(http.StatusBadRequest, apitypes.Error("invalid query params"))
return
}
// TODO: implement levels
dockerHost, ok := docker.GetDockerHostByContainerID(id)
if !ok {
c.JSON(http.StatusNotFound, apitypes.Error(fmt.Sprintf("container %s not found", id)))
return
}
dockerClient, err := docker.NewClient(dockerHost)
if err != nil {
c.Error(apitypes.InternalServerError(err, "failed to get docker client"))
return
}
defer dockerClient.Close()
opts := client.ContainerLogsOptions{
ShowStdout: queryParams.Stdout,
ShowStderr: queryParams.Stderr,
Since: queryParams.Since,
Until: queryParams.Until,
Timestamps: true,
Follow: true,
Tail: "100",
}
if queryParams.Levels != "" {
opts.Details = true
}
logs, err := dockerClient.ContainerLogs(c.Request.Context(), id, opts)
if err != nil {
c.Error(apitypes.InternalServerError(err, "failed to get container logs"))
return
}
defer logs.Close()
manager, err := websocket.NewManagerWithUpgrade(c)
if err != nil {
c.Error(apitypes.InternalServerError(err, "failed to create websocket manager"))
return
}
defer manager.Close()
writer := manager.NewWriter(websocket.TextMessage)
_, err = stdcopy.StdCopy(writer, writer, logs) // de-multiplex logs
if err != nil {
if errors.Is(err, context.Canceled) || errors.Is(err, task.ErrProgramExiting) {
return
}
log.Err(err).
Str("server", dockerHost).
Str("container", id).
Msg("failed to de-multiplex logs")
}
}

View File

@@ -0,0 +1,58 @@
package dockerapi
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"
// @Success 200 {object} apitypes.SuccessResponse
// @Failure 400 {object} apitypes.ErrorResponse "Invalid request"
// @Failure 403 {object} apitypes.ErrorResponse
// @Failure 404 {object} apitypes.ErrorResponse "Container not found"
// @Failure 500 {object} apitypes.ErrorResponse
// @Router /docker/restart [post]
func Restart(c *gin.Context) {
var req RestartRequest
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, apitypes.Error("invalid request", err))
return
}
dockerHost, ok := docker.GetDockerHostByContainerID(req.ID)
if !ok {
c.JSON(http.StatusNotFound, apitypes.Error("container not found"))
return
}
client, err := docker.NewClient(dockerHost)
if err != nil {
c.Error(apitypes.InternalServerError(err, "failed to create docker client"))
return
}
defer client.Close()
_, err = client.ContainerRestart(c.Request.Context(), req.ID, req.ContainerRestartOptions)
if err != nil {
c.Error(apitypes.InternalServerError(err, "failed to restart container"))
return
}
c.JSON(http.StatusOK, apitypes.Success("container restarted"))
}

View File

@@ -0,0 +1,58 @@
package dockerapi
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 StartRequest struct {
ID string `json:"id" binding:"required"`
client.ContainerStartOptions
}
// @x-id "start"
// @BasePath /api/v1
// @Summary Start container
// @Description Start container by container id
// @Tags docker
// @Produce json
// @Param request body StartRequest true "Request"
// @Success 200 {object} apitypes.SuccessResponse
// @Failure 400 {object} apitypes.ErrorResponse "Invalid request"
// @Failure 403 {object} apitypes.ErrorResponse
// @Failure 404 {object} apitypes.ErrorResponse "Container not found"
// @Failure 500 {object} apitypes.ErrorResponse
// @Router /docker/start [post]
func Start(c *gin.Context) {
var req StartRequest
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, apitypes.Error("invalid request", err))
return
}
dockerHost, ok := docker.GetDockerHostByContainerID(req.ID)
if !ok {
c.JSON(http.StatusNotFound, apitypes.Error("container not found"))
return
}
client, err := docker.NewClient(dockerHost)
if err != nil {
c.Error(apitypes.InternalServerError(err, "failed to create docker client"))
return
}
defer client.Close()
_, err = client.ContainerStart(c.Request.Context(), req.ID, req.ContainerStartOptions)
if err != nil {
c.Error(apitypes.InternalServerError(err, "failed to start container"))
return
}
c.JSON(http.StatusOK, apitypes.Success("container started"))
}

View File

@@ -0,0 +1,58 @@
package dockerapi
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 StopRequest struct {
ID string `json:"id" binding:"required"`
client.ContainerStopOptions
}
// @x-id "stop"
// @BasePath /api/v1
// @Summary Stop container
// @Description Stop container by container id
// @Tags docker
// @Produce json
// @Param request body StopRequest true "Request"
// @Success 200 {object} apitypes.SuccessResponse
// @Failure 400 {object} apitypes.ErrorResponse "Invalid request"
// @Failure 403 {object} apitypes.ErrorResponse
// @Failure 404 {object} apitypes.ErrorResponse "Container not found"
// @Failure 500 {object} apitypes.ErrorResponse
// @Router /docker/stop [post]
func Stop(c *gin.Context) {
var req StopRequest
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, apitypes.Error("invalid request", err))
return
}
dockerHost, ok := docker.GetDockerHostByContainerID(req.ID)
if !ok {
c.JSON(http.StatusNotFound, apitypes.Error("container not found"))
return
}
client, err := docker.NewClient(dockerHost)
if err != nil {
c.Error(apitypes.InternalServerError(err, "failed to create docker client"))
return
}
defer client.Close()
_, err = client.ContainerStop(c.Request.Context(), req.ID, req.ContainerStopOptions)
if err != nil {
c.Error(apitypes.InternalServerError(err, "failed to stop container"))
return
}
c.JSON(http.StatusOK, apitypes.Success("container stopped"))
}

View File

@@ -0,0 +1,54 @@
package dockerapi
import (
"context"
"net/http"
"time"
"github.com/gin-gonic/gin"
"github.com/yusing/godoxy/internal/docker"
apitypes "github.com/yusing/goutils/apitypes"
gperr "github.com/yusing/goutils/errs"
"github.com/yusing/goutils/http/httpheaders"
"github.com/yusing/goutils/http/websocket"
)
type (
DockerClients map[string]*docker.SharedClient
ResultType[T any] interface {
map[string]T | []T
}
)
// closeAllClients closes all docker clients after a delay.
//
// This is used to ensure that all docker clients are closed after the http handler returns.
func closeAllClients(dockerClients DockerClients) {
for _, dockerClient := range dockerClients {
dockerClient.Close()
}
}
func handleResult[V any, T ResultType[V]](c *gin.Context, errs error, result T) {
if errs != nil {
if len(result) == 0 {
c.Error(apitypes.InternalServerError(errs, "docker errors"))
return
}
}
c.JSON(http.StatusOK, result)
}
func serveHTTP[V any, T ResultType[V]](c *gin.Context, getResult func(ctx context.Context, dockerClients DockerClients) (T, gperr.Error)) {
dockerClients := docker.Clients()
defer closeAllClients(dockerClients)
if httpheaders.IsWebsocket(c.Request.Header) {
websocket.PeriodicWrite(c, 5*time.Second, func() (any, error) {
return getResult(c.Request.Context(), dockerClients)
})
} else {
result, err := getResult(c.Request.Context(), dockerClients)
handleResult[V](c, err, result)
}
}

View File

@@ -1,54 +0,0 @@
package dockerapi
import (
"context"
"net/http"
"sort"
"github.com/docker/docker/api/types/container"
"github.com/yusing/go-proxy/internal/gperr"
)
type Container struct {
Server string `json:"server"`
Name string `json:"name"`
ID string `json:"id"`
Image string `json:"image"`
State string `json:"state"`
}
func Containers(w http.ResponseWriter, r *http.Request) {
serveHTTP[Container](w, r, GetContainers)
}
func GetContainers(ctx context.Context, dockerClients DockerClients) ([]Container, gperr.Error) {
errs := gperr.NewBuilder("failed to get containers")
containers := make([]Container, 0)
for server, dockerClient := range dockerClients {
conts, err := dockerClient.ContainerList(ctx, container.ListOptions{All: true})
if err != nil {
errs.Add(err)
continue
}
for _, cont := range conts {
containers = append(containers, Container{
Server: server,
Name: cont.Names[0],
ID: cont.ID,
Image: cont.Image,
State: cont.State,
})
}
}
sort.Slice(containers, func(i, j int) bool {
return containers[i].Name < containers[j].Name
})
if err := errs.Error(); err != nil {
gperr.LogError("failed to get containers", err)
if len(containers) == 0 {
return nil, err
}
return containers, nil
}
return containers, nil
}

View File

@@ -1,56 +0,0 @@
package dockerapi
import (
"context"
"encoding/json"
"net/http"
"sort"
dockerSystem "github.com/docker/docker/api/types/system"
"github.com/yusing/go-proxy/internal/gperr"
"github.com/yusing/go-proxy/internal/utils/strutils"
)
type dockerInfo dockerSystem.Info
func (d *dockerInfo) MarshalJSON() ([]byte, error) {
return json.Marshal(map[string]any{
"name": d.Name,
"version": d.ServerVersion,
"containers": map[string]int{
"total": d.Containers,
"running": d.ContainersRunning,
"paused": d.ContainersPaused,
"stopped": d.ContainersStopped,
},
"images": d.Images,
"n_cpu": d.NCPU,
"memory": strutils.FormatByteSize(d.MemTotal),
})
}
func DockerInfo(w http.ResponseWriter, r *http.Request) {
serveHTTP[dockerInfo](w, r, GetDockerInfo)
}
func GetDockerInfo(ctx context.Context, dockerClients DockerClients) ([]dockerInfo, gperr.Error) {
errs := gperr.NewBuilder("failed to get docker info")
dockerInfos := make([]dockerInfo, len(dockerClients))
i := 0
for name, dockerClient := range dockerClients {
info, err := dockerClient.Info(ctx)
if err != nil {
errs.Add(err)
continue
}
info.Name = name
dockerInfos[i] = dockerInfo(info)
i++
}
sort.Slice(dockerInfos, func(i, j int) bool {
return dockerInfos[i].Name < dockerInfos[j].Name
})
return dockerInfos, errs.Error()
}

View File

@@ -1,77 +0,0 @@
package dockerapi
import (
"context"
"errors"
"net/http"
"strconv"
"github.com/docker/docker/api/types/container"
"github.com/docker/docker/pkg/stdcopy"
"github.com/gorilla/websocket"
"github.com/rs/zerolog/log"
"github.com/yusing/go-proxy/internal/net/gphttp"
"github.com/yusing/go-proxy/internal/net/gphttp/gpwebsocket"
"github.com/yusing/go-proxy/internal/task"
)
// FIXME: agent logs not updating.
func Logs(w http.ResponseWriter, r *http.Request) {
query := r.URL.Query()
server := r.PathValue("server")
containerID := r.PathValue("container")
stdout, _ := strconv.ParseBool(query.Get("stdout"))
stderr, _ := strconv.ParseBool(query.Get("stderr"))
since := query.Get("from")
until := query.Get("to")
levels := query.Get("levels") // TODO: implement levels
dockerClient, found, err := getDockerClient(server)
if err != nil {
gphttp.BadRequest(w, err.Error())
return
}
if !found {
gphttp.NotFound(w, "server not found")
return
}
defer dockerClient.Close()
opts := container.LogsOptions{
ShowStdout: stdout,
ShowStderr: stderr,
Since: since,
Until: until,
Timestamps: true,
Follow: true,
Tail: "100",
}
if levels != "" {
opts.Details = true
}
logs, err := dockerClient.ContainerLogs(r.Context(), containerID, opts)
if err != nil {
gphttp.BadRequest(w, err.Error())
return
}
defer logs.Close()
conn, err := gpwebsocket.Initiate(w, r)
if err != nil {
return
}
defer conn.Close()
writer := gpwebsocket.NewWriter(r.Context(), conn, websocket.TextMessage)
_, err = stdcopy.StdCopy(writer, writer, logs) // de-multiplex logs
if err != nil {
if errors.Is(err, context.Canceled) || errors.Is(err, task.ErrProgramExiting) {
return
}
log.Err(err).
Str("server", server).
Str("container", containerID).
Msg("failed to de-multiplex logs")
}
}

View File

@@ -1,126 +0,0 @@
package dockerapi
import (
"context"
"encoding/json"
"net/http"
"time"
"github.com/gorilla/websocket"
"github.com/yusing/go-proxy/agent/pkg/agent"
config "github.com/yusing/go-proxy/internal/config/types"
"github.com/yusing/go-proxy/internal/docker"
"github.com/yusing/go-proxy/internal/gperr"
"github.com/yusing/go-proxy/internal/net/gphttp/gpwebsocket"
"github.com/yusing/go-proxy/internal/net/gphttp/httpheaders"
)
type (
DockerClients map[string]*docker.SharedClient
ResultType[T any] interface {
map[string]T | []T
}
)
// getDockerClients returns a map of docker clients for the current config.
//
// Returns a map of docker clients by server name and an error if any.
//
// Even if there are errors, the map of docker clients might not be empty.
func getDockerClients() (DockerClients, gperr.Error) {
cfg := config.GetInstance()
dockerHosts := cfg.Value().Providers.Docker
dockerClients := make(DockerClients)
connErrs := gperr.NewBuilder("failed to connect to docker")
for name, host := range dockerHosts {
dockerClient, err := docker.NewClient(host)
if err != nil {
connErrs.Add(err)
continue
}
dockerClients[name] = dockerClient
}
for _, agent := range agent.ListAgents() {
dockerClient, err := docker.NewClient(agent.FakeDockerHost())
if err != nil {
connErrs.Add(err)
continue
}
dockerClients[agent.Name()] = dockerClient
}
return dockerClients, connErrs.Error()
}
func getDockerClient(server string) (*docker.SharedClient, bool, error) {
cfg := config.GetInstance()
var host string
for name, h := range cfg.Value().Providers.Docker {
if name == server {
host = h
break
}
}
if host == "" {
for _, agent := range agent.ListAgents() {
if agent.Name() == server {
host = agent.FakeDockerHost()
break
}
}
}
if host == "" {
return nil, false, nil
}
dockerClient, err := docker.NewClient(host)
if err != nil {
return nil, false, err
}
return dockerClient, true, nil
}
// closeAllClients closes all docker clients after a delay.
//
// This is used to ensure that all docker clients are closed after the http handler returns.
func closeAllClients(dockerClients DockerClients) {
for _, dockerClient := range dockerClients {
dockerClient.Close()
}
}
func handleResult[V any, T ResultType[V]](w http.ResponseWriter, errs error, result T) {
if errs != nil {
gperr.LogError("docker errors", errs)
if len(result) == 0 {
http.Error(w, "docker errors", http.StatusInternalServerError)
return
}
}
json.NewEncoder(w).Encode(result) //nolint
}
func serveHTTP[V any, T ResultType[V]](w http.ResponseWriter, r *http.Request, getResult func(ctx context.Context, dockerClients DockerClients) (T, gperr.Error)) {
dockerClients, err := getDockerClients()
if err != nil {
handleResult[V, T](w, err, nil)
return
}
defer closeAllClients(dockerClients)
if httpheaders.IsWebsocket(r.Header) {
gpwebsocket.Periodic(w, r, 5*time.Second, func(conn *websocket.Conn) error {
result, err := getResult(r.Context(), dockerClients)
if err != nil {
return err
}
return conn.WriteJSON(result)
})
} else {
result, err := getResult(r.Context(), dockerClients)
handleResult[V](w, err, result)
}
}

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,94 @@
package v1
import (
"context"
"net/http"
"github.com/gin-gonic/gin"
"github.com/yusing/godoxy/internal/homepage"
"github.com/yusing/godoxy/internal/route/routes"
apitypes "github.com/yusing/goutils/apitypes"
_ "unsafe"
)
type GetFavIconRequest struct {
URL string `form:"url" binding:"required_without=Alias"`
Alias string `form:"alias" binding:"required_without=URL"`
} // @name GetFavIconRequest
// @x-id "favicon"
// @BasePath /api/v1
// @Summary Get favicon
// @Description Get favicon
// @Tags v1
// @Accept json
// @Produce image/svg+xml,image/x-icon,image/png,image/webp
// @Param url query string false "URL of the route"
// @Param alias query string false "Alias of the route"
// @Success 200 {array} homepage.FetchResult
// @Failure 400 {object} apitypes.ErrorResponse "Bad Request: alias is empty or route is not HTTPRoute"
// @Failure 403 {object} apitypes.ErrorResponse "Forbidden: unauthorized"
// @Failure 404 {object} apitypes.ErrorResponse "Not Found: route or icon not found"
// @Failure 500 {object} apitypes.ErrorResponse "Internal Server Error: internal error"
// @Router /favicon [get]
func FavIcon(c *gin.Context) {
var request GetFavIconRequest
if err := c.ShouldBindQuery(&request); err != nil {
c.JSON(http.StatusBadRequest, apitypes.Error("invalid request", err))
return
}
// try with url
if request.URL != "" {
var iconURL homepage.IconURL
if err := iconURL.Parse(request.URL); err != nil {
c.JSON(http.StatusBadRequest, apitypes.Error("invalid url", err))
return
}
fetchResult, err := homepage.FetchFavIconFromURL(c.Request.Context(), &iconURL)
if err != nil {
homepage.GinFetchError(c, fetchResult.StatusCode, err)
return
}
c.Data(fetchResult.StatusCode, fetchResult.ContentType(), fetchResult.Icon)
return
}
// try with alias
result, err := GetFavIconFromAlias(c.Request.Context(), request.Alias)
if err != nil {
homepage.GinFetchError(c, result.StatusCode, err)
return
}
c.Data(result.StatusCode, result.ContentType(), result.Icon)
}
//go:linkname GetFavIconFromAlias v1.GetFavIconFromAlias
func GetFavIconFromAlias(ctx context.Context, alias string) (homepage.FetchResult, error) {
// try with route.Icon
r, ok := routes.HTTP.Get(alias)
if !ok {
return homepage.FetchResultWithErrorf(http.StatusNotFound, "route not found")
}
var (
result homepage.FetchResult
err error
)
hp := r.HomepageItem()
if hp.Icon != nil {
if hp.Icon.IconSource == homepage.IconSourceRelative {
result, err = homepage.FindIcon(ctx, r, *hp.Icon.FullURL)
} else {
result, err = homepage.FetchFavIconFromURL(ctx, hp.Icon)
}
} else {
// try extract from "link[rel=icon]"
result, err = homepage.FindIcon(ctx, r, "/")
}
if result.StatusCode == 0 {
result.StatusCode = http.StatusOK
}
return result, err
}

View File

@@ -1,81 +0,0 @@
package favicon
import (
"net/http"
"github.com/yusing/go-proxy/internal/homepage"
"github.com/yusing/go-proxy/internal/net/gphttp"
"github.com/yusing/go-proxy/internal/route/routes"
)
// GetFavIcon returns the favicon of the route
//
// Returns:
// - 200 OK: if icon found
// - 400 Bad Request: if alias is empty or route is not HTTPRoute
// - 404 Not Found: if route or icon not found
// - 500 Internal Server Error: if internal error
// - others: depends on route handler response
func GetFavIcon(w http.ResponseWriter, req *http.Request) {
url, alias := req.FormValue("url"), req.FormValue("alias")
if url == "" && alias == "" {
gphttp.MissingKey(w, "url or alias")
return
}
if url != "" && alias != "" {
gphttp.BadRequest(w, "url and alias are mutually exclusive")
return
}
// try with url
if url != "" {
var iconURL homepage.IconURL
if err := iconURL.Parse(url); err != nil {
gphttp.ClientError(w, req, err, http.StatusBadRequest)
return
}
fetchResult := homepage.FetchFavIconFromURL(req.Context(), &iconURL)
if !fetchResult.OK() {
http.Error(w, fetchResult.ErrMsg, fetchResult.StatusCode)
return
}
w.Header().Set("Content-Type", fetchResult.ContentType())
gphttp.WriteBody(w, fetchResult.Icon)
return
}
// try with alias
GetFavIconFromAlias(w, req, alias)
return
}
func GetFavIconFromAlias(w http.ResponseWriter, req *http.Request, alias string) {
// try with route.Icon
r, ok := routes.HTTP.Get(alias)
if !ok {
gphttp.ValueNotFound(w, "route", alias)
return
}
var result *homepage.FetchResult
hp := r.HomepageItem()
if hp.Icon != nil {
if hp.Icon.IconSource == homepage.IconSourceRelative {
result = homepage.FindIcon(req.Context(), r, *hp.Icon.FullURL)
} else {
result = homepage.FetchFavIconFromURL(req.Context(), hp.Icon)
}
} else {
// try extract from "link[rel=icon]"
result = homepage.FindIcon(req.Context(), r, "/")
}
if result.StatusCode == 0 {
result.StatusCode = http.StatusOK
}
if !result.OK() {
http.Error(w, result.ErrMsg, result.StatusCode)
return
}
w.Header().Set("Content-Type", result.ContentType())
gphttp.WriteBody(w, result.Icon)
}

View File

@@ -0,0 +1,73 @@
package fileapi
import (
"net/http"
"os"
"path"
"strings"
"github.com/gin-gonic/gin"
"github.com/yusing/godoxy/internal/common"
apitypes "github.com/yusing/goutils/apitypes"
)
type FileType string // @name FileType
const (
FileTypeConfig FileType = "config" // @name FileTypeConfig
FileTypeProvider FileType = "provider" // @name FileTypeProvider
FileTypeMiddleware FileType = "middleware" // @name FileTypeMiddleware
)
type GetFileContentRequest struct {
FileType FileType `form:"type" binding:"required,oneof=config provider middleware"`
Filename string `form:"filename" binding:"required" format:"filename"`
} // @name GetFileContentRequest
// @x-id "get"
// @BasePath /api/v1
// @Summary Get file content
// @Description Get file content
// @Tags file
// @Accept json
// @Produce json,application/godoxy+yaml
// @Param query query GetFileContentRequest true "Request"
// @Success 200 {string} application/godoxy+yaml "File content"
// @Failure 400 {object} apitypes.ErrorResponse
// @Failure 403 {object} apitypes.ErrorResponse
// @Failure 500 {object} apitypes.ErrorResponse
// @Router /file/content [get]
func Get(c *gin.Context) {
var request GetFileContentRequest
if err := c.ShouldBindQuery(&request); err != nil {
c.JSON(http.StatusBadRequest, apitypes.Error("invalid request", err))
return
}
content, err := os.ReadFile(request.FileType.GetPath(request.Filename))
if err != nil {
c.Error(apitypes.InternalServerError(err, "failed to read file"))
return
}
// RFC 9512: https://www.rfc-editor.org/rfc/rfc9512.html
// xxx/yyy+yaml
c.Data(http.StatusOK, "application/godoxy+yaml", content)
}
func GetFileType(file string) FileType {
switch {
case strings.HasPrefix(path.Base(file), "config."):
return FileTypeConfig
case strings.HasPrefix(file, common.MiddlewareComposeBasePath):
return FileTypeMiddleware
}
return FileTypeProvider
}
func (t FileType) GetPath(filename string) string {
if t == FileTypeMiddleware {
return path.Join(common.MiddlewareComposeBasePath, filename)
}
return path.Join(common.ConfigBasePath, filename)
}

View File

@@ -0,0 +1,62 @@
package fileapi
import (
"net/http"
"strings"
"github.com/gin-gonic/gin"
"github.com/yusing/godoxy/internal/common"
apitypes "github.com/yusing/goutils/apitypes"
"github.com/yusing/goutils/fs"
)
type ListFilesResponse struct {
Config []string `json:"config"`
Provider []string `json:"provider"`
Middleware []string `json:"middleware"`
} // @name ListFilesResponse
// @x-id "list"
// @BasePath /api/v1
// @Summary List files
// @Description List files
// @Tags file
// @Accept json
// @Produce json
// @Success 200 {object} ListFilesResponse
// @Failure 403 {object} apitypes.ErrorResponse
// @Failure 500 {object} apitypes.ErrorResponse
// @Router /file/list [get]
func List(c *gin.Context) {
resp := map[FileType][]string{
FileTypeConfig: make([]string, 0),
FileTypeProvider: make([]string, 0),
FileTypeMiddleware: make([]string, 0),
}
// config/
files, err := fs.ListFiles(common.ConfigBasePath, 0, true)
if err != nil {
c.Error(apitypes.InternalServerError(err, "failed to list files"))
return
}
for _, file := range files {
t := GetFileType(file)
file = strings.TrimPrefix(file, common.ConfigBasePath+"/")
resp[t] = append(resp[t], file)
}
// config/middlewares/
mids, err := fs.ListFiles(common.MiddlewareComposeBasePath, 0, true)
if err != nil {
c.Error(apitypes.InternalServerError(err, "failed to list files"))
return
}
for _, mid := range mids {
mid = strings.TrimPrefix(mid, common.MiddlewareComposeBasePath+"/")
resp[FileTypeMiddleware] = append(resp[FileTypeMiddleware], mid)
}
c.JSON(http.StatusOK, resp)
}

View File

@@ -0,0 +1,52 @@
package fileapi
import (
"net/http"
"os"
"github.com/gin-gonic/gin"
apitypes "github.com/yusing/goutils/apitypes"
)
type SetFileContentRequest GetFileContentRequest
// @x-id "set"
// @BasePath /api/v1
// @Summary Set file content
// @Description Set file content
// @Tags file
// @Accept text/plain
// @Produce json
// @Param type query FileType true "Type"
// @Param filename query string true "Filename"
// @Param file body string true "File"
// @Success 200 {object} apitypes.SuccessResponse
// @Failure 400 {object} apitypes.ErrorResponse
// @Failure 403 {object} apitypes.ErrorResponse
// @Failure 500 {object} apitypes.ErrorResponse
// @Router /file/content [put]
func Set(c *gin.Context) {
var request SetFileContentRequest
if err := c.ShouldBindQuery(&request); err != nil {
c.JSON(http.StatusBadRequest, apitypes.Error("invalid request", err))
return
}
content, err := c.GetRawData()
if err != nil {
c.Error(apitypes.InternalServerError(err, "failed to read file"))
return
}
if valErr := validateFile(request.FileType, content); valErr != nil {
c.JSON(http.StatusBadRequest, apitypes.Error("invalid file", valErr))
return
}
err = os.WriteFile(request.FileType.GetPath(request.Filename), content, 0o644)
if err != nil {
c.Error(apitypes.InternalServerError(err, "failed to write file"))
return
}
c.JSON(http.StatusOK, apitypes.Success("file set"))
}

View File

@@ -0,0 +1,64 @@
package fileapi
import (
"net/http"
"github.com/gin-gonic/gin"
config "github.com/yusing/godoxy/internal/config/types"
"github.com/yusing/godoxy/internal/net/gphttp/middleware"
"github.com/yusing/godoxy/internal/route/provider"
apitypes "github.com/yusing/goutils/apitypes"
gperr "github.com/yusing/goutils/errs"
)
type ValidateFileRequest struct {
FileType FileType `form:"type" validate:"required,oneof=config provider middleware"`
} // @name ValidateFileRequest
// @x-id "validate"
// @BasePath /api/v1
// @Summary Validate file
// @Description Validate file
// @Tags file
// @Accept text/plain
// @Produce json
// @Param type query FileType true "Type"
// @Param file body string true "File content"
// @Success 200 {object} apitypes.SuccessResponse "File validated"
// @Failure 400 {object} apitypes.ErrorResponse "Bad request"
// @Failure 403 {object} apitypes.ErrorResponse "Forbidden"
// @Failure 417 {object} any "Validation failed"
// @Failure 500 {object} apitypes.ErrorResponse "Internal server error"
// @Router /file/validate [post]
func Validate(c *gin.Context) {
var request ValidateFileRequest
if err := c.ShouldBindQuery(&request); err != nil {
c.JSON(http.StatusBadRequest, apitypes.Error("invalid request", err))
return
}
content, err := c.GetRawData()
if err != nil {
c.Error(apitypes.InternalServerError(err, "failed to read file"))
return
}
c.Request.Body.Close()
if valErr := validateFile(request.FileType, content); valErr != nil {
c.JSON(http.StatusExpectationFailed, valErr)
return
}
c.JSON(http.StatusOK, apitypes.Success("file validated"))
}
func validateFile(fileType FileType, content []byte) gperr.Error {
switch fileType {
case FileTypeConfig:
return config.Validate(content)
case FileTypeMiddleware:
errs := gperr.NewBuilder("middleware errors")
middleware.BuildMiddlewaresFromYAML("", content, &errs)
return errs.Error()
}
return provider.Validate(content)
}

View File

@@ -4,19 +4,33 @@ import (
"net/http"
"time"
"github.com/gorilla/websocket"
"github.com/yusing/go-proxy/internal/net/gphttp"
"github.com/yusing/go-proxy/internal/net/gphttp/gpwebsocket"
"github.com/yusing/go-proxy/internal/net/gphttp/httpheaders"
"github.com/yusing/go-proxy/internal/route/routes"
"github.com/gin-gonic/gin"
"github.com/yusing/godoxy/internal/route/routes"
"github.com/yusing/goutils/http/httpheaders"
"github.com/yusing/goutils/http/websocket"
_ "github.com/yusing/goutils/apitypes"
)
func Health(w http.ResponseWriter, r *http.Request) {
if httpheaders.IsWebsocket(r.Header) {
gpwebsocket.Periodic(w, r, 1*time.Second, func(conn *websocket.Conn) error {
return conn.WriteJSON(routes.HealthMap())
type HealthMap = map[string]routes.HealthInfo // @name HealthMap
// @x-id "health"
// @BasePath /api/v1
// @Summary Get routes health info
// @Description Get health info by route name
// @Tags v1,websocket
// @Accept json
// @Produce json
// @Success 200 {object} HealthMap "Health info by route name"
// @Failure 403 {object} apitypes.ErrorResponse
// @Failure 500 {object} apitypes.ErrorResponse
// @Router /health [get]
func Health(c *gin.Context) {
if httpheaders.IsWebsocket(c.Request.Header) {
websocket.PeriodicWrite(c, 1*time.Second, func() (any, error) {
return routes.GetHealthInfo(), nil
})
} else {
gphttp.RespondJSON(w, r, routes.HealthMap())
c.JSON(http.StatusOK, routes.GetHealthInfo())
}
}

View File

@@ -0,0 +1,44 @@
package homepageapi
import (
"net/http"
"github.com/gin-gonic/gin"
"github.com/yusing/godoxy/internal/homepage"
"github.com/yusing/godoxy/internal/route/routes"
_ "github.com/yusing/goutils/apitypes"
)
// @x-id "categories"
// @BasePath /api/v1
// @Summary List homepage categories
// @Description List homepage categories
// @Tags homepage
// @Accept json
// @Produce json
// @Success 200 {array} string
// @Failure 403 {object} apitypes.ErrorResponse
// @Router /homepage/categories [get]
func Categories(c *gin.Context) {
c.JSON(http.StatusOK, HomepageCategories())
}
func HomepageCategories() []string {
check := make(map[string]struct{})
categories := make([]string, 0)
categories = append(categories, homepage.CategoryAll)
categories = append(categories, homepage.CategoryFavorites)
for _, r := range routes.HTTP.Iter {
item := r.HomepageItem()
if item.Category == "" {
continue
}
if _, ok := check[item.Category]; ok {
continue
}
check[item.Category] = struct{}{}
categories = append(categories, item.Category)
}
return categories
}

View File

@@ -0,0 +1,36 @@
package homepageapi
import (
"net/http"
"github.com/gin-gonic/gin"
"github.com/yusing/godoxy/internal/homepage"
apitypes "github.com/yusing/goutils/apitypes"
)
type HomepageOverrideItemClickParams struct {
Which string `form:"which" binding:"required"`
} // @name HomepageOverrideItemClickParams
// @x-id "item-click"
// @BasePath /api/v1
// @Summary Increment item click
// @Description Increment item click.
// @Tags homepage
// @Accept json
// @Produce json
// @Param request query HomepageOverrideItemClickParams true "Increment item click"
// @Success 200 {object} apitypes.SuccessResponse
// @Failure 400 {object} apitypes.ErrorResponse
// @Failure 500 {object} apitypes.ErrorResponse
// @Router /homepage/item_click [post]
func ItemClick(c *gin.Context) {
var params HomepageOverrideItemClickParams
if err := c.ShouldBindQuery(&params); err != nil {
c.JSON(http.StatusBadRequest, apitypes.Error("invalid request", err))
return
}
overrides := homepage.GetOverrideConfig()
overrides.IncrementItemClicks(params.Which)
c.JSON(http.StatusOK, apitypes.Success("success"))
}

View File

@@ -0,0 +1,133 @@
package homepageapi
import (
"fmt"
"net/http"
"net/url"
"slices"
"strings"
"time"
"github.com/gin-gonic/gin"
"github.com/lithammer/fuzzysearch/fuzzy"
"github.com/yusing/godoxy/internal/homepage"
"github.com/yusing/godoxy/internal/route/routes"
apitypes "github.com/yusing/goutils/apitypes"
"github.com/yusing/goutils/http/httpheaders"
"github.com/yusing/goutils/http/websocket"
)
type HomepageItemsRequest struct {
SearchQuery string `form:"search"` // Search query
Category string `form:"category"` // Category filter
Provider string `form:"provider"` // Provider filter
// Sort method
SortMethod homepage.SortMethod `form:"sort_method" default:"alphabetical" binding:"omitempty,oneof=clicks alphabetical custom"`
} // @name HomepageItemsRequest
// @x-id "items"
// @BasePath /api/v1
// @Summary Homepage items
// @Description Homepage items
// @Tags homepage,websocket
// @Accept json
// @Produce json
// @Param query query HomepageItemsRequest false "Query parameters"
// @Success 200 {object} homepage.Homepage
// @Failure 400 {object} apitypes.ErrorResponse
// @Failure 403 {object} apitypes.ErrorResponse
// @Router /homepage/items [get]
func Items(c *gin.Context) {
var request HomepageItemsRequest
if err := c.ShouldBindQuery(&request); err != nil {
c.JSON(http.StatusBadRequest, apitypes.Error("invalid request", err))
return
}
proto := "http"
if c.Request.TLS != nil || c.GetHeader("X-Forwarded-Proto") == "https" {
proto = "https"
}
hostname := c.Request.Host
if host := c.GetHeader("X-Forwarded-Host"); host != "" {
hostname = host
}
if httpheaders.IsWebsocket(c.Request.Header) {
websocket.PeriodicWrite(c, 2*time.Second, func() (any, error) {
return HomepageItems(proto, hostname, &request), nil
})
} else {
c.JSON(http.StatusOK, HomepageItems(proto, hostname, &request))
}
}
func HomepageItems(proto, hostname string, request *HomepageItemsRequest) homepage.Homepage {
switch proto {
case "http", "https":
default:
proto = "http"
}
hp := homepage.NewHomepageMap(routes.HTTP.Size())
if strings.Count(hostname, ".") > 1 {
_, hostname, _ = strings.Cut(hostname, ".") // remove the subdomain
}
for _, r := range routes.HTTP.Iter {
if request.Provider != "" && r.ProviderName() != request.Provider {
continue
}
item := r.HomepageItem()
if request.Category != "" && item.Category != request.Category {
continue
}
if request.SearchQuery != "" && !fuzzy.MatchFold(request.SearchQuery, item.Name) {
continue
}
// clear url if invalid
_, err := url.Parse(item.URL)
if err != nil {
item.URL = ""
}
// append hostname if provided and only if alias is not FQDN
if hostname != "" && item.URL == "" {
isFQDNAlias := strings.Contains(item.Alias, ".")
if !isFQDNAlias {
item.URL = fmt.Sprintf("%s://%s.%s", proto, item.Alias, hostname)
} else {
item.URL = fmt.Sprintf("%s://%s", proto, item.Alias)
}
}
// prepend protocol if not exists
if !strings.HasPrefix(item.URL, "http://") && !strings.HasPrefix(item.URL, "https://") {
item.URL = fmt.Sprintf("%s://%s", proto, item.URL)
}
hp.Add(&item)
}
ret := hp.Values()
// sort items in each category
for _, category := range ret {
category.Sort(request.SortMethod)
}
// sort categories
overrides := homepage.GetOverrideConfig()
slices.SortStableFunc(ret, func(a, b *homepage.Category) int {
// if category is "Hidden", move it to the end of the list
if a.Name == homepage.CategoryHidden {
return 1
}
if b.Name == homepage.CategoryHidden {
return -1
}
// sort categories by order in config
return overrides.CategoryOrder[a.Name] - overrides.CategoryOrder[b.Name]
})
return ret
}

View File

@@ -0,0 +1,217 @@
package homepageapi
import (
"net/http"
"github.com/gin-gonic/gin"
"github.com/yusing/godoxy/internal/homepage"
apitypes "github.com/yusing/goutils/apitypes"
)
type (
HomepageOverrideItemParams struct {
Which string `json:"which"`
Value homepage.ItemConfig `json:"value"`
} // @name HomepageOverrideItemParams
HomepageOverrideItemsBatchParams struct {
Value map[string]homepage.ItemConfig `json:"value"`
} // @name HomepageOverrideItemsBatchParams
HomepageOverrideCategoryOrderParams struct {
Which string `json:"which"`
Value int `json:"value"`
} // @name HomepageOverrideCategoryOrderParams
HomepageOverrideItemSortOrderParams HomepageOverrideCategoryOrderParams // @name HomepageOverrideItemSortOrderParams
HomepageOverrideItemAllSortOrderParams HomepageOverrideCategoryOrderParams // @name HomepageOverrideItemAllSortOrderParams
HomepageOverrideItemFavSortOrderParams HomepageOverrideCategoryOrderParams // @name HomepageOverrideItemFavSortOrderParams
HomepageOverrideItemVisibleParams struct {
Which []string `json:"which"`
Value bool `json:"value"`
} // @name HomepageOverrideItemVisibleParams
HomepageOverrideItemFavoriteParams HomepageOverrideItemVisibleParams // @name HomepageOverrideItemFavoriteParams
)
// @x-id "set-item"
// @BasePath /api/v1
// @Summary Override single homepage item
// @Description Override single homepage item.
// @Tags homepage
// @Accept json
// @Produce json
// @Param request body HomepageOverrideItemParams true "Override single item"
// @Success 200 {object} apitypes.SuccessResponse
// @Failure 400 {object} apitypes.ErrorResponse
// @Failure 500 {object} apitypes.ErrorResponse
// @Router /homepage/set/item [post]
func SetItem(c *gin.Context) {
var params HomepageOverrideItemParams
if err := c.ShouldBindJSON(&params); err != nil {
c.JSON(http.StatusBadRequest, apitypes.Error("invalid request", err))
return
}
overrides := homepage.GetOverrideConfig()
overrides.OverrideItem(params.Which, params.Value)
c.JSON(http.StatusOK, apitypes.Success("success"))
}
// @x-id "set-items-batch"
// @BasePath /api/v1
// @Summary Override multiple homepage items
// @Description Override multiple homepage items.
// @Tags homepage
// @Accept json
// @Produce json
// @Param request body HomepageOverrideItemsBatchParams true "Override multiple items"
// @Success 200 {object} apitypes.SuccessResponse
// @Failure 400 {object} apitypes.ErrorResponse
// @Failure 500 {object} apitypes.ErrorResponse
// @Router /homepage/set/items_batch [post]
func SetItemsBatch(c *gin.Context) {
var params HomepageOverrideItemsBatchParams
if err := c.ShouldBindJSON(&params); err != nil {
c.JSON(http.StatusBadRequest, apitypes.Error("invalid request", err))
return
}
overrides := homepage.GetOverrideConfig()
overrides.OverrideItems(params.Value)
c.JSON(http.StatusOK, apitypes.Success("success"))
}
// @x-id "set-item-visible"
// @BasePath /api/v1
// @Summary Set homepage item visibility
// @Description POST list of item ids and visibility value.
// @Tags homepage
// @Accept json
// @Produce json
// @Param request body HomepageOverrideItemVisibleParams true "Set item visibility"
// @Success 200 {object} apitypes.SuccessResponse
// @Failure 400 {object} apitypes.ErrorResponse
// @Failure 500 {object} apitypes.ErrorResponse
// @Router /homepage/set/item_visible [post]
func SetItemVisible(c *gin.Context) {
var params HomepageOverrideItemVisibleParams
if err := c.ShouldBindJSON(&params); err != nil {
c.JSON(http.StatusBadRequest, apitypes.Error("invalid request", err))
return
}
overrides := homepage.GetOverrideConfig()
overrides.SetItemsVisibility(params.Which, params.Value)
c.JSON(http.StatusOK, apitypes.Success("success"))
}
// @x-id "set-item-favorite"
// @BasePath /api/v1
// @Summary Set homepage item favorite
// @Description Set homepage item favorite.
// @Tags homepage
// @Accept json
// @Produce json
// @Param request body HomepageOverrideItemFavoriteParams true "Set item favorite"
// @Success 200 {object} apitypes.SuccessResponse
// @Failure 400 {object} apitypes.ErrorResponse
// @Failure 500 {object} apitypes.ErrorResponse
// @Router /homepage/set/item_favorite [post]
func SetItemFavorite(c *gin.Context) {
var params HomepageOverrideItemFavoriteParams
if err := c.ShouldBindJSON(&params); err != nil {
c.JSON(http.StatusBadRequest, apitypes.Error("invalid request", err))
return
}
overrides := homepage.GetOverrideConfig()
overrides.SetItemsFavorite(params.Which, params.Value)
c.JSON(http.StatusOK, apitypes.Success("success"))
}
// @x-id "set-item-sort-order"
// @BasePath /api/v1
// @Summary Set homepage item sort order
// @Description Set homepage item sort order.
// @Tags homepage
// @Accept json
// @Produce json
// @Param request body HomepageOverrideItemSortOrderParams true "Set item sort order"
// @Success 200 {object} apitypes.SuccessResponse
// @Failure 400 {object} apitypes.ErrorResponse
// @Failure 500 {object} apitypes.ErrorResponse
// @Router /homepage/set/item_sort_order [post]
func SetItemSortOrder(c *gin.Context) {
var params HomepageOverrideItemSortOrderParams
if err := c.ShouldBindJSON(&params); err != nil {
c.JSON(http.StatusBadRequest, apitypes.Error("invalid request", err))
return
}
overrides := homepage.GetOverrideConfig()
overrides.SetSortOrder(params.Which, params.Value)
c.JSON(http.StatusOK, apitypes.Success("success"))
}
// @x-id "set-item-all-sort-order"
// @BasePath /api/v1
// @Summary Set homepage item all sort order
// @Description Set homepage item all sort order.
// @Tags homepage
// @Accept json
// @Produce json
// @Param request body HomepageOverrideItemAllSortOrderParams true "Set item all sort order"
// @Success 200 {object} apitypes.SuccessResponse
// @Failure 400 {object} apitypes.ErrorResponse
// @Failure 500 {object} apitypes.ErrorResponse
// @Router /homepage/set/item_all_sort_order [post]
func SetItemAllSortOrder(c *gin.Context) {
var params HomepageOverrideItemAllSortOrderParams
if err := c.ShouldBindJSON(&params); err != nil {
c.JSON(http.StatusBadRequest, apitypes.Error("invalid request", err))
return
}
overrides := homepage.GetOverrideConfig()
overrides.SetAllSortOrder(params.Which, params.Value)
c.JSON(http.StatusOK, apitypes.Success("success"))
}
// @x-id "set-item-fav-sort-order"
// @BasePath /api/v1
// @Summary Set homepage item fav sort order
// @Description Set homepage item fav sort order.
// @Tags homepage
// @Accept json
// @Produce json
// @Param request body HomepageOverrideItemFavSortOrderParams true "Set item fav sort order"
// @Success 200 {object} apitypes.SuccessResponse
// @Failure 400 {object} apitypes.ErrorResponse
// @Failure 500 {object} apitypes.ErrorResponse
// @Router /homepage/set/item_fav_sort_order [post]
func SetItemFavSortOrder(c *gin.Context) {
var params HomepageOverrideItemFavSortOrderParams
if err := c.ShouldBindJSON(&params); err != nil {
c.JSON(http.StatusBadRequest, apitypes.Error("invalid request", err))
return
}
overrides := homepage.GetOverrideConfig()
overrides.SetFavSortOrder(params.Which, params.Value)
c.JSON(http.StatusOK, apitypes.Success("success"))
}
// @x-id "set-category-order"
// @BasePath /api/v1
// @Summary Set homepage category order
// @Description Set homepage category order.
// @Tags homepage
// @Accept json
// @Produce json
// @Param request body HomepageOverrideCategoryOrderParams true "Override category order"
// @Success 200 {object} apitypes.SuccessResponse
// @Failure 400 {object} apitypes.ErrorResponse
// @Failure 500 {object} apitypes.ErrorResponse
// @Router /homepage/set/category_order [post]
func SetCategoryOrder(c *gin.Context) {
var params HomepageOverrideCategoryOrderParams
if err := c.ShouldBindJSON(&params); err != nil {
c.JSON(http.StatusBadRequest, apitypes.Error("invalid request", err))
return
}
overrides := homepage.GetOverrideConfig()
overrides.SetCategoryOrder(params.Which, params.Value)
c.JSON(http.StatusOK, apitypes.Success("success"))
}

View File

@@ -1,90 +0,0 @@
package v1
import (
"encoding/json"
"io"
"net/http"
"github.com/yusing/go-proxy/internal/homepage"
"github.com/yusing/go-proxy/internal/net/gphttp"
)
const (
HomepageOverrideItem = "item"
HomepageOverrideItemsBatch = "items_batch"
HomepageOverrideCategoryOrder = "category_order"
HomepageOverrideItemVisible = "item_visible"
)
type (
HomepageOverrideItemParams struct {
Which string `json:"which"`
Value homepage.ItemConfig `json:"value"`
}
HomepageOverrideItemsBatchParams struct {
Value map[string]*homepage.ItemConfig `json:"value"`
}
HomepageOverrideCategoryOrderParams struct {
Which string `json:"which"`
Value int `json:"value"`
}
HomepageOverrideItemVisibleParams struct {
Which []string `json:"which"`
Value bool `json:"value"`
}
)
func SetHomePageOverrides(w http.ResponseWriter, r *http.Request) {
what := r.FormValue("what")
if what == "" {
gphttp.BadRequest(w, "missing what or which")
return
}
data, err := io.ReadAll(r.Body)
if err != nil {
gphttp.ClientError(w, r, err, http.StatusBadRequest)
return
}
r.Body.Close()
overrides := homepage.GetOverrideConfig()
switch what {
case HomepageOverrideItem:
var params HomepageOverrideItemParams
if err := json.Unmarshal(data, &params); err != nil {
gphttp.ClientError(w, r, err, http.StatusBadRequest)
return
}
overrides.OverrideItem(params.Which, &params.Value)
case HomepageOverrideItemsBatch:
var params HomepageOverrideItemsBatchParams
if err := json.Unmarshal(data, &params); err != nil {
gphttp.ClientError(w, r, err, http.StatusBadRequest)
return
}
overrides.OverrideItems(params.Value)
case HomepageOverrideItemVisible: // POST /v1/item_visible [a,b,c], false => hide a, b, c
var params HomepageOverrideItemVisibleParams
if err := json.Unmarshal(data, &params); err != nil {
gphttp.ClientError(w, r, err, http.StatusBadRequest)
return
}
if params.Value {
overrides.UnhideItems(params.Which)
} else {
overrides.HideItems(params.Which)
}
case HomepageOverrideCategoryOrder:
var params HomepageOverrideCategoryOrderParams
if err := json.Unmarshal(data, &params); err != nil {
gphttp.ClientError(w, r, err, http.StatusBadRequest)
return
}
overrides.SetCategoryOrder(params.Which, params.Value)
default:
http.Error(w, "invalid what", http.StatusBadRequest)
return
}
w.WriteHeader(http.StatusOK)
}

37
internal/api/v1/icons.go Normal file
View File

@@ -0,0 +1,37 @@
package v1
import (
"net/http"
"github.com/gin-gonic/gin"
"github.com/yusing/godoxy/internal/homepage"
apitypes "github.com/yusing/goutils/apitypes"
)
type ListIconsRequest struct {
Limit int `form:"limit" validate:"omitempty,min=0"`
Keyword string `form:"keyword" validate:"required"`
} // @name ListIconsRequest
// @x-id "icons"
// @BasePath /api/v1
// @Summary List icons
// @Description List icons
// @Tags v1
// @Accept json
// @Produce json
// @Param limit query int false "Limit"
// @Param keyword query string false "Keyword"
// @Success 200 {array} homepage.IconMetaSearch
// @Failure 400 {object} apitypes.ErrorResponse
// @Failure 403 {object} apitypes.ErrorResponse
// @Router /icons [get]
func Icons(c *gin.Context) {
var request ListIconsRequest
if err := c.ShouldBindQuery(&request); err != nil {
c.JSON(http.StatusBadRequest, apitypes.Error("invalid request", err))
return
}
icons := homepage.SearchIcons(request.Keyword, request.Limit)
c.JSON(http.StatusOK, icons)
}

View File

@@ -1,11 +0,0 @@
package v1
import (
"net/http"
"github.com/yusing/go-proxy/internal/net/gphttp"
)
func Index(w http.ResponseWriter, r *http.Request) {
gphttp.WriteBody(w, []byte("API ready"))
}

View File

@@ -1,22 +0,0 @@
package v1
import (
"net/http"
"time"
"github.com/gorilla/websocket"
"github.com/yusing/go-proxy/agent/pkg/agent"
"github.com/yusing/go-proxy/internal/net/gphttp"
"github.com/yusing/go-proxy/internal/net/gphttp/gpwebsocket"
"github.com/yusing/go-proxy/internal/net/gphttp/httpheaders"
)
func ListAgents(w http.ResponseWriter, r *http.Request) {
if httpheaders.IsWebsocket(r.Header) {
gpwebsocket.Periodic(w, r, 10*time.Second, func(conn *websocket.Conn) error {
return conn.WriteJSON(agent.ListAgents())
})
} else {
gphttp.RespondJSON(w, r, agent.ListAgents())
}
}

View File

@@ -1,41 +0,0 @@
package v1
import (
"net/http"
"strings"
"github.com/yusing/go-proxy/internal/common"
config "github.com/yusing/go-proxy/internal/config/types"
"github.com/yusing/go-proxy/internal/net/gphttp"
"github.com/yusing/go-proxy/internal/utils"
)
func ListFilesHandler(cfg config.ConfigInstance, w http.ResponseWriter, r *http.Request) {
files, err := utils.ListFiles(common.ConfigBasePath, 0, true)
if err != nil {
gphttp.ServerError(w, r, err)
return
}
resp := map[FileType][]string{
FileTypeConfig: make([]string, 0),
FileTypeProvider: make([]string, 0),
FileTypeMiddleware: make([]string, 0),
}
for _, file := range files {
t := fileType(file)
file = strings.TrimPrefix(file, common.ConfigBasePath+"/")
resp[t] = append(resp[t], file)
}
mids, err := utils.ListFiles(common.MiddlewareComposeBasePath, 0, true)
if err != nil {
gphttp.ServerError(w, r, err)
return
}
for _, mid := range mids {
mid = strings.TrimPrefix(mid, common.MiddlewareComposeBasePath+"/")
resp[FileTypeMiddleware] = append(resp[FileTypeMiddleware], mid)
}
gphttp.RespondJSON(w, r, resp)
}

View File

@@ -1,13 +0,0 @@
package v1
import (
"net/http"
config "github.com/yusing/go-proxy/internal/config/types"
"github.com/yusing/go-proxy/internal/net/gphttp"
"github.com/yusing/go-proxy/internal/route/routes"
)
func ListHomepageCategoriesHandler(cfg config.ConfigInstance, w http.ResponseWriter, r *http.Request) {
gphttp.RespondJSON(w, r, routes.HomepageCategories())
}

View File

@@ -1,13 +0,0 @@
package v1
import (
"net/http"
config "github.com/yusing/go-proxy/internal/config/types"
"github.com/yusing/go-proxy/internal/net/gphttp"
"github.com/yusing/go-proxy/internal/route/routes"
)
func ListHomepageConfigHandler(cfg config.ConfigInstance, w http.ResponseWriter, r *http.Request) {
gphttp.RespondJSON(w, r, routes.HomepageConfig(r.FormValue("category"), r.FormValue("provider")))
}

View File

@@ -1,23 +0,0 @@
package v1
import (
"net/http"
"strconv"
config "github.com/yusing/go-proxy/internal/config/types"
"github.com/yusing/go-proxy/internal/homepage"
"github.com/yusing/go-proxy/internal/net/gphttp"
)
func ListIconsHandler(cfg config.ConfigInstance, w http.ResponseWriter, r *http.Request) {
limit, err := strconv.Atoi(r.FormValue("limit"))
if err != nil {
limit = 0
}
icons, err := homepage.SearchIcons(r.FormValue("keyword"), limit)
if err != nil {
gphttp.ClientError(w, r, err)
return
}
gphttp.RespondJSON(w, r, icons)
}

View File

@@ -1,19 +0,0 @@
package v1
import (
"net/http"
config "github.com/yusing/go-proxy/internal/config/types"
"github.com/yusing/go-proxy/internal/net/gphttp"
"github.com/yusing/go-proxy/internal/route/routes"
)
func ListRouteHandler(cfg config.ConfigInstance, w http.ResponseWriter, r *http.Request) {
which := r.PathValue("which")
route, ok := routes.Get(which)
if ok {
gphttp.RespondJSON(w, r, route)
} else {
gphttp.RespondJSON(w, r, nil)
}
}

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