Compare commits

...

415 Commits

Author SHA1 Message Date
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
yusing
68ac4f952d fix(autocert): wrong path for last failure file 2025-07-21 09:39:17 +08:00
yusing
a2e6688056 chore: upgrade dependencies 2025-07-20 13:29:03 +08:00
yusing
e02cacdf2a feat(middleware): add themed middleware with customizable themes and styles
- Introduced a new themed middleware that allows for dynamic theme application.
- Added support for multiple themes: dark, dark-grey, solarized-dark, and custom CSS.
- Included CSS files for each theme and a font CSS template for font customization.
- Updated middleware registry to include the new themed middleware.
2025-07-20 12:59:25 +08:00
yusing
46c7ee4d84 feat(middleware): optimize HTML modification with byte pool 2025-07-20 12:47:32 +08:00
yusing
f39513483b refactor(middleware): improve common options handling in apply method 2025-07-20 12:46:30 +08:00
yusing
731121595c feat(autocert): enhance failure handling and cooldown logic
- Added last failure tracking to the Provider struct to manage certificate renewal failures.
- Implemented methods to get, update, and clear the last failure timestamp.
- Introduced cooldown durations to prevent immediate retries after failures.
- Updated ObtainCert and ScheduleRenewal methods to utilize the new failure handling logic.
2025-07-20 12:42:52 +08:00
yusing
8025af6067 feat(healthcheck): add retries before notifying (default: 3 times)
- Introduced NotifyFunc type for customizable notification handling in tests.
- Added Retries field to HealthCheckConfig for controlling notification thresholds.
- Implemented tests for notification behavior under various health check scenarios.
2025-07-20 12:03:45 +08:00
yusing
47910774dd fix(homepage): missing homepage.url option 2025-07-13 15:47:26 +08:00
yusing
b6bfd19cc2 fix(middleware): set Accept-Encoding to "" in modify_html middleware 2025-07-13 15:42:39 +08:00
yusing
e3b53a548d refactor(misc): small code refactor 2025-07-13 14:33:58 +08:00
yusing
a954ac8946 feat(middleware): introduct modify_html middleware 2025-07-13 14:01:36 +08:00
yusing
814ff33352 fix(tests): refactor and fix login in middleware test utils 2025-07-13 13:33:48 +08:00
yusing
b1d5c4b091 chore: upgrade go to 1.24.5 2025-07-13 13:10:19 +08:00
yusing
72dc783e23 fix(icons): update selfh.st icon json url 2025-07-01 02:14:27 +08:00
yusing
1c95bbba6e fix: Makefile 2025-07-01 02:14:12 +08:00
yusing
0c552c9cea chore: upgrade dependenocies 2025-07-01 02:14:06 +08:00
yusing
5631b1540a fix(notif): refactor and fix json payload validation and handling 2025-06-28 14:46:54 +08:00
yusing
24f949f053 fix(notif): stack overflow due to self recursion 2025-06-28 14:44:06 +08:00
yusing
9d712b91ff fix(rules): routes without default rule panics 2025-06-16 21:36:20 +08:00
yusing
4189ffa1db fix(docker): wildcard properties not working with FQDN aliases 2025-06-16 21:23:23 +08:00
yusing
e906b358fa tweak(entrypoint): introducing ResponseRecorder specialized for access logger for better throughput 2025-06-15 17:42:38 +08:00
yusing
f179de9231 fix(setup): update DNS provider docs link in setup script 2025-06-15 10:00:53 +08:00
yusing
1d546624de fix(serialization): call of reflect.Value.IsNil on string Value 2025-06-14 22:12:24 +08:00
yusing
ecc9d306d1 refactor(agent): move agent pool to agent package, rename route.Agent() to route.GetAgent() (cont. 7d17a01) 2025-06-14 22:04:10 +08:00
yusing
5ce1c7865e feat(agent): allow specifying agent for routes in route files 2025-06-14 20:05:11 +08:00
yusing
7d17a01de1 refactor(agent): move agent pool to agent package, rename route.Agent() to route.GetAgent() 2025-06-14 20:04:39 +08:00
yusing
cabb840a91 tweak(docker): add hint when specified network not found 2025-06-14 19:32:36 +08:00
yusing
4825f768f3 feat(docker): allow specifying docker network, handle error when no network available 2025-06-14 17:08:07 +08:00
yusing
5fdb023188 feat(docker): add network field to container info 2025-06-14 10:05:45 +08:00
yusing
4abf61a421 refactor(notif): enhance retry mechanism with exponential backoff and jitter; replace retry channel with a set for managing retry messages 2025-06-14 09:31:09 +08:00
yusing
96b7c3fcec chore: upgrade dependenocies 2025-06-13 23:06:34 +08:00
yusing
f8c57d930f fix(docker): wildcard labels not applying properly for YAML style values and alias without labels 2025-06-13 23:02:25 +08:00
yusing
880d66c75e docs: update links in config.example.yml to point to the new documentation site 2025-06-12 21:34:50 +08:00
yusing
4649c8d479 chore: update .gitignore to include .cursor directory 2025-06-12 21:08:00 +08:00
DarinDev1000
20021b3cae add GODOXY_API_JWT_SECURE=true to .env 2025-06-12 21:08:00 +08:00
yusing
cfa9201f82 fix(shutdown): change gracefulShutdown to call root.Finish directly instead of in a goroutine 2025-06-09 22:20:49 +08:00
yusing
b5328fe5e7 feat(idlesleep): support idlesleep for stream routes, rewritten and fixed stream implementation 2025-06-09 22:20:26 +08:00
yusing
25fbcc4ab9 fix(label): expand wildcard labels before unmarshaling and add corresponding test 2025-06-09 20:46:39 +08:00
yusing
421aaecba4 refactor: rename net/types to nettypes 2025-06-08 17:59:48 +08:00
yusing
01773976d1 fix(compose): rename container from godoxy to godoxy-proxy to prevent route conflict 2025-06-08 17:55:17 +08:00
yusing
2263d6063e fix(favicon): add GetFavIconFromAlias function and update favicon handling in idlewatcher 2025-06-08 15:13:38 +08:00
yusing
cfe0f6bb70 fix(route): remove x-properties routes during loading 2025-06-07 22:28:44 +08:00
yusing
a90d2b90d1 fix(systeminfo): safer time interval calculations and fix divided by zero crash 2025-06-07 19:42:53 +08:00
yusing
af9629424e fix(route): incorrect url for loadbalanced route 2025-06-07 19:13:31 +08:00
yusing
ee6cf29bc1 chore: upgrade dependenocies 2025-06-07 19:05:04 +08:00
yusing
c4a780e061 chore: updated to go1.24.4 2025-06-07 18:54:23 +08:00
yusing
09c244ef3c fix(route): add mutex lock for load balancer updates to prevent race conditions 2025-06-05 18:53:11 +08:00
yusing
bd0fe36c53 fix(idlewatcher): should not print idle_timeout fields on dependencies 2025-06-05 18:49:11 +08:00
yusing
d240da4393 fix(route): incorrect health status for idlewatcher dependencies 2025-06-05 18:40:40 +08:00
yusing
9470a14fe8 refactor(route): unify common fields into routes.go 2025-06-05 18:25:15 +08:00
yusing
d3568d9c35 fix: conflict error on load-balanced and excluded routes 2025-06-05 01:16:53 +08:00
yusing
44ef351840 fix(panic): Route.ProviderName before provider is set 2025-06-05 00:13:29 +08:00
yusing
a39d527fc1 feat(idlesleep): support container dependencies, including custom and docker depends_on, code refactor 2025-06-04 23:26:38 +08:00
yusing
22ab043e06 refactor(route): improve route handling 2025-06-04 23:17:41 +08:00
yusing
b670cdbd49 refactor(provider): improve route handling 2025-06-04 23:15:56 +08:00
yusing
45e34d691a tweak(healthcheck): allow custom base context 2025-06-04 23:14:46 +08:00
yusing
e82480a639 refactor: rename route/provider/types to provider 2025-06-04 23:13:42 +08:00
yusing
e39407886d fix: improved docker image parsing 2025-06-04 23:00:53 +08:00
yusing
3135e377a9 tweak(route): start routes in parallel 2025-06-03 23:32:59 +08:00
yusing
bdb3343a7c fix(healthcheck): handle cases for zero port 2025-06-03 22:56:00 +08:00
yusing
b411c6d504 feat(route): add api info for whether route is excluded 2025-06-03 22:48:35 +08:00
yusing
966a59b5c9 tweak: improve port and scheme detection 2025-06-03 22:41:31 +08:00
yusing
58db228e25 refactor(query): replace anonymous functions with sequence and for loop 2025-06-03 20:51:00 +08:00
yusing
e737737415 fix(idlewatcher): wake time outs before actual timeout 2025-06-02 23:26:47 +08:00
yusing
9087c4f195 feat(healthcheck): allow health checking for excluded routes 2025-06-02 23:19:30 +08:00
yusing
4705989f4b fix(websocket): 403 forbidden agent metrics 2025-06-01 00:31:56 +08:00
yusing
cb506120dd fix(serialization): remove debug stack from error message in mapUnmarshalValidate 2025-05-31 22:51:30 +08:00
yusing
88aaf956e5 fix(ci): fix wrong indentation for commented out gha cache in Docker workflow 2025-05-31 22:42:44 +08:00
yusing
ecfd018b0b fix(websocket): http: superfluous response.WriteHeader on websocket initiation failure 2025-05-31 22:29:02 +08:00
yusing
54bf84dcba fix(websocket): fix local address checks 2025-05-31 13:55:29 +08:00
yusing
57200bc1e9 refactor(io): enhance HTTP flusher handling 2025-05-31 13:54:50 +08:00
yusing
6f9bb410f5 fix(agent): use godoxy-to-agent latency for health check 2025-05-31 07:39:01 +08:00
yusing
e62e667b49 ci: remove gha cache in Docker workflow 2025-05-31 07:15:39 +08:00
yusing
abe81541db fix(login): login not working with user password 2025-05-31 07:11:19 +08:00
yusing
9e5d33714c chore: update dependencies 2025-05-30 20:50:31 +08:00
yusing
93a81fd558 fix(task): fix task tests 2025-05-29 20:42:36 +08:00
yusing
72923b8cfa refactor: remove Tracer from middleware implementations and related debugging functionality 2025-05-29 20:27:25 +08:00
yusing
24ba4c2a46 fix(oidc): update login handler to set redirect header for frontend requests 2025-05-29 20:08:52 +08:00
yusing
ed07bf42ce fix(routes): accidentally finished all routes when one route changed 2025-05-29 19:22:42 +08:00
yusing
371e756307 refactor: small refactor on maxmind code 2025-05-28 22:14:10 +08:00
yusing
32d8292b17 fix(oidc): apply rate limit to fix oocasional oauth state error due to race condition 2025-05-28 22:12:41 +08:00
yusing
717fd0e58c feat(net): add option to disable compression 2025-05-28 22:09:25 +08:00
yusing
2628d9e8a8 fix(task): refactor task module and fix reload stuck/error, fix some logic 2025-05-28 22:07:13 +08:00
yusing
c90795e614 fix(notification): unknown format: &{} 2025-05-26 20:03:17 +08:00
yusing
4a6bed7728 refactor: headers handling 2025-05-26 19:57:39 +08:00
yusing
216c03c5ff fix(task): revert to context based approach and fix tasks stuck, improve error handling 2025-05-26 00:32:59 +08:00
yusing
2e9f113224 fix(autocert): update test server configuration to use TLS with custom CA and add IPAddresses field 2025-05-25 18:50:18 +08:00
yusing
9d58977fa6 feat(autocert): add CACerts field to autocert Config for custom CA 2025-05-25 17:33:13 +08:00
yusing
8469b6406c tweak: consolidate bytes pool management and enhance CopyClose functionality for improved performance 2025-05-25 16:20:12 +08:00
yusing
b163771956 refactor: improve task management with xsync for concurrent access and enhance callback and subtasks handling as well as memory allocation 2025-05-25 15:13:30 +08:00
yusing
c1221e61d4 refactor: improve task management with xsync for concurrent access and enhance callback and subtasks handling as well as memory allocation 2025-05-25 15:01:44 +08:00
Yuzerion
4a8bd48ad5 fix: optimize memory usage, fix agent and code refactor (#118)
* refactor: simplify io code and make utils module independent

* fix(docker): agent and socket-proxy docker event flushing with modified reverse proxy handler

* refactor: remove unused code

* refactor: remove the use of logging module in most code

* refactor: streamline domain mismatch check in certState function

* tweak: use ecdsa p-256 for autocert

* fix(tests): update health check tests for invalid host and add case for port in host

* feat(acme): custom acme directory

* refactor: code refactor and improved context and error handling

* tweak: optimize memory usage under load

* fix(oidc): restore old user matching behavior

* docs: add ChatGPT assistant to README

---------

Co-authored-by: yusing <yusing@6uo.me>
2025-05-25 09:45:57 +08:00
yusing
ade93d49a3 docs: add ChatGPT assistant to README 2025-05-25 09:36:22 +08:00
yusing
82ee75daab fix(oidc): restore old user matching behavior 2025-05-25 09:14:54 +08:00
yusing
f0ab14cb1e tweak: optimize memory usage under load 2025-05-24 22:05:42 +08:00
yusing
5b7c392297 refactor: code refactor and improved context and error handling 2025-05-24 10:02:24 +08:00
yusing
1f1ae38e4d feat(acme): custom acme directory 2025-05-24 09:41:19 +08:00
yusing
22d44a6bb0 fix(tests): update health check tests for invalid host and add case for port in host 2025-05-24 08:42:10 +08:00
yusing
6a5cd1266b tweak: use ecdsa p-256 for autocert 2025-05-24 07:29:11 +08:00
yusing
1cf18657b6 refactor: streamline domain mismatch check in certState function 2025-05-24 07:20:58 +08:00
yusing
63c4bdc73d refactor: remove the use of logging module in most code 2025-05-23 23:28:33 +08:00
yusing
20a1649275 tweak: disable bytes pool for socket-proxy and agent 2025-05-23 23:01:27 +08:00
yusing
0f3b8e68ce refactor: remove unused code 2025-05-23 22:46:35 +08:00
yusing
5a3e3f19c7 fix(imports): due to previous refactor 2025-05-23 22:24:22 +08:00
yusing
df193a42fc fix(docker): agent and socket-proxy docker events not flushing with modified reverse proxy handler 2025-05-23 22:20:32 +08:00
yusing
f1e204f7fd refactor: simplify io code and make utils module independent 2025-05-23 22:19:14 +08:00
yusing
ff08c40403 fix(agent): docker handler 2025-05-23 21:12:46 +08:00
yusing
d8266f779f chore: update dependencies 2025-05-23 21:08:11 +08:00
yusing
9711867fbe tweak: optimize memory usage and allocation 2025-05-22 22:43:11 +08:00
yusing
fc8592ab45 fix(systeminfo): divide by zero on disk I/O speed calculation 2025-05-22 22:41:39 +08:00
yusing
3dbab118af refactor: docker event watcher 2025-05-19 23:15:47 +08:00
yusing
1f50ee7f2f tweak: replace coder/websocket with gorilla/websocket 2025-05-19 23:15:11 +08:00
yusing
cee6eaecff fix(healthcheck): retry on error and stop afte 5 trials 2025-05-18 22:16:12 +08:00
yusing
67a6b89ea5 fix(agent): improve install script command handling and add agent running check 2025-05-17 08:51:22 +08:00
yusing
78be9b1c71 fix(agent): update script url 2025-05-17 08:39:03 +08:00
yusing
26856b612a docs: replace broken/old links and update README_CHT key features section 2025-05-17 08:38:40 +08:00
yusing
36ceba3ae7 security: switch from RSA-2048 to ECDSA-P256 for agent certs and update certificate config and handling 2025-05-17 08:29:01 +08:00
yusing
f45f3fba79 refactor: logic refactor for setting xff header 2025-05-16 20:14:03 +08:00
yusing
4bbff323e3 chore: update dependencies 2025-05-16 07:19:33 +08:00
yusing
2e68baa93e tweak: optimize memory allocation and increase throughput 2025-05-16 07:15:45 +08:00
yusing
a162371ec5 feat: parallelize system info collection and refactor code 2025-05-14 21:38:28 +08:00
yusing
8f9c76daa5 chore: update dependencies 2025-05-14 21:00:53 +08:00
yusing
8b3e058885 fix: error formatting 2025-05-14 20:34:41 +08:00
yusing
023cbc81bc ci: update Docker CI workflows to exclude tags for socket-proxy and improve caching 2025-05-14 13:50:12 +08:00
yusing
b490e8c475 fix(acl): maxmind error even if configured, refactor 2025-05-14 13:44:43 +08:00
yusing
8e27886235 fix: incorrect unmarshal behavior for pointer primitives 2025-05-14 12:20:52 +08:00
yusing
7435b8e485 tests: add test for acl matchers 2025-05-13 20:11:16 +08:00
yusing
21724c037f fix: error formatting 2025-05-13 20:11:03 +08:00
yusing
44b4cff35e fix: acl matcher parsing, refactor 2025-05-13 19:40:43 +08:00
yusing
1e24765b17 fix: nil when printing error in edge cases 2025-05-13 19:40:04 +08:00
yusing
a1f2a84a16 fix(oidc): multiple state cookies being sent to frontend causing invalid oauth state 2025-05-12 14:19:18 +08:00
yusing
453262832a security: disallow tls1.0/1.1 2025-05-12 12:22:52 +08:00
yusing
99e975145c tweak default docker compose 2025-05-11 23:40:38 +08:00
yusing
e300170c51 fix: route autoconfiguration 2025-05-11 21:38:43 +08:00
yusing
1382137f20 tweak(cicd): attempt on better build caching 2025-05-11 07:00:34 +08:00
yusing
54d7508f5d style: gofmt and fix golangcl-ilint 2025-05-11 06:34:35 +08:00
yusing
71ca8c738e fix: middleware bypass 2025-05-11 06:33:22 +08:00
yusing
f1eefde964 fix(oidc): add timeout to oidc initialization 2025-05-11 05:58:18 +08:00
yusing
84e7a6591e fix(agent): health check logic 2025-05-11 00:05:01 +08:00
yusing
30c76cfc5f refactor: health check logic 2025-05-10 22:55:20 +08:00
yusing
a8ba42e360 fix: routes iter missing stream 2025-05-10 21:31:38 +08:00
yusing
cd291556fc fix(oid); redirect 2025-05-10 21:25:27 +08:00
yusing
0d41809630 fix(middleware): move bypass after finalize 2025-05-10 21:19:03 +08:00
yusing
53acf75c04 fix(homepage): item not hiding after config override 2025-05-10 18:14:10 +08:00
yusing
cf30fe6cfc feat(homepage): custom app sort order 2025-05-10 17:36:51 +08:00
yusing
55bbcae911 feat(api): refined list route api 2025-05-10 15:22:30 +08:00
yusing
b30c0d7dc0 feat(api): include agent version in response 2025-05-10 13:37:51 +08:00
yusing
198ae2cd02 refactor(api): restructure existing routes and remove unused debug endpoints and command line arguments 2025-05-10 13:12:41 +08:00
yusing
26938eb6ed feat(api): add new route for listing routes by provider 2025-05-10 12:58:37 +08:00
yusing
48823a860f fix(docker-compose): remove default proxy.exclude 2025-05-10 12:28:08 +08:00
yusing
985ff0a74d fix(deps): use dummy version for go-proxy module 2025-05-10 12:27:48 +08:00
yusing
43b493c60e fix(agent): docker handler 2025-05-10 12:26:50 +08:00
yusing
e0e0fab127 fix(agent): disable socket proxy by default 2025-05-10 12:26:06 +08:00
yusing
fc0dbd940c fix: Dockerfile caching 2025-05-10 12:12:39 +08:00
yusing
0208e6286f fix: docker socket handler 2025-05-10 11:24:28 +08:00
yusing
2c0b68c8c2 fix(build): Dockerfile 2025-05-10 10:50:26 +08:00
yusing
c05059765d style: coed cleanup and fix styling 2025-05-10 10:46:31 +08:00
yusing
a06787593c style: update golangci-lint and trunk configurations 2025-05-10 10:46:03 +08:00
yusing
8fe94d6d14 feat(socket-proxy): implement Docker socket proxy and related configurations
- Updated Dockerfile and Makefile for socket-proxy build.
- Modified go.mod to include necessary dependencies.
- Updated CI workflows for socket-proxy integration.
- Better module isolation
- Code refactor
2025-05-10 09:47:03 +08:00
yusing
4ddfb48b9d fix(setup): skipped autocert configuration 2025-05-09 14:31:32 +08:00
yusing
31dc112591 fix(middleware): middleware chain error handling
- Removed unnecessary initialization of befores and modResps in middlewareChain.
- modifyResponse should return immediately on error.
2025-05-09 12:29:50 +08:00
yusing
6797897814 fix(healthcheck): ensure detail is included on error 2025-05-09 12:26:31 +08:00
yusing
99eccd0b95 fix(monitor): reduce health check result initialization 2025-05-09 12:14:34 +08:00
yusing
0387739b94 fix(homepage): prioritize container name and alias as display name 2025-05-09 11:42:33 +08:00
yusing
ead27c72f1 fix(agent): typo for /distribution endpoint and update related configurations 2025-05-09 11:37:41 +08:00
yusing
455a85e6a0 feat(docker): add Docker socket proxy support and related configurations
- Introduced Docker socket proxy handling in the agent.
- Added environment variables for Docker socket configuration.
- Implemented new Docker handler with endpoint permissions based on environment settings.
- Added tests for Docker handler functionality.
- Updated go.mod to include gorilla/mux for routing.
2025-05-08 20:59:32 +08:00
yusing
8424fd9f1a chore: upgrade dependencies 2025-05-08 17:57:08 +08:00
yusing
75ee0e63bd fix(middleware): fix route bypass matching
- replace upstream headers approach with context value
2025-05-08 17:49:36 +08:00
yusing
1ce607029a Merge branch 'main' into dev 2025-05-07 23:27:02 +08:00
yusing
1e80ad2a44 fix(docker): host network_mode port selection 2025-05-07 23:26:51 +08:00
yusing
4daefa19d1 build: update Go version to 1.24.3 in Dockerfile and go.mod 2025-05-07 23:12:55 +08:00
yusing
491231e439 Merge branch 'main' into dev 2025-05-06 20:27:37 +08:00
yusing
c90ec8caa1 feat(container): add UpdatePorts method and support for host network mode 2025-05-06 20:27:25 +08:00
yusing
9eb674029e tweak(logging): rename write count variable and adjust buffer check interval 2025-05-05 20:59:43 +08:00
yusing
e41c6530ab chore: update dependencies and Makefile 2025-05-05 20:41:25 +08:00
yusing
afd35c183d test: fix failed tests after code changes 2025-05-05 20:41:25 +08:00
yusing
f190483b4e feat(rules.on): support route directive 2025-05-05 20:41:25 +08:00
yusing
7b0ed09772 fix(error): self referencing 2025-05-05 20:41:25 +08:00
yusing
4415bffc35 feat(rules.on): support & as logical AND 2025-05-05 20:41:25 +08:00
yusing
ddab2766b4 feat(middlewares): middleware bypass rules 2025-05-05 20:41:25 +08:00
yusing
ef95682116 feat(rules): compile path rules directly to glob 2025-05-05 20:41:25 +08:00
yusing
dd65a8d04b style: replace for loops with slices.Contains 2025-05-05 20:41:25 +08:00
yusing
aa23b5b595 test: add unit tests for FormatByteSize function 2025-05-05 20:41:25 +08:00
yusing
c55c6c84bc feat(health): add health check detail to health api 2025-05-05 20:41:25 +08:00
yusing
a45e5e17db chore: update dependencies and Makefile 2025-05-05 20:39:05 +08:00
yusing
b8c0961de3 test: fix failed tests after code changes 2025-05-05 20:05:47 +08:00
yusing
62d3d200e6 feat(rules.on): support route directive 2025-05-05 19:34:24 +08:00
yusing
bf32cafd90 fix(error): self referencing 2025-05-05 19:32:55 +08:00
yusing
1c182b5a7d feat(rules.on): support & as logical AND 2025-05-05 19:15:35 +08:00
yusing
ad60f377ba feat(middlewares): middleware bypass rules 2025-05-05 18:01:07 +08:00
yusing
75db09b1f3 feat(rules): compile path rules directly to glob 2025-05-05 14:42:55 +08:00
yusing
6dd849f480 style: replace for loops with slices.Contains 2025-05-05 13:36:08 +08:00
yusing
e2ae29795d test: add unit tests for FormatByteSize function 2025-05-05 13:27:51 +08:00
vSLY
92fa0f8168 Update README.md (#104)
Clarify setup process
2025-05-05 13:27:25 +08:00
yusing
b090598b68 feat(health): add health check detail to health api 2025-05-05 13:27:00 +08:00
vSLY
2cec88d3ce Update README.md (#104)
Clarify setup process
2025-05-05 00:45:29 +08:00
yusing
4df31263b5 fix(sensor): ignore "no data available" error 2025-05-05 00:33:43 +08:00
yusing
9eae809690 chore: move middleware trace to trace level 2025-05-04 23:58:47 +08:00
yusing
f1ba554a24 fix(notif): http 204 treated as error 2025-05-04 23:54:16 +08:00
yusing
f9a8aede20 feat: hCaptcha middleware 2025-05-04 17:21:12 +08:00
yusing
e275ee634c fix(http): content type detection 2025-05-04 16:18:46 +08:00
yusing
797d88772f fix: timeout waiting for maxmind db on shutdown 2025-05-04 16:04:17 +08:00
yusing
8ef8015a7f feat: improved icon and category lookup mechanism 2025-05-04 09:37:15 +08:00
yusing
5fce4b445b fix: error formatting 2025-05-04 07:12:28 +08:00
yusing
7552a706a7 chore: deps upgrade 2025-05-04 06:33:08 +08:00
yusing
e1bc6d1f44 fix: nil panic when formatting error 2025-05-04 06:33:00 +08:00
yusing
56850a9580 fix: update http error handling 2025-05-04 02:34:34 +08:00
yusing
5f780f4902 feat: improved port selection 2025-05-04 01:32:01 +08:00
yusing
ccb4639f43 breaking: move maxmind config to config.providers
- moved maxmind to separate module
- code refactored
- simplified test
2025-05-03 20:58:09 +08:00
yusing
ac1470d81d fix: remove incorrect comment from getOAuthRefreshToken function 2025-05-03 19:38:02 +08:00
yusing
efaabfa63a fix: access log field names 2025-05-03 19:32:02 +08:00
yusing
9043cf25c5 feat: push notification for config errors 2025-05-03 17:41:50 +08:00
yusing
98e90d7a0b refactor: improve error handling and response formatting in API 2025-05-03 17:41:10 +08:00
yusing
82c829de18 feat: notifications retry mechanism and improved error formatting 2025-05-03 14:30:40 +08:00
yusing
2fe4fef779 fix(oidc): enforce https redirection to prevent errors 2025-05-03 04:56:32 +08:00
yusing
91302ceed7 feat: simplify and optimize system info 2025-05-02 10:31:04 +08:00
yusing
7fa7b55b18 feat: notify for cert renewal result 2025-05-02 05:55:34 +08:00
yusing
69ee8495d8 refactor: notifications 2025-05-02 05:51:15 +08:00
yusing
28d9a72908 fix: icon not exists error on loading icon cache 2025-05-02 05:19:16 +08:00
yusing
770c698332 fix(idlewatcher): "unexpected container action" after unexpected EOF 2025-05-02 04:56:27 +08:00
yusing
cd4c843025 feat: light/dark variant icons and selfh.st tag as default category
- code refactor
- reduce memory usage
2025-05-02 03:38:50 +08:00
yusing
f0cf89060b chore(logging): move "adjusted buffer size" to debug level 2025-05-01 23:42:45 +08:00
yusing
f79a15bac6 update license 2025-05-01 07:29:48 +08:00
yusing
2b4a70a550 fix(docker): fixed retry mechanism 2025-05-01 06:48:38 +08:00
yusing
f06741428c fix(idlewatcher): log error and retry instead instead of stopping 2025-05-01 06:46:24 +08:00
yusing
16e6e72454 feat(access_log): dynamic buffer size 2025-05-01 05:57:02 +08:00
yusing
100d2c392f chore: memory optimization for access log 2025-04-30 18:30:46 +08:00
yusing
829eb08e37 feat: tunable rotate interval 2025-04-30 18:19:00 +08:00
yusing
53d54a09b0 fix: rotate result file size, add "saved" and omit empty values 2025-04-30 18:17:09 +08:00
yusing
62c551c7fe fix: tests 2025-04-30 17:42:51 +08:00
yusing
80e59bb481 fix: nil panic on unmarshaling zero value 2025-04-30 12:06:49 +08:00
yusing
7a5afc3612 fix; compose example 2025-04-30 04:03:11 +08:00
yusing
2c0349c11c chore: remove debug statement 2025-04-30 00:14:53 +08:00
yusing
8e3c2cc8d4 fix: issues when using socket-proxy 2025-04-29 23:56:15 +08:00
yusing
d35afdb3c9 security: exclude socket-proxy from proxying 2025-04-29 16:23:30 +08:00
yusing
ae093ebf40 docs: update wiki URL, add website URL 2025-04-29 15:22:31 +08:00
yusing
aa8af4185b chore: update schema url 2025-04-29 14:45:38 +08:00
yusing
0029cf69d6 fix: setup script and compose 2025-04-29 09:24:22 +08:00
Yuzerion
33e400a17e security: run in rootless by default and drop unnecessary caps (#101)
Co-authored-by: yusing <yusing@6uo.me>
2025-04-29 08:42:30 +08:00
yusing
1d22bcfed9 fix(access_log): file size calculation 2025-04-29 07:33:51 +08:00
yusing
978d82060e docs: move schema to frontend 2025-04-29 07:26:14 +08:00
yusing
7aa1215491 refactor: rename Deserialize to MapUnmarshalValidate 2025-04-29 07:26:14 +08:00
yusing
0b69589586 chore: disable unused last version parsing 2025-04-29 00:47:13 +08:00
yusing
bca3cd84d1 fix(accesslog): os: invalid use of WriteAt on file opened with O_APPEND 2025-04-29 00:46:30 +08:00
yusing
ce4bf2f646 fix(idlewatcher): not started for docker containers 2025-04-28 23:54:13 +08:00
yusing
c49016f22c fix: go.mod and deps upgrade 2025-04-28 11:32:01 +08:00
yusing
8da63daf02 refactor: simplify and remove duplicated code for icon caching 2025-04-28 11:22:49 +08:00
yusing
c5fd21552e fix(oidc): token not being refreshed when receiving simutaneous requests from the same session 2025-04-28 11:19:57 +08:00
yusing
27409abc24 fix: missing proxmox initialization 2025-04-28 05:08:14 +08:00
yusing
21c9e46274 fix: remove redundant event logging 2025-04-28 05:03:17 +08:00
yusing
22a12d3116 chore: remove redundant loadbalancer debug message 2025-04-28 04:57:26 +08:00
yusing
89d93dd878 chore: better error message 2025-04-28 00:48:20 +08:00
yusing
66853dfc52 fix: cloudflare realIP should defaults to be recursive 2025-04-27 23:53:04 +08:00
yusing
c72f66d64b feat(acl): add FORCE_RESOLVE_COUNTRY option to resolve country 2025-04-26 09:48:43 +08:00
yusing
59bc342a40 fix: notfications not being sent 2025-04-26 09:20:03 +08:00
yusing
e11579df10 chore(maxm): improved database update mechanism, fixed db being downloaded twice on first run 2025-04-26 09:08:03 +08:00
yusing
6a8f6fb4b5 chore(accesslog): reduce buffering for stdout 2025-04-26 08:29:55 +08:00
yusing
8f20bd3840 fix(acl): caching logic 2025-04-26 08:05:26 +08:00
yusing
f1abb745fe fix(tcp): return a dummy connection instead of nil 2025-04-26 07:57:20 +08:00
yusing
cb2990f6e8 chore: enrich example config 2025-04-26 07:40:55 +08:00
yusing
fb2f850311 fix(oidc): incorrect redirect url 2025-04-26 06:57:02 +08:00
yusing
2b9c0f09ee fix version checking 2025-04-26 06:50:43 +08:00
yusing
efe3eb4ce7 fix: autocert panic 2025-04-26 06:41:15 +08:00
yusing
a1c1a79976 fix: github workflow 2025-04-26 05:55:43 +08:00
yusing
90ba355d16 fix: Dockerfile 2025-04-26 05:51:37 +08:00
yusing
01179adfa8 fix: loosen agent version checking
- warn instead of error when version mismatch
- check for major version only
- better version parsing
2025-04-26 05:38:59 +08:00
yusing
e4be403bef fix(agent): reduce the size of agent binary by modules separation 2025-04-26 05:22:40 +08:00
yusing
e1cdf4da0f feat: add update functionality to agent script 2025-04-26 05:19:09 +08:00
yusing
5148cb3b8b refactor: remove unused constant CookieOauthSessionID, better error message 2025-04-26 03:55:16 +08:00
yusing
56c6a9f8fe chore: add groups scope to default OIDC scopes 2025-04-26 03:31:44 +08:00
yusing
be257b0532 refactor: change OIDCScopes to GetCommaSepEnv 2025-04-26 03:30:22 +08:00
yusing
0534bc38b2 fix(oidc): logout not working when user is denied 2025-04-26 03:26:45 +08:00
506 changed files with 32856 additions and 11942 deletions

View File

@@ -1,6 +1,15 @@
# docker image tag (latest, nightly)
TAG=latest
# set timezone to get correct log timestamp
TZ=ETC/UTC
# container uid and gid (must match the owner of mounted directories)
GODOXY_UID=1000
GODOXY_GID=1000
# Set GODOXY_API_JWT_SECURE=false to allow http
GODOXY_API_JWT_SECURE=true
# API JWT Configuration (common)
# generate secret with `openssl rand -base64 32`
GODOXY_API_JWT_SECRET=
@@ -16,12 +25,11 @@ GODOXY_API_PASSWORD=password
# OIDC Configuration (optional)
# Uncomment and configure these values to enable OIDC authentication.
# For `GODOXY_OIDC_SCOPES` you may also include `offline_access` if your Idp supports it (e.g. Authentik)
#
# GODOXY_OIDC_ISSUER_URL=https://accounts.google.com
# GODOXY_OIDC_CLIENT_ID=your-client-id
# GODOXY_OIDC_CLIENT_SECRET=your-client-secret
# GODOXY_OIDC_SCOPES=openid, profile, email
# GODOXY_OIDC_SCOPES=openid, profile, email, groups # you may also include `offline_access` if your Idp supports it (e.g. Authentik, Pocket ID)
#
# User definitions: Uncomment and configure these values to restrict access to specific users or groups.
# These two fields act as a logical AND operator. For example, given the following membership:
@@ -42,14 +50,29 @@ GODOXY_API_PASSWORD=password
GODOXY_HTTP_ADDR=:80
GODOXY_HTTPS_ADDR=:443
# Enable HTTP3
GODOXY_HTTP3_ENABLED=true
# API listening address
GODOXY_API_ADDR=127.0.0.1:8888
# Metrics
GODOXY_METRICS_DISABLE_CPU=false
GODOXY_METRICS_DISABLE_MEMORY=false
GODOXY_METRICS_DISABLE_DISK=false
GODOXY_METRICS_DISABLE_NETWORK=false
GODOXY_METRICS_DISABLE_SENSORS=false
# Frontend listening port
GODOXY_FRONTEND_PORT=3000
# Prometheus Metrics
GODOXY_PROMETHEUS_ENABLED=true
# Frontend aliases (subdomains / FQDNs, e.g. godoxy, godoxy.domain.com)
GODOXY_FRONTEND_ALIASES=godoxy
# Docker socket
# /var/run/podman/podman.sock for podman
DOCKER_SOCKET=/var/run/docker.sock
SOCKET_PROXY_LISTEN_ADDR=127.0.0.1:2375
# Debug mode
GODOXY_DEBUG=false

View File

@@ -36,9 +36,6 @@ jobs:
- name: Check binary
run: |
file bin/${{ matrix.binary_name }}
- name: Test
run: |
go test -v ./agent/...
- name: Upload
uses: actions/upload-artifact@v4
with:

View File

@@ -15,9 +15,10 @@ jobs:
with:
image_name: ${{ github.repository_owner }}/godoxy
tag: nightly
target: main
build-nightly-agent:
uses: ./.github/workflows/docker-image.yml
with:
image_name: ${{ github.repository_owner }}/godoxy-agent
tag: nightly
agent: true
target: agent

View File

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

View File

@@ -0,0 +1,23 @@
name: Docker Image CI (socket-proxy)
on:
push:
branches:
- main
paths:
- "socket-proxy/**"
tags-ignore:
- '**'
workflow_dispatch:
permissions:
contents: read
jobs:
build:
uses: ./.github/workflows/docker-image.yml
with:
image_name: ${{ github.repository_owner }}/socket-proxy
tag: latest
target: socket-proxy
dockerfile: socket-proxy.Dockerfile

View File

@@ -12,16 +12,20 @@ on:
old_image_name:
required: false
type: string
agent:
target:
required: true
type: string
dockerfile:
required: false
default: false
type: boolean
type: string
default: Dockerfile
env:
REGISTRY: ghcr.io
MAKE_ARGS: agent=${{ inputs.agent && '1' || '0' }}
DIGEST_PATH: /tmp/digests/${{ inputs.agent && 'agent' || 'main' }}
DIGEST_NAME_SUFFIX: ${{ inputs.agent && 'agent' || 'main' }}
MAKE_ARGS: ${{ inputs.target }}=1
DIGEST_PATH: /tmp/digests/${{ inputs.target }}
DIGEST_NAME_SUFFIX: ${{ inputs.target }}
DOCKERFILE: ${{ inputs.dockerfile }}
jobs:
build:
@@ -76,11 +80,14 @@ jobs:
with:
platforms: ${{ matrix.platform }}
labels: ${{ steps.meta.outputs.labels }}
file: ${{ env.DOCKERFILE }}
outputs: type=image,name=${{ env.REGISTRY }}/${{ inputs.image_name }},push-by-digest=true,name-canonical=true,push=true
cache-from: |
type=registry,ref=${{ env.REGISTRY }}/${{ inputs.image_name }}:buildcache-${{ env.PLATFORM_PAIR }}-${{ inputs.tag }}
type=registry,ref=${{ env.REGISTRY }}/${{ inputs.image_name }}:buildcache-${{ env.PLATFORM_PAIR }}
# type=gha,scope=${{ github.workflow }}-${{ env.PLATFORM_PAIR }}
cache-to: |
type=registry,ref=${{ env.REGISTRY }}/${{ inputs.image_name }}:buildcache-${{ env.PLATFORM_PAIR }}-${{ inputs.tag }},mode=max
type=registry,ref=${{ env.REGISTRY }}/${{ inputs.image_name }}:buildcache-${{ env.PLATFORM_PAIR }},mode=max
# type=gha,scope=${{ github.workflow }}-${{ env.PLATFORM_PAIR }},mode=max
build-args: |
VERSION=${{ github.ref_name }}
MAKE_ARGS=${{ env.MAKE_ARGS }}

4
.gitignore vendored
View File

@@ -29,7 +29,9 @@ todo.md
.aider*
mtrace.json
.env
*.env
.cursorrules
.cursor/
.windsurfrules
test.Dockerfile
@@ -37,4 +39,4 @@ node_modules/
tsconfig.tsbuildinfo
!agent.compose.yml
!agent/pkg/**
!agent/pkg/**

View File

@@ -1,135 +1,153 @@
run:
timeout: 10m
linters-settings:
govet:
enable-all: true
disable:
- shadow
- fieldalignment
gocyclo:
min-complexity: 14
misspell:
locale: US
funlen:
lines: -1
statements: 120
forbidigo:
forbid:
- ^print(ln)?$
godox:
keywords:
- FIXME
tagalign:
align: false
sort: true
order:
- description
- json
- toml
- yaml
- yml
- label
- label-slice-as-struct
- file
- kv
- export
stylecheck:
dot-import-whitelist:
- github.com/yusing/go-proxy/internal/utils/testing # go tests only
- github.com/yusing/go-proxy/internal/api/v1/utils # api only
revive:
rules:
- name: struct-tag
- name: blank-imports
- name: context-as-argument
- name: context-keys-type
- name: error-return
- name: error-strings
- name: error-naming
- name: exported
disabled: true
- name: if-return
- name: increment-decrement
- name: var-naming
- name: var-declaration
- name: package-comments
disabled: true
- name: range
- name: receiver-naming
- name: time-naming
- name: unexported-return
- name: indent-error-flow
- name: errorf
- name: empty-block
- name: superfluous-else
- name: unused-parameter
disabled: true
- name: unreachable-code
- name: redefines-builtin-id
gomoddirectives:
replace-allow-list:
- github.com/abbot/go-http-auth
- github.com/gorilla/mux
- github.com/mailgun/minheap
- github.com/mailgun/multibuf
- github.com/jaguilar/vt100
- github.com/cucumber/godog
- github.com/http-wasm/http-wasm-host-go
testifylint:
disable:
- suite-dont-use-pkg
- require-error
- go-require
staticcheck:
checks:
- all
- -SA1019
errcheck:
exclude-functions:
- fmt.Fprintln
version: "2"
linters:
enable-all: true
default: all
disable:
- execinquery # deprecated
- gomnd # deprecated
- sqlclosecheck # not relevant (SQL)
- rowserrcheck # not relevant (SQL)
- cyclop # duplicate of gocyclo
- depguard # Not relevant
- nakedret # Too strict
- lll # Not relevant
- gocyclo # must be fixed
- gocognit # Too strict
- nestif # Too many false-positive.
- prealloc # Too many false-positive.
- makezero # Not relevant
- dupl # Too strict
- gci # I don't care
- goconst # Too annoying
- gosec # Too strict
- gochecknoinits
# - bodyclose
- containedctx
# - contextcheck
- cyclop
- depguard
# - dupl
- err113
- exhaustive
- exhaustruct
- funcorder
- forcetypeassert
- gochecknoglobals
- wsl # Too strict
- nlreturn # Not relevant
- mnd # Too strict
- testpackage # Too strict
- tparallel # Not relevant
- paralleltest # Not relevant
- exhaustive # Not relevant
- exhaustruct # Not relevant
- err113 # Too strict
- wrapcheck # Too strict
- noctx # Too strict
- bodyclose # too many false-positive
- forcetypeassert # Too strict
- tagliatelle # Too strict
- varnamelen # Not relevant
- nilnil # Not relevant
- ireturn # Not relevant
- contextcheck # too many false-positive
- containedctx # too many false-positive
- maintidx # kind of duplicate of gocyclo
- nonamedreturns # Too strict
- gosmopolitan # not relevant
- exportloopref # Not relevant since go1.22
- gochecknoinits
- gocognit
- goconst
- gocyclo
- godot
- gomoddirectives
- gosmopolitan
- ireturn
- lll
- maintidx
- makezero
- mnd
- nakedret
- nestif
- nlreturn
- nonamedreturns
- noinlineerr
- paralleltest
- revive
- rowserrcheck
- sqlclosecheck
- tagalign
- tagliatelle
- testpackage
- tparallel
- varnamelen
- wrapcheck
- wsl
- wsl_v5
settings:
errcheck:
exclude-functions:
- fmt.Fprintln
forbidigo:
forbid:
- pattern: ^print(ln)?$
funlen:
lines: -1
statements: 120
gocyclo:
min-complexity: 14
godox:
keywords:
- FIXME
gomoddirectives:
replace-allow-list:
- github.com/abbot/go-http-auth
- github.com/gorilla/mux
- github.com/mailgun/minheap
- github.com/mailgun/multibuf
- github.com/jaguilar/vt100
- github.com/cucumber/godog
- github.com/http-wasm/http-wasm-host-go
govet:
disable:
- shadow
- fieldalignment
enable-all: true
misspell:
locale: US
revive:
rules:
- name: struct-tag
- name: blank-imports
- name: context-as-argument
- name: context-keys-type
- name: error-return
- name: error-strings
- name: error-naming
- name: exported
disabled: true
- name: if-return
- name: increment-decrement
- name: var-naming
- name: var-declaration
- name: package-comments
disabled: true
- name: range
- name: receiver-naming
- name: time-naming
- name: unexported-return
- name: indent-error-flow
- name: errorf
- name: empty-block
- name: superfluous-else
- name: unused-parameter
disabled: true
- name: unreachable-code
- name: redefines-builtin-id
staticcheck:
checks:
- all
- -SA1019
dot-import-whitelist:
- github.com/yusing/go-proxy/internal/utils/testing
- github.com/yusing/go-proxy/internal/api/v1/utils
tagalign:
align: false
sort: true
order:
- description
- json
- toml
- yaml
- yml
- label
- label-slice-as-struct
- file
- kv
- export
testifylint:
disable:
- suite-dont-use-pkg
- require-error
- go-require
exclusions:
generated: lax
presets:
- comments
- common-false-positives
- legacy
- std-error-handling
paths:
- third_party$
- builtin$
- examples$
formatters:
enable:
- gofmt
- gofumpt
- goimports
exclusions:
generated: lax
paths:
- third_party$
- builtin$
- examples$

View File

@@ -2,36 +2,37 @@
# To learn more about the format of this file, see https://docs.trunk.io/reference/trunk-yaml
version: 0.1
cli:
version: 1.22.10
version: 1.25.0
# Trunk provides extensibility via plugins. (https://docs.trunk.io/plugins)
plugins:
sources:
- id: trunk
ref: v1.6.7
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.23.2
- go@1.24.3
# This is the section where you manage your linters. (https://docs.trunk.io/check/configuration)
lint:
disabled:
- markdownlint
- yamllint
enabled:
- checkov@3.2.467
- golangci-lint2@2.4.0
- hadolint@2.12.1-beta
- actionlint@1.7.7
- git-diff-check
- gofmt@1.20.4
- golangci-lint@1.64.5
- osv-scanner@1.9.2
- oxipng@9.1.4
- prettier@3.5.1
- shellcheck@0.10.0
- osv-scanner@2.2.2
- oxipng@9.1.5
- prettier@3.6.2
- shellcheck@0.11.0
- shfmt@3.6.0
- trufflehog@3.88.9
- trufflehog@3.90.5
actions:
disabled:
- trunk-announce

View File

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

View File

@@ -1,29 +1,33 @@
# Stage 1: deps
FROM golang:1.24.2-alpine AS deps
FROM golang:1.25.1-alpine AS deps
HEALTHCHECK NONE
# package version does not matter
# trunk-ignore(hadolint/DL3018)
RUN apk add --no-cache tzdata make libcap-setcap
ENV GOPATH=/root/go
WORKDIR /src
# Only copy go.mod and go.sum initially for better caching
COPY go.mod go.sum /src/
COPY go.mod go.sum ./
ENV GOPATH=/root/go
RUN go mod download -x
# remove godoxy stuff from go.mod first
RUN sed -i '/^module github\.com\/yusing\/go-proxy/!{/github\.com\/yusing\/go-proxy/d}' go.mod && \
go mod download -x
# Stage 2: builder
FROM deps AS builder
WORKDIR /src
COPY go.mod go.sum ./
COPY Makefile ./
COPY cmd ./cmd
COPY internal ./internal
COPY pkg ./pkg
COPY agent ./agent
COPY socket-proxy ./socket-proxy
ARG VERSION
ENV VERSION=${VERSION}
@@ -33,24 +37,23 @@ ENV MAKE_ARGS=${MAKE_ARGS}
ENV GOCACHE=/root/.cache/go-build
ENV GOPATH=/root/go
RUN make ${MAKE_ARGS} build link-binary && \
mv bin /app/ && \
mkdir -p /app/error_pages /app/certs
RUN --mount=type=cache,target=/root/.cache/go-build \
--mount=type=cache,target=/root/go/pkg/mod \
make ${MAKE_ARGS} docker=1 build
# Stage 3: Final image
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
# copy binary
COPY --from=builder /app /app
# copy example config
COPY config.example.yml /app/config/config.yml
COPY --from=builder /app/run /app/run
# copy certs
COPY --from=builder /etc/ssl/certs /etc/ssl/certs

26
LICENSE
View File

@@ -1,6 +1,6 @@
MIT License
Copyright (c) 2024 [fullname]
Copyright (c) 2024 - present Yusing
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
@@ -19,3 +19,27 @@ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
---
internal/net/gphttp/reverseproxy/reverse_proxy_mod.go is copied from et/http/httputil/reverseproxy.go with modifications to adapt to this project.
Copyright 2011 The Go Authors. All rights reserved.
Use of this source code is governed by a BSD-style
license that can be found in the LICENSE file.
---
internal/utils/io.go has a modified version of io.Copy with context and HTTP flusher handling.
Copyright 2009 The Go Authors. All rights reserved.
Use of this source code is governed by a BSD-style
license that can be found in the LICENSE file.
---
internal/utils/strutils/split_join.go is copied from strings.Split and strings.Join with modifications to adapt to this project.
Copyright 2009 The Go Authors. All rights reserved.
Use of this source code is governed by a BSD-style
license that can be found in the LICENSE file.

101
Makefile
View File

@@ -1,16 +1,22 @@
shell := /bin/sh
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-frontend
DOCS_DIR ?= ../godoxy-wiki
LDFLAGS = -X github.com/yusing/go-proxy/pkg.version=${VERSION} -checklinkname=0
ifeq ($(agent), 1)
NAME = godoxy-agent
CMD_PATH = ./agent/cmd
PWD = ${shell pwd}/agent
else ifeq ($(socket-proxy), 1)
NAME = godoxy-socket-proxy
PWD = ${shell pwd}/socket-proxy
else
NAME = godoxy
CMD_PATH = ./cmd
PWD = ${shell pwd}
endif
ifeq ($(trace), 1)
@@ -25,9 +31,9 @@ ifeq ($(race), 1)
endif
ifeq ($(debug), 1)
CGO_ENABLED = 0
CGO_ENABLED = 1
GODOXY_DEBUG = 1
BUILD_FLAGS += -gcflags=all='-N -l' -tags debug
BUILD_FLAGS += -gcflags=all='-N -l' -tags debug -asan
else ifeq ($(pprof), 1)
CGO_ENABLED = 1
GORACE = log_path=logs/pprof strip_path_prefix=$(shell pwd)/ halt_on_error=1
@@ -40,9 +46,9 @@ else
endif
BUILD_FLAGS += -ldflags='$(LDFLAGS)'
BIN_PATH := $(shell pwd)/bin/${NAME}
export NAME
export CMD_PATH
export CGO_ENABLED
export GODOXY_DEBUG
export GODOXY_TRACE
@@ -56,30 +62,68 @@ else
SETCAP_CMD = sudo setcap
endif
# CAP_NET_BIND_SERVICE: permission for binding to :80 and :443
POST_BUILD = $(SETCAP_CMD) CAP_NET_BIND_SERVICE=+ep ${BIN_PATH};
ifeq ($(docker), 1)
POST_BUILD += mkdir -p /app && mv ${BIN_PATH} /app/run;
endif
.PHONY: debug
test:
GODOXY_TEST=1 go test ./internal/...
get:
go get -u ./cmd && go mod tidy
docker-build-test:
docker build -t godoxy .
docker build --build-arg=MAKE_ARGS=agent=1 -t godoxy-agent .
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)
gomod_paths := $(shell find . -name go.mod -type f | xargs dirname)
update-go:
for file in ${files}; do \
echo "updating $$file"; \
sed -i 's|go \([0-9]\+\.[0-9]\+\.[0-9]\+\)|go ${go_ver}|g' $$file; \
sed -i 's|FROM golang:.*-alpine|FROM golang:${go_ver}-alpine|g' $$file; \
done
for path in ${gomod_paths}; do \
echo "go mod tidy $$path"; \
cd ${PWD}/$$path && go mod tidy; \
done
update-deps:
for path in ${gomod_paths}; do \
echo "go get -u $$path"; \
cd ${PWD}/$$path && go get -u ./... && go mod tidy; \
done
mod-tidy:
for path in ${gomod_paths}; do \
echo "go mod tidy $$path"; \
cd ${PWD}/$$path && go mod tidy; \
done
build:
mkdir -p bin
go build ${BUILD_FLAGS} -o bin/${NAME} ${CMD_PATH}
# CAP_NET_BIND_SERVICE: permission for binding to :80 and :443
$(SETCAP_CMD) CAP_NET_BIND_SERVICE=+ep bin/${NAME}
mkdir -p $(shell dirname ${BIN_PATH})
cd ${PWD} && go build ${BUILD_FLAGS} -o ${BIN_PATH} ./cmd
${POST_BUILD}
run:
[ -f .env ] && godotenv -f .env go run ${BUILD_FLAGS} ${CMD_PATH}
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 up -t 0 -d
dev-build: build
docker compose -f dev.compose.yml up -t 0 -d --build
dev-logs:
docker compose -f dev.compose.yml logs -f app
mtrace:
bin/godoxy debug-ls-mtrace > mtrace.json
${BIN_PATH} debug-ls-mtrace > mtrace.json
rapid-crash:
docker run --restart=always --name test_crash -p 80 debian:bookworm-slim /bin/cat &&\
@@ -94,10 +138,21 @@ ci-test:
act -n --artifact-server-path /tmp/artifacts -s GITHUB_TOKEN="$$(gh auth token)"
cloc:
cloc --not-match-f '_test.go$$' cmd internal pkg
link-binary:
ln -s /app/${NAME} bin/run
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 -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
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
pnpx 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

View File

@@ -1,40 +1,45 @@
<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_godoxy)
[![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_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)
A lightweight, simple, and [performant](https://github.com/yusing/godoxy/wiki/Benchmarks) reverse proxy with WebUI.
A lightweight, simple, and performant reverse proxy with WebUI.
For full documentation, check out **[Wiki](https://github.com/yusing/godoxy/wiki)**
<h5>
<a href="https://docs.godoxy.dev">Website</a> | <a href="https://docs.godoxy.dev/Home.html">Wiki</a> | <a href="https://discord.gg/umReR62nRd">Discord</a>
</h5>
**EN** | <a href="README_CHT.md">中文</a>
<h5>EN | <a href="README_CHT.md">中文</a></h5>
<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)
- [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
@@ -45,8 +50,8 @@ For full documentation, check out **[Wiki](https://github.com/yusing/godoxy/wiki
## Key Features
- **Simple**
- Effortless configuration with [simple labels](https://github.com/yusing/godoxy/wiki/Docker-labels-and-Route-Files) or WebUI
- [Simple multi-node setup](https://github.com/yusing/godoxy/wiki/Configurations#multi-docker-nodes-setup)
- Effortless configuration with [simple labels](https://docs.godoxy.dev/Docker-labels-and-Route-Files) or WebUI
- [Simple multi-node setup](https://docs.godoxy.dev/Configurations#multi-docker-nodes-setup)
- Detailed error messages for easy troubleshooting.
- **ACL**: connection / request level access control
- IP/CIDR
@@ -54,9 +59,12 @@ For full documentation, check out **[Wiki](https://github.com/yusing/godoxy/wiki
- Timezone **(Maxmind account required)**
- **Access logging**
- **Advanced Automation**
- Automatic SSL certificate management with Let's Encrypt ([using DNS-01 Challenge](https://github.com/yusing/go-proxy/wiki/Supported-DNS%E2%80%9001-Providers))
- 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
@@ -64,9 +72,10 @@ For full documentation, check out **[Wiki](https://github.com/yusing/godoxy/wiki
- 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://github.com/yusing/go-proxy/wiki/Middlewares)
- [Custom error pages support](https://github.com/yusing/go-proxy/wiki/Middlewares#custom-error-pages)
- [HTTP middlewares](https://docs.godoxy.dev/Middlewares)
- [Custom error pages support](https://docs.godoxy.dev/Custom-Error-Pages)
- **Web UI**
- App Dashboard
- Config Editor
@@ -99,7 +108,13 @@ Configure Wildcard DNS Record(s) to point to machine running `GoDoxy`, e.g.
/bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/yusing/godoxy/main/scripts/setup.sh)"
```
3. You may now do some extra configuration on WebUI `https://godoxy.yourdomain.com`
3. Start the docker compose service from generated `compose.yml`:
```shell
docker compose up -d
```
4. You may now do some extra configuration on WebUI `https://godoxy.yourdomain.com`
## How does GoDoxy work
@@ -124,22 +139,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>
@@ -191,4 +196,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,39 +1,44 @@
<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)
輕量、易用、 [高效能](https://github.com/yusing/godoxy/wiki/Benchmarks),且帶有主頁和配置面板的反向代理
輕量、易用、 高效能,且帶有主頁和配置面板的反向代理
完整文檔請查閱 **[Wiki](https://github.com/yusing/godoxy/wiki)**(暫未有中文翻譯)
<h5>
<a href="https://docs.godoxy.dev">網站</a> | <a href="https://docs.godoxy.dev/Home.html">文檔</a> | <a href="https://discord.gg/umReR62nRd">Discord</a>
</h5>
<a href="README.md">EN</a> | **中文**
<h5><a href="README.md">EN</a> | 中文</h5>
<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)
- [目錄](#目錄)
- [運行示例](#運行示例)
- [主要特點](#主要特點)
- [前置需求](#前置需求)
- [安裝](#安裝)
- [手動安裝](#手動安裝)
- [資料夾結構](#資料夾結構)
- [截圖](#截圖)
- [閒置休眠](#閒置休眠)
- [監控](#監控)
- [自行編譯](#自行編譯)
- [目錄](#目錄)
- [運行示例](#運行示例)
- [主要特點](#主要特點)
- [前置需求](#前置需求)
- [安裝](#安裝)
- [手動安裝](#手動安裝)
- [資料夾結構](#資料夾結構)
- [截圖](#截圖)
- [閒置休眠](#閒置休眠)
- [監控](#監控)
- [自行編譯](#自行編譯)
- [Star History](#star-history)
## 運行示例
@@ -43,22 +48,42 @@
## 主要特點
- 容易使用
- 輕鬆配置
- 簡單的多節點設置
- 錯誤訊息清晰詳細,易於排除故障
- 自動 SSL 憑證管理(參見 [支援的 DNS-01 驗證提供商](https://github.com/yusing/godoxy/wiki/Supported-DNS%E2%80%9001-Providers)
- 自動配置 Docker 容器
- 容器狀態/配置文件變更時自動熱重載
- **閒置休眠**在閒置時停止容器有流量時喚醒_可選參見[截圖](#閒置休眠)_
- OpenID Connect輕鬆實現單點登入
- HTTP(s) 反向代理和 TCP 和 UDP 埠轉發
- [HTTP 中介軟體](https://github.com/yusing/godoxy/wiki/Middlewares) 和 [自定義錯誤頁面](https://github.com/yusing/godoxy/wiki/Middlewares#custom-error-pages)
- **網頁介面,具有應用儀表板和配置編輯器**
- 支援 linux/amd64、linux/arm64
- 使用 **[Go](https://go.dev)** 編寫
[🔼 回到頂部](#目錄)
- **簡單易用**
- 透過 Docker[標籤](https://docs.godoxy.dev/Docker-labels-and-Route-Files)或 WebUI 輕鬆設定
- [簡單的多節點設置](https://docs.godoxy.dev/Configurations#multi-docker-nodes-setup)
- 詳細的錯誤訊息,便於故障排除
- **存取控制 (ACL)**:連線/請求層級存取控制
- IP/CIDR
- 國家 **(需要 Maxmind 帳戶)**
- 時區 **(需要 Maxmind 帳戶)**
- **存取日誌記錄**
- **自動化**
- 使用 Let's Encrypt 自動管理 SSL 憑證 ([使用 DNS-01 驗證](https://docs.godoxy.dev/DNS-01-Providers))
- Docker 容器自動配置
- 設定檔與容器狀態變更時自動熱重載
- **容器運行時支援**
- Docker
- Podman
- **閒置休眠**:根據流量停止和喚醒容器 _(參見[截圖](#閒置休眠))_
- Docker 容器
- Proxmox LXC 容器
- **流量管理**
- HTTP 反向代理
- TCP/UDP 連接埠轉送
- **OpenID Connect 支援**:輕鬆實現單點登入 (SSO) 並保護您的應用程式
- **ForwardAuth 支援**:整合任何 auth provider (例如 TinyAuth)
- **客製化**
- [HTTP 中介軟體](https://docs.godoxy.dev/Middlewares)
- [支援自訂錯誤頁面](https://docs.godoxy.dev/Custom-Error-Pages)
- **網頁使用者介面 (Web UI)**
- 應用程式一覽
- 設定編輯器
- 執行時間與系統指標
- Docker 日誌檢視器
- **跨平台支援**
- 支援 **linux/amd64****linux/arm64**
- **高效能**
-**[Go](https://go.dev)** 語言編寫
## 前置需求
@@ -84,8 +109,6 @@
3. 現在可以在 WebUI `https://godoxy.yourdomain.com` 進行額外配置
[🔼 回到頂部](#目錄)
### 手動安裝
1. 建立 `config` 目錄,然後將 `config.example.yml` 下載到 `config/config.yml`
@@ -127,29 +150,17 @@
![閒置休眠](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>
@@ -166,4 +177,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

@@ -3,20 +3,27 @@ package main
import (
"os"
"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/logging"
"github.com/yusing/go-proxy/internal/logging/memlogger"
"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/internal/utils/strutils"
"github.com/yusing/go-proxy/pkg"
socketproxy "github.com/yusing/go-proxy/socketproxy/pkg"
)
func main() {
logging.InitLogger(os.Stderr, memlogger.GetMemLogger())
writer := zerolog.ConsoleWriter{
Out: os.Stderr,
TimeFormat: "01-02 15:04",
}
zerolog.TimeFieldFormat = writer.TimeFormat
log.Logger = zerolog.New(writer).Level(zerolog.InfoLevel).With().Timestamp().Logger()
ca := &agent.PEMPair{}
err := ca.Load(env.AgentCACert)
if err != nil {
@@ -37,11 +44,12 @@ func main() {
gperr.LogFatal("init SSL error", err)
}
logging.Info().Msgf("GoDoxy Agent version %s", pkg.GetVersion())
logging.Info().Msgf("Agent name: %s", env.AgentName)
logging.Info().Msgf("Agent port: %d", env.AgentPort)
log.Info().Msgf("GoDoxy Agent version %s", pkg.GetVersion())
log.Info().Msgf("Agent name: %s", env.AgentName)
log.Info().Msgf("Agent port: %d", env.AgentPort)
log.Info().Msgf("Agent runtime: %s", env.Runtime)
logging.Info().Msg(`
log.Info().Msg(`
Tips:
1. To change the agent name, you can set the AGENT_NAME environment variable.
2. To change the agent port, you can set the AGENT_PORT environment variable.
@@ -55,6 +63,19 @@ Tips:
}
server.StartAgentServer(t, opts)
if socketproxy.ListenAddr != "" {
runtime := strutils.Title(string(env.Runtime))
log.Info().Msgf("%s socket listening on: %s", runtime, socketproxy.ListenAddr)
opts := httpServer.Options{
Name: runtime,
HTTPAddr: socketproxy.ListenAddr,
Handler: socketproxy.NewHandler(),
}
httpServer.StartServer(t, opts)
}
systeminfo.Poller.Start()
task.WaitExit(3)

113
agent/go.mod Normal file
View File

@@ -0,0 +1,113 @@
module github.com/yusing/go-proxy/agent
go 1.25.1
replace github.com/yusing/go-proxy => ..
replace github.com/yusing/go-proxy/socketproxy => ../socket-proxy
replace github.com/yusing/go-proxy/internal/utils => ../internal/utils
replace github.com/shirou/gopsutil/v4 => github.com/godoxy-app/gopsutil/v4 v4.0.0-20250816043325-ee003f88b84d
exclude github.com/containerd/nerdctl/mod/tigron v0.0.0
require (
github.com/gin-gonic/gin v1.10.1
github.com/gorilla/websocket v1.5.3
github.com/puzpuzpuz/xsync/v4 v4.1.0
github.com/rs/zerolog v1.34.0
github.com/stretchr/testify v1.11.1
github.com/yusing/go-proxy v0.17.6
github.com/yusing/go-proxy/internal/utils v0.0.0
github.com/yusing/go-proxy/socketproxy v0.0.0-00010101000000-000000000000
)
require (
github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c // indirect
github.com/Microsoft/go-winio v0.6.2 // indirect
github.com/PuerkitoBio/goquery v1.10.3 // indirect
github.com/andybalholm/cascadia v1.3.3 // indirect
github.com/bytedance/gopkg v0.1.3 // indirect
github.com/bytedance/sonic v1.14.1 // indirect
github.com/bytedance/sonic/loader v0.3.0 // indirect
github.com/cenkalti/backoff/v5 v5.0.3 // 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/distribution/reference v0.6.0 // indirect
github.com/docker/cli v28.4.0+incompatible // indirect
github.com/docker/docker v28.4.0+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/felixge/httpsnoop v1.0.4 // indirect
github.com/gabriel-vasile/mimetype v1.4.10 // indirect
github.com/gin-contrib/sse v1.1.0 // 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-json v0.10.5 // indirect
github.com/goccy/go-yaml v1.18.0 // indirect
github.com/gorilla/mux v1.8.1 // indirect
github.com/gotify/server/v2 v2.7.2 // indirect
github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.2 // indirect
github.com/json-iterator/go v1.1.13-0.20220915233716-71ac16282d12 // 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-20250827001030-24949be3fa54 // indirect
github.com/mattn/go-colorable v0.1.14 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect
github.com/moby/docker-image-spec v1.3.1 // indirect
github.com/moby/sys/sequential v0.6.0 // 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/pelletier/go-toml/v2 v2.2.4 // 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/quic-go/qpack v0.5.1 // indirect
github.com/quic-go/quic-go v0.54.0 // indirect
github.com/samber/lo v1.51.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.8 // indirect
github.com/sirupsen/logrus v1.9.4-0.20230606125235-dd1b4c2e81af // indirect
github.com/spf13/afero v1.15.0 // indirect
github.com/tklauser/go-sysconf v0.3.15 // indirect
github.com/tklauser/numcpus v0.10.0 // indirect
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
github.com/ugorji/go/codec v1.3.0 // indirect
github.com/vincent-petithory/dataurl v1.0.0 // indirect
github.com/yusing/ds v0.1.0 // indirect
github.com/yusufpapurcu/wmi v1.2.4 // indirect
go.opentelemetry.io/auto/sdk v1.2.0 // indirect
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.63.0 // indirect
go.opentelemetry.io/otel v1.38.0 // indirect
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.37.0 // indirect
go.opentelemetry.io/otel/metric v1.38.0 // indirect
go.opentelemetry.io/otel/trace v1.38.0 // indirect
go.opentelemetry.io/proto/otlp v1.7.1 // indirect
go.uber.org/atomic v1.11.0 // indirect
go.uber.org/mock v0.6.0 // indirect
golang.org/x/arch v0.21.0 // indirect
golang.org/x/crypto v0.42.0 // indirect
golang.org/x/mod v0.28.0 // indirect
golang.org/x/net v0.44.0 // indirect
golang.org/x/sync v0.17.0 // indirect
golang.org/x/sys v0.36.0 // indirect
golang.org/x/text v0.29.0 // indirect
golang.org/x/time v0.13.0 // indirect
golang.org/x/tools v0.37.0 // indirect
google.golang.org/protobuf v1.36.9 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
gotest.tools/v3 v3.5.2 // indirect
)

311
agent/go.sum Normal file
View File

@@ -0,0 +1,311 @@
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/andybalholm/cascadia v1.3.3 h1:AG2YHrzJIm4BZ19iwJ/DAua6Btl3IwJX+VI4kktS1LM=
github.com/andybalholm/cascadia v1.3.3/go.mod h1:xNd9bqTn98Ln4DwST8/nG+H0yuB8Hmgu1YHNnWw0GeA=
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.1 h1:FBMC0zVz5XUmE4z9wF4Jey0An5FueFvOsTKKKtwIl7w=
github.com/bytedance/sonic v1.14.1/go.mod h1:gi6uhQLMbTdeP0muCnrjHLeCUPyb70ujhnNlhOylAFc=
github.com/bytedance/sonic/loader v0.3.0 h1:dskwH8edlzNMctoruo8FPTJDF3vLtDT0sXZwvZJyqeA=
github.com/bytedance/sonic/loader v0.3.0/go.mod h1:N8A3vUdtUebEY2/VQC0MyhYeKUFosQU6FxH2JmUe6VI=
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/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-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/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.4.0+incompatible h1:RBcf3Kjw2pMtwui5V0DIMdyeab8glEw5QY0UUU4C9kY=
github.com/docker/cli v28.4.0+incompatible/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8=
github.com/docker/docker v28.4.0+incompatible h1:KVC7bz5zJY/4AZe/78BIvCnPsLaC9T/zh72xnlrTTOk=
github.com/docker/docker v28.4.0+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk=
github.com/docker/go-connections v0.6.0 h1:LlMG9azAe1TqfR7sO+NJttz1gy6KO7VJBh+pMmjSD94=
github.com/docker/go-connections v0.6.0/go.mod h1:AahvXYshr6JgfUJGdDCs2b5EZG/vmaMAntpSFH5BFKE=
github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4=
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/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg=
github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U=
github.com/gabriel-vasile/mimetype v1.4.10 h1:zyueNbySn/z8mJZHLt6IPw0KoZsiQNszIpU+bX4+ZK0=
github.com/gabriel-vasile/mimetype v1.4.10/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.10.1 h1:T0ujvqyCSqRopADpgPgiTT63DUQVSfojyME59Ei63pQ=
github.com/gin-gonic/gin v1.10.1/go.mod h1:4PMNQiOhvDRa013RKVbsiNwoyezlm2rm0uX/T7kzp5Y=
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=
github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
github.com/go-ole/go-ole v1.2.6/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0=
github.com/go-ole/go-ole v1.3.0 h1:Dt6ye7+vXGIKZ7Xtk4s6/xVdGDQynvom7xCFEdWr6uE=
github.com/go-ole/go-ole v1.3.0/go.mod h1:5LS6F96DhAwUc7C+1HLexzMXY1xGRSryjyPPKW6zv78=
github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s=
github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4=
github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA=
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-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.18.0 h1:8W7wMFS12Pcas7KU+VVkaiCng+kG8QiFeFwzFb+rwuw=
github.com/goccy/go-yaml v1.18.0/go.mod h1:XBurs7gK8ATbW4ZPGKgcbrY1Br56PdM69F7LkFRi1kA=
github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
github.com/godoxy-app/gopsutil/v4 v4.0.0-20250816043325-ee003f88b84d h1:bNqtnmyhGDxpBSaFYIo7ferYRIc/QzlaGfIhh/JmMPk=
github.com/godoxy-app/gopsutil/v4 v4.0.0-20250816043325-ee003f88b84d/go.mod h1:7iQ/w4jyGYJCZ56dZLNztwM4atNxj5C2HNTBxhLvV8A=
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.7.2 h1:YRgYl/kB7Uh4OINd7gK60N3QBTY4YEeKTrBLhd67LC4=
github.com/gotify/server/v2 v2.7.2/go.mod h1:KJH+8yhkAxArygPwaRfp9otHjt6tE0YSzdrsgPX/5EE=
github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.2 h1:8Tjv8EJ+pM1xP8mK6egEbD1OgnVTyacbefKhmbLhIhU=
github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.2/go.mod h1:pkJQ2tZHJ0aFOVEEot6oZmaVEZcRme73eIFmhiVuRWs=
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/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-20250827001030-24949be3fa54 h1:mFWunSatvkQQDhpdyuFAYwyAan3hzCuma+Pz8sqvOfg=
github.com/lufia/plan9stats v0.0.0-20250827001030-24949be3fa54/go.mod h1:autxFIvghDt3jPTLoqZ9OZ7s9qTGNAWmYCjVFWPX/zg=
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=
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
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/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/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M=
github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
github.com/morikuni/aec v1.0.0 h1:nP9CBfwrvYnBRgY6qfDQkygYDmYwOilePFkwzv4dU8A=
github.com/morikuni/aec v1.0.0/go.mod h1:BbKIizmSmc5MMPqRYbxO4ZU0S0+P200+tUnFx7PXmsc=
github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U=
github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM=
github.com/opencontainers/image-spec v1.1.1 h1:y0fUlFfIZhPF1W537XOLg0/fcx6zcHCJwooC2xJA040=
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/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/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U=
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.54.0 h1:6s1YB9QotYI6Ospeiguknbp2Znb/jZYjZLRXn9kMQBg=
github.com/quic-go/quic-go v0.54.0/go.mod h1:e68ZEaCdyviluZmy44P6Iey98v/Wfz6HCjQEm+l8zTY=
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/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/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.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/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
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.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/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.0 h1:Qd2W2sQawAfG8XSvzwhBeoGq71zXOC/Q1E9y/wUcsUA=
github.com/ugorji/go/codec v1.3.0/go.mod h1:pRBVtBSKl77K30Bv8R2P+cLSGaTtex6fsA2Wjqmfxj4=
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.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
github.com/yusing/ds v0.1.0 h1:aiZs7jPMN3MEChUsddMYjpZFHhhAmkxrwRyIUnGy5AU=
github.com/yusing/ds v0.1.0/go.mod h1:KC785+mtt+Bau0LLR+slExDaUjeiqLT1k9Or6Rpryh4=
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.2.0 h1:YpRtUFjvhSymycLS2T81lT6IGhcUP+LUPtv0iv1N8bM=
go.opentelemetry.io/auto/sdk v1.2.0/go.mod h1:1deq2zL7rwjwC8mR7XgY2N+tlIl6pjmEUoLDENMEzwk=
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.63.0 h1:RbKq8BG0FI8OiXhBfcRtqqHcZcka+gU3cskNuf05R18=
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.63.0/go.mod h1:h06DGIukJOevXaj/xrNjhi/2098RZzcLTbc0jDAUbsg=
go.opentelemetry.io/otel v1.38.0 h1:RkfdswUDRimDg0m2Az18RKOsnI8UDzppJAtj01/Ymk8=
go.opentelemetry.io/otel v1.38.0/go.mod h1:zcmtmQ1+YmQM9wrNsTGV/q/uyusom3P8RxwExxkZhjM=
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.37.0 h1:Ahq7pZmv87yiyn3jeFz/LekZmPLLdKejuO3NcK9MssM=
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.37.0/go.mod h1:MJTqhM0im3mRLw1i8uGHnCvUEeS7VwRyxlLC78PA18M=
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.37.0 h1:bDMKF3RUSxshZ5OjOTi8rsHGaPKsAt76FaqgvIUySLc=
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.37.0/go.mod h1:dDT67G/IkA46Mr2l9Uj7HsQVwsjASyV9SjGofsiUZDA=
go.opentelemetry.io/otel/metric v1.38.0 h1:Kl6lzIYGAh5M159u9NgiRkmoMKjvbsKtYRwgfrA6WpA=
go.opentelemetry.io/otel/metric v1.38.0/go.mod h1:kB5n/QoRM8YwmUahxvI3bO34eVtQf2i4utNVLr9gEmI=
go.opentelemetry.io/otel/sdk v1.38.0 h1:l48sr5YbNf2hpCUj/FoGhW9yDkl+Ma+LrVl8qaM5b+E=
go.opentelemetry.io/otel/sdk v1.38.0/go.mod h1:ghmNdGlVemJI3+ZB5iDEuk4bWA3GkTpW+DOoZMYBVVg=
go.opentelemetry.io/otel/sdk/metric v1.38.0 h1:aSH66iL0aZqo//xXzQLYozmWrXxyFkBJ6qT5wthqPoM=
go.opentelemetry.io/otel/sdk/metric v1.38.0/go.mod h1:dg9PBnW9XdQ1Hd6ZnRz689CbtrUp0wMMs9iPcgT9EZA=
go.opentelemetry.io/otel/trace v1.38.0 h1:Fxk5bKrDZJUH+AMyyIXGcFAPah0oRcT+LuNtJrmcNLE=
go.opentelemetry.io/otel/trace v1.38.0/go.mod h1:j1P9ivuFsTceSWe1oY+EeW3sc+Pp42sO++GHkg4wwhs=
go.opentelemetry.io/proto/otlp v1.7.1 h1:gTOMpGDb0WTBOP8JaO72iL3auEZhVmAQg4ipjOVAtj4=
go.opentelemetry.io/proto/otlp v1.7.1/go.mod h1:b2rVh6rfI/s2pHWNlB7ILJcRALpcNDzKhACevjI+ZnE=
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.6.0 h1:hyF9dfmbgIX5EfOdasqLsWD6xqpNZlXblLB/Dbnwv3Y=
go.uber.org/mock v0.6.0/go.mod h1:KiVJ4BqZJaMj4svdfmHM0AUx4NJYO8ZNpPnZn1Z+BBU=
golang.org/x/arch v0.21.0 h1:iTC9o7+wP6cPWpDWkivCvQFGAHDQ59SrSxsLPcnkArw=
golang.org/x/arch v0.21.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-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.42.0 h1:chiH31gIWm57EkTXpwnqf8qeuMUi0yekh6mT2AvFlqI=
golang.org/x/crypto v0.42.0/go.mod h1:4+rDnOTJhQCx2q7/j6rAN5XDw8kPjeaXEUR2eL94ix8=
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.28.0 h1:gQBtGhjxykdjY9YhZpSlZIsbnaE2+PgjfLWUQTnoZ1U=
golang.org/x/mod v0.28.0/go.mod h1:yfB/L0NOf/kmEbXjzCPOx1iK1fRutOydrCMsqRhEBxI=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
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=
golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg=
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.44.0 h1:evd8IRDyfNBMBTTY5XRF1vaZlD+EmWx6x8PkhR04H/I=
golang.org/x/net v0.44.0/go.mod h1:ECOoLqd5U3Lhyeyo/QDCEVQ4sNgYsqvCZ722XogGieY=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/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.17.0 h1:l60nONMj9l5drqw6jlhIELNv9I0A4OFgRsG9k2oT9Ug=
golang.org/x/sync v0.17.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-20190916202348-b4ddaad3f8a3/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-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210616094352-59db8d763f22/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-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=
golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
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.36.0 h1:KVRy2GtZBrk1cBYA7MKu5bEZFxQk4NIDV6RLVcC8o0k=
golang.org/x/sys v0.36.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=
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo=
golang.org/x/term v0.12.0/go.mod h1:owVbMEjm3cBLCHdkQu9b1opXd4ETQWc3BhuQGKgXgvU=
golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk=
golang.org/x/term v0.20.0/go.mod h1:8UkIAJTvZgivsXaD6/pH6U9ecQzZ45awqEOzuCvwpFY=
golang.org/x/term v0.27.0/go.mod h1:iMsnZpn0cago0GOrHO2+Y7u7JPn5AylBrcoWkElMTSM=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
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.29.0 h1:1neNs90w9YzJ9BocxfsQNHKuAT4pkghyXc4nhZ6sJvk=
golang.org/x/text v0.29.0/go.mod h1:7MhJOA9CD2qZyOKYazxdYMF85OwPdEr9jTtBpO7ydH4=
golang.org/x/time v0.13.0 h1:eUlYslOIt32DgYD6utsuUeHs4d7AsEYLuIAdg7FlYgI=
golang.org/x/time v0.13.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.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.37.0 h1:DVSRzp7FwePZW356yEAChSdNcQo6Nsp+fex1SUW09lE=
golang.org/x/tools v0.37.0/go.mod h1:MBN5QPQtLMHVdvsbtarmTNukZDdgwdwlO5qGacAzF0w=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
google.golang.org/genproto v0.0.0-20250908214217-97024824d090 h1:ywCL7vA2n3vVHyf+bx1ZV/knaTPRI8GIeKY0MEhEeOc=
google.golang.org/genproto/googleapis/api v0.0.0-20250826171959-ef028d996bc1 h1:APHvLLYBhtZvsbnpkfknDZ7NyH4z5+ub/I0u8L3Oz6g=
google.golang.org/genproto/googleapis/api v0.0.0-20250826171959-ef028d996bc1/go.mod h1:xUjFWUnWDpZ/C0Gu0qloASKFb6f8/QXiiXhSPFsD668=
google.golang.org/genproto/googleapis/rpc v0.0.0-20250908214217-97024824d090 h1:/OQuEa4YWtDt7uQWHd3q3sUMb+QOLQUg1xa8CEsRv5w=
google.golang.org/genproto/googleapis/rpc v0.0.0-20250908214217-97024824d090/go.mod h1:GmFNa4BdJZ2a8G+wCe9Bg3wwThLrJun751XstdJt5Og=
google.golang.org/grpc v1.75.1 h1:/ODCNEuf9VghjgO3rqLcfg8fiOP0nSluljWFlDxELLI=
google.golang.org/grpc v1.75.1/go.mod h1:JtPAzKiq4v1xcAB2hydNlWI2RnF85XXcV0mhKXr2ecQ=
google.golang.org/protobuf v1.36.9 h1:w2gp2mA27hUeUzj9Ex9FBjsBm40zfaDtEWow293U7Iw=
google.golang.org/protobuf v1.36.9/go.mod h1:fuxRtAxBytpl4zzqUh6/eyUujkJdNiuEkXntxiD/uRU=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/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=

View File

@@ -0,0 +1,67 @@
package agent
import (
"iter"
"github.com/puzpuzpuz/xsync/v4"
"github.com/yusing/go-proxy/internal/common"
)
var agentPool = xsync.NewMap[string, *AgentConfig](xsync.WithPresize(10))
func init() {
if common.IsTest {
agentPool.Store("test-agent", &AgentConfig{
Addr: "test-agent",
})
}
}
func GetAgent(agentAddrOrDockerHost string) (*AgentConfig, bool) {
if !IsDockerHostAgent(agentAddrOrDockerHost) {
return getAgentByAddr(agentAddrOrDockerHost)
}
return getAgentByAddr(GetAgentAddrFromDockerHost(agentAddrOrDockerHost))
}
func GetAgentByName(name string) (*AgentConfig, bool) {
for _, agent := range agentPool.Range {
if agent.Name == name {
return agent, true
}
}
return nil, false
}
func AddAgent(agent *AgentConfig) {
agentPool.Store(agent.Addr, agent)
}
func RemoveAgent(agent *AgentConfig) {
agentPool.Delete(agent.Addr)
}
func RemoveAllAgents() {
agentPool.Clear()
}
func ListAgents() []*AgentConfig {
agents := make([]*AgentConfig, 0, agentPool.Size())
for _, agent := range agentPool.Range {
agents = append(agents, agent)
}
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
}

View File

@@ -10,7 +10,17 @@ var (
AGENT_PORT="{{.Port}}" \
AGENT_CA_CERT="{{.CACert}}" \
AGENT_SSL_CERT="{{.SSLCert}}" \
bash -c "$(curl -fsSL https://raw.githubusercontent.com/yusing/go-proxy/main/scripts/install-agent.sh)"`
{{ 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,35 +4,36 @@ import (
"context"
"crypto/tls"
"crypto/x509"
"encoding/json"
"errors"
"fmt"
"net"
"net/http"
"net/url"
"os"
"strings"
"time"
"github.com/rs/zerolog"
"github.com/rs/zerolog/log"
"github.com/yusing/go-proxy/agent/pkg/certs"
"github.com/yusing/go-proxy/internal/gperr"
"github.com/yusing/go-proxy/internal/logging"
gphttp "github.com/yusing/go-proxy/internal/net/gphttp"
"github.com/yusing/go-proxy/internal/net/types"
"github.com/yusing/go-proxy/internal/task"
"github.com/yusing/go-proxy/pkg"
)
type AgentConfig struct {
Addr string
Addr string `json:"addr"`
Name string `json:"name"`
Version string `json:"version"`
Runtime ContainerRuntime `json:"runtime"`
httpClient *http.Client
tlsConfig *tls.Config
name string
l zerolog.Logger
}
} // @name Agent
const (
EndpointVersion = "/version"
EndpointName = "/name"
EndpointRuntime = "/runtime"
EndpointProxyHTTP = "/proxy/http"
EndpointHealth = "/health"
EndpointLogs = "/logs"
@@ -49,9 +50,17 @@ const (
FakeDockerHostPrefixLen = len(FakeDockerHostPrefix)
)
func mustParseURL(urlStr string) *url.URL {
u, err := url.Parse(urlStr)
if err != nil {
panic(err)
}
return u
}
var (
AgentURL = types.MustParseURL(APIBaseURL)
HTTPProxyURL = types.MustParseURL(APIBaseURL + EndpointProxyHTTP)
AgentURL = mustParseURL(APIBaseURL)
HTTPProxyURL = mustParseURL(APIBaseURL + EndpointProxyHTTP)
HTTPProxyURLPrefixLen = len(APIEndpointBase + EndpointProxyHTTP)
)
@@ -72,15 +81,9 @@ func (cfg *AgentConfig) Parse(addr string) error {
return nil
}
func withoutBuildTime(version string) string {
return strings.Split(version, "-")[0]
}
var serverVersion = pkg.GetVersion()
func checkVersion(a, b string) bool {
return withoutBuildTime(a) == withoutBuildTime(b)
}
func (cfg *AgentConfig) StartWithCerts(parent task.Parent, ca, crt, key []byte) error {
func (cfg *AgentConfig) StartWithCerts(ctx context.Context, ca, crt, key []byte) error {
clientCert, err := tls.X509KeyPair(crt, key)
if err != nil {
return err
@@ -90,7 +93,7 @@ func (cfg *AgentConfig) StartWithCerts(parent task.Parent, ca, crt, key []byte)
caCertPool := x509.NewCertPool()
ok := caCertPool.AppendCertsFromPEM(ca)
if !ok {
return gperr.New("invalid ca certificate")
return errors.New("invalid ca certificate")
}
cfg.tlsConfig = &tls.Config{
@@ -102,51 +105,77 @@ func (cfg *AgentConfig) StartWithCerts(parent task.Parent, ca, crt, key []byte)
// create transport and http client
cfg.httpClient = cfg.NewHTTPClient()
ctx, cancel := context.WithTimeout(parent.Context(), 5*time.Second)
ctx, cancel := context.WithTimeout(ctx, 5*time.Second)
defer cancel()
// check agent version
version, _, err := cfg.Fetch(ctx, EndpointVersion)
if err != nil {
return err
}
versionStr := string(version)
// skip version check for dev versions
if strings.HasPrefix(versionStr, "v") && !checkVersion(versionStr, pkg.GetVersion()) {
return gperr.Errorf("agent version mismatch: server: %s, agent: %s", pkg.GetVersion(), versionStr)
}
// get agent name
name, _, err := cfg.Fetch(ctx, EndpointName)
if err != nil {
return err
}
cfg.name = string(name)
cfg.l = logging.With().Str("agent", cfg.name).Logger()
cfg.Name = string(name)
logging.Info().Msgf("agent %q initialized", cfg.name)
cfg.l = log.With().Str("agent", cfg.Name).Logger()
// check agent version
agentVersionBytes, _, err := cfg.Fetch(ctx, EndpointVersion)
if err != nil {
return err
}
// check agent runtime
runtimeBytes, status, err := cfg.Fetch(ctx, EndpointRuntime)
if err != nil {
return err
}
switch status {
case http.StatusOK:
switch string(runtimeBytes) {
case "docker":
cfg.Runtime = ContainerRuntimeDocker
// case "nerdctl":
// cfg.Runtime = ContainerRuntimeNerdctl
case "podman":
cfg.Runtime = ContainerRuntimePodman
default:
return fmt.Errorf("invalid agent runtime: %s", runtimeBytes)
}
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, runtimeBytes)
}
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)
}
log.Info().Msgf("agent %q initialized", cfg.Name)
return nil
}
func (cfg *AgentConfig) Start(parent task.Parent) gperr.Error {
func (cfg *AgentConfig) Start(ctx context.Context) error {
filepath, ok := certs.AgentCertsFilepath(cfg.Addr)
if !ok {
return gperr.New("invalid agent host").Subject(cfg.Addr)
return fmt.Errorf("invalid agent host: %s", cfg.Addr)
}
certData, err := os.ReadFile(filepath)
if err != nil {
return gperr.Wrap(err, "failed to read agent certs")
return fmt.Errorf("failed to read agent certs: %w", err)
}
ca, crt, key, err := certs.ExtractCert(certData)
if err != nil {
return gperr.Wrap(err, "failed to extract agent certs")
return fmt.Errorf("failed to extract agent certs: %w", err)
}
return gperr.Wrap(cfg.StartWithCerts(parent, ca, crt, key))
return cfg.StartWithCerts(ctx, ca, crt, key)
}
func (cfg *AgentConfig) NewHTTPClient() *http.Client {
@@ -170,21 +199,12 @@ func (cfg *AgentConfig) Transport() *http.Transport {
}
}
func (cfg *AgentConfig) DialContext(ctx context.Context) (net.Conn, error) {
return gphttp.DefaultDialer.DialContext(ctx, "tcp", cfg.Addr)
}
var dialer = &net.Dialer{Timeout: 5 * time.Second}
func (cfg *AgentConfig) Name() string {
return cfg.name
func (cfg *AgentConfig) DialContext(ctx context.Context) (net.Conn, error) {
return dialer.DialContext(ctx, "tcp", cfg.Addr)
}
func (cfg *AgentConfig) String() string {
return cfg.name + "@" + cfg.Addr
}
func (cfg *AgentConfig) MarshalJSON() ([]byte, error) {
return json.Marshal(map[string]string{
"name": cfg.Name(),
"addr": cfg.Addr,
})
return cfg.Name + "@" + cfg.Addr
}

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,72 @@
package agent
import (
"context"
"io"
"net/http"
"github.com/gorilla/websocket"
"github.com/yusing/go-proxy/internal/net/gphttp/reverseproxy"
nettypes "github.com/yusing/go-proxy/internal/net/types"
)
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 = 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, err
}
return resp, 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},
})
}
// 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) error {
rp := reverseproxy.NewReverseProxy("agent", nettypes.NewURL(AgentURL), cfg.Transport())
uri := APIEndpointBase + endpoint
if req.URL.RawQuery != "" {
uri += "?" + req.URL.RawQuery
}
r, err := http.NewRequestWithContext(req.Context(), req.Method, uri, req.Body)
if err != nil {
return err
}
r.Header = req.Header
rp.ServeHTTP(w, r)
return nil
}

View File

@@ -1,31 +1,50 @@
package agent
import (
"crypto/aes"
"crypto/cipher"
"crypto/rand"
"crypto/rsa"
"crypto/tls"
"crypto/x509"
"crypto/x509/pkix"
"encoding/base64"
"encoding/pem"
"errors"
"io"
"math/big"
"strings"
"time"
"crypto/ecdsa"
"crypto/elliptic"
"fmt"
)
const (
CertsDNSName = "godoxy.agent"
KeySize = 2048
)
func toPEMPair(certDER []byte, key *rsa.PrivateKey) *PEMPair {
func toPEMPair(certDER []byte, key *ecdsa.PrivateKey) *PEMPair {
marshaledKey, err := marshalECPrivateKey(key)
if err != nil {
// This is a critical internal error during PEM encoding of a newly generated key.
// Panicking is acceptable here as it indicates a fundamental issue.
panic(fmt.Sprintf("failed to marshal EC private key for PEM encoding: %v", err))
}
return &PEMPair{
Cert: pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: certDER}),
Key: pem.EncodeToMemory(&pem.Block{Type: "RSA PRIVATE KEY", Bytes: x509.MarshalPKCS1PrivateKey(key)}),
Key: pem.EncodeToMemory(&pem.Block{Type: "EC PRIVATE KEY", Bytes: marshaledKey}),
}
}
func marshalECPrivateKey(key *ecdsa.PrivateKey) ([]byte, error) {
derBytes, err := x509.MarshalECPrivateKey(key)
if err != nil {
return nil, fmt.Errorf("failed to marshal EC private key: %w", err)
}
return derBytes, nil
}
func b64Encode(data []byte) string {
return base64.StdEncoding.EncodeToString(data)
}
@@ -58,15 +77,84 @@ 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
}
func newSerialNumber() (*big.Int, error) {
serialNumberLimit := new(big.Int).Lsh(big.NewInt(1), 128) // 128-bit random number
serialNumber, err := rand.Int(rand.Reader, serialNumberLimit)
if err != nil {
return nil, fmt.Errorf("failed to generate serial number: %w", err)
}
return serialNumber, nil
}
func NewAgent() (ca, srv, client *PEMPair, err error) {
caSerialNumber, err := newSerialNumber()
if err != nil {
return nil, nil, nil, err
}
// Create the CA's certificate
caTemplate := &x509.Certificate{
SerialNumber: big.NewInt(1),
SerialNumber: caSerialNumber,
Subject: pkix.Name{
Organization: []string{"GoDoxy"},
CommonName: CertsDNSName,
@@ -76,9 +164,12 @@ func NewAgent() (ca, srv, client *PEMPair, err error) {
KeyUsage: x509.KeyUsageCertSign | x509.KeyUsageCRLSign,
BasicConstraintsValid: true,
IsCA: true,
MaxPathLen: 0,
MaxPathLenZero: true,
SignatureAlgorithm: x509.ECDSAWithSHA256,
}
caKey, err := rsa.GenerateKey(rand.Reader, KeySize)
caKey, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
if err != nil {
return nil, nil, nil, err
}
@@ -91,20 +182,29 @@ func NewAgent() (ca, srv, client *PEMPair, err error) {
ca = toPEMPair(caDER, caKey)
// Generate a new private key for the server certificate
serverKey, err := rsa.GenerateKey(rand.Reader, KeySize)
serverKey, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
if err != nil {
return nil, nil, nil, err
}
serverSerialNumber, err := newSerialNumber()
if err != nil {
return nil, nil, nil, err
}
srvTemplate := &x509.Certificate{
SerialNumber: big.NewInt(2),
SerialNumber: serverSerialNumber,
Issuer: caTemplate.Subject,
Subject: caTemplate.Subject,
DNSNames: []string{CertsDNSName},
NotBefore: time.Now(),
NotAfter: time.Now().AddDate(1000, 0, 0), // Add validity period
KeyUsage: x509.KeyUsageDigitalSignature,
ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth},
Subject: pkix.Name{
Organization: caTemplate.Subject.Organization,
OrganizationalUnit: []string{"Server"},
CommonName: CertsDNSName,
},
DNSNames: []string{CertsDNSName},
NotBefore: time.Now(),
NotAfter: time.Now().AddDate(1000, 0, 0), // Add validity period
KeyUsage: x509.KeyUsageDigitalSignature,
ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth},
SignatureAlgorithm: x509.ECDSAWithSHA256,
}
srvCertDER, err := x509.CreateCertificate(rand.Reader, srvTemplate, caTemplate, &serverKey.PublicKey, caKey)
@@ -114,20 +214,29 @@ func NewAgent() (ca, srv, client *PEMPair, err error) {
srv = toPEMPair(srvCertDER, serverKey)
clientKey, err := rsa.GenerateKey(rand.Reader, KeySize)
clientKey, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
if err != nil {
return nil, nil, nil, err
}
clientSerialNumber, err := newSerialNumber()
if err != nil {
return nil, nil, nil, err
}
clientTemplate := &x509.Certificate{
SerialNumber: big.NewInt(3),
SerialNumber: clientSerialNumber,
Issuer: caTemplate.Subject,
Subject: caTemplate.Subject,
DNSNames: []string{CertsDNSName},
NotBefore: time.Now(),
NotAfter: time.Now().AddDate(1000, 0, 0),
KeyUsage: x509.KeyUsageDigitalSignature,
ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageClientAuth},
Subject: pkix.Name{
Organization: caTemplate.Subject.Organization,
OrganizationalUnit: []string{"Client"},
CommonName: CertsDNSName,
},
DNSNames: []string{CertsDNSName},
NotBefore: time.Now(),
NotAfter: time.Now().AddDate(1000, 0, 0),
KeyUsage: x509.KeyUsageDigitalSignature,
ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageClientAuth},
SignatureAlgorithm: x509.ECDSAWithSHA256,
}
clientCertDER, err := x509.CreateCertificate(rand.Reader, clientTemplate, caTemplate, &clientKey.PublicKey, caKey)
if err != nil {

View File

@@ -1,6 +1,7 @@
package agent
import (
"crypto/rand"
"crypto/tls"
"crypto/x509"
"fmt"
@@ -8,59 +9,59 @@ import (
"net/http/httptest"
"testing"
. "github.com/yusing/go-proxy/internal/utils/testing"
"github.com/stretchr/testify/require"
)
func TestNewAgent(t *testing.T) {
ca, srv, client, err := NewAgent()
ExpectNoError(t, err)
ExpectTrue(t, ca != nil)
ExpectTrue(t, srv != nil)
ExpectTrue(t, client != nil)
require.NoError(t, err)
require.NotNil(t, ca)
require.NotNil(t, srv)
require.NotNil(t, client)
}
func TestPEMPair(t *testing.T) {
ca, srv, client, err := NewAgent()
ExpectNoError(t, err)
require.NoError(t, err)
for i, p := range []*PEMPair{ca, srv, client} {
t.Run(fmt.Sprintf("load-%d", i), func(t *testing.T) {
var pp PEMPair
err := pp.Load(p.String())
ExpectNoError(t, err)
ExpectEqual(t, p.Cert, pp.Cert)
ExpectEqual(t, p.Key, pp.Key)
require.NoError(t, err)
require.Equal(t, p.Cert, pp.Cert)
require.Equal(t, p.Key, pp.Key)
})
}
}
func TestPEMPairToTLSCert(t *testing.T) {
ca, srv, client, err := NewAgent()
ExpectNoError(t, err)
require.NoError(t, err)
for i, p := range []*PEMPair{ca, srv, client} {
t.Run(fmt.Sprintf("toTLSCert-%d", i), func(t *testing.T) {
cert, err := p.ToTLSCert()
ExpectNoError(t, err)
ExpectTrue(t, cert != nil)
require.NoError(t, err)
require.NotNil(t, cert)
})
}
}
func TestServerClient(t *testing.T) {
ca, srv, client, err := NewAgent()
ExpectNoError(t, err)
require.NoError(t, err)
srvTLS, err := srv.ToTLSCert()
ExpectNoError(t, err)
ExpectTrue(t, srvTLS != nil)
require.NoError(t, err)
require.NotNil(t, srvTLS)
clientTLS, err := client.ToTLSCert()
ExpectNoError(t, err)
ExpectTrue(t, clientTLS != nil)
require.NoError(t, err)
require.NotNil(t, clientTLS)
caPool := x509.NewCertPool()
ExpectTrue(t, caPool.AppendCertsFromPEM(ca.Cert))
require.True(t, caPool.AppendCertsFromPEM(ca.Cert))
srvTLSConfig := &tls.Config{
Certificates: []tls.Certificate{*srvTLS},
@@ -86,6 +87,26 @@ func TestServerClient(t *testing.T) {
}
resp, err := httpClient.Get(server.URL)
ExpectNoError(t, err)
ExpectEqual(t, resp.StatusCode, http.StatusOK)
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,49 +0,0 @@
package agent
import (
"context"
"io"
"net/http"
"github.com/coder/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) {
return websocket.Dial(ctx, APIBaseURL+endpoint, &websocket.DialOptions{
HTTPClient: cfg.NewHTTPClient(),
Host: AgentHost,
})
}

View File

@@ -9,6 +9,36 @@ services:
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:
- /var/run/docker.sock:/var/run/docker.sock
- ./data:/app/data

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

@@ -6,10 +6,11 @@ import (
"io"
"path/filepath"
"github.com/yusing/go-proxy/internal/common"
"github.com/yusing/go-proxy/internal/utils/strutils"
)
const AgentCertsBasePath = "certs"
func writeFile(zipWriter *zip.Writer, name string, data []byte) error {
w, err := zipWriter.CreateHeader(&zip.FileHeader{
Name: name,
@@ -59,7 +60,7 @@ func AgentCertsFilepath(host string) (filepathOut string, ok bool) {
if !isValidAgentHost(host) {
return "", false
}
return filepath.Join(common.AgentCertsBasePath, host+".zip"), true
return filepath.Join(AgentCertsBasePath, host+".zip"), true
}
func ExtractCert(data []byte) (ca, crt, key []byte, err error) {

View File

@@ -1,19 +1,20 @@
package certs
package certs_test
import (
"testing"
. "github.com/yusing/go-proxy/internal/utils/testing"
"github.com/stretchr/testify/require"
"github.com/yusing/go-proxy/agent/pkg/certs"
)
func TestZipCert(t *testing.T) {
ca, crt, key := []byte("test1"), []byte("test2"), []byte("test3")
zipData, err := ZipCert(ca, crt, key)
ExpectNoError(t, err)
zipData, err := certs.ZipCert(ca, crt, key)
require.NoError(t, err)
ca2, crt2, key2, err := ExtractCert(zipData)
ExpectNoError(t, err)
ExpectEqual(t, ca, ca2)
ExpectEqual(t, crt, crt2)
ExpectEqual(t, key, key2)
ca2, crt2, key2, err := certs.ExtractCert(zipData)
require.NoError(t, err)
require.Equal(t, ca, ca2)
require.Equal(t, crt, crt2)
require.Equal(t, key, key2)
}

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

@@ -3,7 +3,10 @@ package env
import (
"os"
"github.com/yusing/go-proxy/agent/pkg/agent"
"github.com/yusing/go-proxy/internal/common"
"github.com/rs/zerolog/log"
)
func DefaultAgentName() string {
@@ -15,10 +18,32 @@ func DefaultAgentName() string {
}
var (
AgentName = common.GetEnvString("AGENT_NAME", DefaultAgentName())
AgentPort = common.GetEnvInt("AGENT_PORT", 8890)
AgentName string
AgentPort int
AgentSkipClientCertCheck bool
AgentCACert string
AgentSSLCert string
DockerSocket string
Runtime agent.ContainerRuntime
)
func init() {
Load()
}
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)
AgentCACert = common.GetEnvString("AGENT_CA_CERT", "")
AgentCACert = common.GetEnvString("AGENT_CA_CERT", "")
AgentSSLCert = common.GetEnvString("AGENT_SSL_CERT", "")
)
Runtime = agent.ContainerRuntime(common.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,38 +1,38 @@
package handler
import (
"encoding/json"
"fmt"
"net/http"
"net/url"
"os"
"strings"
"github.com/yusing/go-proxy/internal/net/gphttp"
"github.com/yusing/go-proxy/internal/watcher/health"
"github.com/yusing/go-proxy/internal/types"
"github.com/yusing/go-proxy/internal/watcher/health/monitor"
)
var defaultHealthConfig = health.DefaultHealthConfig()
var defaultHealthConfig = types.DefaultHealthConfig()
func CheckHealth(w http.ResponseWriter, r *http.Request) {
query := r.URL.Query()
scheme := query.Get("scheme")
if scheme == "" {
http.Error(w, http.StatusText(http.StatusBadRequest), http.StatusBadRequest)
http.Error(w, "missing scheme", http.StatusBadRequest)
return
}
var result *health.HealthCheckResult
var result *types.HealthCheckResult
var err error
switch scheme {
case "fileserver":
path := query.Get("path")
if path == "" {
http.Error(w, http.StatusText(http.StatusBadRequest), http.StatusBadRequest)
http.Error(w, "missing path", http.StatusBadRequest)
return
}
_, err := os.Stat(path)
result = &health.HealthCheckResult{Healthy: err == nil}
result = &types.HealthCheckResult{Healthy: err == nil}
if err != nil {
result.Detail = err.Error()
}
@@ -40,7 +40,7 @@ func CheckHealth(w http.ResponseWriter, r *http.Request) {
host := query.Get("host")
path := query.Get("path")
if host == "" {
http.Error(w, http.StatusText(http.StatusBadRequest), http.StatusBadRequest)
http.Error(w, "missing host", http.StatusBadRequest)
return
}
result, err = monitor.NewHTTPHealthMonitor(&url.URL{
@@ -51,17 +51,18 @@ func CheckHealth(w http.ResponseWriter, r *http.Request) {
case "tcp", "udp":
host := query.Get("host")
if host == "" {
http.Error(w, http.StatusText(http.StatusBadRequest), http.StatusBadRequest)
http.Error(w, "missing host", http.StatusBadRequest)
return
}
hasPort := strings.Contains(host, ":")
port := query.Get("port")
if port != "" && !hasPort {
host = fmt.Sprintf("%s:%s", host, port)
} else {
http.Error(w, http.StatusText(http.StatusBadRequest), http.StatusBadRequest)
if port != "" && hasPort {
http.Error(w, "port and host with port cannot both be provided", http.StatusBadRequest)
return
}
if port != "" {
host = fmt.Sprintf("%s:%s", host, port)
}
result, err = monitor.NewRawHealthMonitor(&url.URL{
Scheme: scheme,
Host: host,
@@ -73,5 +74,7 @@ func CheckHealth(w http.ResponseWriter, r *http.Request) {
return
}
gphttp.RespondJSON(w, r, result)
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusOK)
json.NewEncoder(w).Encode(result)
}

View File

@@ -12,7 +12,7 @@ import (
"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/go-proxy/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)
@@ -172,9 +172,9 @@ func TestCheckHealthTCPUDP(t *testing.T) {
{
name: "InvalidHost",
scheme: "tcp",
host: "invalid",
host: "",
port: 8080,
expectedStatus: http.StatusOK,
expectedStatus: http.StatusBadRequest,
expectedHealthy: false,
},
{
@@ -188,9 +188,17 @@ func TestCheckHealthTCPUDP(t *testing.T) {
{
name: "InvalidHost",
scheme: "udp",
host: "invalid",
host: "",
port: 8080,
expectedStatus: http.StatusOK,
expectedStatus: http.StatusBadRequest,
expectedHealthy: false,
},
{
name: "Port in both host and port",
scheme: "tcp",
host: "localhost:1234",
port: 1234,
expectedStatus: http.StatusBadRequest,
expectedHealthy: false,
},
}
@@ -208,9 +216,11 @@ func TestCheckHealthTCPUDP(t *testing.T) {
require.Equal(t, recorder.Code, tt.expectedStatus)
var result health.HealthCheckResult
require.NoError(t, json.Unmarshal(recorder.Body.Bytes(), &result))
require.Equal(t, result.Healthy, tt.expectedHealthy)
if tt.expectedStatus == http.StatusOK {
var result types.HealthCheckResult
require.NoError(t, json.Unmarshal(recorder.Body.Bytes(), &result))
require.Equal(t, result.Healthy, tt.expectedHealthy)
}
})
}
}

View File

@@ -1,31 +0,0 @@
package handler
import (
"net/http"
"net/url"
"github.com/docker/docker/client"
"github.com/yusing/go-proxy/internal/common"
"github.com/yusing/go-proxy/internal/docker"
"github.com/yusing/go-proxy/internal/logging"
"github.com/yusing/go-proxy/internal/net/gphttp/reverseproxy"
"github.com/yusing/go-proxy/internal/net/types"
)
func serviceUnavailable(w http.ResponseWriter, r *http.Request) {
http.Error(w, "docker socket is not available", http.StatusServiceUnavailable)
}
func DockerSocketHandler() http.HandlerFunc {
dockerClient, err := docker.NewClient(common.DockerHostFromEnv)
if err != nil {
logging.Warn().Err(err).Msg("failed to connect to docker client")
return serviceUnavailable
}
rp := reverseproxy.NewReverseProxy("docker", types.NewURL(&url.URL{
Scheme: "http",
Host: client.DummyHost,
}), dockerClient.HTTPClient().Transport)
return rp.ServeHTTP
}

View File

@@ -2,48 +2,59 @@ package handler
import (
"fmt"
"io"
"net/http"
"github.com/gin-gonic/gin"
"github.com/gorilla/websocket"
"github.com/yusing/go-proxy/agent/pkg/agent"
"github.com/yusing/go-proxy/agent/pkg/env"
v1 "github.com/yusing/go-proxy/internal/api/v1"
"github.com/yusing/go-proxy/internal/logging/memlogger"
"github.com/yusing/go-proxy/internal/metrics/systeminfo"
"github.com/yusing/go-proxy/internal/utils/strutils"
"github.com/yusing/go-proxy/pkg"
socketproxy "github.com/yusing/go-proxy/socketproxy/pkg"
)
type ServeMux struct{ *http.ServeMux }
func (mux ServeMux) HandleMethods(methods, endpoint string, handler http.HandlerFunc) {
for _, m := range strutils.CommaSeperatedList(methods) {
mux.ServeMux.HandleFunc(m+" "+agent.APIEndpointBase+endpoint, handler)
}
func (mux ServeMux) HandleEndpoint(method, endpoint string, handler http.HandlerFunc) {
mux.ServeMux.HandleFunc(method+" "+agent.APIEndpointBase+endpoint, handler)
}
func (mux ServeMux) HandleFunc(endpoint string, handler http.HandlerFunc) {
mux.ServeMux.HandleFunc(agent.APIEndpointBase+endpoint, handler)
}
type NopWriteCloser struct {
io.Writer
}
func (NopWriteCloser) Close() error {
return nil
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.HandleMethods("GET", agent.EndpointVersion, v1.GetVersion)
mux.HandleMethods("GET", agent.EndpointName, func(w http.ResponseWriter, r *http.Request) {
mux.HandleEndpoint("GET", agent.EndpointVersion, func(w http.ResponseWriter, r *http.Request) {
fmt.Fprint(w, pkg.GetVersion())
})
mux.HandleEndpoint("GET", agent.EndpointName, func(w http.ResponseWriter, r *http.Request) {
fmt.Fprint(w, env.AgentName)
})
mux.HandleMethods("GET", agent.EndpointHealth, CheckHealth)
mux.HandleMethods("GET", agent.EndpointLogs, memlogger.HandlerFunc())
mux.HandleMethods("GET", agent.EndpointSystemInfo, systeminfo.Poller.ServeHTTP)
mux.ServeMux.HandleFunc("/", DockerSocketHandler())
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, metricsHandler.ServeHTTP)
mux.ServeMux.HandleFunc("/", socketproxy.DockerSocketHandler(env.DockerSocket))
return mux
}

View File

@@ -3,18 +3,26 @@ package handler
import (
"crypto/tls"
"net/http"
"net/url"
"net/http/httputil"
"strconv"
"time"
"github.com/yusing/go-proxy/agent/pkg/agent"
"github.com/yusing/go-proxy/agent/pkg/agentproxy"
"github.com/yusing/go-proxy/internal/logging"
"github.com/yusing/go-proxy/internal/net/gphttp"
"github.com/yusing/go-proxy/internal/net/gphttp/reverseproxy"
"github.com/yusing/go-proxy/internal/net/types"
)
func NewTransport() *http.Transport {
return &http.Transport{
MaxIdleConnsPerHost: 100,
IdleConnTimeout: 90 * time.Second,
TLSHandshakeTimeout: 10 * time.Second,
ExpectContinueTimeout: 1 * time.Second,
ResponseHeaderTimeout: 60 * time.Second,
WriteBufferSize: 16 * 1024, // 16KB
ReadBufferSize: 16 * 1024, // 16KB
}
}
func ProxyHTTP(w http.ResponseWriter, r *http.Request) {
host := r.Header.Get(agentproxy.HeaderXProxyHost)
isHTTPS, _ := strconv.ParseBool(r.Header.Get(agentproxy.HeaderXProxyHTTPS))
@@ -34,11 +42,9 @@ func ProxyHTTP(w http.ResponseWriter, r *http.Request) {
scheme = "https"
}
var transport *http.Transport
transport := NewTransport()
if skipTLSVerify {
transport = gphttp.NewTransportWithTLSConfig(&tls.Config{InsecureSkipVerify: true})
} else {
transport = gphttp.NewTransport()
transport.TLSClientConfig = &tls.Config{InsecureSkipVerify: true}
}
if responseHeaderTimeout > 0 {
@@ -49,14 +55,13 @@ func ProxyHTTP(w http.ResponseWriter, r *http.Request) {
r.URL.Host = ""
r.URL.Path = r.URL.Path[agent.HTTPProxyURLPrefixLen:] // strip the {API_BASE}/proxy/http prefix
r.RequestURI = r.URL.String()
r.URL.Host = host
r.URL.Scheme = scheme
logging.Debug().Msgf("proxy http request: %s %s", r.Method, r.URL.String())
rp := reverseproxy.NewReverseProxy("agent", types.NewURL(&url.URL{
Scheme: scheme,
Host: host,
}), transport)
rp := &httputil.ReverseProxy{
Director: func(r *http.Request) {
r.URL.Scheme = scheme
r.URL.Host = host
},
Transport: transport,
}
rp.ServeHTTP(w, r)
}

View File

@@ -6,9 +6,9 @@ import (
"fmt"
"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/logging"
"github.com/yusing/go-proxy/internal/net/gphttp/server"
"github.com/yusing/go-proxy/internal/task"
)
@@ -33,12 +33,11 @@ func StartAgentServer(parent task.Parent, opt Options) {
tlsConfig.ClientAuth = tls.NoClientCert
}
logger := logging.GetLogger()
agentServer := &http.Server{
Addr: fmt.Sprintf(":%d", opt.Port),
Handler: handler.NewAgentHandler(),
TLSConfig: tlsConfig,
}
server.Start(parent, agentServer, nil, logger)
server.Start(parent, agentServer, nil, &log.Logger)
}

BIN
assets/godoxy.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 138 KiB

View File

@@ -1,140 +1,65 @@
package main
import (
"encoding/json"
"log"
"os"
"sync"
"github.com/yusing/go-proxy/internal"
"github.com/yusing/go-proxy/internal/api/v1/query"
"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/route/routes"
"github.com/yusing/go-proxy/internal/task"
"github.com/yusing/go-proxy/pkg"
)
var rawLogger = log.New(os.Stdout, "", 0)
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()
}
func main() {
initProfiling()
args := pkg.GetArgs(common.MainServerCommandValidator{})
switch args.Command {
case common.CommandReload:
if err := query.ReloadServer(); err != nil {
gperr.LogFatal("server reload error", err)
}
rawLogger.Println("ok")
return
case common.CommandListIcons:
icons, err := internal.ListAvailableIcons()
if err != nil {
rawLogger.Fatal(err)
}
printJSON(icons)
return
case common.CommandListRoutes:
routes, err := query.ListRoutes()
if err != nil {
log.Printf("failed to connect to api server: %s", err)
log.Printf("falling back to config file")
} else {
printJSON(routes)
return
}
case common.CommandDebugListMTrace:
trace, err := query.ListMiddlewareTraces()
if err != nil {
log.Fatal(err)
}
printJSON(trace)
return
}
logging.InitLogger(os.Stderr, memlogger.GetMemLogger())
log.Info().Msgf("GoDoxy version %s", pkg.GetVersion())
log.Trace().Msg("trace enabled")
parallel(
dnsproviders.InitProviders,
homepage.InitIconListCache,
systeminfo.Poller.Start,
middleware.LoadComposeFiles,
)
if args.Command == common.CommandStart {
logging.InitLogger(os.Stderr, memlogger.GetMemLogger())
logging.Info().Msgf("GoDoxy version %s", pkg.GetVersion())
logging.Trace().Msg("trace enabled")
parallel(
internal.InitIconListCache,
systeminfo.Poller.Start,
)
if common.APIJWTSecret == nil {
logging.Warn().Msg("API_JWT_SECRET is not set, using random key")
common.APIJWTSecret = common.RandomJWTKey()
}
} else {
logging.DiscardLogger()
}
if args.Command == common.CommandValidate {
data, err := os.ReadFile(common.ConfigPath)
if err == nil {
err = config.Validate(data)
}
if err != nil {
log.Fatal("config error: ", err)
}
log.Print("config OK")
return
if common.APIJWTSecret == nil {
log.Warn().Msg("API_JWT_SECRET is not set, using random key")
common.APIJWTSecret = common.RandomJWTKey()
}
for _, dir := range common.RequiredDirectories {
prepareDirectory(dir)
}
middleware.LoadComposeFiles()
var cfg *config.Config
var err gperr.Error
if cfg, err = config.Load(); err != nil {
cfg, err := config.Load()
if err != nil {
gperr.LogWarn("errors in config", err)
err = nil
}
switch args.Command {
case common.CommandListRoutes:
cfg.StartProxyProviders()
printJSON(routes.ByAlias())
return
case common.CommandListConfigs:
printJSON(cfg.Value())
return
case common.CommandDebugListEntries:
printJSON(cfg.DumpRoutes())
return
case common.CommandDebugListProviders:
printJSON(cfg.DumpRouteProviders())
return
}
cfg.Start(&config.StartServersOptions{
Proxy: true,
})
if err := auth.Initialize(); err != nil {
logging.Fatal().Err(err).Msg("failed to initialize authentication")
log.Fatal().Err(err).Msg("failed to initialize authentication")
}
// API Handler needs to start after auth is initialized.
cfg.StartServers(&config.StartServersOptions{
@@ -150,15 +75,7 @@ func main() {
func prepareDirectory(dir string) {
if _, err := os.Stat(dir); os.IsNotExist(err) {
if err = os.MkdirAll(dir, 0o755); err != nil {
logging.Fatal().Msgf("failed to create directory %s: %v", dir, err)
log.Fatal().Msgf("failed to create directory %s: %v", dir, err)
}
}
}
func printJSON(obj any) {
j, err := json.MarshalIndent(obj, "", " ")
if err != nil {
logging.Fatal().Err(err).Send()
}
rawLogger.Print(string(j)) // raw output for convenience using "jq"
}

View File

@@ -3,18 +3,43 @@
package main
import (
"log"
"net/http"
_ "net/http/pprof"
"runtime"
"runtime/debug"
"time"
"github.com/rs/zerolog/log"
"github.com/yusing/go-proxy/internal/utils/strutils"
)
const mb = 1024 * 1024
func initProfiling() {
runtime.GOMAXPROCS(2)
debug.SetMemoryLimit(100 * 1024 * 1024)
debug.SetMaxStack(15 * 1024 * 1024)
debug.SetGCPercent(-1)
debug.SetMemoryLimit(50 * mb)
debug.SetMaxStack(4 * mb)
go func() {
log.Println(http.ListenAndServe(":7777", nil))
log.Info().Msgf("pprof server started at http://localhost:7777/debug/pprof/")
log.Error().Err(http.ListenAndServe(":7777", nil)).Msg("pprof server failed")
}()
go func() {
ticker := time.NewTicker(time.Second * 10)
defer ticker.Stop()
for range ticker.C {
var m runtime.MemStats
runtime.ReadMemStats(&m)
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))
log.Info().Msgf(" Go Heap - Reserved from OS (HeapSys): %s", strutils.FormatByteSize(m.HeapSys))
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(" Number of Goroutines: %d", runtime.NumGoroutine())
log.Info().Msgf(" Number of GCs: %d", m.NumGC)
log.Info().Msg("-----------------------------------------------------")
}
}()
}

View File

@@ -1,21 +1,48 @@
---
services:
socket-proxy:
container_name: socket-proxy
image: ghcr.io/yusing/socket-proxy:latest
environment:
- ALLOW_START=1
- ALLOW_STOP=1
- ALLOW_RESTARTS=1
- CONTAINERS=1
- EVENTS=1
- INFO=1
- PING=1
- POST=1
- VERSION=1
volumes:
- ${DOCKER_SOCKET:-/var/run/docker.sock}:/var/run/docker.sock
restart: unless-stopped
tmpfs:
- /run
ports:
- ${SOCKET_PROXY_LISTEN_ADDR:-127.0.0.1:2375}:2375
frontend:
image: ghcr.io/yusing/godoxy-frontend:latest
image: ghcr.io/yusing/godoxy-frontend:${TAG:-latest}
container_name: godoxy-frontend
restart: unless-stopped
network_mode: host # do not change this
env_file: .env
user: ${GODOXY_UID:-1000}:${GODOXY_GID:-1000}
read_only: true
tmpfs:
- /app/.next/cache # next image caching
security_opt:
- no-new-privileges:true
cap_drop:
- all
depends_on:
- app
environment:
HOSTNAME: 127.0.0.1
PORT: ${GODOXY_FRONTEND_PORT:-3000}
# modify below to fit your needs
labels:
proxy.aliases: godoxy
proxy.godoxy.port: ${GODOXY_FRONTEND_PORT:-3000}
# proxy.godoxy.middlewares.cidr_whitelist: |
proxy.aliases: ${GODOXY_FRONTEND_ALIASES:-godoxy}
proxy.#1.port: ${GODOXY_FRONTEND_PORT:-3000}
# proxy.#1.middlewares.cidr_whitelist: |
# status: 403
# message: IP not allowed
# allow:
@@ -24,16 +51,27 @@ services:
# - 192.168.0.0/16
# - 172.16.0.0/12
app:
image: ghcr.io/yusing/godoxy:latest
container_name: godoxy
image: ghcr.io/yusing/godoxy:${TAG:-latest}
container_name: godoxy-proxy
restart: always
network_mode: host # do not change this
env_file: .env
user: ${GODOXY_UID:-1000}:${GODOXY_GID:-1000}
depends_on:
socket-proxy:
condition: service_started
security_opt:
- no-new-privileges:true
cap_drop:
- all
cap_add:
- NET_BIND_SERVICE
environment:
- DOCKER_HOST=tcp://${SOCKET_PROXY_LISTEN_ADDR:-127.0.0.1:2375}
volumes:
- /var/run/docker.sock:/var/run/docker.sock
- ./config:/app/config
- ./logs:/app/logs
- ./error_pages:/app/error_pages
- ./error_pages:/app/error_pages:ro
- ./data:/app/data
# To use autocert, certs will be stored in "./certs".

View File

@@ -15,7 +15,7 @@
# options:
# auth_token: c1234565789-abcdefghijklmnopqrst # your zone API token
# 3. other providers, see https://github.com/yusing/godoxy/wiki/Supported-DNS%E2%80%9001-Providers#supported-dns-01-providers
# 3. other providers, see https://docs.godoxy.dev/DNS-01-Providers
# acl:
# default: allow # or deny (default: allow)
@@ -38,19 +38,34 @@
entrypoint:
# Below define an example of middleware config
# 1. block non local IP connections
# 2. redirect HTTP to HTTPS
# 1. set security headers
# 2. block non local IP connections
# 3. redirect HTTP to HTTPS
#
# middlewares:
# - 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
middlewares:
- use: CloudflareRealIP
- use: ModifyResponse
set_headers:
Access-Control-Allow-Methods: GET, POST, PUT, PATCH, DELETE, OPTIONS, HEAD
Access-Control-Allow-Headers: "*"
Access-Control-Allow-Origin: "*"
Access-Control-Max-Age: 180
Vary: "*"
X-XSS-Protection: 1; mode=block
Content-Security-Policy: "object-src 'self'; frame-ancestors 'self';"
X-Content-Type-Options: nosniff
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:
@@ -100,7 +115,7 @@ providers:
# secret: aaaa-bbbb-cccc-dddd
# no_tls_verify: true
# Check https://github.com/yusing/godoxy/wiki/Certificates-and-domain-matching#domain-matching
# Check https://docs.godoxy.dev/Certificates-and-domain-matching
# for explaination of `match_domains`
#
# match_domains:

33
dev.Dockerfile Normal file
View File

@@ -0,0 +1,33 @@
# Stage 1: deps
FROM golang:1.25.0-alpine AS deps
HEALTHCHECK NONE
# package version does not matter
# trunk-ignore(hadolint/DL3018)
RUN apk add --no-cache tzdata make libcap-setcap
# Stage 3: Final image
FROM alpine:3.22
LABEL maintainer="yusing@6uo.me"
LABEL proxy.exclude=1
# copy timezone data
COPY --from=deps /usr/share/zoneinfo /usr/share/zoneinfo
# copy certs
COPY --from=deps /etc/ssl/certs /etc/ssl/certs
ARG TARGET
ENV TARGET=${TARGET}
ENV DOCKER_HOST=unix:///var/run/docker.sock
# copy binary
COPY bin/${TARGET} /app/run
WORKDIR /app
RUN chown -R 1000:1000 /app
CMD ["/app/run"]

43
dev.compose.yml Normal file
View File

@@ -0,0 +1,43 @@
services:
app:
image: godoxy-dev
build:
context: .
dockerfile: dev.Dockerfile
args:
- TARGET=godoxy
container_name: godoxy-proxy-dev
restart: unless-stopped
env_file: dev.env
environment:
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_SECRET: 1234567891234567
labels:
proxy.exclude: true
proxy.#1.healthcheck.disable: true
ipc: host
network_mode: host
volumes:
- /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
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"

279
go.mod
View File

@@ -1,151 +1,143 @@
module github.com/yusing/go-proxy
go 1.24.2
go 1.25.1
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-20250816044348-0630187cb14b
replace github.com/shirou/gopsutil/v4 => github.com/godoxy-app/gopsutil/v4 v4.0.0-20250816043325-ee003f88b84d
require (
github.com/PuerkitoBio/goquery v1.10.3 // parsing HTML for extract fav icon
github.com/coder/websocket v1.8.13 // websocket for API and agent
github.com/coreos/go-oidc/v3 v3.14.1 // oidc authentication
github.com/docker/docker v28.1.1+incompatible // docker daemon
github.com/coreos/go-oidc/v3 v3.15.0 // oidc authentication
github.com/docker/docker v28.4.0+incompatible // docker daemon
github.com/fsnotify/fsnotify v1.9.0 // file watcher
github.com/go-acme/lego/v4 v4.23.1 // acme client
github.com/go-playground/validator/v10 v10.26.0 // validator
github.com/go-acme/lego/v4 v4.26.0 // acme client
github.com/go-playground/validator/v10 v10.27.0 // validator
github.com/gobwas/glob v0.2.3 // glob matcher for route rules
github.com/gotify/server/v2 v2.6.1 // reference the Message struct for json response
github.com/gorilla/websocket v1.5.3 // websocket for API and agent
github.com/gotify/server/v2 v2.7.2 // 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/v3 v3.5.1 // lock free map for concurrent operations
github.com/puzpuzpuz/xsync/v4 v4.1.0 // lock free map for concurrent operations
github.com/rs/zerolog v1.34.0 // logging
github.com/shirou/gopsutil/v4 v4.25.3 // system info metrics
github.com/shirou/gopsutil/v4 v4.25.8 // system info metrics
github.com/vincent-petithory/dataurl v1.0.0 // data url for fav icon
golang.org/x/crypto v0.37.0 // encrypting password with bcrypt
golang.org/x/net v0.39.0 // HTTP header utilities
golang.org/x/oauth2 v0.29.0 // oauth2 authentication
golang.org/x/text v0.24.0 // string utilities
golang.org/x/time v0.11.0 // time utilities
gopkg.in/yaml.v3 v3.0.1 // indirect; yaml parsing for different config files
golang.org/x/crypto v0.42.0 // encrypting password with bcrypt
golang.org/x/net v0.44.0 // HTTP header utilities
golang.org/x/oauth2 v0.31.0 // oauth2 authentication
golang.org/x/sync v0.17.0
golang.org/x/time v0.13.0 // time utilities
)
replace github.com/coreos/go-oidc/v3 => github.com/godoxy-app/go-oidc/v3 v3.14.2
require (
github.com/docker/cli v28.1.1+incompatible
github.com/goccy/go-yaml v1.17.1
github.com/golang-jwt/jwt/v5 v5.2.2
github.com/luthermonson/go-proxmox v0.2.2
github.com/docker/cli v28.4.0+incompatible
github.com/goccy/go-yaml v1.18.0 // yaml parsing for different config files
github.com/golang-jwt/jwt/v5 v5.3.0
github.com/luthermonson/go-proxmox v0.2.3
github.com/oschwald/maxminddb-golang v1.13.1
github.com/quic-go/quic-go v0.51.0
github.com/quic-go/quic-go v0.54.0
github.com/samber/slog-zerolog/v2 v2.7.3
github.com/spf13/afero v1.14.0
github.com/stretchr/testify v1.10.0
go.uber.org/atomic v1.11.0
github.com/spf13/afero v1.15.0
github.com/stretchr/testify v1.11.1
github.com/yusing/go-proxy/agent v0.0.0-20250913143824-493c0afdface
github.com/yusing/go-proxy/internal/dnsproviders v0.0.0-20250913143824-493c0afdface
github.com/yusing/go-proxy/internal/utils v0.0.0
)
replace github.com/docker/docker => github.com/godoxy-app/docker v0.0.0-20250425105916-b2ad800de7a1
require (
cloud.google.com/go/auth v0.16.1 // indirect
cloud.google.com/go/auth v0.16.5 // indirect
cloud.google.com/go/auth/oauth2adapt v0.2.8 // indirect
cloud.google.com/go/compute/metadata v0.6.0 // indirect
cloud.google.com/go/compute/metadata v0.8.0 // indirect
github.com/AdamSLevy/jsonrpc2/v14 v14.1.0 // indirect
github.com/Azure/azure-sdk-for-go/sdk/azcore v1.18.0 // indirect
github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.9.0 // indirect
github.com/Azure/azure-sdk-for-go/sdk/internal v1.11.1 // indirect
github.com/Azure/azure-sdk-for-go/sdk/azcore v1.19.1 // indirect
github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.11.0 // 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.5.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/aliyun/alibaba-cloud-sdk-go v1.63.107 // indirect
github.com/andybalholm/cascadia v1.3.3 // indirect
github.com/aws/aws-sdk-go-v2 v1.36.3 // indirect
github.com/aws/aws-sdk-go-v2/config v1.29.14 // indirect
github.com/aws/aws-sdk-go-v2/credentials v1.17.67 // indirect
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.30 // indirect
github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.34 // indirect
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.34 // indirect
github.com/aws/aws-sdk-go-v2 v1.39.0 // indirect
github.com/aws/aws-sdk-go-v2/config v1.31.8 // indirect
github.com/aws/aws-sdk-go-v2/credentials v1.18.12 // indirect
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.7 // indirect
github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.7 // indirect
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.7 // 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.3 // indirect
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.12.15 // indirect
github.com/aws/aws-sdk-go-v2/service/lightsail v1.43.2 // indirect
github.com/aws/aws-sdk-go-v2/service/route53 v1.51.1 // indirect
github.com/aws/aws-sdk-go-v2/service/sso v1.25.3 // indirect
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.30.1 // indirect
github.com/aws/aws-sdk-go-v2/service/sts v1.33.19 // indirect
github.com/aws/smithy-go v1.22.3 // indirect
github.com/baidubce/bce-sdk-go v0.9.224 // indirect
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.1 // indirect
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.7 // indirect
github.com/aws/aws-sdk-go-v2/service/lightsail v1.48.4 // indirect
github.com/aws/aws-sdk-go-v2/service/route53 v1.58.2 // indirect
github.com/aws/aws-sdk-go-v2/service/sso v1.29.3 // indirect
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.34.4 // indirect
github.com/aws/aws-sdk-go-v2/service/sts v1.38.4 // indirect
github.com/aws/smithy-go v1.23.0 // indirect
github.com/benbjohnson/clock v1.3.5 // indirect
github.com/boombuler/barcode v1.0.2 // indirect
github.com/boombuler/barcode v1.1.0 // indirect
github.com/buger/goterm v1.0.4 // indirect
github.com/cenkalti/backoff/v4 v4.3.0 // indirect
github.com/civo/civogo v0.3.98 // 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/dnsimple/dnsimple-go v1.7.0 // indirect
github.com/docker/go-connections v0.5.0 // indirect
github.com/docker/go-connections v0.6.0
github.com/docker/go-units v0.5.0 // indirect
github.com/ebitengine/purego v0.8.2 // indirect
github.com/exoscale/egoscale/v3 v3.1.15 // indirect
github.com/ebitengine/purego v0.8.4 // indirect
github.com/exoscale/egoscale/v3 v3.1.26 // indirect
github.com/fatih/structs v1.1.0 // indirect
github.com/felixge/httpsnoop v1.0.4 // indirect
github.com/fxamacker/cbor/v2 v2.8.0 // indirect
github.com/gabriel-vasile/mimetype v1.4.9 // indirect
github.com/gabriel-vasile/mimetype v1.4.10 // indirect
github.com/go-errors/errors v1.5.1 // indirect
github.com/go-jose/go-jose/v4 v4.1.0 // indirect
github.com/go-logr/logr v1.4.2 // indirect
github.com/go-jose/go-jose/v4 v4.1.2 // 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-task/slim-sprig/v3 v3.0.0 // indirect
github.com/go-viper/mapstructure/v2 v2.2.1 // indirect
github.com/goccy/go-json v0.10.5 // indirect; indirectindirect
github.com/go-viper/mapstructure/v2 v2.4.0 // indirect
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/google/pprof v0.0.0-20250423184734-337e5dd93bb4 // 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/gax-go/v2 v2.14.1 // 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/gorilla/websocket v1.5.3 // indirect
github.com/hashicorp/go-cleanhttp v0.5.2 // indirect
github.com/hashicorp/go-retryablehttp v0.7.7 // 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.146 // indirect
github.com/iij/doapi v0.0.0-20190504054126-0bbf12d6d7df // indirect
github.com/infobloxopen/infoblox-go-client/v2 v2.9.0 // indirect
github.com/infobloxopen/infoblox-go-client/v2 v2.10.0 // indirect
github.com/jinzhu/copier v0.4.0 // indirect
github.com/jmespath/go-jmespath v0.4.0 // indirect
github.com/json-iterator/go v1.1.12 // indirect
github.com/json-iterator/go v1.1.13-0.20220915233716-71ac16282d12 // indirect
github.com/k0kubun/go-ansi v0.0.0-20180517002512-3bf9e2903213 // indirect
github.com/kolo/xmlrpc v0.0.0-20220921171641-a4b6fa1dd06b // 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.49.0 // indirect
github.com/linode/linodego v1.57.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/lufia/plan9stats v0.0.0-20250827001030-24949be3fa54 // 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.65 // indirect
github.com/miekg/dns v1.1.68 // indirect
github.com/mimuret/golang-iij-dpf v0.9.1 // 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/namedotcom/go v0.0.0-20180403034216-08470befbe04 // 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
@@ -153,91 +145,110 @@ require (
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/onsi/ginkgo/v2 v2.23.4 // indirect
github.com/opencontainers/go-digest v1.0.0 // indirect
github.com/opencontainers/image-spec v1.1.1 // indirect
github.com/opentracing/opentracing-go v1.2.1-0.20220228012449-10b1cf09e00b // indirect
github.com/oracle/oci-go-sdk/v65 v65.89.2 // indirect
github.com/ovh/go-ovh v1.7.0 // indirect
github.com/patrickmn/go-cache v2.1.0+incompatible // indirect
github.com/ovh/go-ovh v1.9.0 // 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.4.0 // 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.2.10 // indirect
github.com/sacloud/api-client-go v0.3.3 // indirect
github.com/sacloud/go-http v0.1.9 // indirect
github.com/sacloud/iaas-api-go v1.14.0 // indirect
github.com/sacloud/iaas-api-go v1.17.1 // indirect
github.com/sacloud/packages-go v0.0.11 // indirect
github.com/sagikazarmark/locafero v0.9.0 // indirect
github.com/samber/lo v1.49.1 // indirect
github.com/samber/slog-common v0.18.1 // indirect
github.com/scaleway/scaleway-sdk-go v1.0.0-beta.33 // indirect
github.com/sagikazarmark/locafero v0.11.0 // indirect
github.com/samber/lo v1.51.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/selectel/go-selvpcclient/v3 v3.2.1 // indirect
github.com/shopspring/decimal v1.4.0 // 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/softlayer-go v1.2.1 // 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.7.1 // indirect
github.com/spf13/pflag v1.0.6 // indirect
github.com/spf13/viper v1.20.1 // indirect
github.com/sourcegraph/conc v0.3.1-0.20240121214520-5f936abd7ae8 // indirect
github.com/spf13/cast v1.10.0 // indirect
github.com/spf13/pflag v1.0.10 // indirect
github.com/spf13/viper v1.21.0 // indirect
github.com/subosito/gotenv v1.6.0 // indirect
github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common v1.0.1151 // indirect
github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/dnspod v1.0.1136 // 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/ultradns/ultradns-go-sdk v1.8.1-20250722213956-faef419 // indirect
github.com/vinyldns/go-vinyldns v0.9.16 // indirect
github.com/volcengine/volc-sdk-golang v1.0.206 // indirect
github.com/vultr/govultr/v3 v3.19.1 // indirect
github.com/x448/float16 v0.8.4 // indirect
github.com/volcengine/volc-sdk-golang v1.0.219 // indirect
github.com/vultr/govultr/v3 v3.23.0 // 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.3 // indirect
go.opentelemetry.io/auto/sdk v1.1.0 // indirect
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.60.0 // indirect
go.opentelemetry.io/otel v1.35.0 // indirect
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.35.0 // indirect
go.opentelemetry.io/otel/metric v1.35.0 // indirect
go.opentelemetry.io/otel/trace v1.35.0 // indirect
go.uber.org/automaxprocs v1.6.0 // indirect
go.uber.org/mock v0.5.1 // indirect
go.uber.org/multierr v1.11.0 // indirect
go.opentelemetry.io/auto/sdk v1.2.0 // indirect
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.63.0 // indirect
go.opentelemetry.io/otel v1.38.0 // indirect
go.opentelemetry.io/otel/metric v1.38.0 // indirect
go.opentelemetry.io/otel/trace v1.38.0 // indirect
go.uber.org/atomic v1.11.0
go.uber.org/mock v0.6.0 // indirect
go.uber.org/ratelimit v0.3.1 // indirect
golang.org/x/mod v0.24.0 // indirect
golang.org/x/sync v0.13.0 // indirect
golang.org/x/sys v0.32.0 // indirect
golang.org/x/tools v0.32.0 // indirect
google.golang.org/api v0.230.0 // indirect
google.golang.org/genproto/googleapis/api v0.0.0-20250422160041-2d3770c4ea7f // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20250422160041-2d3770c4ea7f // indirect
google.golang.org/grpc v1.72.0 // indirect
google.golang.org/protobuf v1.36.6 // indirect
gopkg.in/inf.v0 v0.9.1 // indirect
golang.org/x/mod v0.28.0 // indirect
golang.org/x/sys v0.36.0 // indirect
golang.org/x/text v0.29.0 // indirect
golang.org/x/tools v0.37.0 // indirect
google.golang.org/api v0.249.0 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20250908214217-97024824d090 // indirect
google.golang.org/grpc v1.75.1 // indirect
google.golang.org/protobuf v1.36.9 // indirect
gopkg.in/ini.v1 v1.67.0 // indirect
gopkg.in/ns1/ns1-go.v2 v2.14.2 // indirect
gopkg.in/ns1/ns1-go.v2 v2.15.0 // indirect
gopkg.in/yaml.v2 v2.4.0 // indirect
k8s.io/api v0.33.0 // indirect
k8s.io/apimachinery v0.33.0 // indirect
k8s.io/klog/v2 v2.130.1 // indirect
k8s.io/utils v0.0.0-20250321185631-1f6e0b77f77e // 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.4.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
)
require (
github.com/gin-gonic/gin v1.10.1
github.com/yusing/ds v0.1.0
)
require (
github.com/akamai/AkamaiOPEN-edgegrid-golang/v11 v11.1.0 // indirect
github.com/aziontech/azionapi-go-sdk v0.142.0 // indirect
github.com/bytedance/gopkg v0.1.3 // indirect
github.com/bytedance/sonic v1.14.1 // indirect
github.com/bytedance/sonic/loader v0.3.0 // indirect
github.com/cloudwego/base64x v0.1.6 // indirect
github.com/containerd/errdefs v1.0.0 // indirect
github.com/containerd/errdefs/pkg v0.3.0 // indirect
github.com/containerd/log v0.1.0 // indirect
github.com/dnsimple/dnsimple-go/v4 v4.0.0 // indirect
github.com/fatih/color v1.18.0 // indirect
github.com/gin-contrib/sse v1.1.0 // indirect
github.com/go-ozzo/ozzo-validation/v4 v4.3.0 // indirect
github.com/goccy/go-json v0.10.5 // indirect
github.com/klauspost/compress v1.18.0 // indirect
github.com/klauspost/cpuid/v2 v2.3.0 // indirect
github.com/moby/sys/atomicwriter v0.1.0 // indirect
github.com/moby/term v0.5.2 // indirect
github.com/morikuni/aec v1.0.0 // indirect
github.com/namedotcom/go/v4 v4.0.2 // indirect
github.com/nrdcg/oci-go-sdk/common/v1065 v1065.100.0 // indirect
github.com/nrdcg/oci-go-sdk/dns/v1065 v1065.100.0 // indirect
github.com/onsi/ginkgo/v2 v2.25.1 // indirect
github.com/onsi/gomega v1.38.1 // indirect
github.com/pierrec/lz4/v4 v4.1.21 // indirect
github.com/selectel/go-selvpcclient/v4 v4.1.0 // indirect
github.com/stretchr/objx v0.5.2 // indirect
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
github.com/ugorji/go/codec v1.3.0 // indirect
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.37.0 // indirect
go.yaml.in/yaml/v3 v3.0.4 // indirect
golang.org/x/arch v0.21.0 // indirect
google.golang.org/genproto v0.0.0-20250908214217-97024824d090 // indirect
google.golang.org/genproto/googleapis/api v0.0.0-20250826171959-ef028d996bc1 // indirect
)

572
go.sum

File diff suppressed because it is too large Load Diff

View File

@@ -2,16 +2,14 @@ package acl
import (
"net"
"sync"
"time"
"github.com/oschwald/maxminddb-golang"
"github.com/puzpuzpuz/xsync/v3"
"github.com/rs/zerolog"
acl "github.com/yusing/go-proxy/internal/acl/types"
"github.com/puzpuzpuz/xsync/v4"
"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"
"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"
)
@@ -19,43 +17,24 @@ import (
type Config struct {
Default string `json:"default" validate:"omitempty,oneof=allow deny"` // default: allow
AllowLocal *bool `json:"allow_local"` // default: true
Allow []string `json:"allow"`
Deny []string `json:"deny"`
Allow Matchers `json:"allow"`
Deny Matchers `json:"deny"`
Log *accesslog.ACLLoggerConfig `json:"log"`
MaxMind *MaxMindConfig `json:"maxmind" validate:"omitempty"`
config
valErr gperr.Error
}
type (
MaxMindDatabaseType string
MaxMindConfig struct {
AccountID string `json:"account_id" validate:"required"`
LicenseKey string `json:"license_key" validate:"required"`
Database MaxMindDatabaseType `json:"database" validate:"required,oneof=geolite geoip2"`
logger zerolog.Logger
lastUpdate time.Time
db struct {
*maxminddb.Reader
sync.RWMutex
}
}
)
type config struct {
defaultAllow bool
allowLocal bool
allow []matcher
deny []matcher
ipCache *xsync.MapOf[string, *checkCache]
ipCache *xsync.Map[string, *checkCache]
logAllowed bool
logger *accesslog.AccessLogger
}
type checkCache struct {
*acl.IPInfo
*maxmind.IPInfo
allow bool
created time.Time
}
@@ -63,21 +42,16 @@ type checkCache struct {
const cacheTTL = 1 * time.Minute
func (c *checkCache) Expired() bool {
return c.created.Add(cacheTTL).After(utils.TimeNow())
return c.created.Add(cacheTTL).Before(utils.TimeNow())
}
//TODO: add stats
// TODO: add stats
const (
ACLAllow = "allow"
ACLDeny = "deny"
)
const (
MaxMindGeoLite MaxMindDatabaseType = "geolite"
MaxMindGeoIP2 MaxMindDatabaseType = "geoip2"
)
func (c *Config) Validate() gperr.Error {
switch c.Default {
case "", ACLAllow:
@@ -85,7 +59,8 @@ func (c *Config) Validate() gperr.Error {
case ACLDeny:
c.defaultAllow = false
default:
return gperr.New("invalid default value").Subject(c.Default)
c.valErr = gperr.New("invalid default value").Subject(c.Default)
return c.valErr
}
if c.AllowLocal != nil {
@@ -94,55 +69,24 @@ func (c *Config) Validate() gperr.Error {
c.allowLocal = true
}
if c.MaxMind != nil {
c.MaxMind.logger = logging.With().Str("type", string(c.MaxMind.Database)).Logger()
}
if c.Log != nil {
c.logAllowed = c.Log.LogAllowed
}
errs := gperr.NewBuilder("syntax error")
c.allow = make([]matcher, 0, len(c.Allow))
c.deny = make([]matcher, 0, len(c.Deny))
for _, s := range c.Allow {
m, err := c.parseMatcher(s)
if err != nil {
errs.Add(err.Subject(s))
continue
}
c.allow = append(c.allow, m)
}
for _, s := range c.Deny {
m, err := c.parseMatcher(s)
if err != nil {
errs.Add(err.Subject(s))
continue
}
c.deny = append(c.deny, m)
if !c.allowLocal && !c.defaultAllow && len(c.Allow) == 0 {
c.valErr = gperr.New("allow_local is false and default is deny, but no allow rules are configured")
return c.valErr
}
if errs.HasError() {
c.allow = nil
c.deny = nil
return errMatcherFormat.With(errs.Error())
}
c.ipCache = xsync.NewMapOf[string, *checkCache]()
c.ipCache = xsync.NewMap[string, *checkCache]()
return nil
}
func (c *Config) Valid() bool {
return c != nil && (len(c.allow) > 0 || len(c.deny) > 0 || c.allowLocal)
return c != nil && c.valErr == nil
}
func (c *Config) Start(parent *task.Task) gperr.Error {
if c.MaxMind != nil {
if err := c.MaxMind.LoadMaxMindDB(parent); err != nil {
return err
}
}
if c.Log != nil {
logger, err := accesslog.NewAccessLogger(parent, c.Log)
if err != nil {
@@ -150,10 +94,22 @@ func (c *Config) Start(parent *task.Task) gperr.Error {
}
c.logger = logger
}
if c.valErr != nil {
return c.valErr
}
log.Info().
Str("default", c.Default).
Bool("allow_local", c.allowLocal).
Int("allow_rules", len(c.Allow)).
Int("deny_rules", len(c.Deny)).
Msg("ACL started")
return nil
}
func (c *config) cacheRecord(info *acl.IPInfo, allow bool) {
func (c *Config) cacheRecord(info *maxmind.IPInfo, allow bool) {
if common.ForceResolveCountry && info.City == nil {
maxmind.LookupCity(info)
}
c.ipCache.Store(info.Str, &checkCache{
IPInfo: info,
allow: allow,
@@ -161,7 +117,7 @@ func (c *config) cacheRecord(info *acl.IPInfo, allow bool) {
})
}
func (c *config) log(info *acl.IPInfo, allowed bool) {
func (c *config) log(info *maxmind.IPInfo, allowed bool) {
if c.logger == nil {
return
}
@@ -175,14 +131,13 @@ func (c *Config) IPAllowed(ip net.IP) bool {
return false
}
// always allow private and loopback
// loopback is not logged
// always allow loopback, not logged
if ip.IsLoopback() {
return true
}
if c.allowLocal && ip.IsPrivate() {
c.log(&acl.IPInfo{IP: ip, Str: ip.String()}, true)
c.log(&maxmind.IPInfo{IP: ip, Str: ip.String()}, true)
return true
}
@@ -193,20 +148,16 @@ func (c *Config) IPAllowed(ip net.IP) bool {
return record.allow
}
ipAndStr := &acl.IPInfo{IP: ip, Str: ipStr}
for _, m := range c.allow {
if m(ipAndStr) {
c.log(ipAndStr, true)
c.cacheRecord(ipAndStr, true)
return true
}
ipAndStr := &maxmind.IPInfo{IP: ip, Str: ipStr}
if c.Allow.Match(ipAndStr) {
c.log(ipAndStr, true)
c.cacheRecord(ipAndStr, true)
return true
}
for _, m := range c.deny {
if m(ipAndStr) {
c.log(ipAndStr, false)
c.cacheRecord(ipAndStr, false)
return false
}
if c.Deny.Match(ipAndStr) {
c.log(ipAndStr, false)
c.cacheRecord(ipAndStr, false)
return false
}
c.log(ipAndStr, c.defaultAllow)

View File

@@ -4,11 +4,17 @@ import (
"net"
"strings"
acl "github.com/yusing/go-proxy/internal/acl/types"
"github.com/yusing/go-proxy/internal/gperr"
"github.com/yusing/go-proxy/internal/maxmind"
)
type matcher func(*acl.IPInfo) bool
type MatcherFunc func(*maxmind.IPInfo) bool
type Matcher struct {
match MatcherFunc
}
type Matchers []Matcher
const (
MatcherTypeIP = "ip"
@@ -17,6 +23,9 @@ const (
MatcherTypeCountry = "country"
)
// TODO: use this error in the future
//
//nolint:unused
var errMatcherFormat = gperr.Multiline().AddLines(
"invalid matcher format, expect {type}:{value}",
"Available types: ip|cidr|tz|country",
@@ -25,62 +34,66 @@ var errMatcherFormat = gperr.Multiline().AddLines(
"tz:Asia/Shanghai",
"country:GB",
)
var (
errSyntax = gperr.New("syntax error")
errInvalidIP = gperr.New("invalid IP")
errInvalidCIDR = gperr.New("invalid CIDR")
errMaxMindNotConfigured = gperr.New("MaxMind not configured")
errSyntax = gperr.New("syntax error")
errInvalidIP = gperr.New("invalid IP")
errInvalidCIDR = gperr.New("invalid CIDR")
)
func (cfg *Config) parseMatcher(s string) (matcher, gperr.Error) {
func (matcher *Matcher) Parse(s string) error {
parts := strings.Split(s, ":")
if len(parts) != 2 {
return nil, errSyntax
return errSyntax
}
switch parts[0] {
case MatcherTypeIP:
ip := net.ParseIP(parts[1])
if ip == nil {
return nil, errInvalidIP
return errInvalidIP
}
return matchIP(ip), nil
matcher.match = matchIP(ip)
case MatcherTypeCIDR:
_, net, err := net.ParseCIDR(parts[1])
if err != nil {
return nil, errInvalidCIDR
return errInvalidCIDR
}
return matchCIDR(net), nil
matcher.match = matchCIDR(net)
case MatcherTypeTimeZone:
if cfg.MaxMind == nil {
return nil, errMaxMindNotConfigured
}
return cfg.MaxMind.matchTimeZone(parts[1]), nil
matcher.match = matchTimeZone(parts[1])
case MatcherTypeCountry:
if cfg.MaxMind == nil {
return nil, errMaxMindNotConfigured
}
return cfg.MaxMind.matchISOCode(parts[1]), nil
matcher.match = matchISOCode(parts[1])
default:
return nil, errSyntax
return errSyntax
}
return nil
}
func matchIP(ip net.IP) matcher {
return func(ip2 *acl.IPInfo) bool {
func (matchers Matchers) Match(ip *maxmind.IPInfo) bool {
for _, m := range matchers {
if m.match(ip) {
return true
}
}
return false
}
func matchIP(ip net.IP) MatcherFunc {
return func(ip2 *maxmind.IPInfo) bool {
return ip.Equal(ip2.IP)
}
}
func matchCIDR(n *net.IPNet) matcher {
return func(ip *acl.IPInfo) bool {
func matchCIDR(n *net.IPNet) MatcherFunc {
return func(ip *maxmind.IPInfo) bool {
return n.Contains(ip.IP)
}
}
func (cfg *MaxMindConfig) matchTimeZone(tz string) matcher {
return func(ip *acl.IPInfo) bool {
city, ok := cfg.lookupCity(ip)
func matchTimeZone(tz string) MatcherFunc {
return func(ip *maxmind.IPInfo) bool {
city, ok := maxmind.LookupCity(ip)
if !ok {
return false
}
@@ -88,9 +101,9 @@ func (cfg *MaxMindConfig) matchTimeZone(tz string) matcher {
}
}
func (cfg *MaxMindConfig) matchISOCode(iso string) matcher {
return func(ip *acl.IPInfo) bool {
city, ok := cfg.lookupCity(ip)
func matchISOCode(iso string) MatcherFunc {
return func(ip *maxmind.IPInfo) bool {
city, ok := maxmind.LookupCity(ip)
if !ok {
return false
}

View File

@@ -0,0 +1,49 @@
package acl
import (
"net"
"reflect"
"testing"
maxmind "github.com/yusing/go-proxy/internal/maxmind/types"
"github.com/yusing/go-proxy/internal/serialization"
)
func TestMatchers(t *testing.T) {
strMatchers := []string{
"ip:127.0.0.1",
"cidr:10.0.0.0/8",
}
var mathers Matchers
err := serialization.Convert(reflect.ValueOf(strMatchers), reflect.ValueOf(&mathers), false)
if err != nil {
t.Fatal(err)
}
tests := []struct {
ip string
want bool
}{
{"127.0.0.1", true},
{"10.0.0.1", true},
{"127.0.0.2", false},
{"192.168.0.1", false},
{"11.0.0.1", false},
}
for _, test := range tests {
ip := net.ParseIP(test.ip)
if ip == nil {
t.Fatalf("invalid ip: %s", test.ip)
}
got := mathers.Match(&maxmind.IPInfo{
IP: ip,
Str: test.ip,
})
if got != test.want {
t.Errorf("mathers.Match(%s) = %v, want %v", test.ip, got, test.want)
}
}
}

View File

@@ -1,281 +0,0 @@
package acl
import (
"archive/tar"
"compress/gzip"
"errors"
"fmt"
"io"
"net/http"
"os"
"path/filepath"
"time"
"github.com/oschwald/maxminddb-golang"
"github.com/yusing/go-proxy/internal/common"
"github.com/yusing/go-proxy/internal/gperr"
"github.com/yusing/go-proxy/internal/task"
)
var (
updateInterval = 24 * time.Hour
httpClient = &http.Client{
Timeout: 10 * time.Second,
}
ErrResponseNotOK = gperr.New("response not OK")
ErrDownloadFailure = gperr.New("download failure")
)
func dbPathImpl(dbType MaxMindDatabaseType) string {
if dbType == MaxMindGeoLite {
return filepath.Join(dataDir, "GeoLite2-City.mmdb")
}
return filepath.Join(dataDir, "GeoIP2-City.mmdb")
}
func dbURLimpl(dbType MaxMindDatabaseType) string {
if dbType == MaxMindGeoLite {
return "https://download.maxmind.com/geoip/databases/GeoLite2-City/download?suffix=tar.gz"
}
return "https://download.maxmind.com/geoip/databases/GeoIP2-City/download?suffix=tar.gz"
}
func dbFilename(dbType MaxMindDatabaseType) string {
if dbType == MaxMindGeoLite {
return "GeoLite2-City.mmdb"
}
return "GeoIP2-City.mmdb"
}
func (cfg *MaxMindConfig) LoadMaxMindDB(parent task.Parent) gperr.Error {
if cfg.Database == "" {
return nil
}
path := dbPath(cfg.Database)
reader, err := maxmindDBOpen(path)
exists := true
if err != nil {
switch {
case errors.Is(err, os.ErrNotExist):
default:
// ignore invalid error, just download it again
var invalidErr maxminddb.InvalidDatabaseError
if !errors.As(err, &invalidErr) {
return gperr.Wrap(err)
}
}
exists = false
}
if !exists {
cfg.logger.Info().Msg("MaxMind DB not found/invalid, downloading...")
reader, err = cfg.download()
if err != nil {
return ErrDownloadFailure.With(err)
}
}
cfg.logger.Info().Msg("MaxMind DB loaded")
cfg.db.Reader = reader
go cfg.scheduleUpdate(parent)
return nil
}
func (cfg *MaxMindConfig) loadLastUpdate() {
f, err := os.Stat(dbPath(cfg.Database))
if err != nil {
return
}
cfg.lastUpdate = f.ModTime()
}
func (cfg *MaxMindConfig) setLastUpdate(t time.Time) {
cfg.lastUpdate = t
_ = os.Chtimes(dbPath(cfg.Database), t, t)
}
func (cfg *MaxMindConfig) scheduleUpdate(parent task.Parent) {
task := parent.Subtask("schedule_update", true)
ticker := time.NewTicker(updateInterval)
cfg.loadLastUpdate()
cfg.update()
defer func() {
ticker.Stop()
if cfg.db.Reader != nil {
cfg.db.Reader.Close()
}
task.Finish(nil)
}()
for {
select {
case <-task.Context().Done():
return
case <-ticker.C:
cfg.update()
}
}
}
func (cfg *MaxMindConfig) update() {
// check for update
cfg.logger.Info().Msg("checking for MaxMind DB update...")
remoteLastModified, err := cfg.checkLastest()
if err != nil {
cfg.logger.Err(err).Msg("failed to check MaxMind DB update")
return
}
if remoteLastModified.Equal(cfg.lastUpdate) {
cfg.logger.Info().Msg("MaxMind DB is up to date")
return
}
cfg.logger.Info().
Time("latest", remoteLastModified.Local()).
Time("current", cfg.lastUpdate).
Msg("MaxMind DB update available")
reader, err := cfg.download()
if err != nil {
cfg.logger.Err(err).Msg("failed to update MaxMind DB")
return
}
cfg.db.Lock()
cfg.db.Close()
cfg.db.Reader = reader
cfg.setLastUpdate(*remoteLastModified)
cfg.db.Unlock()
cfg.logger.Info().Msg("MaxMind DB updated")
}
func (cfg *MaxMindConfig) newReq(method string) (*http.Response, error) {
req, err := http.NewRequest(method, dbURL(cfg.Database), nil)
if err != nil {
return nil, err
}
req.SetBasicAuth(cfg.AccountID, cfg.LicenseKey)
resp, err := httpClient.Do(req)
if err != nil {
return nil, err
}
return resp, nil
}
func (cfg *MaxMindConfig) checkLastest() (lastModifiedT *time.Time, err error) {
resp, err := newReq(cfg, http.MethodHead)
if err != nil {
return nil, err
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
return nil, fmt.Errorf("%w: %d", ErrResponseNotOK, resp.StatusCode)
}
lastModified := resp.Header.Get("Last-Modified")
if lastModified == "" {
cfg.logger.Warn().Msg("MaxMind responded no last modified time, update skipped")
return nil, nil
}
lastModifiedTime, err := time.Parse(http.TimeFormat, lastModified)
if err != nil {
cfg.logger.Warn().Err(err).Msg("MaxMind responded invalid last modified time, update skipped")
return nil, err
}
return &lastModifiedTime, nil
}
func (cfg *MaxMindConfig) download() (*maxminddb.Reader, error) {
resp, err := newReq(cfg, http.MethodGet)
if err != nil {
return nil, err
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
return nil, fmt.Errorf("%w: %d", ErrResponseNotOK, resp.StatusCode)
}
path := dbPath(cfg.Database)
tmpPath := path + "-tmp.tar.gz"
file, err := os.OpenFile(tmpPath, os.O_CREATE|os.O_WRONLY, 0o644)
if err != nil {
return nil, err
}
cfg.logger.Info().Msg("MaxMind DB downloading...")
_, err = io.Copy(file, resp.Body)
if err != nil {
file.Close()
return nil, err
}
file.Close()
// extract .tar.gz and move only the dbFilename to path
err = extractFileFromTarGz(tmpPath, dbFilename(cfg.Database), path)
if err != nil {
return nil, gperr.New("failed to extract database from archive").With(err)
}
// cleanup the tar.gz file
_ = os.Remove(tmpPath)
db, err := maxmindDBOpen(path)
if err != nil {
return nil, err
}
return db, nil
}
func extractFileFromTarGz(tarGzPath, targetFilename, destPath string) error {
f, err := os.Open(tarGzPath)
if err != nil {
return err
}
defer f.Close()
gzr, err := gzip.NewReader(f)
if err != nil {
return err
}
defer gzr.Close()
tr := tar.NewReader(gzr)
for {
hdr, err := tr.Next()
if err == io.EOF {
break // End of archive
}
if err != nil {
return err
}
// Only extract the file that matches targetFilename (basename match)
if filepath.Base(hdr.Name) == targetFilename {
outFile, err := os.OpenFile(destPath, os.O_CREATE|os.O_WRONLY|os.O_TRUNC, hdr.FileInfo().Mode())
if err != nil {
return err
}
defer outFile.Close()
_, err = io.Copy(outFile, tr)
if err != nil {
return err
}
return nil // Done
}
}
return fmt.Errorf("file %s not found in archive", targetFilename)
}
var (
dataDir = common.DataDir
dbURL = dbURLimpl
dbPath = dbPathImpl
maxmindDBOpen = maxminddb.Open
newReq = (*MaxMindConfig).newReq
)

View File

@@ -1,213 +0,0 @@
package acl
import (
"io"
"net/http"
"net/http/httptest"
"path/filepath"
"strings"
"testing"
"time"
"github.com/oschwald/maxminddb-golang"
"github.com/rs/zerolog"
"github.com/yusing/go-proxy/internal/task"
)
func Test_dbPath(t *testing.T) {
tmpDataDir := "/tmp/testdata"
oldDataDir := dataDir
dataDir = tmpDataDir
defer func() { dataDir = oldDataDir }()
tests := []struct {
name string
dbType MaxMindDatabaseType
want string
}{
{"GeoLite", MaxMindGeoLite, filepath.Join(tmpDataDir, "GeoLite2-City.mmdb")},
{"GeoIP2", MaxMindGeoIP2, filepath.Join(tmpDataDir, "GeoIP2-City.mmdb")},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if got := dbPath(tt.dbType); got != tt.want {
t.Errorf("dbPath() = %v, want %v", got, tt.want)
}
})
}
}
func Test_dbURL(t *testing.T) {
tests := []struct {
name string
dbType MaxMindDatabaseType
want string
}{
{"GeoLite", MaxMindGeoLite, "https://download.maxmind.com/geoip/databases/GeoLite2-City/download?suffix=tar.gz"},
{"GeoIP2", MaxMindGeoIP2, "https://download.maxmind.com/geoip/databases/GeoIP2-City/download?suffix=tar.gz"},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if got := dbURL(tt.dbType); got != tt.want {
t.Errorf("dbURL() = %v, want %v", got, tt.want)
}
})
}
}
// --- Helper for MaxMindConfig ---
type testLogger struct{ zerolog.Logger }
func (testLogger) Info() *zerolog.Event { return &zerolog.Event{} }
func (testLogger) Warn() *zerolog.Event { return &zerolog.Event{} }
func (testLogger) Err(_ error) *zerolog.Event { return &zerolog.Event{} }
func Test_MaxMindConfig_newReq(t *testing.T) {
cfg := &MaxMindConfig{
AccountID: "testid",
LicenseKey: "testkey",
Database: MaxMindGeoLite,
logger: zerolog.Nop(),
}
// Patch httpClient to use httptest
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if u, p, ok := r.BasicAuth(); !ok || u != "testid" || p != "testkey" {
t.Errorf("basic auth not set correctly")
}
w.WriteHeader(http.StatusOK)
}))
defer server.Close()
oldURL := dbURL
dbURL = func(MaxMindDatabaseType) string { return server.URL }
defer func() { dbURL = oldURL }()
resp, err := cfg.newReq(http.MethodGet)
if err != nil {
t.Fatalf("newReq() error = %v", err)
}
if resp.StatusCode != http.StatusOK {
t.Errorf("unexpected status: %v", resp.StatusCode)
}
}
func Test_MaxMindConfig_checkUpdate(t *testing.T) {
cfg := &MaxMindConfig{
AccountID: "id",
LicenseKey: "key",
Database: MaxMindGeoLite,
logger: zerolog.Nop(),
}
lastMod := time.Now().UTC().Format(http.TimeFormat)
buildTime := time.Now().Add(-time.Hour)
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Last-Modified", lastMod)
w.WriteHeader(http.StatusOK)
}))
defer server.Close()
oldURL := dbURL
dbURL = func(MaxMindDatabaseType) string { return server.URL }
defer func() { dbURL = oldURL }()
latest, err := cfg.checkLastest()
if err != nil {
t.Fatalf("checkUpdate() error = %v", err)
}
if latest.Equal(buildTime) {
t.Errorf("expected update needed")
}
}
type fakeReadCloser struct {
firstRead bool
closed bool
}
func (c *fakeReadCloser) Read(p []byte) (int, error) {
if !c.firstRead {
c.firstRead = true
return strings.NewReader("FAKEMMDB").Read(p)
}
return 0, io.EOF
}
func (c *fakeReadCloser) Close() error {
c.closed = true
return nil
}
func Test_MaxMindConfig_download(t *testing.T) {
cfg := &MaxMindConfig{
AccountID: "id",
LicenseKey: "key",
Database: MaxMindGeoLite,
logger: zerolog.Nop(),
}
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
io.Copy(w, strings.NewReader("FAKEMMDB"))
}))
defer server.Close()
oldURL := dbURL
dbURL = func(MaxMindDatabaseType) string { return server.URL }
defer func() { dbURL = oldURL }()
tmpDir := t.TempDir()
oldDataDir := dataDir
dataDir = tmpDir
defer func() { dataDir = oldDataDir }()
// Patch maxminddb.Open to always succeed
origOpen := maxmindDBOpen
maxmindDBOpen = func(path string) (*maxminddb.Reader, error) {
return &maxminddb.Reader{}, nil
}
defer func() { maxmindDBOpen = origOpen }()
rw := &fakeReadCloser{}
oldNewReq := newReq
newReq = func(cfg *MaxMindConfig, method string) (*http.Response, error) {
return &http.Response{
StatusCode: http.StatusOK,
Body: rw,
}, nil
}
defer func() { newReq = oldNewReq }()
db, err := cfg.download()
if err != nil {
t.Fatalf("download() error = %v", err)
}
if db == nil {
t.Error("expected db instance")
}
if !rw.closed {
t.Error("expected rw to be closed")
}
}
func Test_MaxMindConfig_loadMaxMindDB(t *testing.T) {
// This test should cover both the path where DB exists and where it does not
// For brevity, only the non-existing path is tested here
cfg := &MaxMindConfig{
AccountID: "id",
LicenseKey: "key",
Database: MaxMindGeoLite,
logger: zerolog.Nop(),
}
oldOpen := maxmindDBOpen
maxmindDBOpen = func(path string) (*maxminddb.Reader, error) {
return &maxminddb.Reader{}, nil
}
defer func() { maxmindDBOpen = oldOpen }()
oldDBPath := dbPath
dbPath = func(MaxMindDatabaseType) string { return filepath.Join(t.TempDir(), "maxmind.mmdb") }
defer func() { dbPath = oldDBPath }()
task := task.RootTask("test")
defer task.Finish(nil)
err := cfg.LoadMaxMindDB(task)
if err != nil {
t.Errorf("loadMaxMindDB() error = %v", err)
}
}

View File

@@ -1,7 +1,9 @@
package acl
import (
"io"
"net"
"time"
)
type TCPListener struct {
@@ -9,12 +11,23 @@ type TCPListener struct {
lis net.Listener
}
func (cfg *Config) WrapTCP(lis net.Listener) net.Listener {
if cfg == nil {
type noConn struct{}
func (noConn) Read(b []byte) (int, error) { return 0, io.EOF }
func (noConn) Write(b []byte) (int, error) { return 0, io.EOF }
func (noConn) Close() error { return nil }
func (noConn) LocalAddr() net.Addr { return nil }
func (noConn) RemoteAddr() net.Addr { return nil }
func (noConn) SetDeadline(t time.Time) error { return nil }
func (noConn) SetReadDeadline(t time.Time) error { return nil }
func (noConn) SetWriteDeadline(t time.Time) error { return nil }
func (c *Config) WrapTCP(lis net.Listener) net.Listener {
if c == nil {
return lis
}
return &TCPListener{
acl: cfg,
acl: c,
lis: lis,
}
}
@@ -32,11 +45,11 @@ func (s *TCPListener) Accept() (net.Conn, error) {
if !ok {
// Not a TCPAddr, drop
c.Close()
return nil, nil
return noConn{}, nil
}
if !s.acl.IPAllowed(addr.IP) {
c.Close()
return nil, nil
return noConn{}, nil
}
return c, nil
}

View File

@@ -10,12 +10,12 @@ type UDPListener struct {
lis net.PacketConn
}
func (cfg *Config) WrapUDP(lis net.PacketConn) net.PacketConn {
if cfg == nil {
func (c *Config) WrapUDP(lis net.PacketConn) net.PacketConn {
if c == nil {
return lis
}
return &UDPListener{
acl: cfg,
acl: c,
lis: lis,
}
}

View File

@@ -1,103 +1,219 @@
package api
import (
"fmt"
"net/http"
"strconv"
"time"
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/gin-gonic/gin"
"github.com/gorilla/websocket"
"github.com/rs/zerolog/log"
apitypes "github.com/yusing/go-proxy/internal/api/types"
apiV1 "github.com/yusing/go-proxy/internal/api/v1"
agentApi "github.com/yusing/go-proxy/internal/api/v1/agent"
authApi "github.com/yusing/go-proxy/internal/api/v1/auth"
certApi "github.com/yusing/go-proxy/internal/api/v1/cert"
dockerApi "github.com/yusing/go-proxy/internal/api/v1/docker"
fileApi "github.com/yusing/go-proxy/internal/api/v1/file"
homepageApi "github.com/yusing/go-proxy/internal/api/v1/homepage"
metricsApi "github.com/yusing/go-proxy/internal/api/v1/metrics"
routeApi "github.com/yusing/go-proxy/internal/api/v1/route"
"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/httpheaders"
"github.com/yusing/go-proxy/internal/utils/strutils"
"github.com/yusing/go-proxy/internal/common"
"github.com/yusing/go-proxy/internal/gperr"
)
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())
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)
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) {
httpheaders.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).Use(Cache(time.Hour * 24))
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)
}
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)
}
}
// disable cache by default
r.Use(NoCache())
return r
}
func NoCache() gin.HandlerFunc {
return func(c *gin.Context) {
// skip cache if Cache-Control header is set or if caching is explicitly enabled
if !c.GetBool("cache_enabled") && 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 Cache(duration time.Duration) gin.HandlerFunc {
return func(c *gin.Context) {
// Signal to NoCache middleware that caching is intended
c.Set("cache_enabled", true)
// skip cache if Cache-Control header is set
if c.Writer.Header().Get("Cache-Control") == "" {
c.Header("Cache-Control", "public, max-age="+strconv.FormatFloat(duration.Seconds(), 'f', 0, 64)+", immutable")
c.Header("Pragma", "public")
c.Header("Expires", time.Now().Add(duration).Format(time.RFC1123))
}
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", v1.GetVersion)
mux.HandleFunc("GET", "/v1/stats", v1.Stats, true)
mux.HandleFunc("POST", "/v1/reload", v1.Reload, true)
mux.HandleFunc("GET", "/v1/list", v1.List, true)
mux.HandleFunc("GET", "/v1/list/{what}", v1.List, true)
mux.HandleFunc("GET", "/v1/list/{what}/{which}", v1.List, 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,55 @@
package apitypes
import (
"errors"
"github.com/yusing/go-proxy/internal/gperr"
)
type ErrorResponse struct {
Message string `json:"message"`
Error string `json:"error,omitempty" extensions:"x-nullable"`
} // @name ErrorResponse
type serverError struct {
Message string
Err error
}
// Error returns a generic error response
func Error(message string, err ...error) ErrorResponse {
if len(err) > 0 {
var gpErr gperr.Error
if errors.As(err[0], &gpErr) {
return ErrorResponse{
Message: message,
Error: string(gpErr.Plain()),
}
}
return ErrorResponse{
Message: message,
Error: err[0].Error(),
}
}
return ErrorResponse{
Message: message,
}
}
func InternalServerError(err error, message string) error {
return serverError{
Message: message,
Err: err,
}
}
func (e serverError) Error() string {
if e.Err != nil {
return e.Message + ": " + e.Err.Error()
}
return e.Message
}
func (e serverError) Unwrap() error {
return e.Err
}

View File

@@ -0,0 +1,29 @@
package apitypes
type QueryOptions struct {
Limit int `binding:"required,min=1,max=20" form:"limit"`
Offset int `binding:"omitempty,min=0" form:"offset"`
OrderBy QueryOrder `binding:"omitempty,oneof=created_at updated_at" form:"order_by"`
Order QueryOrderDirection `binding:"omitempty,oneof=asc desc" form:"order"`
}
type QueryOrder string
const (
QueryOrderCreatedAt QueryOrder = "created_at"
QueryOrderUpdatedAt QueryOrder = "updated_at"
)
type QueryOrderDirection string
const (
QueryOrderDirectionAsc QueryOrderDirection = "asc"
QueryOrderDirectionDesc QueryOrderDirection = "desc"
)
type QueryResponse struct {
Total int64 `json:"total"`
Limit int `json:"limit"`
Offset int `json:"offset"`
HasMore bool `json:"has_more"`
}

View File

@@ -0,0 +1,18 @@
package apitypes
type SuccessResponse struct {
Message string `json:"message"`
Details map[string]any `json:"details,omitempty" extensions:"x-nullable"`
} // @name SuccessResponse
func Success(message string, extra ...map[string]any) SuccessResponse {
if len(extra) > 0 {
return SuccessResponse{
Message: message,
Details: extra[0],
}
}
return SuccessResponse{
Message: message,
}
}

View File

@@ -0,0 +1,67 @@
package agentapi
import (
"crypto/rand"
"encoding/base64"
"sync/atomic"
"time"
"github.com/rs/zerolog/log"
"github.com/yusing/go-proxy/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/go-proxy/agent/pkg/agent"
apitypes "github.com/yusing/go-proxy/internal/api/types"
)
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,32 @@
package agentapi
import (
"net/http"
"time"
"github.com/gin-gonic/gin"
"github.com/yusing/go-proxy/agent/pkg/agent"
"github.com/yusing/go-proxy/internal/net/gphttp/httpheaders"
"github.com/yusing/go-proxy/internal/net/gphttp/websocket"
)
// @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
// @Failure 500 {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,78 @@
package agentapi
import (
"fmt"
"net/http"
"os"
"github.com/gin-gonic/gin"
"github.com/yusing/go-proxy/agent/pkg/agent"
"github.com/yusing/go-proxy/agent/pkg/certs"
. "github.com/yusing/go-proxy/internal/api/types"
config "github.com/yusing/go-proxy/internal/config/types"
)
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, Error("invalid request", err))
return
}
filename, ok := certs.AgentCertsFilepath(request.Host)
if !ok {
c.JSON(http.StatusBadRequest, Error("invalid host", nil))
return
}
ca, err := fromEncryptedPEMPairResponse(request.CA)
if err != nil {
c.JSON(http.StatusBadRequest, Error("invalid CA", err))
return
}
client, err := fromEncryptedPEMPairResponse(request.Client)
if err != nil {
c.JSON(http.StatusBadRequest, Error("invalid client", err))
return
}
nRoutesAdded, err := config.GetInstance().VerifyNewAgent(request.Host, ca, client, request.ContainerRuntime)
if err != nil {
c.JSON(http.StatusBadRequest, Error("invalid request", err))
return
}
zip, err := certs.ZipCert(ca.Cert, client.Cert, client.Key)
if err != nil {
c.Error(InternalServerError(err, "failed to zip certs"))
return
}
if err := os.WriteFile(filename, zip, 0o600); err != nil {
c.Error(InternalServerError(err, "failed to write certs"))
return
}
c.JSON(http.StatusOK, Success(fmt.Sprintf("Added %d routes", nRoutesAdded)))
}

View File

@@ -1,24 +0,0 @@
package v1
import (
"net/http"
"time"
"github.com/coder/websocket"
"github.com/coder/websocket/wsjson"
config "github.com/yusing/go-proxy/internal/config/types"
"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(cfg config.ConfigInstance, w http.ResponseWriter, r *http.Request) {
if httpheaders.IsWebsocket(r.Header) {
gpwebsocket.Periodic(w, r, 10*time.Second, func(conn *websocket.Conn) error {
wsjson.Write(r.Context(), conn, cfg.ListAgents())
return nil
})
} else {
gphttp.RespondJSON(w, r, cfg.ListAgents())
}
}

View File

@@ -0,0 +1,24 @@
//nolint:dupword
package auth
import (
"github.com/gin-gonic/gin"
"github.com/yusing/go-proxy/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/go-proxy/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/go-proxy/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/go-proxy/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

@@ -1,9 +1,10 @@
package certapi
import (
"encoding/json"
"net/http"
"github.com/gin-gonic/gin"
apitypes "github.com/yusing/go-proxy/internal/api/types"
config "github.com/yusing/go-proxy/internal/config/types"
)
@@ -14,18 +15,29 @@ type CertInfo struct {
NotAfter int64 `json:"not_after"`
DNSNames []string `json:"dns_names"`
EmailAddresses []string `json:"email_addresses"`
}
} // @name CertInfo
func GetCertInfo(w http.ResponseWriter, r *http.Request) {
// @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 := config.GetInstance().AutoCertProvider()
if autocert == nil {
http.Error(w, "autocert is not enabled", http.StatusNotFound)
c.JSON(http.StatusNotFound, apitypes.Error("autocert is not enabled"))
return
}
cert, err := autocert.GetCert(nil)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
c.Error(apitypes.InternalServerError(err, "failed to get cert info"))
return
}
@@ -37,5 +49,5 @@ func GetCertInfo(w http.ResponseWriter, r *http.Request) {
DNSNames: cert.Leaf.DNSNames,
EmailAddresses: cert.Leaf.EmailAddresses,
}
json.NewEncoder(w).Encode(&certInfo)
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"
apitypes "github.com/yusing/go-proxy/internal/api/types"
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/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 := config.GetInstance().AutoCertProvider()
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,56 +0,0 @@
package certapi
import (
"net/http"
config "github.com/yusing/go-proxy/internal/config/types"
"github.com/yusing/go-proxy/internal/gperr"
"github.com/yusing/go-proxy/internal/logging"
"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 {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
//nolint:errcheck
defer conn.CloseNow()
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(r, conn, err.Error())
} else {
logging.Info().Msg("cert obtained successfully")
}
}()
for {
select {
case l := <-logs:
if err != nil {
return
}
if !gpwebsocket.WriteText(r, conn, string(l)) {
return
}
case <-done:
return
}
}
}

View File

@@ -1,132 +0,0 @@
package v1
import (
"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 = gphttp.ErrInvalidKey("type")
return
}
filename = r.PathValue("filename")
if filename == "" {
err = gphttp.ErrMissingKey("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,63 @@
package dockerapi
import (
"net/http"
"github.com/gin-gonic/gin"
apitypes "github.com/yusing/go-proxy/internal/api/types"
"github.com/yusing/go-proxy/internal/docker"
)
// @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
}
client, err := docker.NewClient(dockerHost)
if err != nil {
c.Error(apitypes.InternalServerError(err, "failed to create docker client"))
return
}
defer client.Close()
cont, err := client.ContainerInspect(c.Request.Context(), id)
if err != nil {
c.Error(apitypes.InternalServerError(err, "failed to inspect container"))
return
}
var state ContainerState
if cont.State != nil {
state = cont.State.Status
}
c.JSON(http.StatusOK, &Container{
Server: dockerHost,
Name: cont.Name,
ID: cont.ID,
Image: cont.Image,
State: state,
})
}

View File

@@ -2,23 +2,35 @@ package dockerapi
import (
"context"
"net/http"
"sort"
"github.com/docker/docker/api/types/container"
"github.com/gin-gonic/gin"
"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"`
}
type ContainerState = container.ContainerState // @name ContainerState
func Containers(w http.ResponseWriter, r *http.Request) {
serveHTTP[Container, []Container](w, r, GetContainers)
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) {

View File

@@ -0,0 +1,79 @@
package dockerapi
import (
"context"
"sort"
dockerSystem "github.com/docker/docker/api/types/system"
"github.com/gin-gonic/gin"
"github.com/yusing/go-proxy/internal/gperr"
"github.com/yusing/go-proxy/internal/utils/strutils"
)
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)
if err != nil {
errs.Add(err)
continue
}
info.Name = name
dockerInfos[i] = toDockerInfo(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/docker/docker/api/types/container"
"github.com/docker/docker/pkg/stdcopy"
"github.com/gin-gonic/gin"
"github.com/rs/zerolog/log"
apitypes "github.com/yusing/go-proxy/internal/api/types"
"github.com/yusing/go-proxy/internal/docker"
"github.com/yusing/go-proxy/internal/net/gphttp/websocket"
"github.com/yusing/go-proxy/internal/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 := container.LogsOptions{
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,52 @@
package dockerapi
import (
"net/http"
"github.com/gin-gonic/gin"
apitypes "github.com/yusing/go-proxy/internal/api/types"
"github.com/yusing/go-proxy/internal/docker"
)
// @x-id "restart"
// @BasePath /api/v1
// @Summary Restart container
// @Description Restart 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/restart [post]
func Restart(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.ContainerRestart(c.Request.Context(), req.ID, req.StopOptions)
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/docker/docker/api/types/container"
"github.com/gin-gonic/gin"
apitypes "github.com/yusing/go-proxy/internal/api/types"
"github.com/yusing/go-proxy/internal/docker"
)
type StartRequest struct {
ID string `json:"id" binding:"required"`
container.StartOptions
}
// @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.StartOptions)
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/docker/docker/api/types/container"
"github.com/gin-gonic/gin"
apitypes "github.com/yusing/go-proxy/internal/api/types"
"github.com/yusing/go-proxy/internal/docker"
)
type StopRequest struct {
ID string `json:"id" binding:"required"`
container.StopOptions
}
// @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.StopOptions)
if err != nil {
c.Error(apitypes.InternalServerError(err, "failed to stop container"))
return
}
c.JSON(http.StatusOK, apitypes.Success("container stopped"))
}

View File

@@ -2,17 +2,17 @@ package dockerapi
import (
"context"
"encoding/json"
"net/http"
"time"
"github.com/coder/websocket"
"github.com/coder/websocket/wsjson"
"github.com/gin-gonic/gin"
"github.com/yusing/go-proxy/agent/pkg/agent"
apitypes "github.com/yusing/go-proxy/internal/api/types"
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"
"github.com/yusing/go-proxy/internal/net/gphttp/websocket"
)
type (
@@ -44,19 +44,19 @@ func getDockerClients() (DockerClients, gperr.Error) {
dockerClients[name] = dockerClient
}
for _, agent := range cfg.ListAgents() {
for _, agent := range agent.ListAgents() {
dockerClient, err := docker.NewClient(agent.FakeDockerHost())
if err != nil {
connErrs.Add(err)
continue
}
dockerClients[agent.Name()] = dockerClient
dockerClients[agent.Name] = dockerClient
}
return dockerClients, connErrs.Error()
}
func getDockerClient(w http.ResponseWriter, server string) (*docker.SharedClient, bool, error) {
func getDockerClient(server string) (*docker.SharedClient, bool, error) {
cfg := config.GetInstance()
var host string
for name, h := range cfg.Value().Providers.Docker {
@@ -65,10 +65,12 @@ func getDockerClient(w http.ResponseWriter, server string) (*docker.SharedClient
break
}
}
for _, agent := range cfg.ListAgents() {
if agent.Name() == server {
host = agent.FakeDockerHost()
break
if host == "" {
for _, agent := range agent.ListAgents() {
if agent.Name == server {
host = agent.FakeDockerHost()
break
}
}
}
if host == "" {
@@ -90,35 +92,30 @@ func closeAllClients(dockerClients DockerClients) {
}
}
func handleResult[V any, T ResultType[V]](w http.ResponseWriter, errs error, result T) {
func handleResult[V any, T ResultType[V]](c *gin.Context, errs error, result T) {
if errs != nil {
gperr.LogError("docker errors", errs)
if len(result) == 0 {
http.Error(w, "docker errors", http.StatusInternalServerError)
c.Error(apitypes.InternalServerError(errs, "docker errors"))
return
}
}
json.NewEncoder(w).Encode(result)
c.JSON(http.StatusOK, result)
}
func serveHTTP[V any, T ResultType[V]](w http.ResponseWriter, r *http.Request, getResult func(ctx context.Context, dockerClients DockerClients) (T, gperr.Error)) {
func serveHTTP[V any, T ResultType[V]](c *gin.Context, getResult func(ctx context.Context, dockerClients DockerClients) (T, gperr.Error)) {
dockerClients, err := getDockerClients()
if err != nil {
handleResult[V, T](w, err, nil)
handleResult[V, T](c, 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 wsjson.Write(r.Context(), conn, result)
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(r.Context(), dockerClients)
handleResult[V, T](w, err, result)
result, err := getResult(c.Request.Context(), dockerClients)
handleResult[V](c, err, result)
}
}

View File

@@ -1,5 +0,0 @@
package dockerapi
import "time"
const reqTimeout = 10 * time.Second

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,69 +0,0 @@
package dockerapi
import (
"net/http"
"strconv"
"github.com/coder/websocket"
"github.com/docker/docker/api/types/container"
"github.com/docker/docker/pkg/stdcopy"
"github.com/yusing/go-proxy/internal/logging"
"github.com/yusing/go-proxy/internal/net/gphttp"
"github.com/yusing/go-proxy/internal/net/gphttp/gpwebsocket"
)
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(w, server)
if err != nil {
gphttp.BadRequest(w, err.Error())
return
}
if !found {
gphttp.NotFound(w, "server not found")
return
}
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.CloseNow()
writer := gpwebsocket.NewWriter(r.Context(), conn, websocket.MessageText)
_, err = stdcopy.StdCopy(writer, writer, logs) // de-multiplex logs
if err != nil {
logging.Err(err).
Str("server", server).
Str("container", containerID).
Msg("failed to de-multiplex logs")
}
}

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,91 @@
package v1
import (
"context"
"net/http"
"github.com/gin-gonic/gin"
apitypes "github.com/yusing/go-proxy/internal/api/types"
"github.com/yusing/go-proxy/internal/homepage"
"github.com/yusing/go-proxy/internal/route/routes"
)
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 := homepage.FetchFavIconFromURL(c.Request.Context(), &iconURL)
if !fetchResult.OK() {
c.JSON(fetchResult.StatusCode, apitypes.Error(fetchResult.ErrMsg))
return
}
c.Data(fetchResult.StatusCode, fetchResult.ContentType(), fetchResult.Icon)
return
}
// try with alias
result := GetFavIconFromAlias(c.Request.Context(), request.Alias)
if !result.OK() {
c.JSON(result.StatusCode, apitypes.Error(result.ErrMsg))
return
}
c.Data(result.StatusCode, result.ContentType(), result.Icon)
}
func GetFavIconFromAlias(ctx context.Context, alias string) *homepage.FetchResult {
// try with route.Icon
r, ok := routes.HTTP.Get(alias)
if !ok {
return &homepage.FetchResult{
StatusCode: http.StatusNotFound,
ErrMsg: "route not found",
}
}
var result *homepage.FetchResult
hp := r.HomepageItem()
if hp.Icon != nil {
if hp.Icon.IconSource == homepage.IconSourceRelative {
result = homepage.FindIcon(ctx, r, *hp.Icon.FullURL)
} else {
result = homepage.FetchFavIconFromURL(ctx, hp.Icon)
}
} else {
// try extract from "link[rel=icon]"
result = homepage.FindIcon(ctx, r, "/")
}
if result.StatusCode == 0 {
result.StatusCode = http.StatusOK
}
return result
}

View File

@@ -1,77 +0,0 @@
package favicon
import (
"errors"
"net/http"
"github.com/yusing/go-proxy/internal/gperr"
"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.ClientError(w, gphttp.ErrMissingKey("url or alias"), http.StatusBadRequest)
return
}
if url != "" && alias != "" {
gphttp.ClientError(w, gperr.New("url and alias are mutually exclusive"), http.StatusBadRequest)
return
}
// try with url
if url != "" {
var iconURL homepage.IconURL
if err := iconURL.Parse(url); err != nil {
gphttp.ClientError(w, 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 route.Icon
r, ok := routes.HTTP.Get(alias)
if !ok {
gphttp.ClientError(w, errors.New("no such route"), http.StatusNotFound)
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.Value)
} 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"
apitypes "github.com/yusing/go-proxy/internal/api/types"
"github.com/yusing/go-proxy/internal/common"
)
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"
apitypes "github.com/yusing/go-proxy/internal/api/types"
"github.com/yusing/go-proxy/internal/common"
"github.com/yusing/go-proxy/internal/utils"
)
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 := utils.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 := utils.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/go-proxy/internal/api/types"
)
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"
apitypes "github.com/yusing/go-proxy/internal/api/types"
config "github.com/yusing/go-proxy/internal/config/types"
"github.com/yusing/go-proxy/internal/gperr"
"github.com/yusing/go-proxy/internal/net/gphttp/middleware"
"github.com/yusing/go-proxy/internal/route/provider"
)
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,20 +4,31 @@ import (
"net/http"
"time"
"github.com/coder/websocket"
"github.com/coder/websocket/wsjson"
"github.com/yusing/go-proxy/internal/net/gphttp"
"github.com/yusing/go-proxy/internal/net/gphttp/gpwebsocket"
"github.com/gin-gonic/gin"
"github.com/yusing/go-proxy/internal/net/gphttp/httpheaders"
"github.com/yusing/go-proxy/internal/net/gphttp/websocket"
"github.com/yusing/go-proxy/internal/route/routes"
)
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 wsjson.Write(r.Context(), conn, 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,42 @@
package homepageapi
import (
"net/http"
"github.com/gin-gonic/gin"
"github.com/yusing/go-proxy/internal/homepage"
"github.com/yusing/go-proxy/internal/route/routes"
)
// @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"
apitypes "github.com/yusing/go-proxy/internal/api/types"
"github.com/yusing/go-proxy/internal/homepage"
)
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"
apitypes "github.com/yusing/go-proxy/internal/api/types"
"github.com/yusing/go-proxy/internal/homepage"
"github.com/yusing/go-proxy/internal/net/gphttp/httpheaders"
"github.com/yusing/go-proxy/internal/net/gphttp/websocket"
"github.com/yusing/go-proxy/internal/route/routes"
)
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"
apitypes "github.com/yusing/go-proxy/internal/api/types"
"github.com/yusing/go-proxy/internal/homepage"
)
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, 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, 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, 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, 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, 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"
apitypes "github.com/yusing/go-proxy/internal/api/types"
"github.com/yusing/go-proxy/internal/homepage"
)
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,128 +0,0 @@
package v1
import (
"fmt"
"net/http"
"strconv"
"strings"
"github.com/yusing/go-proxy/internal"
"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/net/gphttp/middleware"
"github.com/yusing/go-proxy/internal/route/routes"
route "github.com/yusing/go-proxy/internal/route/types"
"github.com/yusing/go-proxy/internal/task"
"github.com/yusing/go-proxy/internal/utils"
)
const (
ListRoute = "route"
ListRoutes = "routes"
ListFiles = "files"
ListMiddlewares = "middlewares"
ListMiddlewareTraces = "middleware_trace"
ListMatchDomains = "match_domains"
ListHomepageConfig = "homepage_config"
ListRouteProviders = "route_providers"
ListHomepageCategories = "homepage_categories"
ListIcons = "icons"
ListTasks = "tasks"
)
func List(cfg config.ConfigInstance, w http.ResponseWriter, r *http.Request) {
what := r.PathValue("what")
if what == "" {
what = ListRoutes
}
which := r.PathValue("which")
switch what {
case ListRoute:
route := listRoute(which)
if route == nil {
http.NotFound(w, r)
} else {
gphttp.RespondJSON(w, r, route)
}
case ListRoutes:
gphttp.RespondJSON(w, r, routes.ByAlias(route.RouteType(r.FormValue("type"))))
case ListFiles:
listFiles(w, r)
case ListMiddlewares:
gphttp.RespondJSON(w, r, middleware.All())
case ListMiddlewareTraces:
gphttp.RespondJSON(w, r, middleware.GetAllTrace())
case ListMatchDomains:
gphttp.RespondJSON(w, r, cfg.Value().MatchDomains)
case ListHomepageConfig:
gphttp.RespondJSON(w, r, routes.HomepageConfig(r.FormValue("category"), r.FormValue("provider")))
case ListRouteProviders:
gphttp.RespondJSON(w, r, cfg.RouteProviderList())
case ListHomepageCategories:
gphttp.RespondJSON(w, r, routes.HomepageCategories())
case ListIcons:
limit, err := strconv.Atoi(r.FormValue("limit"))
if err != nil {
limit = 0
}
icons, err := internal.SearchIcons(r.FormValue("keyword"), limit)
if err != nil {
gphttp.ClientError(w, err)
return
}
if icons == nil {
icons = []string{}
}
gphttp.RespondJSON(w, r, icons)
case ListTasks:
gphttp.RespondJSON(w, r, task.DebugTaskList())
default:
gphttp.BadRequest(w, fmt.Sprintf("invalid what: %s", what))
}
}
// if which is "all" or empty, return map[string]Route of all routes
// otherwise, return a single Route with alias which or nil if not found.
func listRoute(which string) any {
if which == "" || which == "all" {
return routes.ByAlias()
}
routes := routes.ByAlias()
route, ok := routes[which]
if !ok {
return nil
}
return route
}
func listFiles(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

@@ -0,0 +1,267 @@
package metrics
import (
"bytes"
"context"
"encoding/json"
"io"
"net/http"
"sync"
"sync/atomic"
"time"
"github.com/gin-gonic/gin"
"github.com/rs/zerolog/log"
"github.com/yusing/go-proxy/agent/pkg/agent"
apitypes "github.com/yusing/go-proxy/internal/api/types"
"github.com/yusing/go-proxy/internal/gperr"
"github.com/yusing/go-proxy/internal/metrics/period"
"github.com/yusing/go-proxy/internal/metrics/systeminfo"
"github.com/yusing/go-proxy/internal/net/gphttp/httpheaders"
"github.com/yusing/go-proxy/internal/net/gphttp/websocket"
"github.com/yusing/go-proxy/internal/utils/synk"
)
var (
// for json marshaling (unknown size)
allSystemInfoBytesPool = synk.GetBytesPoolWithUniqueMemory()
// for storing http response body (known size)
allSystemInfoFixedSizePool = synk.GetBytesPool()
)
type AllSystemInfoRequest struct {
Period period.Filter `query:"period"`
Aggregate systeminfo.SystemInfoAggregateMode `query:"aggregate"`
Interval time.Duration `query:"interval" swaggertype:"string" format:"duration"`
} // @name AllSystemInfoRequest
type bytesFromPool struct {
json.RawMessage
}
// @x-id "all_system_info"
// @BasePath /api/v1
// @Summary Get system info
// @Description Get system info
// @Tags metrics,websocket
// @Produce json
// @Param request query AllSystemInfoRequest false "Request"
// @Success 200 {object} map[string]systeminfo.SystemInfo "no period specified, system info by agent name"
// @Success 200 {object} map[string]SystemInfoAggregate "period specified, aggregated system info by agent name"
// @Failure 400 {object} apitypes.ErrorResponse
// @Failure 403 {object} apitypes.ErrorResponse
// @Failure 500 {object} apitypes.ErrorResponse
// @Router /metrics/all_system_info [get]
func AllSystemInfo(c *gin.Context) {
var req AllSystemInfoRequest
if err := c.ShouldBindQuery(&req); err != nil {
c.JSON(http.StatusBadRequest, apitypes.Error("invalid query", err))
return
}
if req.Interval < period.PollInterval {
req.Interval = period.PollInterval
}
if !httpheaders.IsWebsocket(c.Request.Header) {
c.JSON(http.StatusBadRequest, apitypes.Error("bad request, websocket is required"))
return
}
manager, err := websocket.NewManagerWithUpgrade(c)
if err != nil {
c.Error(apitypes.InternalServerError(err, "failed to upgrade to websocket"))
return
}
defer manager.Close()
query := c.Request.URL.Query()
queryEncoded := c.Request.URL.Query().Encode()
type SystemInfoData struct {
AgentName string
SystemInfo any
}
// leave 5 extra slots for buffering in case new agents are added.
dataCh := make(chan SystemInfoData, 1+agent.NumAgents()+5)
defer close(dataCh)
ticker := time.NewTicker(req.Interval)
defer ticker.Stop()
go func() {
for {
select {
case <-manager.Done():
return
case data := <-dataCh:
err := marshalSystemInfo(manager, data.AgentName, data.SystemInfo)
if err != nil {
manager.Close()
return
}
}
}
}()
// processing function for one round.
doRound := func() (bool, error) {
var roundWg sync.WaitGroup
var numErrs atomic.Int32
totalAgents := int32(1) // myself
errs := gperr.NewBuilderWithConcurrency()
// get system info for me and all agents in parallel.
roundWg.Go(func() {
data, err := systeminfo.Poller.GetRespData(req.Period, query)
if err != nil {
errs.Add(gperr.Wrap(err, "Main server"))
numErrs.Add(1)
return
}
select {
case <-manager.Done():
return
case dataCh <- SystemInfoData{
AgentName: "GoDoxy",
SystemInfo: data,
}:
}
})
for _, a := range agent.IterAgents() {
totalAgents++
agentShallowCopy := *a
roundWg.Go(func() {
data, err := getAgentSystemInfoWithRetry(manager.Context(), &agentShallowCopy, queryEncoded)
if err != nil {
errs.Add(gperr.Wrap(err, "Agent "+agentShallowCopy.Name))
numErrs.Add(1)
return
}
select {
case <-manager.Done():
return
case dataCh <- SystemInfoData{
AgentName: agentShallowCopy.Name,
SystemInfo: data,
}:
}
})
}
roundWg.Wait()
return numErrs.Load() == totalAgents, errs.Error()
}
// write system info immediately once.
if shouldContinue, err := doRound(); err != nil {
if !shouldContinue {
c.Error(apitypes.InternalServerError(err, "failed to get all system info"))
return
}
}
// then continue on the ticker.
for {
select {
case <-manager.Done():
return
case <-ticker.C:
if shouldContinue, err := doRound(); err != nil {
if !shouldContinue {
c.Error(apitypes.InternalServerError(err, "failed to get all system info"))
return
}
gperr.LogWarn("failed to get some system info", err)
}
}
}
}
func getAgentSystemInfo(ctx context.Context, a *agent.AgentConfig, query string) (json.Marshaler, error) {
ctx, cancel := context.WithTimeout(ctx, 5*time.Second)
defer cancel()
path := agent.EndpointSystemInfo + "?" + query
resp, err := a.Do(ctx, http.MethodGet, path, nil)
if err != nil {
return nil, err
}
defer resp.Body.Close()
// NOTE: buffer will be released by marshalSystemInfo once marshaling is done.
if resp.ContentLength >= 0 {
bytesBuf := allSystemInfoFixedSizePool.GetSized(int(resp.ContentLength))
_, err = io.ReadFull(resp.Body, bytesBuf)
if err != nil {
// prevent pool leak on error.
allSystemInfoFixedSizePool.Put(bytesBuf)
return nil, err
}
return bytesFromPool{json.RawMessage(bytesBuf)}, nil
}
// Fallback when content length is unknown (should not happen but just in case).
data, err := io.ReadAll(resp.Body)
if err != nil {
return nil, err
}
return json.RawMessage(data), nil
}
func getAgentSystemInfoWithRetry(ctx context.Context, a *agent.AgentConfig, query string) (json.Marshaler, error) {
const maxRetries = 3
var lastErr error
for attempt := range maxRetries {
// Apply backoff delay for retries (not for first attempt)
if attempt > 0 {
delay := max((1<<attempt)*time.Second, 5*time.Second)
select {
case <-ctx.Done():
return nil, ctx.Err()
case <-time.After(delay):
}
}
data, err := getAgentSystemInfo(ctx, a, query)
if err == nil {
return data, nil
}
lastErr = err
log.Debug().Str("agent", a.Name).Int("attempt", attempt+1).Str("error", err.Error()).Msg("Agent request attempt failed")
// Don't retry on context cancellation
if ctx.Err() != nil {
return nil, ctx.Err()
}
}
return nil, lastErr
}
func marshalSystemInfo(ws *websocket.Manager, agentName string, systemInfo any) error {
bytesBuf := allSystemInfoBytesPool.Get()
defer allSystemInfoBytesPool.Put(bytesBuf)
// release the buffer retrieved from getAgentSystemInfo
if bufFromPool, ok := systemInfo.(bytesFromPool); ok {
defer allSystemInfoFixedSizePool.Put(bufFromPool.RawMessage)
}
buf := bytes.NewBuffer(bytesBuf)
err := json.NewEncoder(buf).Encode(map[string]any{
agentName: systemInfo,
})
if err != nil {
return err
}
return ws.WriteData(websocket.TextMessage, buf.Bytes(), 3*time.Second)
}

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